Repository navigation
feat: opt-in read-only API Keys page (LiteLLM virtual keys via a host-side bridge) - #314
travellingsoldier85 wants to merge 8 commits into
Conversation
…ix socket Fixed GET-only call list (health, key list, key info). Bridge socket and token come from SPARKDASH_APIKEYS_BRIDGE_SOCKET / SPARKDASH_APIKEYS_BRIDGE_TOKEN, never settings.json. 404 while apiKeysEnabled is off. Unreadable values stay null (unknown), never 0. A rejected token backs off 30 s doubling to 15 min. Refs MiaAI-Lab#297
… read-only calls Node, no dependencies; holds the LiteLLM master key via systemd LoadCredential, checks the sha256 of the dashboard token, locks out after 10 bad tokens in 10 min. Example unit included.
…s is on No nav entry, palette command or mobile Stats entry while the setting is off; a deep link to /api-keys then lands on Overview. Unreported spend renders as unknown, never $0. Refs MiaAI-Lab#297
…, document lockout scope
|
First review, at |
…t total_pages Review follow-up for the reference bridge: - LiteLLM replies are read from Response.body chunk by chunk. Bytes are counted as they arrive and the body is cancelled (undici then closes the connection) as soon as the total passes 8 MB, or before reading when Content-Length already says it will, the way bridgeClient.js destroys the request. Such a reply is still a failure (status 0), but it is no longer buffered whole first. - /v1/keys answers 502 when a /key/list page has no total_pages that is a positive whole number, instead of treating that page as the last one. The one exception is LiteLLM's own reply for an empty key table (keys [], total_count 0, total_pages 0, from the ceiling division in list_key_helper), which stays an empty list. More than 50 pages (5000 keys) is now refused from the first page instead of after 50 fetches. Tests: a streamed over-limit body is cancelled at most one chunk past the limit; a Content-Length over the limit is refused with nothing read; missing, null, 0, negative, fractional and string total_pages, and non-JSON NaN/Infinity, give 502; the empty-table reply is an empty list; a 3-page list sent in 1-byte chunks (multi-byte characters split across reads) comes back whole.
Resolves the three conflicts with 92ac62c (kiosk review follow-ups) in src/constants.ts, src/App.tsx and src/components/shell/AppSidebar.tsx by keeping both entries: KIOSK_ID and API_KEYS_ID (constant, /kiosk and /api-keys paths, and the imports).
|
Thanks for the review. Both bridge fixes are in 6be543a:
One exception you may want to veto: for an empty key table LiteLLM itself sends Since 92ac62c the branch conflicted with main in (Optional) Main now localises every Settings row, so the new API Keys toggle is the only English-only one left. I can move it into the settings catalog and add |
|
I read the bridge client and service closely: unix socket only, a fixed allowlist of GETs with a 64-hex id check ( (Review notes prepared with Claude Code on behalf of the maintainer.) |
|
Thanks for the close read. Both asks are done, in #365:
Tests on the new head (Node 24): typecheck, build and both i18n checks pass; vitest 109 files / 823 tests pass (main: 108 / 818, the extra ones are this PR's); the server suite, run file by file, gives the same result per file as main plus this PR's 21 tests, which pass (the few failures on my host are GPU/sudo environment tests that fail the same way on main). Rewriting this branch would have meant force-pushing over this PR's history, so the linear version comes from a new branch as #365, and this branch stays as it was. I'm closing this PR in favour of #365; the review history stays here. Understood that the new page and nav need your product sign-off; #365 is ready for that pass. |
Both sides read the token once at startup and the reference bridge accepts a single token hash, so a rotation is: new token and hash, restart the bridge first, then sparkDash (under Docker recreate the container, because docker compose restart keeps the old environment). The paragraph says why that order (sparkDash's backoff, and the socket directory systemd recreates on every bridge restart) and how to clear a bridge lockout. Asked for in the review of #314. (cherry picked from commit 03963a9)
Refs #297. This is the first of the two PRs suggested there: the bridge and the read-only views. Creating, deleting, blocking and changing budgets come in PR #2, so that review can focus on the write call list and the lockout.
Summary
apiKeysEnabled, default off).server/apiKeys/registersGET /api/apikeys/status,/keysand/keys/:idbehind the existing auth and a 60/min rate limit./v1/health,/v1/keysand/v1/keys/<64-hex>.scripts/apikeys-bridge/(Node, no dependencies), with a systemd unit example./api-keys: key table plus a detail view, using the existingtk-*page/card/table styles and theme variables.Maintainer points from #297, one by one
apiKeysEnabled, which sits next tomqttEnabledinsettings.js. The feature lives in its ownserver/apiKeys/module with its own routes and its own README section. While off there is no sidebar, palette or mobile entry, every/api/apikeys/*route answers 404, and a deep link to/api-keyslands on Overview. There is no disabled page.bridgeClient.jsrefuses any path outside the call list before it opens a socket, and the module has no write method at all.SPARKDASH_APIKEYS_BRIDGE_TOKEN(and the socket fromSPARKDASH_APIKEYS_BRIDGE_SOCKET) once at startup, likeSPARKDASH_MQTT_PASSWORD. A test checks that settings.json gets no bridge field.server/secretsStore.js.nullon the server and render as "unknown" (spend) or "—" (limits) on the page. There are tests on both sides.test:server.Security model / call list
LoadCredential, so the key file can stay root-only.How to test
npm run typecheck && npm run build && npm run i18n:checknode --test server/apiKeys/__tests__/*.test.js scripts/apikeys-bridge/*.test.js server/__tests__/settings-apikeys.test.jsnpx vitest run/api/apikeys/keysanswers 404.Scope
Read-only views only. Destructive actions (create with one-time reveal, block/unblock, kill, delete, limits and budgets) come in PR #2, with the bounded audit log and a browser-side admin gate.