Skip to content

Commit dff8210

Browse files
authored
fix(chat ui): fixed llm inference chat with openclaw to work with new LiteLLM gateway (#13)
1 parent 4e8b638 commit dff8210

File tree

8 files changed

+207
-16
lines changed

8 files changed

+207
-16
lines changed

brev/welcome-ui/app.js

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@
116116
let sandboxUrl = null;
117117
let installTriggered = false;
118118
let pollTimer = null;
119+
let keyInjected = false;
120+
let injectInFlight = false;
121+
let injectTimer = null;
122+
let lastSubmittedKey = "";
119123

120124
function stopPolling() {
121125
if (pollTimer) {
@@ -124,12 +128,38 @@
124128
}
125129
}
126130

131+
async function submitKeyForInjection(key) {
132+
if (key === lastSubmittedKey) return;
133+
lastSubmittedKey = key;
134+
keyInjected = false;
135+
injectInFlight = true;
136+
updateButtonState();
137+
try {
138+
await fetch("/api/inject-key", {
139+
method: "POST",
140+
headers: { "Content-Type": "application/json" },
141+
body: JSON.stringify({ key }),
142+
});
143+
} catch {}
144+
injectInFlight = false;
145+
if (!pollTimer && sandboxReady) startPolling();
146+
}
147+
148+
function onApiKeyInput() {
149+
updateButtonState();
150+
const key = apiKeyInput.value.trim();
151+
if (!isApiKeyValid()) return;
152+
if (injectTimer) clearTimeout(injectTimer);
153+
injectTimer = setTimeout(() => submitKeyForInjection(key), 300);
154+
}
155+
127156
/**
128-
* Four-state CTA button:
129-
* 1. API empty + tasks running -> "Waiting for API key…" (disabled)
130-
* 2. API valid + tasks running -> "Provisioning Sandbox…" (disabled, spinner)
131-
* 3. API empty + tasks complete -> "Waiting for API key…" (disabled)
132-
* 4. API valid + tasks complete -> "Open NemoClaw" (enabled)
157+
* Five-state CTA button:
158+
* 1. API empty + tasks running -> "Waiting for API key…" (disabled)
159+
* 2. API valid + tasks running -> "Provisioning Sandbox…" (disabled, spinner)
160+
* 3. API empty + tasks complete -> "Waiting for API key…" (disabled)
161+
* 4. API valid + sandbox ready + !key -> "Configuring API key…" (disabled, spinner)
162+
* 5. API valid + sandbox ready + key -> "Open NemoClaw" (enabled)
133163
*/
134164
function updateButtonState() {
135165
const keyValid = isApiKeyValid();
@@ -148,20 +178,26 @@
148178
}
149179

150180
// Console "ready" line
151-
if (sandboxReady && keyValid) {
181+
if (sandboxReady && keyValid && keyInjected) {
152182
logReady.hidden = false;
153183
logReady.querySelector(".console__icon").textContent = CHECK_CHAR;
154184
logReady.querySelector(".console__icon").className = "console__icon console__icon--done";
155185
} else {
156186
logReady.hidden = true;
157187
}
158188

159-
if (sandboxReady && keyValid) {
189+
if (sandboxReady && keyValid && keyInjected) {
160190
btnLaunch.disabled = false;
161191
btnLaunch.classList.add("btn--ready");
162192
btnSpinner.hidden = true;
163193
btnSpinner.style.display = "none";
164194
btnLaunchLabel.textContent = "Open NemoClaw";
195+
} else if (sandboxReady && keyValid && !keyInjected) {
196+
btnLaunch.disabled = true;
197+
btnLaunch.classList.remove("btn--ready");
198+
btnSpinner.hidden = false;
199+
btnSpinner.style.display = "";
200+
btnLaunchLabel.textContent = "Configuring API key\u2026";
165201
} else if (!sandboxReady && keyValid) {
166202
btnLaunch.disabled = true;
167203
btnLaunch.classList.remove("btn--ready");
@@ -229,19 +265,28 @@
229265
const res = await fetch("/api/sandbox-status");
230266
const data = await res.json();
231267

268+
if (!injectInFlight) {
269+
keyInjected = !!data.key_injected;
270+
}
271+
232272
if (data.status === "running") {
233-
stopPolling();
234273
sandboxReady = true;
235274
sandboxUrl = data.url || null;
236275

237276
setLogIcon(logGatewayIcon, "done");
238277
logGateway.querySelector(".console__text").textContent =
239278
"OpenClaw agent gateway online.";
279+
280+
if (keyInjected) {
281+
stopPolling();
282+
}
240283
updateButtonState();
241284
} else if (data.status === "error") {
242285
stopPolling();
243286
installTriggered = false;
244287
showError(data.error || "Sandbox creation failed");
288+
} else {
289+
updateButtonState();
245290
}
246291
} catch {
247292
// transient fetch error, keep polling
@@ -250,7 +295,7 @@
250295
}
251296

252297
function openOpenClaw() {
253-
if (!sandboxReady || !isApiKeyValid() || !sandboxUrl) return;
298+
if (!sandboxReady || !isApiKeyValid() || !keyInjected || !sandboxUrl) return;
254299

255300
const apiKey = apiKeyInput.value.trim();
256301
const url = new URL(sandboxUrl);
@@ -262,6 +307,8 @@
262307
sandboxReady = false;
263308
sandboxUrl = null;
264309
installTriggered = false;
310+
keyInjected = false;
311+
lastSubmittedKey = "";
265312
stopPolling();
266313

267314
setLogIcon(logSandboxIcon, null);
@@ -277,7 +324,7 @@
277324
triggerInstall();
278325
}
279326

280-
apiKeyInput.addEventListener("input", updateButtonState);
327+
apiKeyInput.addEventListener("input", onApiKeyInput);
281328
btnLaunch.addEventListener("click", openOpenClaw);
282329
btnRetry.addEventListener("click", resetInstall);
283330

@@ -288,6 +335,10 @@
288335
const res = await fetch("/api/sandbox-status");
289336
const data = await res.json();
290337

338+
if (data.key_injected) {
339+
keyInjected = true;
340+
}
341+
291342
if (data.status === "running" && data.url) {
292343
sandboxReady = true;
293344
sandboxUrl = data.url;
@@ -302,6 +353,9 @@
302353
updateButtonState();
303354

304355
showOverlay(overlayInstall);
356+
if (!keyInjected) {
357+
startPolling();
358+
}
305359
} else if (data.status === "creating") {
306360
installTriggered = true;
307361

brev/welcome-ui/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,6 @@ <h4 class="instructions-section__title">4. Manage policies with the TUI</h4>
230230
NemoClaw Sandbox &middot; NVIDIA
231231
</footer>
232232

233-
<script src="app.js?v=11"></script>
233+
<script src="app.js?v=12"></script>
234234
</body>
235235
</html>

brev/welcome-ui/server.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
"""NemoClaw Welcome UI — HTTP server with sandbox lifecycle APIs."""
77

8+
import hashlib
89
import http.client
910
import http.server
1011
import json
@@ -43,6 +44,66 @@
4344
"error": None,
4445
}
4546

47+
_inject_key_lock = threading.Lock()
48+
_inject_key_state = {
49+
"status": "idle", # idle | injecting | done | error
50+
"error": None,
51+
"key_hash": None,
52+
}
53+
54+
55+
def _hash_key(key: str) -> str:
56+
return hashlib.sha256(key.encode()).hexdigest()
57+
58+
59+
def _inject_log(msg: str) -> None:
60+
ts = time.strftime("%H:%M:%S")
61+
sys.stderr.write(f"[inject-key {ts}] {msg}\n")
62+
sys.stderr.flush()
63+
64+
65+
def _run_inject_key(key: str, key_hash: str) -> None:
66+
"""Background thread: update the NemoClaw provider credential."""
67+
_inject_log(f"step 1/3: received key (hash={key_hash[:12]}…)")
68+
cmd = [
69+
"nemoclaw", "provider", "update", "nvidia-inference",
70+
"--type", "openai",
71+
"--credential", f"OPENAI_API_KEY={key}",
72+
"--config", "OPENAI_BASE_URL=https://inference-api.nvidia.com/v1",
73+
]
74+
_inject_log(f"step 2/3: running nemoclaw provider update nvidia-inference …")
75+
try:
76+
t0 = time.time()
77+
result = subprocess.run(
78+
cmd, capture_output=True, text=True, timeout=120,
79+
)
80+
elapsed = time.time() - t0
81+
_inject_log(f" CLI exited {result.returncode} in {elapsed:.1f}s")
82+
if result.stdout.strip():
83+
_inject_log(f" stdout: {result.stdout.strip()}")
84+
if result.stderr.strip():
85+
_inject_log(f" stderr: {result.stderr.strip()}")
86+
87+
if result.returncode != 0:
88+
err = (result.stderr or result.stdout or "unknown error").strip()
89+
_inject_log(f"step 3/3: FAILED — {err}")
90+
with _inject_key_lock:
91+
_inject_key_state["status"] = "error"
92+
_inject_key_state["error"] = err
93+
return
94+
95+
_inject_log(f"step 3/3: SUCCESS — provider nvidia-inference updated")
96+
with _inject_key_lock:
97+
_inject_key_state["status"] = "done"
98+
_inject_key_state["error"] = None
99+
_inject_key_state["key_hash"] = key_hash
100+
101+
except Exception as exc:
102+
_inject_log(f"step 3/3: EXCEPTION — {exc}")
103+
with _inject_key_lock:
104+
_inject_key_state["status"] = "error"
105+
_inject_key_state["error"] = str(exc)
106+
46107

47108
def _sandbox_ready() -> bool:
48109
with _sandbox_lock:
@@ -391,6 +452,8 @@ def _route(self):
391452
return self._handle_install_openclaw()
392453
if path == "/api/policy-sync" and self.command == "POST":
393454
return self._handle_policy_sync()
455+
if path == "/api/inject-key" and self.command == "POST":
456+
return self._handle_inject_key()
394457

395458
if _sandbox_ready():
396459
return self._proxy_to_sandbox()
@@ -551,6 +614,50 @@ def _handle_policy_sync(self):
551614
_log(f"── responding {status}: {json.dumps(result)}")
552615
return self._json_response(status, result)
553616

617+
# -- POST /api/inject-key -------------------------------------------
618+
619+
def _handle_inject_key(self):
620+
"""Asynchronously update the NemoClaw provider credential.
621+
622+
Returns immediately (202) and runs the slow CLI command in a
623+
background thread. The frontend polls /api/sandbox-status to
624+
learn when injection is complete.
625+
"""
626+
content_length = int(self.headers.get("Content-Length", 0))
627+
if content_length == 0:
628+
return self._json_response(400, {"ok": False, "error": "empty body"})
629+
raw = self.rfile.read(content_length).decode("utf-8", errors="replace")
630+
try:
631+
data = json.loads(raw)
632+
except json.JSONDecodeError:
633+
return self._json_response(400, {"ok": False, "error": "invalid JSON"})
634+
635+
key = data.get("key", "").strip()
636+
if not key:
637+
return self._json_response(400, {"ok": False, "error": "missing key"})
638+
639+
key_hash = _hash_key(key)
640+
641+
with _inject_key_lock:
642+
if (_inject_key_state["status"] == "done"
643+
and _inject_key_state["key_hash"] == key_hash):
644+
return self._json_response(200, {"ok": True, "already": True})
645+
646+
if (_inject_key_state["status"] == "injecting"
647+
and _inject_key_state["key_hash"] == key_hash):
648+
return self._json_response(202, {"ok": True, "started": True})
649+
650+
_inject_key_state["status"] = "injecting"
651+
_inject_key_state["error"] = None
652+
_inject_key_state["key_hash"] = key_hash
653+
654+
thread = threading.Thread(
655+
target=_run_inject_key, args=(key, key_hash), daemon=True,
656+
)
657+
thread.start()
658+
659+
return self._json_response(202, {"ok": True, "started": True})
660+
554661
# -- GET /api/sandbox-status ----------------------------------------
555662

556663
def _handle_sandbox_status(self):
@@ -568,10 +675,16 @@ def _handle_sandbox_status(self):
568675
state["status"] = "running"
569676
state["url"] = url
570677

678+
with _inject_key_lock:
679+
key_injected = _inject_key_state["status"] == "done"
680+
key_inject_error = _inject_key_state.get("error")
681+
571682
return self._json_response(200, {
572683
"status": state["status"],
573684
"url": state.get("url"),
574685
"error": state.get("error"),
686+
"key_injected": key_injected,
687+
"key_inject_error": key_inject_error,
575688
})
576689

577690
# -- GET /api/connection-details ------------------------------------

sandboxes/nemoclaw/nemoclaw-start.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ fi
6969
# Onboard and start the gateway
7070
# --------------------------------------------------------------------------
7171
export NVIDIA_API_KEY="${NVIDIA_INFERENCE_API_KEY:- }"
72+
_ONBOARD_KEY="${NVIDIA_INFERENCE_API_KEY:-not-used}"
7273
openclaw onboard \
7374
--non-interactive \
7475
--accept-risk \
@@ -79,7 +80,7 @@ openclaw onboard \
7980
--auth-choice custom-api-key \
8081
--custom-base-url "https://inference.local/v1" \
8182
--custom-model-id "aws/anthropic/bedrock-claude-opus-4-6" \
82-
--custom-api-key "not-used" \
83+
--custom-api-key "$_ONBOARD_KEY" \
8384
--secret-input-mode plaintext \
8485
--custom-compatibility openai \
8586
--gateway-port 18788 \

sandboxes/nemoclaw/nemoclaw-ui-extension/extension/index.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import "./styles.css";
1414
import { injectButton } from "./deploy-modal.ts";
1515
import { injectNavGroup, activateNemoPage, watchOpenClawNavClicks } from "./nav-group.ts";
1616
import { injectModelSelector, watchChatCompose } from "./model-selector.ts";
17-
import { ingestKeysFromUrl, DEFAULT_MODEL, resolveApiKey } from "./model-registry.ts";
17+
import { ingestKeysFromUrl, DEFAULT_MODEL, resolveApiKey, isKeyConfigured } from "./model-registry.ts";
1818
import { waitForClient, patchConfig, waitForReconnect } from "./gateway-bridge.ts";
1919

2020
function inject(): boolean {
@@ -38,6 +38,22 @@ function watchGotoLinks() {
3838
});
3939
}
4040

41+
/**
42+
* Update the NemoClaw provider credential on the host so the sandbox
43+
* proxy / inference router uses the real key for inference.local requests.
44+
* Mirrors the policy-sync pattern in policy-page.ts.
45+
*/
46+
function injectKeyViaHost(key: string): void {
47+
fetch("/api/inject-key", {
48+
method: "POST",
49+
headers: { "Content-Type": "application/json" },
50+
body: JSON.stringify({ key }),
51+
})
52+
.then((r) => r.json())
53+
.then((b) => console.log("[NeMoClaw] inject-key:", b))
54+
.catch((e) => console.warn("[NeMoClaw] inject-key failed:", e));
55+
}
56+
4157
/**
4258
* When API keys arrive via URL parameters (from the welcome UI), apply
4359
* the default model's provider config so the gateway has a valid key
@@ -103,8 +119,10 @@ function bootstrap() {
103119
watchChatCompose();
104120
watchGotoLinks();
105121

106-
if (keysIngested) {
122+
const defaultKey = resolveApiKey(DEFAULT_MODEL.keyType);
123+
if (keysIngested || isKeyConfigured(defaultKey)) {
107124
applyIngestedKeys();
125+
injectKeyViaHost(defaultKey);
108126
}
109127

110128
if (inject()) {

sandboxes/nemoclaw/nemoclaw-ui-extension/extension/model-registry.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,13 @@ export function setIntegrateApiKey(key: string): void {
4545
else localStorage.removeItem(LS_INTEGRATE_KEY);
4646
}
4747

48+
const PLACEHOLDER_KEYS = ["not-used", "unused", "placeholder", "none", "null", "undefined"];
49+
4850
export function isKeyConfigured(key: string): boolean {
49-
return !!key && !key.startsWith("__");
51+
if (!key || !key.trim()) return false;
52+
const lower = key.trim().toLowerCase();
53+
if (lower.startsWith("__")) return false;
54+
return !PLACEHOLDER_KEYS.includes(lower);
5055
}
5156

5257
/**

0 commit comments

Comments
 (0)