detached 072bd14

Merge branch 'main' into fix/settings-dialog-short-terminal

commit
072bd1406e0dd3a594c33b1ec51c571795fe293c
author
Patric Eckhart · · 2026-09-26 10:30 UTC
committer
GitHub · · 2026-09-26 10:30 UTC
merge of
a6a2017 + ff10c69 · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
9 files · +799 -23
M README.md +25 -1
@@ -544,7 +544,7 @@ Use `/login` to store API keys or subscription credentials. `/model` only shows
544544 `--list-models` or the `/model` picker shows the full catalog across all built-in providers. Three sources:
545545
546546 - **Catalog**: models baked into zot, covering Claude, GPT/Codex, Gemini/Gemma, Kimi/Moonshot, DeepSeek, Groq-hosted Llama/Gemma/Compound, OpenRouter-routed models, Bedrock model ids, Vertex model ids, Azure OpenAI deployments, Copilot models, and other provider-specific catalog entries.
547-- **Live**: IDs discovered from `GET /v1/models` using your stored API key (cached for 6h in `$ZOT_HOME/models-cache.json`, refreshed in the background on startup).
547+- **Live**: IDs discovered from the provider using your stored credentials — usually `GET /v1/models`, or provider-specific listing APIs for Amazon Bedrock (see [Amazon Bedrock](#amazon-bedrock)). Cached for 6h in `$ZOT_HOME/models-cache.json` and refreshed in the background on startup.
548548 - **Speculative**: IDs that appear in the upstream generator but aren't live on the public API yet. They'll 404 today and start working the moment the provider ships them.
549549
550550 The context meter in the status line uses the model's advertised context window to show how much of it your last turn consumed.
@@ -785,6 +785,30 @@ Access tokens are cached in memory and refreshed on demand.
785785
786786 If none of these are available, zot errors with `vertex: no auth — set GOOGLE_CLOUD_API_KEY or GOOGLE_APPLICATION_CREDENTIALS`.
787787
788+### Amazon Bedrock
789+
790+zot talks to Bedrock's Converse streaming API and supports two credential styles:
791+
792+- **Bedrock API key** (bearer token) via `AWS_BEARER_TOKEN_BEDROCK`. This is the simplest setup: a single opaque string, region-scoped to wherever it was minted. It requires only the `bedrock:CallWithBearerToken` permission.
793+- **SigV4** via `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` (plus optional `AWS_SESSION_TOKEN`), an `AWS_PROFILE`, or a default / SSO / assume-role / `credential_process` profile resolved through the AWS CLI. SigV4 (AWS Signature Version 4) is AWS's request-signing scheme: your secret key signs each request locally and only the signature travels over the wire. zot implements it directly so it does not depend on the AWS SDK.
794+
795+If both are present, the bearer token wins for inference, while live model discovery uses SigV4. The region defaults to `AWS_REGION` / `AWS_DEFAULT_REGION`, falling back to `us-east-1`. A `403` on a freshly-copied API key almost always means a region mismatch — set `AWS_REGION` to the region the key was created in, or pass `--base-url https://bedrock-runtime.<region>.amazonaws.com`.
796+
797+#### Catalog vs. live discovery
798+
799+Unlike most providers, Bedrock model **pricing, context windows, and capability flags are hand-maintained** in zot's baked-in catalog. AWS does not expose usable pricing programmatically for current models — its Price List API returns only stale, input-only rates for a few legacy models — so the catalog is authoritative for cost and metadata. This means:
800+
801+- **Bearer-token-only users** still get the full curated Bedrock model list with correct pricing from the catalog. No AWS API listing is attempted.
802+- **Users with SigV4 credentials** additionally get **live model discovery**, even if inference uses a bearer token: on startup, zot lists the foundation models and cross-region inference profiles your account can actually use (via `ListFoundationModels` and `ListInferenceProfiles`) and merges them into the catalog. New Bedrock models therefore appear in `/model` without waiting for a zot release. Discovered IDs already known to the catalog keep their curated pricing; genuinely new IDs show up with placeholder pricing until the catalog is updated.
803+
804+Live discovery requires SigV4 because the listing APIs live on the Bedrock control plane, which rejects the runtime bearer token. It runs in the background at startup and is cached for 6h like other providers, so `/model` reflects the latest list without any manual refresh. If discovery fails (no SigV4 creds, expired SSO login, missing IAM permissions), zot silently falls back to the catalog. Discovery needs `bedrock:ListFoundationModels` and `bedrock:ListInferenceProfiles`.
805+
806+#### Cross-region pricing caveat
807+
808+Bedrock model IDs carry a routing prefix: `global.` (global cross-region inference), a geography such as `us.` / `eu.` / `apac.` / `au.` / `jp.` (geo cross-region inference), or none (in-region). zot's catalog prices **every** variant at the model's base (US) rate.
809+
810+AWS charges a surcharge for **geo cross-region inference** in some regions — for example, Anthropic models routed through the Asia Pacific (Melbourne) geo profile cost about 10% more than the base rate. zot does **not** yet capture these per-region uplifts, so if you use a geo-prefixed profile in a region that charges the surcharge, zot's reported cost may be **lower than your actual AWS bill**. Global cross-region and plain in-region usage match the base rate and are unaffected. The context/usage accounting is still correct; only the dollar figure can under-report in this edge case. If you need exact costs, check the per-region table on the [Amazon Bedrock pricing page](https://aws.amazon.com/bedrock/pricing/).
811+
788812 ### Local models with ollama
789813
790814 zot works with [ollama](https://ollama.com) out of the box. Ollama serves an OpenAI-compatible API locally, so any model you have pulled works with zot.
@@ -544,7 +544,7 @@ Use `/login` to store API keys or subscription credentials. `/model` only shows
544 `--list-models` or the `/model` picker shows the full catalog across all built-in providers. Three sources:544 `--list-models` or the `/model` picker shows the full catalog across all built-in providers. Three sources:
545 545
546 - **Catalog**: models baked into zot, covering Claude, GPT/Codex, Gemini/Gemma, Kimi/Moonshot, DeepSeek, Groq-hosted Llama/Gemma/Compound, OpenRouter-routed models, Bedrock model ids, Vertex model ids, Azure OpenAI deployments, Copilot models, and other provider-specific catalog entries.546 - **Catalog**: models baked into zot, covering Claude, GPT/Codex, Gemini/Gemma, Kimi/Moonshot, DeepSeek, Groq-hosted Llama/Gemma/Compound, OpenRouter-routed models, Bedrock model ids, Vertex model ids, Azure OpenAI deployments, Copilot models, and other provider-specific catalog entries.
547-- **Live**: IDs discovered from `GET /v1/models` using your stored API key (cached for 6h in `$ZOT_HOME/models-cache.json`, refreshed in the background on startup).547+- **Live**: IDs discovered from the provider using your stored credentials — usually `GET /v1/models`, or provider-specific listing APIs for Amazon Bedrock (see [Amazon Bedrock](#amazon-bedrock)). Cached for 6h in `$ZOT_HOME/models-cache.json` and refreshed in the background on startup.
548 - **Speculative**: IDs that appear in the upstream generator but aren't live on the public API yet. They'll 404 today and start working the moment the provider ships them.548 - **Speculative**: IDs that appear in the upstream generator but aren't live on the public API yet. They'll 404 today and start working the moment the provider ships them.
549 549
550 The context meter in the status line uses the model's advertised context window to show how much of it your last turn consumed.550 The context meter in the status line uses the model's advertised context window to show how much of it your last turn consumed.
@@ -785,6 +785,30 @@ Access tokens are cached in memory and refreshed on demand.
785 785
786 If none of these are available, zot errors with `vertex: no auth — set GOOGLE_CLOUD_API_KEY or GOOGLE_APPLICATION_CREDENTIALS`.786 If none of these are available, zot errors with `vertex: no auth — set GOOGLE_CLOUD_API_KEY or GOOGLE_APPLICATION_CREDENTIALS`.
787 787
788+### Amazon Bedrock
789+
790+zot talks to Bedrock's Converse streaming API and supports two credential styles:
791+
792+- **Bedrock API key** (bearer token) via `AWS_BEARER_TOKEN_BEDROCK`. This is the simplest setup: a single opaque string, region-scoped to wherever it was minted. It requires only the `bedrock:CallWithBearerToken` permission.
793+- **SigV4** via `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` (plus optional `AWS_SESSION_TOKEN`), an `AWS_PROFILE`, or a default / SSO / assume-role / `credential_process` profile resolved through the AWS CLI. SigV4 (AWS Signature Version 4) is AWS's request-signing scheme: your secret key signs each request locally and only the signature travels over the wire. zot implements it directly so it does not depend on the AWS SDK.
794+
795+If both are present, the bearer token wins for inference, while live model discovery uses SigV4. The region defaults to `AWS_REGION` / `AWS_DEFAULT_REGION`, falling back to `us-east-1`. A `403` on a freshly-copied API key almost always means a region mismatch — set `AWS_REGION` to the region the key was created in, or pass `--base-url https://bedrock-runtime.<region>.amazonaws.com`.
796+
797+#### Catalog vs. live discovery
798+
799+Unlike most providers, Bedrock model **pricing, context windows, and capability flags are hand-maintained** in zot's baked-in catalog. AWS does not expose usable pricing programmatically for current models — its Price List API returns only stale, input-only rates for a few legacy models — so the catalog is authoritative for cost and metadata. This means:
800+
801+- **Bearer-token-only users** still get the full curated Bedrock model list with correct pricing from the catalog. No AWS API listing is attempted.
802+- **Users with SigV4 credentials** additionally get **live model discovery**, even if inference uses a bearer token: on startup, zot lists the foundation models and cross-region inference profiles your account can actually use (via `ListFoundationModels` and `ListInferenceProfiles`) and merges them into the catalog. New Bedrock models therefore appear in `/model` without waiting for a zot release. Discovered IDs already known to the catalog keep their curated pricing; genuinely new IDs show up with placeholder pricing until the catalog is updated.
803+
804+Live discovery requires SigV4 because the listing APIs live on the Bedrock control plane, which rejects the runtime bearer token. It runs in the background at startup and is cached for 6h like other providers, so `/model` reflects the latest list without any manual refresh. If discovery fails (no SigV4 creds, expired SSO login, missing IAM permissions), zot silently falls back to the catalog. Discovery needs `bedrock:ListFoundationModels` and `bedrock:ListInferenceProfiles`.
805+
806+#### Cross-region pricing caveat
807+
808+Bedrock model IDs carry a routing prefix: `global.` (global cross-region inference), a geography such as `us.` / `eu.` / `apac.` / `au.` / `jp.` (geo cross-region inference), or none (in-region). zot's catalog prices **every** variant at the model's base (US) rate.
809+
810+AWS charges a surcharge for **geo cross-region inference** in some regions — for example, Anthropic models routed through the Asia Pacific (Melbourne) geo profile cost about 10% more than the base rate. zot does **not** yet capture these per-region uplifts, so if you use a geo-prefixed profile in a region that charges the surcharge, zot's reported cost may be **lower than your actual AWS bill**. Global cross-region and plain in-region usage match the base rate and are unaffected. The context/usage accounting is still correct; only the dollar figure can under-report in this edge case. If you need exact costs, check the per-region table on the [Amazon Bedrock pricing page](https://aws.amazon.com/bedrock/pricing/).
811+
788 ### Local models with ollama812 ### Local models with ollama
789 813
790 zot works with [ollama](https://ollama.com) out of the box. Ollama serves an OpenAI-compatible API locally, so any model you have pulled works with zot.814 zot works with [ollama](https://ollama.com) out of the box. Ollama serves an OpenAI-compatible API locally, so any model you have pulled works with zot.
M packages/agent/build.go +6 -0
@@ -596,6 +596,12 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
596596 // models like ollama don't need real API keys).
597597 if resolvedModel.BaseURL != "" && credErr != nil {
598598 cred = "ollama"
599+ if provName == "amazon-bedrock" {
600+ // The default AWS CLI profile is resolved by the Bedrock client,
601+ // not the general credential resolver. Never pass the local-model
602+ // placeholder as a bearer token.
603+ cred = "<aws>"
604+ }
599605 credErr = nil
600606 requireCred = false
601607 }
@@ -596,6 +596,12 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
596 // models like ollama don't need real API keys).596 // models like ollama don't need real API keys).
597 if resolvedModel.BaseURL != "" && credErr != nil {597 if resolvedModel.BaseURL != "" && credErr != nil {
598 cred = "ollama"598 cred = "ollama"
599+ if provName == "amazon-bedrock" {
600+ // The default AWS CLI profile is resolved by the Bedrock client,
601+ // not the general credential resolver. Never pass the local-model
602+ // placeholder as a bearer token.
603+ cred = "<aws>"
604+ }
599 credErr = nil605 credErr = nil
600 requireCred = false606 requireCred = false
601 }607 }
M packages/agent/modelsync.go +6 -0
@@ -255,6 +255,12 @@ func refreshModels() {
255255 all = append(all, live...)
256256 }
257257 }
258+ // Bedrock resolves SigV4 credentials independently of inference auth.
259+ // A default AWS CLI profile can be usable even when zot has no
260+ // Bedrock credential registered with its general resolver.
261+ if live, err := provider.DiscoverBedrock(ctx, ""); err == nil {
262+ all = append(all, live...)
263+ }
258264 if haveOpenRouter {
259265 // /models is public; gate on a credential so the picker only
260266 // fills with OpenRouter's hundreds of routes for users who use it.
@@ -255,6 +255,12 @@ func refreshModels() {
255 all = append(all, live...)255 all = append(all, live...)
256 }256 }
257 }257 }
258+ // Bedrock resolves SigV4 credentials independently of inference auth.
259+ // A default AWS CLI profile can be usable even when zot has no
260+ // Bedrock credential registered with its general resolver.
261+ if live, err := provider.DiscoverBedrock(ctx, ""); err == nil {
262+ all = append(all, live...)
263+ }
258 if haveOpenRouter {264 if haveOpenRouter {
259 // /models is public; gate on a credential so the picker only265 // /models is public; gate on a credential so the picker only
260 // fills with OpenRouter's hundreds of routes for users who use it.266 // fills with OpenRouter's hundreds of routes for users who use it.
M packages/agent/modelsync_test.go +52 -0
@@ -4,16 +4,68 @@ import (
44 "context"
55 "encoding/json"
66 "fmt"
7+ "io"
78 "net/http"
89 "net/http/httptest"
910 "os"
1011 "path/filepath"
12+ "runtime"
13+ "strings"
1114 "testing"
1215 "time"
1316
1417 "github.com/patriceckhart/zot/packages/provider"
1518 )
1619
20+func TestRefreshModelsBedrockWithDefaultCLIProfile(t *testing.T) {
21+ if runtime.GOOS == "windows" {
22+ t.Skip("test uses a shell script to simulate the AWS CLI")
23+ }
24+ t.Setenv("ZOT_HOME", t.TempDir())
25+ t.Setenv("AWS_PROFILE", "")
26+ t.Setenv("AWS_ACCESS_KEY_ID", "")
27+ t.Setenv("AWS_SECRET_ACCESS_KEY", "")
28+ t.Setenv("AWS_BEARER_TOKEN_BEDROCK", "")
29+ for _, name := range []string{"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "KIMI_API_KEY", "GOOGLE_API_KEY", "OPENROUTER_API_KEY", "GONDOLA_API_KEY", "YOLO_AUTO_API_KEY"} {
30+ t.Setenv(name, "")
31+ }
32+ bin := t.TempDir()
33+ script := "#!/bin/sh\nprintf '%s\\n' '{\"AccessKeyId\":\"AKID\",\"SecretAccessKey\":\"SECRET\"}'\n"
34+ if err := os.WriteFile(filepath.Join(bin, "aws"), []byte(script), 0o755); err != nil {
35+ t.Fatal(err)
36+ }
37+ t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
38+ orig := http.DefaultTransport
39+ t.Cleanup(func() {
40+ http.DefaultTransport = orig
41+ provider.SetLiveModels(nil)
42+ })
43+ http.DefaultTransport = yoloAutoTestTransport(func(r *http.Request) (*http.Response, error) {
44+ var body string
45+ switch r.URL.Path {
46+ case "/foundation-models":
47+ body = `{"modelSummaries":[{"modelId":"example.cli-only-model","outputModalities":["TEXT"]}]}`
48+ case "/inference-profiles":
49+ body = `{"inferenceProfileSummaries":[]}`
50+ default:
51+ return nil, fmt.Errorf("unexpected discovery request: %s", r.URL)
52+ }
53+ return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(body)), Header: make(http.Header)}, nil
54+ })
55+
56+ refreshModels()
57+ if _, err := provider.FindModel("amazon-bedrock", "example.cli-only-model"); err != nil {
58+ t.Fatal("default AWS CLI profile was not discovered:", err)
59+ }
60+ resolved, err := Resolve(Args{Provider: "amazon-bedrock", Model: "example.cli-only-model"}, true)
61+ if err != nil {
62+ t.Fatal("default AWS CLI profile could not resolve for inference:", err)
63+ }
64+ if resolved.Credential != "<aws>" {
65+ t.Fatalf("inference credential = %q, want AWS sentinel", resolved.Credential)
66+ }
67+}
68+
1769 func TestRefreshLlamaCPPModelsAddsOnlyLoadedModels(t *testing.T) {
1870 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1971 if r.URL.Path != "/models" {
@@ -4,16 +4,68 @@ import (
4 "context"4 "context"
5 "encoding/json"5 "encoding/json"
6 "fmt"6 "fmt"
7+ "io"
7 "net/http"8 "net/http"
8 "net/http/httptest"9 "net/http/httptest"
9 "os"10 "os"
10 "path/filepath"11 "path/filepath"
12+ "runtime"
13+ "strings"
11 "testing"14 "testing"
12 "time"15 "time"
13 16
14 "github.com/patriceckhart/zot/packages/provider"17 "github.com/patriceckhart/zot/packages/provider"
15 )18 )
16 19
20+func TestRefreshModelsBedrockWithDefaultCLIProfile(t *testing.T) {
21+ if runtime.GOOS == "windows" {
22+ t.Skip("test uses a shell script to simulate the AWS CLI")
23+ }
24+ t.Setenv("ZOT_HOME", t.TempDir())
25+ t.Setenv("AWS_PROFILE", "")
26+ t.Setenv("AWS_ACCESS_KEY_ID", "")
27+ t.Setenv("AWS_SECRET_ACCESS_KEY", "")
28+ t.Setenv("AWS_BEARER_TOKEN_BEDROCK", "")
29+ for _, name := range []string{"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "KIMI_API_KEY", "GOOGLE_API_KEY", "OPENROUTER_API_KEY", "GONDOLA_API_KEY", "YOLO_AUTO_API_KEY"} {
30+ t.Setenv(name, "")
31+ }
32+ bin := t.TempDir()
33+ script := "#!/bin/sh\nprintf '%s\\n' '{\"AccessKeyId\":\"AKID\",\"SecretAccessKey\":\"SECRET\"}'\n"
34+ if err := os.WriteFile(filepath.Join(bin, "aws"), []byte(script), 0o755); err != nil {
35+ t.Fatal(err)
36+ }
37+ t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
38+ orig := http.DefaultTransport
39+ t.Cleanup(func() {
40+ http.DefaultTransport = orig
41+ provider.SetLiveModels(nil)
42+ })
43+ http.DefaultTransport = yoloAutoTestTransport(func(r *http.Request) (*http.Response, error) {
44+ var body string
45+ switch r.URL.Path {
46+ case "/foundation-models":
47+ body = `{"modelSummaries":[{"modelId":"example.cli-only-model","outputModalities":["TEXT"]}]}`
48+ case "/inference-profiles":
49+ body = `{"inferenceProfileSummaries":[]}`
50+ default:
51+ return nil, fmt.Errorf("unexpected discovery request: %s", r.URL)
52+ }
53+ return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(body)), Header: make(http.Header)}, nil
54+ })
55+
56+ refreshModels()
57+ if _, err := provider.FindModel("amazon-bedrock", "example.cli-only-model"); err != nil {
58+ t.Fatal("default AWS CLI profile was not discovered:", err)
59+ }
60+ resolved, err := Resolve(Args{Provider: "amazon-bedrock", Model: "example.cli-only-model"}, true)
61+ if err != nil {
62+ t.Fatal("default AWS CLI profile could not resolve for inference:", err)
63+ }
64+ if resolved.Credential != "<aws>" {
65+ t.Fatalf("inference credential = %q, want AWS sentinel", resolved.Credential)
66+ }
67+}
68+
17 func TestRefreshLlamaCPPModelsAddsOnlyLoadedModels(t *testing.T) {69 func TestRefreshLlamaCPPModelsAddsOnlyLoadedModels(t *testing.T) {
18 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {70 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
19 if r.URL.Path != "/models" {71 if r.URL.Path != "/models" {
M packages/provider/amazon_bedrock.go +97 -19
@@ -50,6 +50,7 @@ import (
5050 "net/http"
5151 "net/url"
5252 "os"
53+ "os/exec"
5354 "sort"
5455 "strings"
5556 "time"
@@ -82,17 +83,13 @@ type bedrockSigV4Creds struct {
8283 // 3. AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY (+ optional
8384 // AWS_SESSION_TOKEN) -> SigV4 route.
8485 // 4. AWS_PROFILE -> read ~/.aws/credentials, take that profile's keys.
86+// 5. AWS_PROFILE -> `aws configure export-credentials` (SSO, assume-role,
87+// credential_process) when the profile has no static keys.
8588 //
8689 // region defaults to us-east-1 unless AWS_REGION / AWS_DEFAULT_REGION is
8790 // set or the baseURL embeds a region.
8891 func NewBedrockClient(apiKey, baseURL string) Client {
89- region := os.Getenv("AWS_REGION")
90- if region == "" {
91- region = os.Getenv("AWS_DEFAULT_REGION")
92- }
93- if region == "" {
94- region = "us-east-1"
95- }
92+ region := bedrockResolveRegion()
9693 if baseURL == "" {
9794 baseURL = "https://bedrock-runtime." + region + ".amazonaws.com"
9895 }
@@ -102,36 +99,117 @@ func NewBedrockClient(apiKey, baseURL string) Client {
10299 http: &http.Client{Timeout: 0},
103100 }
104101
105- // Bearer route.
102+ bearer, sigv4 := resolveBedrockAuth(apiKey)
103+ if bearer != "" {
104+ c.bearerToken = bearer
105+ return c
106+ }
107+ if sigv4 != nil {
108+ c.sigv4 = sigv4
109+ return c
110+ }
111+
112+ return &unimplementedClient{
113+ name: "amazon-bedrock",
114+ hint: "no Bedrock credentials found (set AWS_BEARER_TOKEN_BEDROCK, AWS_ACCESS_KEY_ID+AWS_SECRET_ACCESS_KEY, or AWS_PROFILE)",
115+ }
116+}
117+
118+// bedrockResolveRegion returns the AWS region for Bedrock calls,
119+// honoring AWS_REGION then AWS_DEFAULT_REGION, defaulting to us-east-1.
120+func bedrockResolveRegion() string {
121+ region := os.Getenv("AWS_REGION")
122+ if region == "" {
123+ region = os.Getenv("AWS_DEFAULT_REGION")
124+ }
125+ if region == "" {
126+ region = "us-east-1"
127+ }
128+ return region
129+}
130+
131+// resolveBedrockAuth determines Bedrock credentials from the same
132+// sources NewBedrockClient uses, so discovery and inference share one
133+// resolution path. Exactly one of (bearer, sigv4) is non-empty on
134+// success; both are zero when no credentials are found.
135+//
136+// Resolution order (first match wins): explicit apiKey bearer, then
137+// AWS_BEARER_TOKEN_BEDROCK, then AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY
138+// env vars, then ~/.aws/credentials via AWS_PROFILE.
139+func resolveBedrockAuth(apiKey string) (bearer string, sigv4 *bedrockSigV4Creds) {
106140 token := apiKey
107141 if token == "" || token == "<aws>" {
108142 token = os.Getenv("AWS_BEARER_TOKEN_BEDROCK")
109143 }
110144 if token != "" && token != "<aws>" {
111- c.bearerToken = token
112- return c
145+ return token, nil
113146 }
114147
115- // SigV4 route: env vars first.
148+ return "", resolveBedrockDiscoveryCreds()
149+}
150+
151+// resolveBedrockDiscoveryCreds ignores inference bearer tokens: Bedrock's
152+// control-plane listing endpoints require SigV4 even when inference uses a bearer.
153+func resolveBedrockDiscoveryCreds() *bedrockSigV4Creds {
116154 ak := os.Getenv("AWS_ACCESS_KEY_ID")
117155 sk := os.Getenv("AWS_SECRET_ACCESS_KEY")
118156 st := os.Getenv("AWS_SESSION_TOKEN")
119157 if ak != "" && sk != "" {
120- c.sigv4 = &bedrockSigV4Creds{accessKeyID: ak, secretAccessKey: sk, sessionToken: st}
121- return c
158+ return &bedrockSigV4Creds{accessKeyID: ak, secretAccessKey: sk, sessionToken: st}
122159 }
123160
124- // SigV4 route: ~/.aws/credentials via AWS_PROFILE.
125161 if profile := os.Getenv("AWS_PROFILE"); profile != "" {
126162 if creds, err := readAWSCredentialsFile(profile); err == nil {
127- c.sigv4 = creds
128- return c
163+ return creds
129164 }
130165 }
131166
132- return &unimplementedClient{
133- name: "amazon-bedrock",
134- hint: "no Bedrock credentials found (set AWS_BEARER_TOKEN_BEDROCK, AWS_ACCESS_KEY_ID+AWS_SECRET_ACCESS_KEY, or AWS_PROFILE)",
167+ // Last resort: ask the AWS CLI to resolve credentials for the active
168+ // profile. This handles SSO, assumed roles, credential_process, and
169+ // web-identity setups whose credentials are not stored as static keys
170+ // in ~/.aws/credentials. Best-effort: absent CLI or an expired login
171+ // simply yields no credentials.
172+ if creds := resolveBedrockCredsViaCLI(); creds != nil {
173+ return creds
174+ }
175+
176+ return nil
177+}
178+
179+// resolveBedrockCredsViaCLI shells out to `aws configure
180+// export-credentials --format process`, which returns fully-resolved
181+// credentials for the active profile regardless of how that profile is
182+// configured (static keys, SSO, assume-role, credential_process). The
183+// AWS CLI must be installed and the profile's login (if any) still
184+// valid. Returns nil on any failure so the caller can fall through.
185+func resolveBedrockCredsViaCLI() *bedrockSigV4Creds {
186+ if _, err := exec.LookPath("aws"); err != nil {
187+ return nil
188+ }
189+ ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
190+ defer cancel()
191+ // AWS_PROFILE / AWS_REGION already in the environment are inherited,
192+ // so the CLI resolves the same profile zot was pointed at.
193+ cmd := exec.CommandContext(ctx, "aws", "configure", "export-credentials", "--format", "process")
194+ out, err := cmd.Output()
195+ if err != nil {
196+ return nil
197+ }
198+ var exported struct {
199+ AccessKeyID string `json:"AccessKeyId"`
200+ SecretAccessKey string `json:"SecretAccessKey"`
201+ SessionToken string `json:"SessionToken"`
202+ }
203+ if err := json.Unmarshal(out, &exported); err != nil {
204+ return nil
205+ }
206+ if exported.AccessKeyID == "" || exported.SecretAccessKey == "" {
207+ return nil
208+ }
209+ return &bedrockSigV4Creds{
210+ accessKeyID: exported.AccessKeyID,
211+ secretAccessKey: exported.SecretAccessKey,
212+ sessionToken: exported.SessionToken,
135213 }
136214 }
137215
@@ -50,6 +50,7 @@ import (
50 "net/http"50 "net/http"
51 "net/url"51 "net/url"
52 "os"52 "os"
53+ "os/exec"
53 "sort"54 "sort"
54 "strings"55 "strings"
55 "time"56 "time"
@@ -82,17 +83,13 @@ type bedrockSigV4Creds struct {
82 // 3. AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY (+ optional83 // 3. AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY (+ optional
83 // AWS_SESSION_TOKEN) -> SigV4 route.84 // AWS_SESSION_TOKEN) -> SigV4 route.
84 // 4. AWS_PROFILE -> read ~/.aws/credentials, take that profile's keys.85 // 4. AWS_PROFILE -> read ~/.aws/credentials, take that profile's keys.
86+// 5. AWS_PROFILE -> `aws configure export-credentials` (SSO, assume-role,
87+// credential_process) when the profile has no static keys.
85 //88 //
86 // region defaults to us-east-1 unless AWS_REGION / AWS_DEFAULT_REGION is89 // region defaults to us-east-1 unless AWS_REGION / AWS_DEFAULT_REGION is
87 // set or the baseURL embeds a region.90 // set or the baseURL embeds a region.
88 func NewBedrockClient(apiKey, baseURL string) Client {91 func NewBedrockClient(apiKey, baseURL string) Client {
89- region := os.Getenv("AWS_REGION")92+ region := bedrockResolveRegion()
90- if region == "" {
91- region = os.Getenv("AWS_DEFAULT_REGION")
92- }
93- if region == "" {
94- region = "us-east-1"
95- }
96 if baseURL == "" {93 if baseURL == "" {
97 baseURL = "https://bedrock-runtime." + region + ".amazonaws.com"94 baseURL = "https://bedrock-runtime." + region + ".amazonaws.com"
98 }95 }
@@ -102,36 +99,117 @@ func NewBedrockClient(apiKey, baseURL string) Client {
102 http: &http.Client{Timeout: 0},99 http: &http.Client{Timeout: 0},
103 }100 }
104 101
105- // Bearer route.102+ bearer, sigv4 := resolveBedrockAuth(apiKey)
103+ if bearer != "" {
104+ c.bearerToken = bearer
105+ return c
106+ }
107+ if sigv4 != nil {
108+ c.sigv4 = sigv4
109+ return c
110+ }
111+
112+ return &unimplementedClient{
113+ name: "amazon-bedrock",
114+ hint: "no Bedrock credentials found (set AWS_BEARER_TOKEN_BEDROCK, AWS_ACCESS_KEY_ID+AWS_SECRET_ACCESS_KEY, or AWS_PROFILE)",
115+ }
116+}
117+
118+// bedrockResolveRegion returns the AWS region for Bedrock calls,
119+// honoring AWS_REGION then AWS_DEFAULT_REGION, defaulting to us-east-1.
120+func bedrockResolveRegion() string {
121+ region := os.Getenv("AWS_REGION")
122+ if region == "" {
123+ region = os.Getenv("AWS_DEFAULT_REGION")
124+ }
125+ if region == "" {
126+ region = "us-east-1"
127+ }
128+ return region
129+}
130+
131+// resolveBedrockAuth determines Bedrock credentials from the same
132+// sources NewBedrockClient uses, so discovery and inference share one
133+// resolution path. Exactly one of (bearer, sigv4) is non-empty on
134+// success; both are zero when no credentials are found.
135+//
136+// Resolution order (first match wins): explicit apiKey bearer, then
137+// AWS_BEARER_TOKEN_BEDROCK, then AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY
138+// env vars, then ~/.aws/credentials via AWS_PROFILE.
139+func resolveBedrockAuth(apiKey string) (bearer string, sigv4 *bedrockSigV4Creds) {
106 token := apiKey140 token := apiKey
107 if token == "" || token == "<aws>" {141 if token == "" || token == "<aws>" {
108 token = os.Getenv("AWS_BEARER_TOKEN_BEDROCK")142 token = os.Getenv("AWS_BEARER_TOKEN_BEDROCK")
109 }143 }
110 if token != "" && token != "<aws>" {144 if token != "" && token != "<aws>" {
111- c.bearerToken = token145+ return token, nil
112- return c
113 }146 }
114 147
115- // SigV4 route: env vars first.148+ return "", resolveBedrockDiscoveryCreds()
149+}
150+
151+// resolveBedrockDiscoveryCreds ignores inference bearer tokens: Bedrock's
152+// control-plane listing endpoints require SigV4 even when inference uses a bearer.
153+func resolveBedrockDiscoveryCreds() *bedrockSigV4Creds {
116 ak := os.Getenv("AWS_ACCESS_KEY_ID")154 ak := os.Getenv("AWS_ACCESS_KEY_ID")
117 sk := os.Getenv("AWS_SECRET_ACCESS_KEY")155 sk := os.Getenv("AWS_SECRET_ACCESS_KEY")
118 st := os.Getenv("AWS_SESSION_TOKEN")156 st := os.Getenv("AWS_SESSION_TOKEN")
119 if ak != "" && sk != "" {157 if ak != "" && sk != "" {
120- c.sigv4 = &bedrockSigV4Creds{accessKeyID: ak, secretAccessKey: sk, sessionToken: st}158+ return &bedrockSigV4Creds{accessKeyID: ak, secretAccessKey: sk, sessionToken: st}
121- return c
122 }159 }
123 160
124- // SigV4 route: ~/.aws/credentials via AWS_PROFILE.
125 if profile := os.Getenv("AWS_PROFILE"); profile != "" {161 if profile := os.Getenv("AWS_PROFILE"); profile != "" {
126 if creds, err := readAWSCredentialsFile(profile); err == nil {162 if creds, err := readAWSCredentialsFile(profile); err == nil {
127- c.sigv4 = creds163+ return creds
128- return c
129 }164 }
130 }165 }
131 166
132- return &unimplementedClient{167+ // Last resort: ask the AWS CLI to resolve credentials for the active
133- name: "amazon-bedrock",168+ // profile. This handles SSO, assumed roles, credential_process, and
134- hint: "no Bedrock credentials found (set AWS_BEARER_TOKEN_BEDROCK, AWS_ACCESS_KEY_ID+AWS_SECRET_ACCESS_KEY, or AWS_PROFILE)",169+ // web-identity setups whose credentials are not stored as static keys
170+ // in ~/.aws/credentials. Best-effort: absent CLI or an expired login
171+ // simply yields no credentials.
172+ if creds := resolveBedrockCredsViaCLI(); creds != nil {
173+ return creds
174+ }
175+
176+ return nil
177+}
178+
179+// resolveBedrockCredsViaCLI shells out to `aws configure
180+// export-credentials --format process`, which returns fully-resolved
181+// credentials for the active profile regardless of how that profile is
182+// configured (static keys, SSO, assume-role, credential_process). The
183+// AWS CLI must be installed and the profile's login (if any) still
184+// valid. Returns nil on any failure so the caller can fall through.
185+func resolveBedrockCredsViaCLI() *bedrockSigV4Creds {
186+ if _, err := exec.LookPath("aws"); err != nil {
187+ return nil
188+ }
189+ ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
190+ defer cancel()
191+ // AWS_PROFILE / AWS_REGION already in the environment are inherited,
192+ // so the CLI resolves the same profile zot was pointed at.
193+ cmd := exec.CommandContext(ctx, "aws", "configure", "export-credentials", "--format", "process")
194+ out, err := cmd.Output()
195+ if err != nil {
196+ return nil
197+ }
198+ var exported struct {
199+ AccessKeyID string `json:"AccessKeyId"`
200+ SecretAccessKey string `json:"SecretAccessKey"`
201+ SessionToken string `json:"SessionToken"`
202+ }
203+ if err := json.Unmarshal(out, &exported); err != nil {
204+ return nil
205+ }
206+ if exported.AccessKeyID == "" || exported.SecretAccessKey == "" {
207+ return nil
208+ }
209+ return &bedrockSigV4Creds{
210+ accessKeyID: exported.AccessKeyID,
211+ secretAccessKey: exported.SecretAccessKey,
212+ sessionToken: exported.SessionToken,
135 }213 }
136 }214 }
137 215
M packages/provider/catalog_builtin.go +9 -3
@@ -34,14 +34,16 @@ var builtinCatalog = []Model{
3434 {Provider: "amazon-bedrock", ID: "anthropic.claude-opus-4-6-v1", DisplayName: "Claude Opus 4.6", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
3535 {Provider: "amazon-bedrock", ID: "anthropic.claude-opus-4-7", DisplayName: "Claude Opus 4.7", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
3636 {Provider: "amazon-bedrock", ID: "anthropic.claude-opus-4-8", DisplayName: "Claude Opus 4.8", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
37+ {Provider: "amazon-bedrock", ID: "anthropic.claude-opus-5-5", DisplayName: "Claude Opus 5.5", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 4, PriceOutput: 20, PriceCacheRead: 0.2, PriceCacheWrite: 5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
3738 {Provider: "amazon-bedrock", ID: "anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
3839 {Provider: "amazon-bedrock", ID: "anthropic.claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
3940 {Provider: "amazon-bedrock", ID: "anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
4041 {Provider: "amazon-bedrock", ID: "au.anthropic.claude-haiku-4-5-20251001-v1:0", DisplayName: "Claude Haiku 4.5 (AU)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 1, PriceOutput: 5, PriceCacheRead: 0.1, PriceCacheWrite: 1.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
41- {Provider: "amazon-bedrock", ID: "au.anthropic.claude-opus-4-6-v1", DisplayName: "AU Anthropic Claude Opus 4.6", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 16.5, PriceOutput: 82.5, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
42- {Provider: "amazon-bedrock", ID: "au.anthropic.claude-opus-4-8", DisplayName: "AU Anthropic Claude Opus 4.8", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 16.5, PriceOutput: 82.5, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
42+ {Provider: "amazon-bedrock", ID: "au.anthropic.claude-opus-4-6-v1", DisplayName: "AU Anthropic Claude Opus 4.6", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
43+ {Provider: "amazon-bedrock", ID: "au.anthropic.claude-opus-4-8", DisplayName: "AU Anthropic Claude Opus 4.8", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
44+ {Provider: "amazon-bedrock", ID: "au.anthropic.claude-opus-5-5", DisplayName: "Claude Opus 5.5 (AU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 4, PriceOutput: 20, PriceCacheRead: 0.2, PriceCacheWrite: 5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
4345 {Provider: "amazon-bedrock", ID: "au.anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5 (AU)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
44- {Provider: "amazon-bedrock", ID: "au.anthropic.claude-sonnet-4-6", DisplayName: "AU Anthropic Claude Sonnet 4.6", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 3.3, PriceOutput: 16.5, PriceCacheRead: 0.33, PriceCacheWrite: 4.125, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
46+ {Provider: "amazon-bedrock", ID: "au.anthropic.claude-sonnet-4-6", DisplayName: "AU Anthropic Claude Sonnet 4.6", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
4547 {Provider: "amazon-bedrock", ID: "au.anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5 (AU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
4648 {Provider: "amazon-bedrock", ID: "deepseek.r1-v1:0", DisplayName: "DeepSeek-R1", ContextWindow: 128000, MaxOutput: 32768, Reasoning: true, PriceInput: 1.35, PriceOutput: 5.4, PriceCacheRead: 0, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
4749 {Provider: "amazon-bedrock", ID: "deepseek.v3-v1:0", DisplayName: "DeepSeek-V3.1", ContextWindow: 163840, MaxOutput: 81920, Reasoning: true, PriceInput: 0.58, PriceOutput: 1.68, PriceCacheRead: 0, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
@@ -52,6 +54,7 @@ var builtinCatalog = []Model{
5254 {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-opus-4-6-v1", DisplayName: "Claude Opus 4.6 (EU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"},
5355 {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-opus-4-7", DisplayName: "Claude Opus 4.7 (EU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"},
5456 {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-opus-4-8", DisplayName: "Claude Opus 4.8 (EU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"},
57+ {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-opus-5-5", DisplayName: "Claude Opus 5.5 (EU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 4, PriceOutput: 20, PriceCacheRead: 0.2, PriceCacheWrite: 5, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"},
5558 {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5 (EU)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"},
5659 {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6 (EU)", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"},
5760 {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5 (EU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"},
@@ -62,6 +65,7 @@ var builtinCatalog = []Model{
6265 {Provider: "amazon-bedrock", ID: "global.anthropic.claude-opus-4-7", DisplayName: "Claude Opus 4.7 (Global)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
6366 {Provider: "amazon-bedrock", ID: "global.anthropic.claude-opus-4-8", DisplayName: "Claude Opus 4.8 (Global)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
6467 {Provider: "amazon-bedrock", ID: "global.anthropic.claude-opus-5", DisplayName: "Claude Opus 5 (Global)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
68+ {Provider: "amazon-bedrock", ID: "global.anthropic.claude-opus-5-5", DisplayName: "Claude Opus 5.5 (Global)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 4, PriceOutput: 20, PriceCacheRead: 0.2, PriceCacheWrite: 5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
6569 {Provider: "amazon-bedrock", ID: "global.anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5 (Global)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
6670 {Provider: "amazon-bedrock", ID: "global.anthropic.claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6 (Global)", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
6771 {Provider: "amazon-bedrock", ID: "global.anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5 (Global)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
@@ -569,6 +573,7 @@ var builtinCatalog = []Model{
569573 {Provider: "amazon-bedrock", ID: "google.gemma-3-4b-it", DisplayName: "Gemma 3 4B IT", ContextWindow: 128000, MaxOutput: 4096, Reasoning: false, PriceInput: 0.04, PriceOutput: 0.08, PriceCacheRead: 0, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
570574 {Provider: "amazon-bedrock", ID: "jp.anthropic.claude-opus-4-7", DisplayName: "Claude Opus 4.7 (JP)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
571575 {Provider: "amazon-bedrock", ID: "jp.anthropic.claude-opus-4-8", DisplayName: "Claude Opus 4.8 (JP)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
576+ {Provider: "amazon-bedrock", ID: "jp.anthropic.claude-opus-5-5", DisplayName: "Claude Opus 5.5 (JP)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 4, PriceOutput: 20, PriceCacheRead: 0.2, PriceCacheWrite: 5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
572577 {Provider: "amazon-bedrock", ID: "jp.anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5 (JP)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
573578 {Provider: "amazon-bedrock", ID: "jp.anthropic.claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6 (JP)", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
574579 {Provider: "amazon-bedrock", ID: "jp.anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5 (JP)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
@@ -613,6 +618,7 @@ var builtinCatalog = []Model{
613618 {Provider: "amazon-bedrock", ID: "us.anthropic.claude-opus-4-6-v1", DisplayName: "Claude Opus 4.6 (US)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
614619 {Provider: "amazon-bedrock", ID: "us.anthropic.claude-opus-4-7", DisplayName: "Claude Opus 4.7 (US)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
615620 {Provider: "amazon-bedrock", ID: "us.anthropic.claude-opus-4-8", DisplayName: "Claude Opus 4.8 (US)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
621+ {Provider: "amazon-bedrock", ID: "us.anthropic.claude-opus-5-5", DisplayName: "Claude Opus 5.5 (US)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 4, PriceOutput: 20, PriceCacheRead: 0.2, PriceCacheWrite: 5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
616622 {Provider: "amazon-bedrock", ID: "us.anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5 (US)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
617623 {Provider: "amazon-bedrock", ID: "us.anthropic.claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6 (US)", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
618624 {Provider: "amazon-bedrock", ID: "us.anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5 (US)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
@@ -34,14 +34,16 @@ var builtinCatalog = []Model{
34 {Provider: "amazon-bedrock", ID: "anthropic.claude-opus-4-6-v1", DisplayName: "Claude Opus 4.6", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},34 {Provider: "amazon-bedrock", ID: "anthropic.claude-opus-4-6-v1", DisplayName: "Claude Opus 4.6", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
35 {Provider: "amazon-bedrock", ID: "anthropic.claude-opus-4-7", DisplayName: "Claude Opus 4.7", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},35 {Provider: "amazon-bedrock", ID: "anthropic.claude-opus-4-7", DisplayName: "Claude Opus 4.7", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
36 {Provider: "amazon-bedrock", ID: "anthropic.claude-opus-4-8", DisplayName: "Claude Opus 4.8", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},36 {Provider: "amazon-bedrock", ID: "anthropic.claude-opus-4-8", DisplayName: "Claude Opus 4.8", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
37+ {Provider: "amazon-bedrock", ID: "anthropic.claude-opus-5-5", DisplayName: "Claude Opus 5.5", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 4, PriceOutput: 20, PriceCacheRead: 0.2, PriceCacheWrite: 5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
37 {Provider: "amazon-bedrock", ID: "anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},38 {Provider: "amazon-bedrock", ID: "anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
38 {Provider: "amazon-bedrock", ID: "anthropic.claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},39 {Provider: "amazon-bedrock", ID: "anthropic.claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
39 {Provider: "amazon-bedrock", ID: "anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},40 {Provider: "amazon-bedrock", ID: "anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
40 {Provider: "amazon-bedrock", ID: "au.anthropic.claude-haiku-4-5-20251001-v1:0", DisplayName: "Claude Haiku 4.5 (AU)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 1, PriceOutput: 5, PriceCacheRead: 0.1, PriceCacheWrite: 1.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},41 {Provider: "amazon-bedrock", ID: "au.anthropic.claude-haiku-4-5-20251001-v1:0", DisplayName: "Claude Haiku 4.5 (AU)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 1, PriceOutput: 5, PriceCacheRead: 0.1, PriceCacheWrite: 1.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
41- {Provider: "amazon-bedrock", ID: "au.anthropic.claude-opus-4-6-v1", DisplayName: "AU Anthropic Claude Opus 4.6", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 16.5, PriceOutput: 82.5, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},42+ {Provider: "amazon-bedrock", ID: "au.anthropic.claude-opus-4-6-v1", DisplayName: "AU Anthropic Claude Opus 4.6", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
42- {Provider: "amazon-bedrock", ID: "au.anthropic.claude-opus-4-8", DisplayName: "AU Anthropic Claude Opus 4.8", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 16.5, PriceOutput: 82.5, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},43+ {Provider: "amazon-bedrock", ID: "au.anthropic.claude-opus-4-8", DisplayName: "AU Anthropic Claude Opus 4.8", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
44+ {Provider: "amazon-bedrock", ID: "au.anthropic.claude-opus-5-5", DisplayName: "Claude Opus 5.5 (AU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 4, PriceOutput: 20, PriceCacheRead: 0.2, PriceCacheWrite: 5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
43 {Provider: "amazon-bedrock", ID: "au.anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5 (AU)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},45 {Provider: "amazon-bedrock", ID: "au.anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5 (AU)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
44- {Provider: "amazon-bedrock", ID: "au.anthropic.claude-sonnet-4-6", DisplayName: "AU Anthropic Claude Sonnet 4.6", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 3.3, PriceOutput: 16.5, PriceCacheRead: 0.33, PriceCacheWrite: 4.125, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},46+ {Provider: "amazon-bedrock", ID: "au.anthropic.claude-sonnet-4-6", DisplayName: "AU Anthropic Claude Sonnet 4.6", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
45 {Provider: "amazon-bedrock", ID: "au.anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5 (AU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},47 {Provider: "amazon-bedrock", ID: "au.anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5 (AU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
46 {Provider: "amazon-bedrock", ID: "deepseek.r1-v1:0", DisplayName: "DeepSeek-R1", ContextWindow: 128000, MaxOutput: 32768, Reasoning: true, PriceInput: 1.35, PriceOutput: 5.4, PriceCacheRead: 0, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},48 {Provider: "amazon-bedrock", ID: "deepseek.r1-v1:0", DisplayName: "DeepSeek-R1", ContextWindow: 128000, MaxOutput: 32768, Reasoning: true, PriceInput: 1.35, PriceOutput: 5.4, PriceCacheRead: 0, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
47 {Provider: "amazon-bedrock", ID: "deepseek.v3-v1:0", DisplayName: "DeepSeek-V3.1", ContextWindow: 163840, MaxOutput: 81920, Reasoning: true, PriceInput: 0.58, PriceOutput: 1.68, PriceCacheRead: 0, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},49 {Provider: "amazon-bedrock", ID: "deepseek.v3-v1:0", DisplayName: "DeepSeek-V3.1", ContextWindow: 163840, MaxOutput: 81920, Reasoning: true, PriceInput: 0.58, PriceOutput: 1.68, PriceCacheRead: 0, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
@@ -52,6 +54,7 @@ var builtinCatalog = []Model{
52 {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-opus-4-6-v1", DisplayName: "Claude Opus 4.6 (EU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"},54 {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-opus-4-6-v1", DisplayName: "Claude Opus 4.6 (EU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"},
53 {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-opus-4-7", DisplayName: "Claude Opus 4.7 (EU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"},55 {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-opus-4-7", DisplayName: "Claude Opus 4.7 (EU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"},
54 {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-opus-4-8", DisplayName: "Claude Opus 4.8 (EU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"},56 {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-opus-4-8", DisplayName: "Claude Opus 4.8 (EU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"},
57+ {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-opus-5-5", DisplayName: "Claude Opus 5.5 (EU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 4, PriceOutput: 20, PriceCacheRead: 0.2, PriceCacheWrite: 5, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"},
55 {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5 (EU)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"},58 {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5 (EU)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"},
56 {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6 (EU)", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"},59 {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6 (EU)", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"},
57 {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5 (EU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"},60 {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5 (EU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"},
@@ -62,6 +65,7 @@ var builtinCatalog = []Model{
62 {Provider: "amazon-bedrock", ID: "global.anthropic.claude-opus-4-7", DisplayName: "Claude Opus 4.7 (Global)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},65 {Provider: "amazon-bedrock", ID: "global.anthropic.claude-opus-4-7", DisplayName: "Claude Opus 4.7 (Global)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
63 {Provider: "amazon-bedrock", ID: "global.anthropic.claude-opus-4-8", DisplayName: "Claude Opus 4.8 (Global)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},66 {Provider: "amazon-bedrock", ID: "global.anthropic.claude-opus-4-8", DisplayName: "Claude Opus 4.8 (Global)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
64 {Provider: "amazon-bedrock", ID: "global.anthropic.claude-opus-5", DisplayName: "Claude Opus 5 (Global)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},67 {Provider: "amazon-bedrock", ID: "global.anthropic.claude-opus-5", DisplayName: "Claude Opus 5 (Global)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
68+ {Provider: "amazon-bedrock", ID: "global.anthropic.claude-opus-5-5", DisplayName: "Claude Opus 5.5 (Global)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 4, PriceOutput: 20, PriceCacheRead: 0.2, PriceCacheWrite: 5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
65 {Provider: "amazon-bedrock", ID: "global.anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5 (Global)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},69 {Provider: "amazon-bedrock", ID: "global.anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5 (Global)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
66 {Provider: "amazon-bedrock", ID: "global.anthropic.claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6 (Global)", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},70 {Provider: "amazon-bedrock", ID: "global.anthropic.claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6 (Global)", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
67 {Provider: "amazon-bedrock", ID: "global.anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5 (Global)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},71 {Provider: "amazon-bedrock", ID: "global.anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5 (Global)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
@@ -569,6 +573,7 @@ var builtinCatalog = []Model{
569 {Provider: "amazon-bedrock", ID: "google.gemma-3-4b-it", DisplayName: "Gemma 3 4B IT", ContextWindow: 128000, MaxOutput: 4096, Reasoning: false, PriceInput: 0.04, PriceOutput: 0.08, PriceCacheRead: 0, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},573 {Provider: "amazon-bedrock", ID: "google.gemma-3-4b-it", DisplayName: "Gemma 3 4B IT", ContextWindow: 128000, MaxOutput: 4096, Reasoning: false, PriceInput: 0.04, PriceOutput: 0.08, PriceCacheRead: 0, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
570 {Provider: "amazon-bedrock", ID: "jp.anthropic.claude-opus-4-7", DisplayName: "Claude Opus 4.7 (JP)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},574 {Provider: "amazon-bedrock", ID: "jp.anthropic.claude-opus-4-7", DisplayName: "Claude Opus 4.7 (JP)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
571 {Provider: "amazon-bedrock", ID: "jp.anthropic.claude-opus-4-8", DisplayName: "Claude Opus 4.8 (JP)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},575 {Provider: "amazon-bedrock", ID: "jp.anthropic.claude-opus-4-8", DisplayName: "Claude Opus 4.8 (JP)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
576+ {Provider: "amazon-bedrock", ID: "jp.anthropic.claude-opus-5-5", DisplayName: "Claude Opus 5.5 (JP)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 4, PriceOutput: 20, PriceCacheRead: 0.2, PriceCacheWrite: 5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
572 {Provider: "amazon-bedrock", ID: "jp.anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5 (JP)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},577 {Provider: "amazon-bedrock", ID: "jp.anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5 (JP)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
573 {Provider: "amazon-bedrock", ID: "jp.anthropic.claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6 (JP)", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},578 {Provider: "amazon-bedrock", ID: "jp.anthropic.claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6 (JP)", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
574 {Provider: "amazon-bedrock", ID: "jp.anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5 (JP)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},579 {Provider: "amazon-bedrock", ID: "jp.anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5 (JP)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
@@ -613,6 +618,7 @@ var builtinCatalog = []Model{
613 {Provider: "amazon-bedrock", ID: "us.anthropic.claude-opus-4-6-v1", DisplayName: "Claude Opus 4.6 (US)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},618 {Provider: "amazon-bedrock", ID: "us.anthropic.claude-opus-4-6-v1", DisplayName: "Claude Opus 4.6 (US)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
614 {Provider: "amazon-bedrock", ID: "us.anthropic.claude-opus-4-7", DisplayName: "Claude Opus 4.7 (US)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},619 {Provider: "amazon-bedrock", ID: "us.anthropic.claude-opus-4-7", DisplayName: "Claude Opus 4.7 (US)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
615 {Provider: "amazon-bedrock", ID: "us.anthropic.claude-opus-4-8", DisplayName: "Claude Opus 4.8 (US)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},620 {Provider: "amazon-bedrock", ID: "us.anthropic.claude-opus-4-8", DisplayName: "Claude Opus 4.8 (US)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
621+ {Provider: "amazon-bedrock", ID: "us.anthropic.claude-opus-5-5", DisplayName: "Claude Opus 5.5 (US)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 4, PriceOutput: 20, PriceCacheRead: 0.2, PriceCacheWrite: 5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
616 {Provider: "amazon-bedrock", ID: "us.anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5 (US)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},622 {Provider: "amazon-bedrock", ID: "us.anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5 (US)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
617 {Provider: "amazon-bedrock", ID: "us.anthropic.claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6 (US)", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},623 {Provider: "amazon-bedrock", ID: "us.anthropic.claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6 (US)", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
618 {Provider: "amazon-bedrock", ID: "us.anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5 (US)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},624 {Provider: "amazon-bedrock", ID: "us.anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5 (US)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"},
A packages/provider/discover_bedrock.go +203 -0
@@ -0,0 +1,203 @@
1+package provider
2+
3+// Amazon Bedrock live model discovery.
4+//
5+// Enumerates the foundation models and cross-region inference profiles
6+// available to the caller's AWS credentials, using the Bedrock control
7+// plane (bedrock.{region}.amazonaws.com), SigV4-signed with the same
8+// signer as the runtime client (packages/provider/amazon_bedrock.go):
9+//
10+// - ListFoundationModels: the base foundation-model IDs.
11+// - ListInferenceProfiles: the us./eu./global./... prefixed IDs zot
12+// actually invokes for on-demand throughput.
13+//
14+// IMPORTANT auth note: the runtime bearer token (AWS_BEARER_TOKEN_BEDROCK,
15+// scoped to bedrock:CallWithBearerToken) cannot call these control-plane
16+// APIs. Discovery therefore requires SigV4 credentials (env keys, an
17+// AWS_PROFILE, or CLI-resolved SSO/assume-role creds). When only a
18+// bearer token is present, discovery is skipped and the built-in catalog
19+// remains the source of truth.
20+//
21+// Pricing is intentionally NOT fetched. The AWS Price List API
22+// (GetProducts, ServiceCode=AmazonBedrock) was evaluated against live
23+// data and found to carry only stale, input-only rows for a handful of
24+// legacy models (Claude 2.x / 3 Sonnet / 3 Haiku) with no output or
25+// cache prices for any current model. See aws-cli issue #9567. The
26+// hand-maintained catalog in catalog_builtin.go stays authoritative for
27+// prices, context windows, and capability flags; MergeCatalog keeps that
28+// metadata when a discovered ID matches a catalog entry, and unknown
29+// discovered IDs surface in the picker with placeholder prices.
30+
31+import (
32+ "bytes"
33+ "context"
34+ "encoding/json"
35+ "fmt"
36+ "io"
37+ "net/http"
38+ "net/url"
39+ "strings"
40+ "time"
41+)
42+
43+// bytesReader returns a reader for payload, or nil when payload is nil
44+// so GET requests carry no body.
45+func bytesReader(payload []byte) io.Reader {
46+ if payload == nil {
47+ return nil
48+ }
49+ return bytes.NewReader(payload)
50+}
51+
52+// DiscoverBedrock lists the foundation models and inference profiles
53+// available to the caller's AWS credentials in the given region.
54+//
55+// SigV4 credentials are resolved from the standard AWS environment /
56+// profile / CLI sources, even when a bearer token is used for inference.
57+//
58+// region defaults to AWS_REGION / AWS_DEFAULT_REGION / us-east-1 when
59+// empty. A nil error with a nil slice means "no SigV4 credentials" or
60+// "no models"; callers should treat that as a skip.
61+func DiscoverBedrock(ctx context.Context, region string) ([]Model, error) {
62+ if region == "" {
63+ region = bedrockResolveRegion()
64+ }
65+ sigv4 := resolveBedrockDiscoveryCreds()
66+ if sigv4 == nil {
67+ // Bearer-only or unauthenticated: control-plane APIs are
68+ // unreachable. Skip quietly so the catalog stands.
69+ return nil, nil
70+ }
71+
72+ client := &http.Client{Timeout: 15 * time.Second}
73+ ids, err := bedrockListModelIDs(ctx, client, sigv4, region, "https://bedrock."+region+".amazonaws.com")
74+ if err != nil {
75+ return nil, err
76+ }
77+ if len(ids) == 0 {
78+ return nil, nil
79+ }
80+
81+ out := make([]Model, 0, len(ids))
82+ for _, id := range ids {
83+ out = append(out, Model{
84+ Provider: "amazon-bedrock",
85+ ID: id,
86+ Source: "live",
87+ BaseURL: "https://bedrock-runtime." + region + ".amazonaws.com",
88+ })
89+ }
90+ return out, nil
91+}
92+
93+// bedrockListModelIDs enumerates foundation-model IDs and inference-
94+// profile IDs available in the region. Profile IDs are the geo-prefixed
95+// forms zot invokes; both are returned so the picker shows every
96+// selectable variant. controlPlaneBase is the scheme+host (no trailing
97+// slash) of the Bedrock control plane, parameterized for testing.
98+func bedrockListModelIDs(ctx context.Context, client *http.Client, creds *bedrockSigV4Creds, region, controlPlaneBase string) ([]string, error) {
99+ seen := map[string]bool{}
100+ var ids []string
101+ add := func(id string) {
102+ id = strings.TrimSpace(id)
103+ if id == "" || seen[id] {
104+ return
105+ }
106+ seen[id] = true
107+ ids = append(ids, id)
108+ }
109+
110+ // ListFoundationModels: GET, no pagination token in the response.
111+ fmURL := controlPlaneBase + "/foundation-models"
112+ body, err := bedrockSignedControlPlane(ctx, client, creds, region, http.MethodGet, fmURL, nil)
113+ if err != nil {
114+ return nil, err
115+ }
116+ var fm struct {
117+ ModelSummaries []struct {
118+ ModelID string `json:"modelId"`
119+ OutputModalities []string `json:"outputModalities"`
120+ } `json:"modelSummaries"`
121+ }
122+ if err := json.Unmarshal(body, &fm); err != nil {
123+ return nil, fmt.Errorf("bedrock discover: parse foundation-models: %w", err)
124+ }
125+ for _, s := range fm.ModelSummaries {
126+ // Keep text-output models only; skip embeddings/image generators.
127+ if !bedrockEmitsText(s.OutputModalities) {
128+ continue
129+ }
130+ add(s.ModelID)
131+ }
132+
133+ // ListInferenceProfiles: GET, paginated via ?nextToken=.
134+ next := ""
135+ for {
136+ query := url.Values{"maxResults": {"1000"}}
137+ if next != "" {
138+ query.Set("nextToken", next)
139+ }
140+ ipURL := controlPlaneBase + "/inference-profiles?" + query.Encode()
141+ ipBody, err := bedrockSignedControlPlane(ctx, client, creds, region, http.MethodGet, ipURL, nil)
142+ if err != nil {
143+ // Non-fatal: foundation models already gathered.
144+ break
145+ }
146+ var ip struct {
147+ InferenceProfileSummaries []struct {
148+ InferenceProfileID string `json:"inferenceProfileId"`
149+ } `json:"inferenceProfileSummaries"`
150+ NextToken string `json:"nextToken"`
151+ }
152+ if err := json.Unmarshal(ipBody, &ip); err != nil {
153+ break
154+ }
155+ for _, s := range ip.InferenceProfileSummaries {
156+ add(s.InferenceProfileID)
157+ }
158+ if ip.NextToken == "" {
159+ break
160+ }
161+ next = ip.NextToken
162+ }
163+
164+ return ids, nil
165+}
166+
167+func bedrockEmitsText(modalities []string) bool {
168+ if len(modalities) == 0 {
169+ return true // Unknown: assume usable rather than hide it.
170+ }
171+ for _, m := range modalities {
172+ if strings.EqualFold(m, "TEXT") {
173+ return true
174+ }
175+ }
176+ return false
177+}
178+
179+// bedrockSignedControlPlane issues a SigV4-signed request to a Bedrock
180+// control-plane endpoint and returns the response body. Non-2xx is an
181+// error carrying the status and trimmed body.
182+func bedrockSignedControlPlane(ctx context.Context, client *http.Client, creds *bedrockSigV4Creds, region, method, url string, payload []byte) ([]byte, error) {
183+ req, err := http.NewRequestWithContext(ctx, method, url, bytesReader(payload))
184+ if err != nil {
185+ return nil, err
186+ }
187+ if payload != nil {
188+ req.Header.Set("content-type", "application/json")
189+ }
190+ if err := signSigV4(req, payload, "bedrock", region, creds, time.Now().UTC()); err != nil {
191+ return nil, fmt.Errorf("bedrock discover: sign: %w", err)
192+ }
193+ resp, err := client.Do(req)
194+ if err != nil {
195+ return nil, fmt.Errorf("bedrock discover: %w", err)
196+ }
197+ defer resp.Body.Close()
198+ b, _ := io.ReadAll(io.LimitReader(resp.Body, 16<<20))
199+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
200+ return nil, fmt.Errorf("bedrock discover: http %d: %s", resp.StatusCode, strings.TrimSpace(string(b)))
201+ }
202+ return b, nil
203+}
@@ -0,0 +1,203 @@
1+package provider
2+
3+// Amazon Bedrock live model discovery.
4+//
5+// Enumerates the foundation models and cross-region inference profiles
6+// available to the caller's AWS credentials, using the Bedrock control
7+// plane (bedrock.{region}.amazonaws.com), SigV4-signed with the same
8+// signer as the runtime client (packages/provider/amazon_bedrock.go):
9+//
10+// - ListFoundationModels: the base foundation-model IDs.
11+// - ListInferenceProfiles: the us./eu./global./... prefixed IDs zot
12+// actually invokes for on-demand throughput.
13+//
14+// IMPORTANT auth note: the runtime bearer token (AWS_BEARER_TOKEN_BEDROCK,
15+// scoped to bedrock:CallWithBearerToken) cannot call these control-plane
16+// APIs. Discovery therefore requires SigV4 credentials (env keys, an
17+// AWS_PROFILE, or CLI-resolved SSO/assume-role creds). When only a
18+// bearer token is present, discovery is skipped and the built-in catalog
19+// remains the source of truth.
20+//
21+// Pricing is intentionally NOT fetched. The AWS Price List API
22+// (GetProducts, ServiceCode=AmazonBedrock) was evaluated against live
23+// data and found to carry only stale, input-only rows for a handful of
24+// legacy models (Claude 2.x / 3 Sonnet / 3 Haiku) with no output or
25+// cache prices for any current model. See aws-cli issue #9567. The
26+// hand-maintained catalog in catalog_builtin.go stays authoritative for
27+// prices, context windows, and capability flags; MergeCatalog keeps that
28+// metadata when a discovered ID matches a catalog entry, and unknown
29+// discovered IDs surface in the picker with placeholder prices.
30+
31+import (
32+ "bytes"
33+ "context"
34+ "encoding/json"
35+ "fmt"
36+ "io"
37+ "net/http"
38+ "net/url"
39+ "strings"
40+ "time"
41+)
42+
43+// bytesReader returns a reader for payload, or nil when payload is nil
44+// so GET requests carry no body.
45+func bytesReader(payload []byte) io.Reader {
46+ if payload == nil {
47+ return nil
48+ }
49+ return bytes.NewReader(payload)
50+}
51+
52+// DiscoverBedrock lists the foundation models and inference profiles
53+// available to the caller's AWS credentials in the given region.
54+//
55+// SigV4 credentials are resolved from the standard AWS environment /
56+// profile / CLI sources, even when a bearer token is used for inference.
57+//
58+// region defaults to AWS_REGION / AWS_DEFAULT_REGION / us-east-1 when
59+// empty. A nil error with a nil slice means "no SigV4 credentials" or
60+// "no models"; callers should treat that as a skip.
61+func DiscoverBedrock(ctx context.Context, region string) ([]Model, error) {
62+ if region == "" {
63+ region = bedrockResolveRegion()
64+ }
65+ sigv4 := resolveBedrockDiscoveryCreds()
66+ if sigv4 == nil {
67+ // Bearer-only or unauthenticated: control-plane APIs are
68+ // unreachable. Skip quietly so the catalog stands.
69+ return nil, nil
70+ }
71+
72+ client := &http.Client{Timeout: 15 * time.Second}
73+ ids, err := bedrockListModelIDs(ctx, client, sigv4, region, "https://bedrock."+region+".amazonaws.com")
74+ if err != nil {
75+ return nil, err
76+ }
77+ if len(ids) == 0 {
78+ return nil, nil
79+ }
80+
81+ out := make([]Model, 0, len(ids))
82+ for _, id := range ids {
83+ out = append(out, Model{
84+ Provider: "amazon-bedrock",
85+ ID: id,
86+ Source: "live",
87+ BaseURL: "https://bedrock-runtime." + region + ".amazonaws.com",
88+ })
89+ }
90+ return out, nil
91+}
92+
93+// bedrockListModelIDs enumerates foundation-model IDs and inference-
94+// profile IDs available in the region. Profile IDs are the geo-prefixed
95+// forms zot invokes; both are returned so the picker shows every
96+// selectable variant. controlPlaneBase is the scheme+host (no trailing
97+// slash) of the Bedrock control plane, parameterized for testing.
98+func bedrockListModelIDs(ctx context.Context, client *http.Client, creds *bedrockSigV4Creds, region, controlPlaneBase string) ([]string, error) {
99+ seen := map[string]bool{}
100+ var ids []string
101+ add := func(id string) {
102+ id = strings.TrimSpace(id)
103+ if id == "" || seen[id] {
104+ return
105+ }
106+ seen[id] = true
107+ ids = append(ids, id)
108+ }
109+
110+ // ListFoundationModels: GET, no pagination token in the response.
111+ fmURL := controlPlaneBase + "/foundation-models"
112+ body, err := bedrockSignedControlPlane(ctx, client, creds, region, http.MethodGet, fmURL, nil)
113+ if err != nil {
114+ return nil, err
115+ }
116+ var fm struct {
117+ ModelSummaries []struct {
118+ ModelID string `json:"modelId"`
119+ OutputModalities []string `json:"outputModalities"`
120+ } `json:"modelSummaries"`
121+ }
122+ if err := json.Unmarshal(body, &fm); err != nil {
123+ return nil, fmt.Errorf("bedrock discover: parse foundation-models: %w", err)
124+ }
125+ for _, s := range fm.ModelSummaries {
126+ // Keep text-output models only; skip embeddings/image generators.
127+ if !bedrockEmitsText(s.OutputModalities) {
128+ continue
129+ }
130+ add(s.ModelID)
131+ }
132+
133+ // ListInferenceProfiles: GET, paginated via ?nextToken=.
134+ next := ""
135+ for {
136+ query := url.Values{"maxResults": {"1000"}}
137+ if next != "" {
138+ query.Set("nextToken", next)
139+ }
140+ ipURL := controlPlaneBase + "/inference-profiles?" + query.Encode()
141+ ipBody, err := bedrockSignedControlPlane(ctx, client, creds, region, http.MethodGet, ipURL, nil)
142+ if err != nil {
143+ // Non-fatal: foundation models already gathered.
144+ break
145+ }
146+ var ip struct {
147+ InferenceProfileSummaries []struct {
148+ InferenceProfileID string `json:"inferenceProfileId"`
149+ } `json:"inferenceProfileSummaries"`
150+ NextToken string `json:"nextToken"`
151+ }
152+ if err := json.Unmarshal(ipBody, &ip); err != nil {
153+ break
154+ }
155+ for _, s := range ip.InferenceProfileSummaries {
156+ add(s.InferenceProfileID)
157+ }
158+ if ip.NextToken == "" {
159+ break
160+ }
161+ next = ip.NextToken
162+ }
163+
164+ return ids, nil
165+}
166+
167+func bedrockEmitsText(modalities []string) bool {
168+ if len(modalities) == 0 {
169+ return true // Unknown: assume usable rather than hide it.
170+ }
171+ for _, m := range modalities {
172+ if strings.EqualFold(m, "TEXT") {
173+ return true
174+ }
175+ }
176+ return false
177+}
178+
179+// bedrockSignedControlPlane issues a SigV4-signed request to a Bedrock
180+// control-plane endpoint and returns the response body. Non-2xx is an
181+// error carrying the status and trimmed body.
182+func bedrockSignedControlPlane(ctx context.Context, client *http.Client, creds *bedrockSigV4Creds, region, method, url string, payload []byte) ([]byte, error) {
183+ req, err := http.NewRequestWithContext(ctx, method, url, bytesReader(payload))
184+ if err != nil {
185+ return nil, err
186+ }
187+ if payload != nil {
188+ req.Header.Set("content-type", "application/json")
189+ }
190+ if err := signSigV4(req, payload, "bedrock", region, creds, time.Now().UTC()); err != nil {
191+ return nil, fmt.Errorf("bedrock discover: sign: %w", err)
192+ }
193+ resp, err := client.Do(req)
194+ if err != nil {
195+ return nil, fmt.Errorf("bedrock discover: %w", err)
196+ }
197+ defer resp.Body.Close()
198+ b, _ := io.ReadAll(io.LimitReader(resp.Body, 16<<20))
199+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
200+ return nil, fmt.Errorf("bedrock discover: http %d: %s", resp.StatusCode, strings.TrimSpace(string(b)))
201+ }
202+ return b, nil
203+}
A packages/provider/discover_bedrock_capture_live_test.go +230 -0
@@ -0,0 +1,230 @@
1+package provider
2+
3+import (
4+ "context"
5+ "encoding/json"
6+ "net/http"
7+ "os"
8+ "strings"
9+ "testing"
10+ "time"
11+)
12+
13+// Live Bedrock discovery diagnostic / capture helper.
14+//
15+// This is NOT a validation test. It lets a developer with real AWS
16+// credentials capture actual control-plane and Price List responses to
17+// verify discovery output and re-check the pricing situation. It never
18+// runs in `go test ./...` unless explicitly enabled, so CI and unpaid
19+// checkouts never hit the network.
20+//
21+// Background: a live capture (Sep 2026, us-east-1) confirmed the AWS
22+// Price List API returns only stale, input-only rows for legacy Anthropic
23+// models (Claude 2.x / 3 Sonnet / 3 Haiku) and no output/cache prices for
24+// any current model, so zot does NOT enrich pricing from it — the catalog
25+// stays authoritative. This helper is retained to detect if/when AWS
26+// fixes that, and to confirm ListFoundationModels / ListInferenceProfiles
27+// shapes when they change.
28+//
29+// Enable and run:
30+//
31+// ZOT_BEDROCK_CAPTURE=1 \
32+// AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... [AWS_SESSION_TOKEN=...] \
33+// AWS_REGION=us-east-1 \
34+// go test ./packages/provider -run TestBedrockCaptureLive -v
35+//
36+// Or with a profile:
37+//
38+// ZOT_BEDROCK_CAPTURE=1 AWS_PROFILE=my-sso AWS_REGION=us-east-1 \
39+// go test ./packages/provider -run TestBedrockCaptureLive -v
40+//
41+// Output is written to files under $ZOT_BEDROCK_CAPTURE_DIR (default: a
42+// temp dir printed in the log). Secrets are never captured — only the
43+// public model/pricing JSON.
44+//
45+// Requires the caller's IAM principal to allow:
46+// - bedrock:ListFoundationModels
47+// - bedrock:ListInferenceProfiles
48+// - pricing:GetProducts
49+func TestBedrockCaptureLive(t *testing.T) {
50+ if strings.TrimSpace(os.Getenv("ZOT_BEDROCK_CAPTURE")) == "" {
51+ t.Skip("set ZOT_BEDROCK_CAPTURE=1 (with AWS creds) to capture live Bedrock responses")
52+ }
53+
54+ _, sigv4 := resolveBedrockAuth("")
55+ if sigv4 == nil {
56+ t.Fatal("no SigV4 credentials found; set AWS_ACCESS_KEY_ID+AWS_SECRET_ACCESS_KEY or AWS_PROFILE")
57+ }
58+ region := bedrockResolveRegion()
59+ t.Logf("capturing Bedrock responses for region=%s", region)
60+
61+ outDir := strings.TrimSpace(os.Getenv("ZOT_BEDROCK_CAPTURE_DIR"))
62+ if outDir == "" {
63+ outDir = t.TempDir()
64+ }
65+ t.Logf("writing captures to %s", outDir)
66+
67+ client := &http.Client{Timeout: 30 * time.Second}
68+ ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
69+ defer cancel()
70+
71+ // 1. ListFoundationModels (control plane, GET).
72+ fmURL := "https://bedrock." + region + ".amazonaws.com/foundation-models"
73+ if body, err := bedrockSignedControlPlane(ctx, client, sigv4, region, http.MethodGet, fmURL, nil); err != nil {
74+ t.Errorf("ListFoundationModels failed: %v", err)
75+ } else {
76+ writeCapture(t, outDir, "foundation-models.json", body)
77+ // Also log the summarized model IDs so the capturer can eyeball them.
78+ logBedrockModelIDs(t, body)
79+ }
80+
81+ // 2. ListInferenceProfiles (control plane, GET, paginated).
82+ ipURL := "https://bedrock." + region + ".amazonaws.com/inference-profiles?maxResults=1000"
83+ if body, err := bedrockSignedControlPlane(ctx, client, sigv4, region, http.MethodGet, ipURL, nil); err != nil {
84+ t.Errorf("ListInferenceProfiles failed: %v", err)
85+ } else {
86+ writeCapture(t, outDir, "inference-profiles.json", body)
87+ }
88+
89+ // 3. GetProducts for AmazonBedrock (Price List API, POST).
90+ // Capture the FIRST page raw so we can inspect real attribute
91+ // names, inferenceType values, feature strings, and units. We
92+ // deliberately do not paginate here — one page is enough to fix
93+ // the fixtures, and it keeps the captured file small.
94+ captureBedrockPricingPage(ctx, t, client, sigv4, region, outDir)
95+
96+ t.Logf("done. Inspect the files in %s and share them (they contain only public pricing/model data).", outDir)
97+}
98+
99+// captureBedrockPricingPage issues a single GetProducts page filtered to
100+// the region and the Anthropic provider (the family we care about most)
101+// and writes the raw response. Anthropic is the case the aws-cli #9567
102+// report flagged as possibly missing output-token rows, so capturing it
103+// specifically lets us confirm or refute that.
104+func captureBedrockPricingPage(ctx context.Context, t *testing.T, client *http.Client, creds *bedrockSigV4Creds, region, outDir string) {
105+ const pricingRegion = "us-east-1"
106+ url := "https://api.pricing." + pricingRegion + ".amazonaws.com/"
107+
108+ reqBody := map[string]any{
109+ "ServiceCode": "AmazonBedrock",
110+ "FormatVersion": "aws_v1",
111+ "MaxResults": 100,
112+ "Filters": []map[string]string{
113+ {"Type": "TERM_MATCH", "Field": "regionCode", "Value": region},
114+ {"Type": "TERM_MATCH", "Field": "provider", "Value": "Anthropic"},
115+ },
116+ }
117+ payload, err := json.Marshal(reqBody)
118+ if err != nil {
119+ t.Errorf("marshal pricing request: %v", err)
120+ return
121+ }
122+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytesReader(payload))
123+ if err != nil {
124+ t.Errorf("build pricing request: %v", err)
125+ return
126+ }
127+ req.Header.Set("content-type", "application/x-amz-json-1.1")
128+ req.Header.Set("x-amz-target", "AWSPriceListService.GetProducts")
129+ if err := signSigV4(req, payload, "pricing", pricingRegion, creds, time.Now().UTC()); err != nil {
130+ t.Errorf("sign pricing request: %v", err)
131+ return
132+ }
133+ resp, err := client.Do(req)
134+ if err != nil {
135+ t.Errorf("pricing request failed: %v", err)
136+ return
137+ }
138+ defer resp.Body.Close()
139+ var page struct {
140+ PriceList []string `json:"PriceList"`
141+ NextToken string `json:"NextToken"`
142+ }
143+ body := readAll(t, resp)
144+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
145+ t.Errorf("pricing http %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
146+ return
147+ }
148+ writeCapture(t, outDir, "pricing-anthropic-raw.json", body)
149+
150+ // Pretty-print each PriceList entry (they are JSON-encoded strings)
151+ // into a single readable file, and log a compact attribute summary.
152+ if err := json.Unmarshal(body, &page); err != nil {
153+ t.Errorf("parse pricing page: %v", err)
154+ return
155+ }
156+ t.Logf("pricing page returned %d PriceList entries (NextToken present: %v)", len(page.PriceList), page.NextToken != "")
157+ var pretty strings.Builder
158+ for _, raw := range page.PriceList {
159+ var obj any
160+ if err := json.Unmarshal([]byte(raw), &obj); err != nil {
161+ continue
162+ }
163+ b, _ := json.MarshalIndent(obj, "", " ")
164+ pretty.Write(b)
165+ pretty.WriteByte('\n')
166+ logBedrockPriceAttributes(t, raw)
167+ }
168+ writeCapture(t, outDir, "pricing-anthropic-pretty.json", []byte(pretty.String()))
169+}
170+
171+func logBedrockModelIDs(t *testing.T, body []byte) {
172+ var fm struct {
173+ ModelSummaries []struct {
174+ ModelID string `json:"modelId"`
175+ OutputModalities []string `json:"outputModalities"`
176+ } `json:"modelSummaries"`
177+ }
178+ if err := json.Unmarshal(body, &fm); err != nil {
179+ return
180+ }
181+ t.Logf("ListFoundationModels: %d models", len(fm.ModelSummaries))
182+ for _, s := range fm.ModelSummaries {
183+ if strings.Contains(strings.ToLower(s.ModelID), "anthropic") {
184+ t.Logf(" modelId=%s outputModalities=%v", s.ModelID, s.OutputModalities)
185+ }
186+ }
187+}
188+
189+func logBedrockPriceAttributes(t *testing.T, raw string) {
190+ var entry struct {
191+ Product struct {
192+ Attributes map[string]string `json:"attributes"`
193+ } `json:"product"`
194+ }
195+ if err := json.Unmarshal([]byte(raw), &entry); err != nil {
196+ return
197+ }
198+ a := entry.Product.Attributes
199+ t.Logf(" price: model=%q inferenceType=%q feature=%q usagetype=%q",
200+ a["model"], a["inferenceType"], a["feature"], a["usagetype"])
201+}
202+
203+func writeCapture(t *testing.T, dir, name string, body []byte) {
204+ t.Helper()
205+ path := dir + string(os.PathSeparator) + name
206+ if err := os.WriteFile(path, body, 0o644); err != nil {
207+ t.Errorf("write %s: %v", path, err)
208+ return
209+ }
210+ t.Logf("wrote %s (%d bytes)", path, len(body))
211+}
212+
213+func readAll(t *testing.T, resp *http.Response) []byte {
214+ t.Helper()
215+ buf := make([]byte, 0, 1<<20)
216+ tmp := make([]byte, 32<<10)
217+ for {
218+ n, err := resp.Body.Read(tmp)
219+ if n > 0 {
220+ buf = append(buf, tmp[:n]...)
221+ }
222+ if err != nil {
223+ break
224+ }
225+ if len(buf) > 32<<20 {
226+ break
227+ }
228+ }
229+ return buf
230+}
@@ -0,0 +1,230 @@
1+package provider
2+
3+import (
4+ "context"
5+ "encoding/json"
6+ "net/http"
7+ "os"
8+ "strings"
9+ "testing"
10+ "time"
11+)
12+
13+// Live Bedrock discovery diagnostic / capture helper.
14+//
15+// This is NOT a validation test. It lets a developer with real AWS
16+// credentials capture actual control-plane and Price List responses to
17+// verify discovery output and re-check the pricing situation. It never
18+// runs in `go test ./...` unless explicitly enabled, so CI and unpaid
19+// checkouts never hit the network.
20+//
21+// Background: a live capture (Sep 2026, us-east-1) confirmed the AWS
22+// Price List API returns only stale, input-only rows for legacy Anthropic
23+// models (Claude 2.x / 3 Sonnet / 3 Haiku) and no output/cache prices for
24+// any current model, so zot does NOT enrich pricing from it — the catalog
25+// stays authoritative. This helper is retained to detect if/when AWS
26+// fixes that, and to confirm ListFoundationModels / ListInferenceProfiles
27+// shapes when they change.
28+//
29+// Enable and run:
30+//
31+// ZOT_BEDROCK_CAPTURE=1 \
32+// AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... [AWS_SESSION_TOKEN=...] \
33+// AWS_REGION=us-east-1 \
34+// go test ./packages/provider -run TestBedrockCaptureLive -v
35+//
36+// Or with a profile:
37+//
38+// ZOT_BEDROCK_CAPTURE=1 AWS_PROFILE=my-sso AWS_REGION=us-east-1 \
39+// go test ./packages/provider -run TestBedrockCaptureLive -v
40+//
41+// Output is written to files under $ZOT_BEDROCK_CAPTURE_DIR (default: a
42+// temp dir printed in the log). Secrets are never captured — only the
43+// public model/pricing JSON.
44+//
45+// Requires the caller's IAM principal to allow:
46+// - bedrock:ListFoundationModels
47+// - bedrock:ListInferenceProfiles
48+// - pricing:GetProducts
49+func TestBedrockCaptureLive(t *testing.T) {
50+ if strings.TrimSpace(os.Getenv("ZOT_BEDROCK_CAPTURE")) == "" {
51+ t.Skip("set ZOT_BEDROCK_CAPTURE=1 (with AWS creds) to capture live Bedrock responses")
52+ }
53+
54+ _, sigv4 := resolveBedrockAuth("")
55+ if sigv4 == nil {
56+ t.Fatal("no SigV4 credentials found; set AWS_ACCESS_KEY_ID+AWS_SECRET_ACCESS_KEY or AWS_PROFILE")
57+ }
58+ region := bedrockResolveRegion()
59+ t.Logf("capturing Bedrock responses for region=%s", region)
60+
61+ outDir := strings.TrimSpace(os.Getenv("ZOT_BEDROCK_CAPTURE_DIR"))
62+ if outDir == "" {
63+ outDir = t.TempDir()
64+ }
65+ t.Logf("writing captures to %s", outDir)
66+
67+ client := &http.Client{Timeout: 30 * time.Second}
68+ ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
69+ defer cancel()
70+
71+ // 1. ListFoundationModels (control plane, GET).
72+ fmURL := "https://bedrock." + region + ".amazonaws.com/foundation-models"
73+ if body, err := bedrockSignedControlPlane(ctx, client, sigv4, region, http.MethodGet, fmURL, nil); err != nil {
74+ t.Errorf("ListFoundationModels failed: %v", err)
75+ } else {
76+ writeCapture(t, outDir, "foundation-models.json", body)
77+ // Also log the summarized model IDs so the capturer can eyeball them.
78+ logBedrockModelIDs(t, body)
79+ }
80+
81+ // 2. ListInferenceProfiles (control plane, GET, paginated).
82+ ipURL := "https://bedrock." + region + ".amazonaws.com/inference-profiles?maxResults=1000"
83+ if body, err := bedrockSignedControlPlane(ctx, client, sigv4, region, http.MethodGet, ipURL, nil); err != nil {
84+ t.Errorf("ListInferenceProfiles failed: %v", err)
85+ } else {
86+ writeCapture(t, outDir, "inference-profiles.json", body)
87+ }
88+
89+ // 3. GetProducts for AmazonBedrock (Price List API, POST).
90+ // Capture the FIRST page raw so we can inspect real attribute
91+ // names, inferenceType values, feature strings, and units. We
92+ // deliberately do not paginate here — one page is enough to fix
93+ // the fixtures, and it keeps the captured file small.
94+ captureBedrockPricingPage(ctx, t, client, sigv4, region, outDir)
95+
96+ t.Logf("done. Inspect the files in %s and share them (they contain only public pricing/model data).", outDir)
97+}
98+
99+// captureBedrockPricingPage issues a single GetProducts page filtered to
100+// the region and the Anthropic provider (the family we care about most)
101+// and writes the raw response. Anthropic is the case the aws-cli #9567
102+// report flagged as possibly missing output-token rows, so capturing it
103+// specifically lets us confirm or refute that.
104+func captureBedrockPricingPage(ctx context.Context, t *testing.T, client *http.Client, creds *bedrockSigV4Creds, region, outDir string) {
105+ const pricingRegion = "us-east-1"
106+ url := "https://api.pricing." + pricingRegion + ".amazonaws.com/"
107+
108+ reqBody := map[string]any{
109+ "ServiceCode": "AmazonBedrock",
110+ "FormatVersion": "aws_v1",
111+ "MaxResults": 100,
112+ "Filters": []map[string]string{
113+ {"Type": "TERM_MATCH", "Field": "regionCode", "Value": region},
114+ {"Type": "TERM_MATCH", "Field": "provider", "Value": "Anthropic"},
115+ },
116+ }
117+ payload, err := json.Marshal(reqBody)
118+ if err != nil {
119+ t.Errorf("marshal pricing request: %v", err)
120+ return
121+ }
122+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytesReader(payload))
123+ if err != nil {
124+ t.Errorf("build pricing request: %v", err)
125+ return
126+ }
127+ req.Header.Set("content-type", "application/x-amz-json-1.1")
128+ req.Header.Set("x-amz-target", "AWSPriceListService.GetProducts")
129+ if err := signSigV4(req, payload, "pricing", pricingRegion, creds, time.Now().UTC()); err != nil {
130+ t.Errorf("sign pricing request: %v", err)
131+ return
132+ }
133+ resp, err := client.Do(req)
134+ if err != nil {
135+ t.Errorf("pricing request failed: %v", err)
136+ return
137+ }
138+ defer resp.Body.Close()
139+ var page struct {
140+ PriceList []string `json:"PriceList"`
141+ NextToken string `json:"NextToken"`
142+ }
143+ body := readAll(t, resp)
144+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
145+ t.Errorf("pricing http %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
146+ return
147+ }
148+ writeCapture(t, outDir, "pricing-anthropic-raw.json", body)
149+
150+ // Pretty-print each PriceList entry (they are JSON-encoded strings)
151+ // into a single readable file, and log a compact attribute summary.
152+ if err := json.Unmarshal(body, &page); err != nil {
153+ t.Errorf("parse pricing page: %v", err)
154+ return
155+ }
156+ t.Logf("pricing page returned %d PriceList entries (NextToken present: %v)", len(page.PriceList), page.NextToken != "")
157+ var pretty strings.Builder
158+ for _, raw := range page.PriceList {
159+ var obj any
160+ if err := json.Unmarshal([]byte(raw), &obj); err != nil {
161+ continue
162+ }
163+ b, _ := json.MarshalIndent(obj, "", " ")
164+ pretty.Write(b)
165+ pretty.WriteByte('\n')
166+ logBedrockPriceAttributes(t, raw)
167+ }
168+ writeCapture(t, outDir, "pricing-anthropic-pretty.json", []byte(pretty.String()))
169+}
170+
171+func logBedrockModelIDs(t *testing.T, body []byte) {
172+ var fm struct {
173+ ModelSummaries []struct {
174+ ModelID string `json:"modelId"`
175+ OutputModalities []string `json:"outputModalities"`
176+ } `json:"modelSummaries"`
177+ }
178+ if err := json.Unmarshal(body, &fm); err != nil {
179+ return
180+ }
181+ t.Logf("ListFoundationModels: %d models", len(fm.ModelSummaries))
182+ for _, s := range fm.ModelSummaries {
183+ if strings.Contains(strings.ToLower(s.ModelID), "anthropic") {
184+ t.Logf(" modelId=%s outputModalities=%v", s.ModelID, s.OutputModalities)
185+ }
186+ }
187+}
188+
189+func logBedrockPriceAttributes(t *testing.T, raw string) {
190+ var entry struct {
191+ Product struct {
192+ Attributes map[string]string `json:"attributes"`
193+ } `json:"product"`
194+ }
195+ if err := json.Unmarshal([]byte(raw), &entry); err != nil {
196+ return
197+ }
198+ a := entry.Product.Attributes
199+ t.Logf(" price: model=%q inferenceType=%q feature=%q usagetype=%q",
200+ a["model"], a["inferenceType"], a["feature"], a["usagetype"])
201+}
202+
203+func writeCapture(t *testing.T, dir, name string, body []byte) {
204+ t.Helper()
205+ path := dir + string(os.PathSeparator) + name
206+ if err := os.WriteFile(path, body, 0o644); err != nil {
207+ t.Errorf("write %s: %v", path, err)
208+ return
209+ }
210+ t.Logf("wrote %s (%d bytes)", path, len(body))
211+}
212+
213+func readAll(t *testing.T, resp *http.Response) []byte {
214+ t.Helper()
215+ buf := make([]byte, 0, 1<<20)
216+ tmp := make([]byte, 32<<10)
217+ for {
218+ n, err := resp.Body.Read(tmp)
219+ if n > 0 {
220+ buf = append(buf, tmp[:n]...)
221+ }
222+ if err != nil {
223+ break
224+ }
225+ if len(buf) > 32<<20 {
226+ break
227+ }
228+ }
229+ return buf
230+}
A packages/provider/discover_bedrock_test.go +171 -0
@@ -0,0 +1,171 @@
1+package provider
2+
3+import (
4+ "context"
5+ "io"
6+ "net/http"
7+ "net/http/httptest"
8+ "strings"
9+ "testing"
10+)
11+
12+type bedrockDiscoveryTestTransport func(*http.Request) (*http.Response, error)
13+
14+func (f bedrockDiscoveryTestTransport) RoundTrip(r *http.Request) (*http.Response, error) {
15+ return f(r)
16+}
17+
18+func TestDiscoverBedrockUsesSigV4WithBearer(t *testing.T) {
19+ t.Setenv("AWS_BEARER_TOKEN_BEDROCK", "bearer")
20+ t.Setenv("AWS_ACCESS_KEY_ID", "AKID")
21+ t.Setenv("AWS_SECRET_ACCESS_KEY", "SECRET")
22+ original := http.DefaultTransport
23+ t.Cleanup(func() { http.DefaultTransport = original })
24+ http.DefaultTransport = bedrockDiscoveryTestTransport(func(r *http.Request) (*http.Response, error) {
25+ if !strings.Contains(r.Header.Get("Authorization"), "Credential=AKID/") {
26+ t.Errorf("discovery request was not signed with SigV4")
27+ }
28+ body := `{"modelSummaries":[{"modelId":"test.new-model","outputModalities":["TEXT"]}]}`
29+ if r.URL.Path == "/inference-profiles" {
30+ body = `{"inferenceProfileSummaries":[]}`
31+ }
32+ return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(body)), Header: make(http.Header)}, nil
33+ })
34+ models, err := DiscoverBedrock(context.Background(), "us-east-1")
35+ if err != nil || len(models) != 1 || models[0].ID != "test.new-model" {
36+ t.Fatalf("discovered models = %v, err = %v", models, err)
37+ }
38+ if bearer, sigv4 := resolveBedrockAuth("bearer"); bearer != "bearer" || sigv4 != nil {
39+ t.Fatal("inference must continue to prefer the bearer token")
40+ }
41+}
42+
43+func TestBedrockEmitsText(t *testing.T) {
44+ if !bedrockEmitsText(nil) {
45+ t.Error("nil modalities should default to text")
46+ }
47+ if !bedrockEmitsText([]string{"TEXT"}) {
48+ t.Error("TEXT should emit text")
49+ }
50+ if bedrockEmitsText([]string{"IMAGE"}) {
51+ t.Error("IMAGE-only should not emit text")
52+ }
53+ if !bedrockEmitsText([]string{"IMAGE", "TEXT"}) {
54+ t.Error("mixed with TEXT should emit text")
55+ }
56+}
57+
58+func TestResolveBedrockAuthBearerVsSigV4(t *testing.T) {
59+ t.Setenv("AWS_BEARER_TOKEN_BEDROCK", "")
60+ t.Setenv("AWS_ACCESS_KEY_ID", "")
61+ t.Setenv("AWS_SECRET_ACCESS_KEY", "")
62+ t.Setenv("AWS_SESSION_TOKEN", "")
63+ t.Setenv("AWS_PROFILE", "")
64+
65+ // Explicit bearer via apiKey.
66+ if b, s := resolveBedrockAuth("tok-123"); b != "tok-123" || s != nil {
67+ t.Errorf("explicit bearer: got (%q,%v)", b, s)
68+ }
69+
70+ // Sentinel <aws> is not a bearer; falls through to SigV4 env.
71+ t.Setenv("AWS_ACCESS_KEY_ID", "AKID")
72+ t.Setenv("AWS_SECRET_ACCESS_KEY", "SECRET")
73+ b, s := resolveBedrockAuth("<aws>")
74+ if b != "" || s == nil {
75+ t.Fatalf("sentinel should resolve SigV4, got (%q,%v)", b, s)
76+ }
77+ if s.accessKeyID != "AKID" || s.secretAccessKey != "SECRET" {
78+ t.Errorf("sigv4 creds = %+v", s)
79+ }
80+}
81+
82+// TestBedrockListModelIDs parses control-plane responses shaped exactly
83+// like the real ones captured from a live account (see the doc note in
84+// discover_bedrock.go): ListFoundationModels returns modelSummaries with
85+// modelId + outputModalities; ListInferenceProfiles returns
86+// inferenceProfileSummaries with inferenceProfileId and paginates via
87+// nextToken. Embedding/image-only models are filtered out; profile IDs
88+// are included; duplicates are de-duplicated.
89+func TestBedrockListModelIDs(t *testing.T) {
90+ page1 := `{"inferenceProfileSummaries":[{"inferenceProfileId":"us.anthropic.claude-opus-5-5"}],"nextToken":"N+E/T= &"}`
91+ page2 := `{"inferenceProfileSummaries":[{"inferenceProfileId":"eu.anthropic.claude-sonnet-5"}]}`
92+ foundation := `{"modelSummaries":[
93+ {"modelId":"anthropic.claude-opus-5-5","outputModalities":["TEXT"]},
94+ {"modelId":"amazon.titan-embed-text-v2","outputModalities":["EMBEDDING"]},
95+ {"modelId":"stability.sd3-large","outputModalities":["IMAGE"]},
96+ {"modelId":"anthropic.claude-opus-5-5","outputModalities":["TEXT"]}
97+ ]}`
98+
99+ var ipHits int
100+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
101+ w.Header().Set("content-type", "application/json")
102+ switch {
103+ case strings.Contains(r.URL.Path, "/foundation-models"):
104+ w.Write([]byte(foundation))
105+ case strings.Contains(r.URL.Path, "/inference-profiles"):
106+ ipHits++
107+ switch token := r.URL.Query().Get("nextToken"); token {
108+ case "":
109+ w.Write([]byte(page1))
110+ case "N+E/T= &":
111+ w.Write([]byte(page2))
112+ default:
113+ t.Errorf("unexpected pagination token %q", token)
114+ w.WriteHeader(http.StatusBadRequest)
115+ }
116+ default:
117+ w.WriteHeader(http.StatusNotFound)
118+ }
119+ }))
120+ defer srv.Close()
121+
122+ creds := &bedrockSigV4Creds{accessKeyID: "AKID", secretAccessKey: "SECRET"}
123+ ids, err := bedrockListModelIDs(context.Background(), srv.Client(), creds, "us-east-1", srv.URL)
124+ if err != nil {
125+ t.Fatalf("bedrockListModelIDs: %v", err)
126+ }
127+
128+ want := []string{
129+ "anthropic.claude-opus-5-5", // text foundation model, de-duplicated
130+ "us.anthropic.claude-opus-5-5", // profile page 1
131+ "eu.anthropic.claude-sonnet-5", // profile page 2 (pagination followed)
132+ }
133+ got := map[string]int{}
134+ for _, id := range ids {
135+ got[id]++
136+ }
137+ for _, w := range want {
138+ if got[w] != 1 {
139+ t.Errorf("expected exactly one %q, got %d (all=%v)", w, got[w], ids)
140+ }
141+ }
142+ // Embedding and image-only models must be filtered out.
143+ for _, bad := range []string{"amazon.titan-embed-text-v2", "stability.sd3-large"} {
144+ if got[bad] != 0 {
145+ t.Errorf("non-text model %q should be filtered out", bad)
146+ }
147+ }
148+ if ipHits != 2 {
149+ t.Errorf("expected 2 inference-profile page requests (pagination), got %d", ipHits)
150+ }
151+}
152+
153+// TestBedrockControlPlaneHTTPError confirms non-2xx responses surface as
154+// errors with the status and body, so discovery fails loudly (and the
155+// caller then falls back to the catalog) rather than silently parsing junk.
156+func TestBedrockControlPlaneHTTPError(t *testing.T) {
157+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
158+ w.WriteHeader(http.StatusForbidden)
159+ w.Write([]byte(`{"message":"not authorized"}`))
160+ }))
161+ defer srv.Close()
162+
163+ creds := &bedrockSigV4Creds{accessKeyID: "AKID", secretAccessKey: "SECRET"}
164+ _, err := bedrockSignedControlPlane(context.Background(), srv.Client(), creds, "us-east-1", http.MethodGet, srv.URL+"/foundation-models", nil)
165+ if err == nil {
166+ t.Fatal("expected error on 403")
167+ }
168+ if !strings.Contains(err.Error(), "403") || !strings.Contains(err.Error(), "not authorized") {
169+ t.Errorf("error should carry status and body: %v", err)
170+ }
171+}
@@ -0,0 +1,171 @@
1+package provider
2+
3+import (
4+ "context"
5+ "io"
6+ "net/http"
7+ "net/http/httptest"
8+ "strings"
9+ "testing"
10+)
11+
12+type bedrockDiscoveryTestTransport func(*http.Request) (*http.Response, error)
13+
14+func (f bedrockDiscoveryTestTransport) RoundTrip(r *http.Request) (*http.Response, error) {
15+ return f(r)
16+}
17+
18+func TestDiscoverBedrockUsesSigV4WithBearer(t *testing.T) {
19+ t.Setenv("AWS_BEARER_TOKEN_BEDROCK", "bearer")
20+ t.Setenv("AWS_ACCESS_KEY_ID", "AKID")
21+ t.Setenv("AWS_SECRET_ACCESS_KEY", "SECRET")
22+ original := http.DefaultTransport
23+ t.Cleanup(func() { http.DefaultTransport = original })
24+ http.DefaultTransport = bedrockDiscoveryTestTransport(func(r *http.Request) (*http.Response, error) {
25+ if !strings.Contains(r.Header.Get("Authorization"), "Credential=AKID/") {
26+ t.Errorf("discovery request was not signed with SigV4")
27+ }
28+ body := `{"modelSummaries":[{"modelId":"test.new-model","outputModalities":["TEXT"]}]}`
29+ if r.URL.Path == "/inference-profiles" {
30+ body = `{"inferenceProfileSummaries":[]}`
31+ }
32+ return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(body)), Header: make(http.Header)}, nil
33+ })
34+ models, err := DiscoverBedrock(context.Background(), "us-east-1")
35+ if err != nil || len(models) != 1 || models[0].ID != "test.new-model" {
36+ t.Fatalf("discovered models = %v, err = %v", models, err)
37+ }
38+ if bearer, sigv4 := resolveBedrockAuth("bearer"); bearer != "bearer" || sigv4 != nil {
39+ t.Fatal("inference must continue to prefer the bearer token")
40+ }
41+}
42+
43+func TestBedrockEmitsText(t *testing.T) {
44+ if !bedrockEmitsText(nil) {
45+ t.Error("nil modalities should default to text")
46+ }
47+ if !bedrockEmitsText([]string{"TEXT"}) {
48+ t.Error("TEXT should emit text")
49+ }
50+ if bedrockEmitsText([]string{"IMAGE"}) {
51+ t.Error("IMAGE-only should not emit text")
52+ }
53+ if !bedrockEmitsText([]string{"IMAGE", "TEXT"}) {
54+ t.Error("mixed with TEXT should emit text")
55+ }
56+}
57+
58+func TestResolveBedrockAuthBearerVsSigV4(t *testing.T) {
59+ t.Setenv("AWS_BEARER_TOKEN_BEDROCK", "")
60+ t.Setenv("AWS_ACCESS_KEY_ID", "")
61+ t.Setenv("AWS_SECRET_ACCESS_KEY", "")
62+ t.Setenv("AWS_SESSION_TOKEN", "")
63+ t.Setenv("AWS_PROFILE", "")
64+
65+ // Explicit bearer via apiKey.
66+ if b, s := resolveBedrockAuth("tok-123"); b != "tok-123" || s != nil {
67+ t.Errorf("explicit bearer: got (%q,%v)", b, s)
68+ }
69+
70+ // Sentinel <aws> is not a bearer; falls through to SigV4 env.
71+ t.Setenv("AWS_ACCESS_KEY_ID", "AKID")
72+ t.Setenv("AWS_SECRET_ACCESS_KEY", "SECRET")
73+ b, s := resolveBedrockAuth("<aws>")
74+ if b != "" || s == nil {
75+ t.Fatalf("sentinel should resolve SigV4, got (%q,%v)", b, s)
76+ }
77+ if s.accessKeyID != "AKID" || s.secretAccessKey != "SECRET" {
78+ t.Errorf("sigv4 creds = %+v", s)
79+ }
80+}
81+
82+// TestBedrockListModelIDs parses control-plane responses shaped exactly
83+// like the real ones captured from a live account (see the doc note in
84+// discover_bedrock.go): ListFoundationModels returns modelSummaries with
85+// modelId + outputModalities; ListInferenceProfiles returns
86+// inferenceProfileSummaries with inferenceProfileId and paginates via
87+// nextToken. Embedding/image-only models are filtered out; profile IDs
88+// are included; duplicates are de-duplicated.
89+func TestBedrockListModelIDs(t *testing.T) {
90+ page1 := `{"inferenceProfileSummaries":[{"inferenceProfileId":"us.anthropic.claude-opus-5-5"}],"nextToken":"N+E/T= &"}`
91+ page2 := `{"inferenceProfileSummaries":[{"inferenceProfileId":"eu.anthropic.claude-sonnet-5"}]}`
92+ foundation := `{"modelSummaries":[
93+ {"modelId":"anthropic.claude-opus-5-5","outputModalities":["TEXT"]},
94+ {"modelId":"amazon.titan-embed-text-v2","outputModalities":["EMBEDDING"]},
95+ {"modelId":"stability.sd3-large","outputModalities":["IMAGE"]},
96+ {"modelId":"anthropic.claude-opus-5-5","outputModalities":["TEXT"]}
97+ ]}`
98+
99+ var ipHits int
100+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
101+ w.Header().Set("content-type", "application/json")
102+ switch {
103+ case strings.Contains(r.URL.Path, "/foundation-models"):
104+ w.Write([]byte(foundation))
105+ case strings.Contains(r.URL.Path, "/inference-profiles"):
106+ ipHits++
107+ switch token := r.URL.Query().Get("nextToken"); token {
108+ case "":
109+ w.Write([]byte(page1))
110+ case "N+E/T= &":
111+ w.Write([]byte(page2))
112+ default:
113+ t.Errorf("unexpected pagination token %q", token)
114+ w.WriteHeader(http.StatusBadRequest)
115+ }
116+ default:
117+ w.WriteHeader(http.StatusNotFound)
118+ }
119+ }))
120+ defer srv.Close()
121+
122+ creds := &bedrockSigV4Creds{accessKeyID: "AKID", secretAccessKey: "SECRET"}
123+ ids, err := bedrockListModelIDs(context.Background(), srv.Client(), creds, "us-east-1", srv.URL)
124+ if err != nil {
125+ t.Fatalf("bedrockListModelIDs: %v", err)
126+ }
127+
128+ want := []string{
129+ "anthropic.claude-opus-5-5", // text foundation model, de-duplicated
130+ "us.anthropic.claude-opus-5-5", // profile page 1
131+ "eu.anthropic.claude-sonnet-5", // profile page 2 (pagination followed)
132+ }
133+ got := map[string]int{}
134+ for _, id := range ids {
135+ got[id]++
136+ }
137+ for _, w := range want {
138+ if got[w] != 1 {
139+ t.Errorf("expected exactly one %q, got %d (all=%v)", w, got[w], ids)
140+ }
141+ }
142+ // Embedding and image-only models must be filtered out.
143+ for _, bad := range []string{"amazon.titan-embed-text-v2", "stability.sd3-large"} {
144+ if got[bad] != 0 {
145+ t.Errorf("non-text model %q should be filtered out", bad)
146+ }
147+ }
148+ if ipHits != 2 {
149+ t.Errorf("expected 2 inference-profile page requests (pagination), got %d", ipHits)
150+ }
151+}
152+
153+// TestBedrockControlPlaneHTTPError confirms non-2xx responses surface as
154+// errors with the status and body, so discovery fails loudly (and the
155+// caller then falls back to the catalog) rather than silently parsing junk.
156+func TestBedrockControlPlaneHTTPError(t *testing.T) {
157+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
158+ w.WriteHeader(http.StatusForbidden)
159+ w.Write([]byte(`{"message":"not authorized"}`))
160+ }))
161+ defer srv.Close()
162+
163+ creds := &bedrockSigV4Creds{accessKeyID: "AKID", secretAccessKey: "SECRET"}
164+ _, err := bedrockSignedControlPlane(context.Background(), srv.Client(), creds, "us-east-1", http.MethodGet, srv.URL+"/foundation-models", nil)
165+ if err == nil {
166+ t.Fatal("expected error on 403")
167+ }
168+ if !strings.Contains(err.Error(), "403") || !strings.Contains(err.Error(), "not authorized") {
169+ t.Errorf("error should carry status and body: %v", err)
170+ }
171+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close