diff --git a/sandboxes/nemoclaw/nemoclaw-start.sh b/sandboxes/nemoclaw/nemoclaw-start.sh
index 8ea681f..56cb944 100644
--- a/sandboxes/nemoclaw/nemoclaw-start.sh
+++ b/sandboxes/nemoclaw/nemoclaw-start.sh
@@ -79,7 +79,7 @@ openclaw onboard \
--skip-health \
--auth-choice custom-api-key \
--custom-base-url "https://inference.local/v1" \
- --custom-model-id "aws/anthropic/bedrock-claude-opus-4-6" \
+ --custom-model-id "-" \
--custom-api-key "$_ONBOARD_KEY" \
--secret-input-mode plaintext \
--custom-compatibility openai \
diff --git a/sandboxes/nemoclaw/nemoclaw-ui-extension/extension/api-keys-page.ts b/sandboxes/nemoclaw/nemoclaw-ui-extension/extension/api-keys-page.ts
index bcbaecd..29fc9ac 100644
--- a/sandboxes/nemoclaw/nemoclaw-ui-extension/extension/api-keys-page.ts
+++ b/sandboxes/nemoclaw/nemoclaw-ui-extension/extension/api-keys-page.ts
@@ -6,7 +6,7 @@
* model-registry.ts getter functions.
*/
-import { ICON_KEY, ICON_EYE, ICON_EYE_OFF, ICON_CHECK } from "./icons.ts";
+import { ICON_KEY, ICON_EYE, ICON_EYE_OFF, ICON_CHECK, ICON_LOADER, ICON_CLOSE } from "./icons.ts";
import {
getInferenceApiKey,
getIntegrateApiKey,
@@ -24,6 +24,7 @@ interface KeyFieldDef {
label: string;
description: string;
placeholder: string;
+ serverCredentialKey: string;
get: () => string;
set: (v: string) => void;
}
@@ -34,6 +35,7 @@ const KEY_FIELDS: KeyFieldDef[] = [
label: "Inference API Key",
description: "For inference-api.nvidia.com — powers NVIDIA Claude Opus 4.6",
placeholder: "nvapi-...",
+ serverCredentialKey: "OPENAI_API_KEY",
get: getInferenceApiKey,
set: setInferenceApiKey,
},
@@ -42,11 +44,67 @@ const KEY_FIELDS: KeyFieldDef[] = [
label: "Integrate API Key",
description: "For integrate.api.nvidia.com — powers Kimi K2.5, Nemotron Ultra, DeepSeek V3.2",
placeholder: "nvapi-...",
+ serverCredentialKey: "NVIDIA_API_KEY",
get: getIntegrateApiKey,
set: setIntegrateApiKey,
},
];
+// ---------------------------------------------------------------------------
+// Sync localStorage keys to server-side provider credentials
+// ---------------------------------------------------------------------------
+
+interface ProviderSummary {
+ name: string;
+ type: string;
+ credentialKeys: string[];
+}
+
+/**
+ * Push localStorage API keys to every server-side provider whose credential
+ * key matches. This bridges the gap between the browser-only API Keys tab
+ * and the NemoClaw proxy which reads credentials from the server-side store.
+ */
+export async function syncKeysToProviders(): Promise {
+ const res = await fetch("/api/providers");
+ if (!res.ok) throw new Error(`Failed to fetch providers: ${res.status}`);
+ const body = await res.json();
+ if (!body.ok) throw new Error(body.error || "Failed to fetch providers");
+
+ const providers: ProviderSummary[] = body.providers || [];
+ const errors: string[] = [];
+
+ for (const provider of providers) {
+ for (const field of KEY_FIELDS) {
+ const key = field.get();
+ if (!isKeyConfigured(key)) continue;
+ if (!provider.credentialKeys?.includes(field.serverCredentialKey)) continue;
+
+ try {
+ const updateRes = await fetch(`/api/providers/${encodeURIComponent(provider.name)}`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ type: provider.type,
+ credentials: { [field.serverCredentialKey]: key },
+ config: {},
+ }),
+ });
+ const updateBody = await updateRes.json();
+ if (!updateBody.ok) {
+ errors.push(`${provider.name}: ${updateBody.error || "update failed"}`);
+ }
+ } catch (err) {
+ errors.push(`${provider.name}: ${err}`);
+ }
+ }
+ }
+
+ if (errors.length > 0) {
+ throw new Error(errors.join("; "));
+ }
+}
+
// ---------------------------------------------------------------------------
// Render the API Keys page into a container element
// ---------------------------------------------------------------------------
@@ -71,7 +129,7 @@ export function renderApiKeysPage(container: HTMLElement): void {
Enter your NVIDIA API keys to enable model switching and DGX deployment.
Keys are stored locally in your browser and never sent to third parties.
-
+
Get your keys at build.nvidia.com →
`;
page.appendChild(intro);
@@ -100,7 +158,7 @@ export function renderApiKeysPage(container: HTMLElement): void {
form.appendChild(actions);
page.appendChild(form);
- saveBtn.addEventListener("click", () => {
+ saveBtn.addEventListener("click", async () => {
for (const field of KEY_FIELDS) {
const input = form.querySelector(`[data-key-id="${field.id}"]`);
if (input) field.set(input.value.trim());
@@ -108,12 +166,25 @@ export function renderApiKeysPage(container: HTMLElement): void {
updateStatusDots();
- feedback.className = "nemoclaw-key-feedback nemoclaw-key-feedback--success";
- feedback.innerHTML = `${ICON_CHECK}Keys saved`;
- setTimeout(() => {
- feedback.className = "nemoclaw-key-feedback";
- feedback.textContent = "";
- }, 3000);
+ feedback.className = "nemoclaw-key-feedback nemoclaw-key-feedback--saving";
+ feedback.innerHTML = `${ICON_LOADER}Syncing keys to providers\u2026`;
+ saveBtn.disabled = true;
+
+ try {
+ await syncKeysToProviders();
+ feedback.className = "nemoclaw-key-feedback nemoclaw-key-feedback--success";
+ feedback.innerHTML = `${ICON_CHECK}Keys saved & synced to providers`;
+ } catch (err) {
+ console.warn("[NeMoClaw] Provider key sync failed:", err);
+ feedback.className = "nemoclaw-key-feedback nemoclaw-key-feedback--error";
+ feedback.innerHTML = `${ICON_CLOSE}Keys saved locally but sync failed`;
+ } finally {
+ saveBtn.disabled = false;
+ setTimeout(() => {
+ feedback.className = "nemoclaw-key-feedback";
+ feedback.textContent = "";
+ }, 4000);
+ }
});
}
diff --git a/sandboxes/nemoclaw/nemoclaw-ui-extension/extension/index.ts b/sandboxes/nemoclaw/nemoclaw-ui-extension/extension/index.ts
index d5e6f1c..5ff25a2 100644
--- a/sandboxes/nemoclaw/nemoclaw-ui-extension/extension/index.ts
+++ b/sandboxes/nemoclaw/nemoclaw-ui-extension/extension/index.ts
@@ -5,7 +5,7 @@
* 1. A green "Deploy DGX Spark/Station" CTA button in the topbar
* 2. A "NeMoClaw" collapsible nav group with Policy, Inference Routes,
* and API Keys pages
- * 3. A model selector wired to NVIDIA endpoints via config.patch
+ * 3. A model selector wired to NVIDIA endpoints
*
* Operates purely as an overlay — no original OpenClaw source files are modified.
*/
@@ -15,7 +15,8 @@ import { injectButton } from "./deploy-modal.ts";
import { injectNavGroup, activateNemoPage, watchOpenClawNavClicks } from "./nav-group.ts";
import { injectModelSelector, watchChatCompose } from "./model-selector.ts";
import { ingestKeysFromUrl, DEFAULT_MODEL, resolveApiKey, isKeyConfigured } from "./model-registry.ts";
-import { waitForClient, patchConfig, waitForReconnect } from "./gateway-bridge.ts";
+import { waitForClient, waitForReconnect, patchConfig } from "./gateway-bridge.ts";
+import { syncKeysToProviders } from "./api-keys-page.ts";
function inject(): boolean {
const hasButton = injectButton();
@@ -38,50 +39,6 @@ function watchGotoLinks() {
});
}
-/**
- * Update the NemoClaw provider credential on the host so the sandbox
- * proxy / inference router uses the real key for inference.local requests.
- * Mirrors the policy-sync pattern in policy-page.ts.
- */
-function injectKeyViaHost(key: string): void {
- fetch("/api/inject-key", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ key }),
- })
- .then((r) => r.json())
- .then((b) => console.log("[NeMoClaw] inject-key:", b))
- .catch((e) => console.warn("[NeMoClaw] inject-key failed:", e));
-}
-
-/**
- * When API keys arrive via URL parameters (from the welcome UI), apply
- * the default model's provider config so the gateway has a valid key
- * immediately rather than the placeholder set during onboarding.
- */
-function applyIngestedKeys(): void {
- waitForClient().then(async () => {
- const apiKey = resolveApiKey(DEFAULT_MODEL.keyType);
- await patchConfig({
- models: {
- providers: {
- [DEFAULT_MODEL.providerKey]: {
- baseUrl: DEFAULT_MODEL.providerConfig.baseUrl,
- api: DEFAULT_MODEL.providerConfig.api,
- models: DEFAULT_MODEL.providerConfig.models,
- apiKey,
- },
- },
- },
- agents: {
- defaults: { model: { primary: DEFAULT_MODEL.modelRef } },
- },
- });
- }).catch((err) => {
- console.error("[NeMoClaw] Failed to apply ingested API key:", err);
- });
-}
-
/**
* Insert a full-screen loading overlay that covers the OpenClaw UI while the
* gateway connects and auto-pairs the device. The overlay is styled via
@@ -108,10 +65,51 @@ function revealApp(): void {
}
}
+/**
+ * Read the live OpenClaw config, find the active model.primary ref, and
+ * patch streaming: true for it. For proxy-managed models the model.primary
+ * never changes after onboard, so enabling it once covers every proxy model
+ * switch.
+ */
+async function enableStreamingForActiveModel(): Promise {
+ const client = await waitForClient();
+ const snapshot = await client.request>("config.get", {});
+
+ const agents = snapshot?.agents as Record | undefined;
+ const defaults = agents?.defaults as Record | undefined;
+ const model = defaults?.model as Record | undefined;
+ const primary = model?.primary as string | undefined;
+
+ if (!primary) {
+ console.warn("[NeMoClaw] Could not determine active model primary from config");
+ return;
+ }
+
+ const models = defaults?.models as Record> | undefined;
+ if (models?.[primary]?.streaming === true) return;
+
+ await patchConfig({
+ agents: {
+ defaults: {
+ models: {
+ [primary]: { streaming: true },
+ },
+ },
+ },
+ });
+}
+
function bootstrap() {
showConnectOverlay();
- waitForReconnect(30_000).then(revealApp).catch(revealApp);
+ waitForReconnect(30_000)
+ .then(() => {
+ revealApp();
+ enableStreamingForActiveModel().catch((err) =>
+ console.warn("[NeMoClaw] Failed to enable streaming:", err),
+ );
+ })
+ .catch(revealApp);
const keysIngested = ingestKeysFromUrl();
@@ -121,8 +119,9 @@ function bootstrap() {
const defaultKey = resolveApiKey(DEFAULT_MODEL.keyType);
if (keysIngested || isKeyConfigured(defaultKey)) {
- applyIngestedKeys();
- injectKeyViaHost(defaultKey);
+ syncKeysToProviders().catch((e) =>
+ console.warn("[NeMoClaw] bootstrap provider key sync failed:", e),
+ );
}
if (inject()) {
diff --git a/sandboxes/nemoclaw/nemoclaw-ui-extension/extension/inference-page.ts b/sandboxes/nemoclaw/nemoclaw-ui-extension/extension/inference-page.ts
index d1fd47e..2c30b13 100644
--- a/sandboxes/nemoclaw/nemoclaw-ui-extension/extension/inference-page.ts
+++ b/sandboxes/nemoclaw/nemoclaw-ui-extension/extension/inference-page.ts
@@ -379,6 +379,7 @@ function buildQuickChip(modelId: string, name: string, providerName: string, cur
const items = getCustomQuickSelects().filter((i) => i.modelId !== modelId);
saveCustomQuickSelects(items);
chip.remove();
+ refreshModelSelector().catch(() => {});
});
chip.appendChild(removeBtn);
}
@@ -439,6 +440,7 @@ function showAddQuickSelectForm(section: HTMLElement): void {
saveCustomQuickSelects(items);
form.remove();
rerenderQuickPicker(section);
+ refreshModelSelector().catch(() => {});
});
btns.appendChild(addConfirm);
diff --git a/sandboxes/nemoclaw/nemoclaw-ui-extension/extension/model-registry.ts b/sandboxes/nemoclaw/nemoclaw-ui-extension/extension/model-registry.ts
index 885fb49..9016971 100644
--- a/sandboxes/nemoclaw/nemoclaw-ui-extension/extension/model-registry.ts
+++ b/sandboxes/nemoclaw/nemoclaw-ui-extension/extension/model-registry.ts
@@ -118,8 +118,8 @@ export interface ModelEntry {
}
// ---------------------------------------------------------------------------
-// Curated models — hardcoded presets backed by the nvidia-inference provider.
-// All route through inference.local; the NemoClaw proxy injects credentials.
+// Curated models — hardcoded presets routed through inference.local.
+// The NemoClaw proxy injects credentials based on the providerName.
// ---------------------------------------------------------------------------
export interface CuratedModel {
@@ -130,6 +130,12 @@ export interface CuratedModel {
}
export const CURATED_MODELS: readonly CuratedModel[] = [
+ {
+ id: "curated-kimi-k25",
+ name: "Kimi K2.5",
+ modelId: "moonshotai/kimi-k2.5",
+ providerName: "nvidia-endpoints",
+ },
{
id: "curated-claude-opus",
name: "Claude Opus 4.6",
@@ -137,28 +143,28 @@ export const CURATED_MODELS: readonly CuratedModel[] = [
providerName: "nvidia-inference",
},
{
- id: "curated-gpt-oss",
- name: "GPT-OSS 20B",
- modelId: "nvidia/openai/gpt-oss-20b",
- providerName: "nvidia-inference",
+ id: "curated-minimax-m25",
+ name: "MiniMax M2.5",
+ modelId: "minimaxai/minimax-m2.5",
+ providerName: "nvidia-endpoints",
},
{
- id: "curated-nemotron-super",
- name: "Nemotron 3 Super",
- modelId: "nvidia/nvidia/nemotron-3-super-preview",
- providerName: "nvidia-inference",
+ id: "curated-glm5",
+ name: "GLM 5",
+ modelId: "z-ai/glm5",
+ providerName: "nvidia-endpoints",
},
{
- id: "curated-qwen3",
- name: "Qwen3 Next 80B",
- modelId: "nvidia/qwen/qwen3-next-80b-a3b-instruct",
- providerName: "nvidia-inference",
+ id: "curated-qwen35",
+ name: "Qwen 3.5 397B",
+ modelId: "qwen/qwen3.5-397b-a17b",
+ providerName: "nvidia-endpoints",
},
{
- id: "curated-llama-70b",
- name: "Llama 3.3 70B",
- modelId: "nvidia/meta/llama-3.3-70b-instruct",
- providerName: "nvidia-inference",
+ id: "curated-gpt-oss-120b",
+ name: "GPT-OSS 120B",
+ modelId: "openai/gpt-oss-120b",
+ providerName: "nvidia-endpoints",
},
];
@@ -167,7 +173,7 @@ export function curatedToModelEntry(c: CuratedModel): ModelEntry {
return {
id: c.id,
name: c.name,
- isDefault: c.id === "curated-claude-opus",
+ isDefault: c.id === "curated-kimi-k25",
providerKey: key,
modelRef: `${key}/${c.modelId}`,
keyType: "inference",
@@ -198,27 +204,27 @@ export function getCuratedByModelId(modelId: string): CuratedModel | undefined {
// Legacy MODEL_REGISTRY — kept as the default model reference for bootstrap
// ---------------------------------------------------------------------------
-const DEFAULT_PROVIDER_KEY = "custom-inference-api-nvidia-com";
+const DEFAULT_PROVIDER_KEY = "curated-nvidia-endpoints";
export const MODEL_REGISTRY: readonly ModelEntry[] = [
{
- id: "nvidia-claude-opus-4-6",
- name: "Claude Opus 4.6",
+ id: "curated-kimi-k25",
+ name: "Kimi K2.5",
isDefault: true,
providerKey: DEFAULT_PROVIDER_KEY,
- modelRef: `${DEFAULT_PROVIDER_KEY}/aws/anthropic/bedrock-claude-opus-4-6`,
+ modelRef: `${DEFAULT_PROVIDER_KEY}/moonshotai/kimi-k2.5`,
keyType: "inference",
providerConfig: {
baseUrl: "https://inference.local/v1",
api: "openai-completions",
models: [
{
- id: "aws/anthropic/bedrock-claude-opus-4-6",
- name: "Claude Opus 4.6",
+ id: "moonshotai/kimi-k2.5",
+ name: "Kimi K2.5",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
- contextWindow: 200_000,
+ contextWindow: 128_000,
maxTokens: 8192,
},
],
@@ -299,6 +305,46 @@ export function buildDynamicEntry(
};
}
+/**
+ * Build a ModelEntry for a user-defined Quick Select shortcut.
+ * Uses a unique ID derived from providerName + modelId to avoid
+ * collisions when multiple shortcuts share the same provider.
+ */
+export function buildQuickSelectEntry(
+ providerName: string,
+ modelId: string,
+ displayName: string,
+): ModelEntry {
+ const curated = getCuratedByModelId(modelId);
+ if (curated) return curatedToModelEntry(curated);
+
+ const key = `qs-${providerName}-${modelId.replace(/\//g, "-")}`;
+ return {
+ id: key,
+ name: displayName,
+ isDefault: false,
+ providerKey: `qs-${providerName}`,
+ modelRef: `qs-${providerName}/${modelId}`,
+ keyType: "inference",
+ isDynamic: true,
+ providerConfig: {
+ baseUrl: "https://inference.local/v1",
+ api: "openai-completions",
+ models: [
+ {
+ id: modelId,
+ name: displayName,
+ reasoning: false,
+ input: ["text"],
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
+ contextWindow: 128_000,
+ maxTokens: 8192,
+ },
+ ],
+ },
+ };
+}
+
// ---------------------------------------------------------------------------
// Deploy targets (used by deploy-modal.ts)
// ---------------------------------------------------------------------------
diff --git a/sandboxes/nemoclaw/nemoclaw-ui-extension/extension/model-selector.ts b/sandboxes/nemoclaw/nemoclaw-ui-extension/extension/model-selector.ts
index da3bf4d..3c897ce 100644
--- a/sandboxes/nemoclaw/nemoclaw-ui-extension/extension/model-selector.ts
+++ b/sandboxes/nemoclaw/nemoclaw-ui-extension/extension/model-selector.ts
@@ -19,6 +19,7 @@ import {
resolveApiKey,
isKeyConfigured,
buildDynamicEntry,
+ buildQuickSelectEntry,
setDynamicModels,
getDynamicModels,
CURATED_MODELS,
@@ -69,6 +70,9 @@ export function buildModelPatch(entry: ModelEntry): Record | nu
agents: {
defaults: {
model: { primary: entry.modelRef },
+ models: {
+ [entry.modelRef]: { streaming: true },
+ },
},
},
};
@@ -119,6 +123,20 @@ async function fetchDynamic(): Promise {
entries.push(buildDynamicEntry(route.providerName, route.modelId, provType));
}
+ const curatedIds = new Set(CURATED_MODELS.map((c) => c.modelId));
+ const existingModelIds = new Set(entries.map((e) => e.providerConfig.models[0]?.id));
+ try {
+ const raw = localStorage.getItem("nemoclaw:custom-quick-selects");
+ if (raw) {
+ const customQS: { modelId: string; name: string; providerName: string }[] = JSON.parse(raw);
+ for (const qs of customQS) {
+ if (curatedIds.has(qs.modelId) || existingModelIds.has(qs.modelId)) continue;
+ entries.push(buildQuickSelectEntry(qs.providerName, qs.modelId, qs.name));
+ existingModelIds.add(qs.modelId);
+ }
+ }
+ } catch { /* ignore malformed localStorage data */ }
+
setDynamicModels(entries);
} catch {
// Non-fatal -- static models still work
diff --git a/sandboxes/nemoclaw/nemoclaw-ui-extension/extension/policy-page.ts b/sandboxes/nemoclaw/nemoclaw-ui-extension/extension/policy-page.ts
index c81ade9..3389b89 100644
--- a/sandboxes/nemoclaw/nemoclaw-ui-extension/extension/policy-page.ts
+++ b/sandboxes/nemoclaw/nemoclaw-ui-extension/extension/policy-page.ts
@@ -454,7 +454,7 @@ function buildNetworkPoliciesSection(): HTMLElement {
headerRow.className = "nemoclaw-policy-section__header";
headerRow.innerHTML = `
${ICON_GLOBE}
- Network Policies
+ Allowed Network Policies
${policyCount}`;
const searchInput = document.createElement("input");
diff --git a/sandboxes/nemoclaw/nemoclaw-ui-extension/extension/styles.css b/sandboxes/nemoclaw/nemoclaw-ui-extension/extension/styles.css
index 432598d..43ad9e7 100644
--- a/sandboxes/nemoclaw/nemoclaw-ui-extension/extension/styles.css
+++ b/sandboxes/nemoclaw/nemoclaw-ui-extension/extension/styles.css
@@ -1204,7 +1204,7 @@ body.nemoclaw-switching openclaw-app {
background: var(--bg-elevated, #1a1d25);
padding: 14px;
position: relative;
- overflow: hidden;
+ overflow: visible;
}
:root[data-theme="light"] .nemoclaw-policy-imm-card {
@@ -1737,8 +1737,7 @@ body.nemoclaw-switching openclaw-app {
content: attr(data-tip);
position: absolute;
bottom: calc(100% + 6px);
- left: 50%;
- transform: translateX(-50%);
+ left: 0;
padding: 6px 10px;
border-radius: 6px;
background: var(--card, #181b22);