Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion sandboxes/nemoclaw/nemoclaw-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -24,6 +24,7 @@ interface KeyFieldDef {
label: string;
description: string;
placeholder: string;
serverCredentialKey: string;
get: () => string;
set: (v: string) => void;
}
Expand All @@ -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,
},
Expand All @@ -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<void> {
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
// ---------------------------------------------------------------------------
Expand All @@ -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.
</p>
<a class="nemoclaw-key-intro__link" href="https://build.nvidia.com/models" target="_blank" rel="noopener noreferrer">
<a class="nemoclaw-key-intro__link" href="https://build.nvidia.com/settings/api-keys" target="_blank" rel="noopener noreferrer">
Get your keys at build.nvidia.com &rarr;
</a>`;
page.appendChild(intro);
Expand Down Expand Up @@ -100,20 +158,33 @@ 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<HTMLInputElement>(`[data-key-id="${field.id}"]`);
if (input) field.set(input.value.trim());
}

updateStatusDots();

feedback.className = "nemoclaw-key-feedback nemoclaw-key-feedback--success";
feedback.innerHTML = `${ICON_CHECK}<span>Keys saved</span>`;
setTimeout(() => {
feedback.className = "nemoclaw-key-feedback";
feedback.textContent = "";
}, 3000);
feedback.className = "nemoclaw-key-feedback nemoclaw-key-feedback--saving";
feedback.innerHTML = `${ICON_LOADER}<span>Syncing keys to providers\u2026</span>`;
saveBtn.disabled = true;

try {
await syncKeysToProviders();
feedback.className = "nemoclaw-key-feedback nemoclaw-key-feedback--success";
feedback.innerHTML = `${ICON_CHECK}<span>Keys saved &amp; synced to providers</span>`;
} catch (err) {
console.warn("[NeMoClaw] Provider key sync failed:", err);
feedback.className = "nemoclaw-key-feedback nemoclaw-key-feedback--error";
feedback.innerHTML = `${ICON_CLOSE}<span>Keys saved locally but sync failed</span>`;
} finally {
saveBtn.disabled = false;
setTimeout(() => {
feedback.className = "nemoclaw-key-feedback";
feedback.textContent = "";
}, 4000);
}
});
}

Expand Down
97 changes: 48 additions & 49 deletions sandboxes/nemoclaw/nemoclaw-ui-extension/extension/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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();
Expand All @@ -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
Expand All @@ -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<void> {
const client = await waitForClient();
const snapshot = await client.request<Record<string, unknown>>("config.get", {});

const agents = snapshot?.agents as Record<string, unknown> | undefined;
const defaults = agents?.defaults as Record<string, unknown> | undefined;
const model = defaults?.model as Record<string, unknown> | 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<string, Record<string, unknown>> | 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();

Expand All @@ -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()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -439,6 +440,7 @@ function showAddQuickSelectForm(section: HTMLElement): void {
saveCustomQuickSelects(items);
form.remove();
rerenderQuickPicker(section);
refreshModelSelector().catch(() => {});
});

btns.appendChild(addConfirm);
Expand Down
Loading