From 64a96443a46dcf3c51e8cd64faa46f634ff395ae Mon Sep 17 00:00:00 2001 From: Scott Mraz Date: Wed, 22 Apr 2026 16:00:10 -0600 Subject: [PATCH] Add Amazon Bedrock provider for the LLM settings (Converse + OpenAI) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds first-class Amazon Bedrock as an LLM provider alongside the existing generic API and local huggingface options. Works with the unchanged streaming reader in the browser because the server-side Bedrock code re-emits OpenAI-shaped JSON / SSE. ## What the user sees New "Bedrock" tab in both the Admin Agent and the Onscreen Agent settings dialogs (the dashboard's "Set LLM API key" pill and the admin view open the same tab). Bedrock tab contents: - Server credentials panel — live-populated from a new GET /api/bedrock/config; shows auth mode (AWS profile SigV4 vs. Bedrock API key), profile name, region, and which server env vars are driving it. - Credential mode radio — "Use server credentials" (Node signs with the configured AWS profile or server-side Bedrock key; works for all models) or "Paste a Bedrock API key" (key forwarded as Authorization: Bearer; works for the OpenAI-compatible route only, since Anthropic Bedrock requires SigV4). - Model dropdown with six presets: Claude Sonnet 4.6, Opus 4.7, Opus 4.6, Haiku 4.5, OpenAI gpt-oss-20b, gpt-oss-120b. - Endpoint auto-routed from model family — openai.* IDs hit /api/bedrock/openai/v1/chat/completions, everything else hits /api/bedrock/converse/v1/chat/completions. The user never types a URL. The existing API and Local tabs are untouched. Default provider is still API. No existing user's settings are migrated or changed. ## Server — /api/bedrock/** Three endpoints (all auth-gated via the same ensureAuthenticatedOrRespond as /api/proxy): - GET /api/bedrock/config → { mode: "sigv4"|"apikey", profile, region, hasApiKey } - POST /api/bedrock/openai/v1/chat/completions Signed pass-through to Bedrock's native OpenAI route. Honors a client-side Authorization: Bearer header so the "Paste a Bedrock API key" UI mode round-trips the key. Falls back to SigV4 with the configured AWS profile otherwise. - POST /api/bedrock/converse/v1/chat/completions OpenAI-chat → Bedrock Converse adapter. Translates messages/system/inferenceConfig, calls /converse or /converse-stream, emits a single OpenAI chat.completion (non-stream) or a stream of chat.completion.chunk frames + data: [DONE]. Server config is env-driven; no new config files: | Env var | Purpose | |------------------------------|-----------------------------------------------| | SPACE_BEDROCK_MODE | "apikey" or "sigv4"; auto-detected if unset | | SPACE_BEDROCK_REGION | Bedrock region; falls back to AWS_REGION | | SPACE_BEDROCK_API_KEY | Long-term Bedrock API key (apikey mode) | | SPACE_BEDROCK_AWS_PROFILE | AWS profile for SigV4; falls back to AWS_PROFILE | Standard AWS_* variables (AWS_ACCESS_KEY_ID etc.) also work. Profiles using credential_process are supported natively. ## Why the Converse adapter Anthropic models on Bedrock do not respond to the OpenAI-compatible /openai/v1/... route. They expose the Converse API at /model/{id}/converse[-stream]. The adapter is what makes the browser client talk to Claude on Bedrock without any client-side changes. Notes: - Claude 4.5+ models deprecate `temperature` and `top_p`; the adapter strips both so requests don't 400 out. Older models still get them. - Streaming path decodes Bedrock's binary eventstream on the server (server/lib/bedrock/eventstream.js) and re-emits plain text/event-stream chunks so the existing browser SSE reader works. ## New files - server/lib/bedrock/sigv4.js (242 lines) — SigV4 signer with env / shared-ini / credential_process credential resolution. - server/lib/bedrock/proxy.js (164 lines) — request router. - server/lib/bedrock/converse.js (371 lines) — OpenAI↔Converse adapter. - server/lib/bedrock/eventstream.js (115 lines) — AWS eventstream binary decoder. - app/L0/_all/mod/_core/llm_settings/panel.html (157 lines) — shared settings component. Matches the existing huggingface/config-sidebar.html pattern of a single component with a mode="admin|onscreen" attribute, so the admin and onscreen dialogs render identical UX from one file. ## Modified files Admin + onscreen config/store/storage/panel.html: - New BEDROCK enum value in ADMIN_CHAT_LLM_PROVIDER / ONSCREEN_AGENT_LLM_PROVIDER. - bedrockCredMode, bedrockModel, bedrockApiKey added to draft / saved settings with safe defaults. - New getters (isSettingsDraftUsingBedrockProvider, isSettingsDraftUsingBedrockClientKey, bedrockServerConfig, bedrockModelPresets). - New actions (setSettingsBedrockCredMode, setSettingsBedrockModel, loadBedrockServerConfig). - saveSettingsFromDialog projects the Bedrock draft onto apiEndpoint + apiKey + model at persist time so the existing streamer code path in api.js works unchanged. - panel.html: the inline provider block becomes a single ``. server/router/router.js: - Wires /api/bedrock and /api/bedrock/** into the auth-gated router the same way /api/proxy is wired. ## file_read `allowMissing` — side fix (opt-in only) First-run dashboard / agent loads trigger /api/file_read 404s for ~/conf/dashboard.yaml and ~/hist/{admin-chat,onscreen-agent}.json that don't yet exist. The calling code already handles the missing case; only DevTools noise remains. This PR adds an opt-in `allowMissing: true` flag on the object form of runtime.api.fileRead(…) / fileDelete(…). When set and the path is missing, the server returns 200 { exists: false, content: "" } instead of 404. When unset, behavior is identical to before. Callers opted in: - dashboard_welcome/dashboard-prefs.js loadDashboardPrefs - admin/views/agent/storage.js loadAdminChatConfig + loadAdminChatHistory - onscreen_agent/storage.js loadOnscreenAgentConfig + loadOnscreenAgentHistory The queueFileRead() coalescing batcher strips unknown body fields when it rebuilds the outgoing request; the PR bypasses the batcher when allowMissing is set so the flag survives to the server. This fix is orthogonal to the Bedrock work but shares the allowMissing plumbing used by the new /api/bedrock/config call. Happy to split into a separate PR if you prefer. ## Backward compatibility - Provider defaults unchanged (API). New BEDROCK enum value has no effect unless the user selects it. - No existing config files are read, written, renamed, or migrated. - No existing public API changed. createFileReadRequest / createFileDeleteRequest are additive. - Server boots and works without any SPACE_BEDROCK_* env vars set; /api/bedrock/** is simply unavailable in that case. - Zero new runtime dependencies. Pure Node built-ins (crypto, child_process, stream, global fetch). ## Live test evidence All calls against bedrock-runtime.us-west-2.amazonaws.com: - POST /api/bedrock/openai/v1/chat/completions, model openai.gpt-oss-20b-1:0 → 200, usage returned. - POST /api/bedrock/converse/v1/chat/completions (non-stream), model us.anthropic.claude-sonnet-4-6 → "pong", usage returned. - POST /api/bedrock/converse/v1/chat/completions (non-stream), model us.anthropic.claude-opus-4-6-v1 → "pong", usage returned. - POST /api/bedrock/converse/v1/chat/completions (non-stream), model us.anthropic.claude-opus-4-7 → "pong", usage returned. - POST /api/bedrock/converse/v1/chat/completions (stream=true), model us.anthropic.claude-sonnet-4-6 → 4 SSE chunks + data: [DONE], finish_reason: stop. - POST /api/bedrock/converse/v1/chat/completions, model us.anthropic.claude-opus-4-7, temperature:0.2 → 200 (adapter strips deprecated temperature for Claude 4.5+, previously 400'd). - POST /api/file_read {path:"~/conf/dashboard.yaml", allowMissing:true} → 200 {exists:false, content:""} (previously 404). - Server boot with no SPACE_BEDROCK_* env vars → works; /api/bedrock/config still 200s but with no useful config; /api/bedrock/** calls 500 with a readable error. Existing OpenRouter / API-tab path unaffected. ## Non-goals / known limitations - Converse adapter only translates text parts. Tool calls, image parts, and documents are not translated yet — happy to follow up. - Bedrock's /openai/v1/ route only serves the OpenAI open-weights models hosted on Bedrock (gpt-oss-20b, gpt-oss-120b). That is an AWS-side limitation, not a client bug. - The model preset list is hand-curated. A maintainer-preferred way to resolve available models dynamically (e.g. list-inference-profiles) would be a nice follow-up; the text field still works for any ID. --- .../mod/_core/admin/views/agent/config.js | 80 +++- .../mod/_core/admin/views/agent/panel.html | 38 +- .../mod/_core/admin/views/agent/storage.js | 16 +- .../_all/mod/_core/admin/views/agent/store.js | 98 ++++- .../dashboard_welcome/dashboard-prefs.js | 5 +- .../_all/mod/_core/framework/js/api-client.js | 47 ++- app/L0/_all/mod/_core/llm_settings/panel.html | 157 ++++++++ .../_all/mod/_core/onscreen_agent/config.js | 49 ++- .../_all/mod/_core/onscreen_agent/panel.html | 38 +- .../_all/mod/_core/onscreen_agent/storage.js | 16 +- app/L0/_all/mod/_core/onscreen_agent/store.js | 96 ++++- .../spaces/thumbnail_experiment/index.js | 2 +- server/api/file_delete.js | 27 +- server/api/file_read.js | 49 ++- server/lib/bedrock/converse.js | 371 ++++++++++++++++++ server/lib/bedrock/eventstream.js | 115 ++++++ server/lib/bedrock/proxy.js | 164 ++++++++ server/lib/bedrock/sigv4.js | 242 ++++++++++++ server/router/router.js | 10 + 19 files changed, 1501 insertions(+), 119 deletions(-) create mode 100644 app/L0/_all/mod/_core/llm_settings/panel.html create mode 100644 server/lib/bedrock/converse.js create mode 100644 server/lib/bedrock/eventstream.js create mode 100644 server/lib/bedrock/proxy.js create mode 100644 server/lib/bedrock/sigv4.js diff --git a/app/L0/_all/mod/_core/admin/views/agent/config.js b/app/L0/_all/mod/_core/admin/views/agent/config.js index 2a00ef4a..f1d1e77d 100644 --- a/app/L0/_all/mod/_core/admin/views/agent/config.js +++ b/app/L0/_all/mod/_core/admin/views/agent/config.js @@ -5,9 +5,70 @@ export const ADMIN_CHAT_HISTORY_PATH = "~/hist/admin-chat.json"; export const DEFAULT_ADMIN_CHAT_MAX_TOKENS = 120_000; export const ADMIN_CHAT_LLM_PROVIDER = { API: "api", + BEDROCK: "bedrock", LOCAL: "local" }; +export const ADMIN_CHAT_BEDROCK_CRED_MODE = { + SERVER: "server", + CLIENT_KEY: "client-key" +}; + +export const ADMIN_CHAT_BEDROCK_ROUTE = { + CONVERSE: "converse", + OPENAI: "openai" +}; + +export const BEDROCK_MODEL_PRESETS = [ + { + id: "us.anthropic.claude-sonnet-4-6", + label: "Claude Sonnet 4.6 (fast, recommended)", + route: "converse" + }, + { + id: "us.anthropic.claude-opus-4-7", + label: "Claude Opus 4.7 (smartest)", + route: "converse" + }, + { + id: "us.anthropic.claude-opus-4-6-v1", + label: "Claude Opus 4.6", + route: "converse" + }, + { + id: "us.anthropic.claude-haiku-4-5-20251001-v1:0", + label: "Claude Haiku 4.5 (cheap)", + route: "converse" + }, + { + id: "openai.gpt-oss-20b-1:0", + label: "OpenAI gpt-oss 20B", + route: "openai" + }, + { + id: "openai.gpt-oss-120b-1:0", + label: "OpenAI gpt-oss 120B", + route: "openai" + } +]; + +export function bedrockRouteForModel(modelId = "") { + const normalized = String(modelId || "").trim().toLowerCase(); + if (!normalized) { + return ADMIN_CHAT_BEDROCK_ROUTE.CONVERSE; + } + if (normalized.startsWith("openai.")) { + return ADMIN_CHAT_BEDROCK_ROUTE.OPENAI; + } + return ADMIN_CHAT_BEDROCK_ROUTE.CONVERSE; +} + +export function bedrockApiEndpointForRoute(route = ADMIN_CHAT_BEDROCK_ROUTE.CONVERSE) { + return route === ADMIN_CHAT_BEDROCK_ROUTE.OPENAI + ? "/api/bedrock/openai/v1/chat/completions" + : "/api/bedrock/converse/v1/chat/completions"; +} + export const ADMIN_CHAT_LOCAL_PROVIDER = { HUGGINGFACE: "huggingface" }; @@ -15,6 +76,9 @@ export const ADMIN_CHAT_LOCAL_PROVIDER = { export const DEFAULT_ADMIN_CHAT_SETTINGS = { apiEndpoint: "https://openrouter.ai/api/v1/chat/completions", apiKey: "", + bedrockApiKey: "", + bedrockCredMode: ADMIN_CHAT_BEDROCK_CRED_MODE.SERVER, + bedrockModel: BEDROCK_MODEL_PRESETS[0].id, huggingfaceDtype: "q4", huggingfaceModel: "", localProvider: ADMIN_CHAT_LOCAL_PROVIDER.HUGGINGFACE, @@ -26,9 +90,19 @@ export const DEFAULT_ADMIN_CHAT_SETTINGS = { }; export function normalizeAdminChatLlmProvider(value) { - return value === ADMIN_CHAT_LLM_PROVIDER.LOCAL - ? ADMIN_CHAT_LLM_PROVIDER.LOCAL - : ADMIN_CHAT_LLM_PROVIDER.API; + if (value === ADMIN_CHAT_LLM_PROVIDER.LOCAL) { + return ADMIN_CHAT_LLM_PROVIDER.LOCAL; + } + if (value === ADMIN_CHAT_LLM_PROVIDER.BEDROCK) { + return ADMIN_CHAT_LLM_PROVIDER.BEDROCK; + } + return ADMIN_CHAT_LLM_PROVIDER.API; +} + +export function normalizeAdminChatBedrockCredMode(value) { + return value === ADMIN_CHAT_BEDROCK_CRED_MODE.CLIENT_KEY + ? ADMIN_CHAT_BEDROCK_CRED_MODE.CLIENT_KEY + : ADMIN_CHAT_BEDROCK_CRED_MODE.SERVER; } export function normalizeAdminChatLocalProvider(value) { diff --git a/app/L0/_all/mod/_core/admin/views/agent/panel.html b/app/L0/_all/mod/_core/admin/views/agent/panel.html index bf67faaa..c180da86 100644 --- a/app/L0/_all/mod/_core/admin/views/agent/panel.html +++ b/app/L0/_all/mod/_core/admin/views/agent/panel.html @@ -179,43 +179,7 @@

Custom system instructions

Provider and model configuration

-
- - -
-
- - - -
-
-
- -
-
+