Skip to content

Add Amazon Bedrock provider for the LLM settings (Converse + OpenAI)#14

Open
aws-scottm wants to merge 1 commit intoagent0ai:mainfrom
aws-scottm:public/bedrock
Open

Add Amazon Bedrock provider for the LLM settings (Converse + OpenAI)#14
aws-scottm wants to merge 1 commit intoagent0ai:mainfrom
aws-scottm:public/bedrock

Conversation

@aws-scottm
Copy link
Copy Markdown

Summary

Adds Amazon Bedrock as a first-class LLM provider in the settings UI, alongside the existing generic API and Local options. Works with Claude on Bedrock (via the Converse API) and the OpenAI open-weights models hosted on Bedrock (via the native /openai/v1/ route). The existing browser streaming reader is unchanged — the server re-emits OpenAI-shaped JSON / SSE for both routes.

Zero new runtime dependencies (pure Node built-ins). No existing code path or default behavior changes.


UX changes

A new Bedrock tab appears in both settings dialogs (the admin view and the dashboard's "Set LLM API key" pill open the same tab).

Bedrock tab contents

  • Server credentials — live-populated from GET /api/bedrock/config. Bulleted summary of the server's current auth mode (AWS profile SigV4 vs. Bedrock API key), profile name, region, and which env vars are driving it.
  • Credential mode — radio:
    • Use server credentials → Node signs with the configured AWS profile (SigV4) or server-side Bedrock key. Works for Claude and gpt-oss.
    • Paste a Bedrock API key → key forwarded as Authorization: Bearer. Works for the OpenAI-compatible route only (Anthropic Bedrock requires SigV4).
  • Model — dropdown with six sensible presets: Claude Sonnet 4.6, Opus 4.7, Opus 4.6, Haiku 4.5, OpenAI gpt-oss-20b, gpt-oss-120b.
  • Endpoint is auto-chosen from model family — openai.*/api/bedrock/openai/v1/chat/completions, everything else → /api/bedrock/converse/v1/chat/completions. The user never types a URL.

The API and Local tabs are untouched. Default provider is still API. No existing user's settings are migrated.

House-style note: the new app/L0/_all/mod/_core/llm_settings/panel.html follows the existing huggingface/config-sidebar.html pattern — a single <x-component> with a mode="admin|onscreen" attribute — so the admin and onscreen dialogs render identical UX from one file.


Server — /api/bedrock/**

Three endpoints, all auth-gated via the same ensureAuthenticatedOrRespond as /api/proxy:

Method Path Behavior
GET /api/bedrock/config Returns { 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].

Env-driven config; no new config files.

Variable 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_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN env vars also work. AWS 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 existing browser client talk to Claude on Bedrock with no client-side changes.

Two behavioral notes baked into the adapter:

  • Claude 4.5+ models deprecate temperature and top_p on Converse; the adapter strips both so requests don't 400 out. Older Claude models still get them.
  • The streaming path decodes Bedrock's binary application/vnd.amazon.eventstream framing on the server (new server/lib/bedrock/eventstream.js) and re-emits plain text/event-stream chunks so the existing browser SSE reader works unchanged.

Files changed (17 total, +1461 / −105)

New Bedrock server code (5 files, +892 lines)

  • server/lib/bedrock/sigv4.js (+242) — SigV4 signer with env / shared-ini / credential_process credential resolution.
  • server/lib/bedrock/proxy.js (+164) — request router for /api/bedrock/**.
  • server/lib/bedrock/converse.js (+371) — OpenAI ↔ Converse adapter.
  • server/lib/bedrock/eventstream.js (+115) — AWS eventstream binary decoder.
  • server/router/router.js (+10) — wires the auth gate.

New shared UI component (1 file, +157 lines)

  • app/L0/_all/mod/_core/llm_settings/panel.html (+157) — single settings component used by both dialogs via mode="…" attribute.

Admin agent wiring (4 files)

  • admin/views/agent/config.js (+80) — enum, presets, route resolver.
  • admin/views/agent/store.js (+98) — getters, actions, save-time projection.
  • admin/views/agent/storage.js (+16/−5) — allowMissing opt-in.
  • admin/views/agent/panel.html (+9/−29) — replaced inline block with <x-component>.

Onscreen agent wiring (4 files)

  • onscreen_agent/config.js, store.js, storage.js, panel.html — parallel changes to the above.

allowMissing plumbing (3 files)

  • server/api/file_read.js (+49/−6) — honors allowMissing; returns 200 {exists:false} on missing.
  • app/L0/_all/mod/_core/framework/js/api-client.js (+22) — threads flag through request builders and bypasses the queue batcher when set.
  • dashboard_welcome/dashboard-prefs.js (+5/−1) — opts in.

See the commit message for the per-file breakdown.


Side fix: /api/file_read 404 noise (opt-in)

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(…) and runtime.api.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.

Shares the plumbing with the new GET /api/bedrock/config. Happy to split into a separate PR if you prefer — let me know.


Backward compatibility

  • Provider defaults unchanged (API). The 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 and the file-op callers are additive.
  • Server boots without any SPACE_BEDROCK_* env vars set; /api/bedrock/** is simply unavailable in that case, and every other path behaves as before.
  • 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:

Route Model Stream Result
/api/bedrock/openai/v1/chat/completions openai.gpt-oss-20b-1:0 no 200, usage returned
/api/bedrock/converse/v1/chat/completions us.anthropic.claude-sonnet-4-6 no 200, "pong"
/api/bedrock/converse/v1/chat/completions us.anthropic.claude-opus-4-6-v1 no 200, "pong"
/api/bedrock/converse/v1/chat/completions us.anthropic.claude-opus-4-7 no 200, "pong"
/api/bedrock/converse/v1/chat/completions us.anthropic.claude-sonnet-4-6 yes 4 SSE chunks + data: [DONE], finish_reason: "stop"
/api/bedrock/converse/v1/chat/completions us.anthropic.claude-opus-4-7 + temperature: 0.2 no 200 (adapter strips deprecated temp; previously 400)
/api/file_read {path:"~/conf/dashboard.yaml", allowMissing:true} 200 {exists:false, content:""} (previously 404)
Server boot with no SPACE_BEDROCK_* env vars set boots; existing API / Local tabs unaffected

What I did not verify:

  • Windows / Linux desktop Electron builds. I only tested via node space serve + browser on macOS.
  • Tool-calls and image parts through Converse (adapter is text-only for now — see non-goals).

Non-goals / known limitations

  • Converse adapter is text-only. Tool calls, image parts, and documents aren't translated yet. Happy to follow up in a separate PR.
  • Bedrock's /openai/v1/ route only serves OpenAI open-weights models on Bedrock (gpt-oss-20b, gpt-oss-120b). That's an AWS-side limitation, not a client bug — hence the route split.
  • Model presets are hand-curated. A maintainer-preferred way to resolve available models dynamically (e.g. list-inference-profiles) would be a nice follow-up; any ID can still be typed.

How to try it

AWS_PROFILE=<your-profile> \
SPACE_BEDROCK_MODE=sigv4 \
SPACE_BEDROCK_REGION=us-west-2 \
  node space serve SINGLE_USER_APP=true

Open http://127.0.0.1:3000, open the agent settings, pick the Bedrock tab, choose a Claude model, save, chat.

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
  `<x-component path="/mod/_core/llm_settings/panel.html" mode="…">`.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant