detached de90baf

Merge pull request #224 from rgasper/fix/bedrock-discovered-max-output

Give discovered Bedrock models real output limits
commit
de90bafbaeca5f54d8687fb145c904668bc7ef17
author
Patric Eckhart · · 2026-09-29 20:00 UTC
committer
GitHub · · 2026-09-29 20:00 UTC
merge of
2ff82fa + a993eb3 · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
refs
tag: v0.4.8tag: v0.4.9
4 files · +206 -8
M README.md +1 -1
@@ -798,7 +798,7 @@ If both are present, the bearer token wins for inference, while live model disco
798798 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:
799799
800800 - **Bearer-token-only users** still get the full curated Bedrock model list with correct pricing from the catalog. No AWS API listing is attempted.
801-- **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.
801+- **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 metadata. An inference profile the catalog doesn't list under that exact ID (for example `apac.` or `us-gov.` variants) inherits the limits, reasoning flags, and pricing of its base model. Models with no matching base catalog entry, including new Claude models, retain placeholder pricing and the client's 4096-token output fallback until their limits are verified and added to the catalog.
802802
803803 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`.
804804
@@ -798,7 +798,7 @@ If both are present, the bearer token wins for inference, while live model disco
798 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:798 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:
799 799
800 - **Bearer-token-only users** still get the full curated Bedrock model list with correct pricing from the catalog. No AWS API listing is attempted.800 - **Bearer-token-only users** still get the full curated Bedrock model list with correct pricing from the catalog. No AWS API listing is attempted.
801-- **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.801+- **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 metadata. An inference profile the catalog doesn't list under that exact ID (for example `apac.` or `us-gov.` variants) inherits the limits, reasoning flags, and pricing of its base model. Models with no matching base catalog entry, including new Claude models, retain placeholder pricing and the client's 4096-token output fallback until their limits are verified and added to the catalog.
802 802
803 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`.803 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`.
804 804
M packages/provider/amazon_bedrock.go +1 -1
@@ -600,7 +600,7 @@ func resolveBedrockInferenceProfileID(modelID, region string) string {
600600 // bedrockGeoPrefixes are the cross-region inference-profile geo
601601 // prefixes Bedrock uses. A model ID that starts with one of these
602602 // (followed by a dot) is already a profile reference.
603-var bedrockGeoPrefixes = []string{"us-gov", "us", "eu", "apac", "ap", "global", "au"}
603+var bedrockGeoPrefixes = []string{"us-gov", "us", "eu", "apac", "ap", "global", "au", "jp"}
604604
605605 func bedrockHasGeoPrefix(modelID string) bool {
606606 for _, p := range bedrockGeoPrefixes {
@@ -600,7 +600,7 @@ func resolveBedrockInferenceProfileID(modelID, region string) string {
600 // bedrockGeoPrefixes are the cross-region inference-profile geo600 // bedrockGeoPrefixes are the cross-region inference-profile geo
601 // prefixes Bedrock uses. A model ID that starts with one of these601 // prefixes Bedrock uses. A model ID that starts with one of these
602 // (followed by a dot) is already a profile reference.602 // (followed by a dot) is already a profile reference.
603-var bedrockGeoPrefixes = []string{"us-gov", "us", "eu", "apac", "ap", "global", "au"}603+var bedrockGeoPrefixes = []string{"us-gov", "us", "eu", "apac", "ap", "global", "au", "jp"}
604 604
605 func bedrockHasGeoPrefix(modelID string) bool {605 func bedrockHasGeoPrefix(modelID string) bool {
606 for _, p := range bedrockGeoPrefixes {606 for _, p := range bedrockGeoPrefixes {
M packages/provider/discover_bedrock.go +97 -6
@@ -34,6 +34,7 @@ import (
3434 "encoding/json"
3535 "fmt"
3636 "io"
37+ "maps"
3738 "net/http"
3839 "net/url"
3940 "strings"
@@ -80,16 +81,106 @@ func DiscoverBedrock(ctx context.Context, region string) ([]Model, error) {
8081
8182 out := make([]Model, 0, len(ids))
8283 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- })
84+ out = append(out, bedrockDiscoveredModel(id, region))
8985 }
9086 return out, nil
9187 }
9288
89+// bedrockDiscoveredModel builds the Model for a discovered ID. The
90+// control plane returns IDs only, so limits and capabilities come from
91+// the static catalog. MergeCatalog already uses an exact catalog match;
92+// this handles IDs without one, most often a geo-prefixed inference
93+// profile (apac., us-gov., ...) whose base model is catalogued. Without
94+// this the model would carry MaxOutput 0 and the Bedrock client would
95+// cap every response at its 4096-token fallback.
96+func bedrockDiscoveredModel(id, region string) Model {
97+ m := Model{
98+ Provider: "amazon-bedrock",
99+ ID: id,
100+ Source: "live",
101+ BaseURL: "https://bedrock-runtime." + region + ".amazonaws.com",
102+ }
103+ if bedrockInCatalog(id) {
104+ // MergeCatalog keeps the exact catalog entry's metadata.
105+ return m
106+ }
107+ prefix, base := bedrockSplitGeoPrefix(id)
108+ if c, ok := bedrockCatalogBase(base); ok {
109+ m.ContextWindow = c.ContextWindow
110+ m.MaxOutput = c.MaxOutput
111+ m.Reasoning = c.Reasoning
112+ m.ReasoningLevelMap = maps.Clone(c.ReasoningLevelMap)
113+ m.AdaptiveThinking = c.AdaptiveThinking
114+ m.PriceInput = c.PriceInput
115+ m.PriceOutput = c.PriceOutput
116+ m.PriceCacheRead = c.PriceCacheRead
117+ m.PriceCacheWrite = c.PriceCacheWrite
118+ m.PriceTierInputTokens = c.PriceTierInputTokens
119+ m.PriceInputAbove = c.PriceInputAbove
120+ m.PriceOutputAbove = c.PriceOutputAbove
121+ m.PriceCacheReadAbove = c.PriceCacheReadAbove
122+ m.PriceCacheWriteAbove = c.PriceCacheWriteAbove
123+ if c.DisplayName != "" {
124+ m.DisplayName = c.DisplayName
125+ if cp, _ := bedrockSplitGeoPrefix(c.ID); cp != "" {
126+ suffix := " (" + cp + ")"
127+ if strings.HasSuffix(strings.ToUpper(m.DisplayName), strings.ToUpper(suffix)) {
128+ m.DisplayName = m.DisplayName[:len(m.DisplayName)-len(suffix)]
129+ }
130+ }
131+ if prefix != "" {
132+ m.DisplayName += " (" + strings.ToUpper(prefix) + ")"
133+ }
134+ }
135+ return m
136+ }
137+ // Unknown models have no verified output or context limits. Keep the
138+ // client's 4096-token fallback rather than guessing from the family:
139+ // even Claude Opus 4 supports only 32k, not the 64k of newer models.
140+ return m
141+}
142+
143+// bedrockSplitGeoPrefix separates a cross-region inference-profile geo
144+// prefix from the foundation-model ID. prefix is "" when there is none.
145+func bedrockSplitGeoPrefix(id string) (prefix, base string) {
146+ for _, p := range bedrockGeoPrefixes {
147+ if strings.HasPrefix(id, p+".") {
148+ return p, id[len(p)+1:]
149+ }
150+ }
151+ return "", id
152+}
153+
154+func bedrockInCatalog(id string) bool {
155+ for _, c := range Catalog {
156+ if c.Provider == "amazon-bedrock" && c.ID == id {
157+ return true
158+ }
159+ }
160+ return false
161+}
162+
163+// bedrockCatalogBase finds catalog metadata for a foundation-model ID:
164+// the bare entry if present, otherwise any geo-prefixed variant of it.
165+func bedrockCatalogBase(base string) (Model, bool) {
166+ var variant Model
167+ found := false
168+ for _, c := range Catalog {
169+ if c.Provider != "amazon-bedrock" {
170+ continue
171+ }
172+ if c.ID == base {
173+ return c, true
174+ }
175+ if !found {
176+ if _, b := bedrockSplitGeoPrefix(c.ID); b == base {
177+ variant, found = c, true
178+ }
179+ }
180+ }
181+ return variant, found
182+}
183+
93184 // bedrockListModelIDs enumerates foundation-model IDs and inference-
94185 // profile IDs available in the region. Profile IDs are the geo-prefixed
95186 // forms zot invokes; both are returned so the picker shows every
@@ -34,6 +34,7 @@ import (
34 "encoding/json"34 "encoding/json"
35 "fmt"35 "fmt"
36 "io"36 "io"
37+ "maps"
37 "net/http"38 "net/http"
38 "net/url"39 "net/url"
39 "strings"40 "strings"
@@ -80,16 +81,106 @@ func DiscoverBedrock(ctx context.Context, region string) ([]Model, error) {
80 81
81 out := make([]Model, 0, len(ids))82 out := make([]Model, 0, len(ids))
82 for _, id := range ids {83 for _, id := range ids {
83- out = append(out, Model{84+ out = append(out, bedrockDiscoveredModel(id, region))
84- Provider: "amazon-bedrock",
85- ID: id,
86- Source: "live",
87- BaseURL: "https://bedrock-runtime." + region + ".amazonaws.com",
88- })
89 }85 }
90 return out, nil86 return out, nil
91 }87 }
92 88
89+// bedrockDiscoveredModel builds the Model for a discovered ID. The
90+// control plane returns IDs only, so limits and capabilities come from
91+// the static catalog. MergeCatalog already uses an exact catalog match;
92+// this handles IDs without one, most often a geo-prefixed inference
93+// profile (apac., us-gov., ...) whose base model is catalogued. Without
94+// this the model would carry MaxOutput 0 and the Bedrock client would
95+// cap every response at its 4096-token fallback.
96+func bedrockDiscoveredModel(id, region string) Model {
97+ m := Model{
98+ Provider: "amazon-bedrock",
99+ ID: id,
100+ Source: "live",
101+ BaseURL: "https://bedrock-runtime." + region + ".amazonaws.com",
102+ }
103+ if bedrockInCatalog(id) {
104+ // MergeCatalog keeps the exact catalog entry's metadata.
105+ return m
106+ }
107+ prefix, base := bedrockSplitGeoPrefix(id)
108+ if c, ok := bedrockCatalogBase(base); ok {
109+ m.ContextWindow = c.ContextWindow
110+ m.MaxOutput = c.MaxOutput
111+ m.Reasoning = c.Reasoning
112+ m.ReasoningLevelMap = maps.Clone(c.ReasoningLevelMap)
113+ m.AdaptiveThinking = c.AdaptiveThinking
114+ m.PriceInput = c.PriceInput
115+ m.PriceOutput = c.PriceOutput
116+ m.PriceCacheRead = c.PriceCacheRead
117+ m.PriceCacheWrite = c.PriceCacheWrite
118+ m.PriceTierInputTokens = c.PriceTierInputTokens
119+ m.PriceInputAbove = c.PriceInputAbove
120+ m.PriceOutputAbove = c.PriceOutputAbove
121+ m.PriceCacheReadAbove = c.PriceCacheReadAbove
122+ m.PriceCacheWriteAbove = c.PriceCacheWriteAbove
123+ if c.DisplayName != "" {
124+ m.DisplayName = c.DisplayName
125+ if cp, _ := bedrockSplitGeoPrefix(c.ID); cp != "" {
126+ suffix := " (" + cp + ")"
127+ if strings.HasSuffix(strings.ToUpper(m.DisplayName), strings.ToUpper(suffix)) {
128+ m.DisplayName = m.DisplayName[:len(m.DisplayName)-len(suffix)]
129+ }
130+ }
131+ if prefix != "" {
132+ m.DisplayName += " (" + strings.ToUpper(prefix) + ")"
133+ }
134+ }
135+ return m
136+ }
137+ // Unknown models have no verified output or context limits. Keep the
138+ // client's 4096-token fallback rather than guessing from the family:
139+ // even Claude Opus 4 supports only 32k, not the 64k of newer models.
140+ return m
141+}
142+
143+// bedrockSplitGeoPrefix separates a cross-region inference-profile geo
144+// prefix from the foundation-model ID. prefix is "" when there is none.
145+func bedrockSplitGeoPrefix(id string) (prefix, base string) {
146+ for _, p := range bedrockGeoPrefixes {
147+ if strings.HasPrefix(id, p+".") {
148+ return p, id[len(p)+1:]
149+ }
150+ }
151+ return "", id
152+}
153+
154+func bedrockInCatalog(id string) bool {
155+ for _, c := range Catalog {
156+ if c.Provider == "amazon-bedrock" && c.ID == id {
157+ return true
158+ }
159+ }
160+ return false
161+}
162+
163+// bedrockCatalogBase finds catalog metadata for a foundation-model ID:
164+// the bare entry if present, otherwise any geo-prefixed variant of it.
165+func bedrockCatalogBase(base string) (Model, bool) {
166+ var variant Model
167+ found := false
168+ for _, c := range Catalog {
169+ if c.Provider != "amazon-bedrock" {
170+ continue
171+ }
172+ if c.ID == base {
173+ return c, true
174+ }
175+ if !found {
176+ if _, b := bedrockSplitGeoPrefix(c.ID); b == base {
177+ variant, found = c, true
178+ }
179+ }
180+ }
181+ return variant, found
182+}
183+
93 // bedrockListModelIDs enumerates foundation-model IDs and inference-184 // bedrockListModelIDs enumerates foundation-model IDs and inference-
94 // profile IDs available in the region. Profile IDs are the geo-prefixed185 // profile IDs available in the region. Profile IDs are the geo-prefixed
95 // forms zot invokes; both are returned so the picker shows every186 // forms zot invokes; both are returned so the picker shows every
A packages/provider/discover_bedrock_metadata_test.go +107 -0
@@ -0,0 +1,107 @@
1+package provider
2+
3+import "testing"
4+
5+// discoveredWireMaxTokens runs a discovered ID through the same path as
6+// production (discovery metadata -> MergeCatalog -> FindModel -> the
7+// agent's MaxTokens -> Bedrock buildRequest) and returns the merged model
8+// plus the maxTokens that would be sent on the wire.
9+func discoveredWireMaxTokens(t *testing.T, id string) (Model, int) {
10+ t.Helper()
11+ withLiveModels(t, []Model{bedrockDiscoveredModel(id, "us-east-1")})
12+ m, err := FindModel("amazon-bedrock", id)
13+ if err != nil {
14+ t.Fatalf("FindModel(%q): %v", id, err)
15+ }
16+ c := &bedrockClient{region: "us-east-1"}
17+ req, err := c.buildRequest(Request{Model: id, MaxTokens: m.MaxOutput})
18+ if err != nil {
19+ t.Fatalf("buildRequest(%q): %v", id, err)
20+ }
21+ return m, req.InferenceConfig.MaxTokens
22+}
23+
24+// Discovered inference-profile IDs with no exact catalog entry used to
25+// carry MaxOutput 0, so every request fell back to maxTokens 4096 and
26+// long responses stopped with max_tokens.
27+func TestBedrockDiscoveredProfileInheritsBaseMetadata(t *testing.T) {
28+ base, err := FindModel("amazon-bedrock", "anthropic.claude-sonnet-4-6")
29+ if err != nil {
30+ t.Fatal(err)
31+ }
32+ for _, id := range []string{"apac.anthropic.claude-sonnet-4-6", "us-gov.anthropic.claude-sonnet-4-6"} {
33+ m, wire := discoveredWireMaxTokens(t, id)
34+ if m.MaxOutput != base.MaxOutput || wire != base.MaxOutput {
35+ t.Errorf("%s: MaxOutput=%d wire=%d, want %d", id, m.MaxOutput, wire, base.MaxOutput)
36+ }
37+ if m.ContextWindow != base.ContextWindow || m.Reasoning != base.Reasoning {
38+ t.Errorf("%s: metadata not inherited: %+v", id, m)
39+ }
40+ if m.PriceOutput != base.PriceOutput {
41+ t.Errorf("%s: PriceOutput=%v, want %v", id, m.PriceOutput, base.PriceOutput)
42+ }
43+ }
44+
45+ m, _ := discoveredWireMaxTokens(t, "apac.anthropic.claude-sonnet-4-6")
46+ if m.DisplayName != "Claude Sonnet 4.6 (APAC)" {
47+ t.Errorf("DisplayName = %q", m.DisplayName)
48+ }
49+}
50+
51+// A base model catalogued only under geo-prefixed IDs still donates its
52+// metadata, and the donor's region label is not carried over.
53+func TestBedrockDiscoveredProfileUsesPrefixedCatalogVariant(t *testing.T) {
54+ const id = "apac.anthropic.claude-opus-5"
55+ m, wire := discoveredWireMaxTokens(t, id)
56+ if wire != 128000 || !m.AdaptiveThinking || m.PriceOutput != 25 {
57+ t.Errorf("%s: wire=%d adaptive=%v price=%v, want 128000 adaptive and $25", id, wire, m.AdaptiveThinking, m.PriceOutput)
58+ }
59+ if m.DisplayName != "Claude Opus 5 (APAC)" {
60+ t.Errorf("DisplayName = %q", m.DisplayName)
61+ }
62+}
63+
64+// An uncatalogued Claude ID must not assume a 64k output limit. Opus 4
65+// supports only 32k, and an unknown future model has no verified limit.
66+func TestBedrockDiscoveredUnknownClaudeKeepsFallback(t *testing.T) {
67+ for _, id := range []string{
68+ "anthropic.claude-opus-4-20250514-v1:0",
69+ "us.anthropic.claude-opus-4-20250514-v1:0",
70+ "anthropic.claude-future-9",
71+ } {
72+ m, wire := discoveredWireMaxTokens(t, id)
73+ if m.MaxOutput != 0 || m.ContextWindow != 0 || wire != 4096 {
74+ t.Errorf("%s: MaxOutput=%d ctx=%d wire=%d, want 0/0/4096", id, m.MaxOutput, m.ContextWindow, wire)
75+ }
76+ }
77+}
78+
79+// Legacy Claude and non-Anthropic models cap output well below 64k and
80+// would reject a larger request, so they keep the client fallback.
81+func TestBedrockDiscoveredLegacyAndOtherModelsKeepFallback(t *testing.T) {
82+ for _, id := range []string{
83+ "anthropic.claude-v2:1",
84+ "anthropic.claude-instant-v1",
85+ "us.anthropic.claude-3-5-sonnet-20241022-v2:0",
86+ "anthropic.claude-3-haiku-20240307-v1:0",
87+ "mistral.unknown-model-v1:0",
88+ } {
89+ m, wire := discoveredWireMaxTokens(t, id)
90+ if m.MaxOutput != 0 || wire != 4096 {
91+ t.Errorf("%s: MaxOutput=%d wire=%d, want 0/4096", id, m.MaxOutput, wire)
92+ }
93+ }
94+}
95+
96+// Exact catalog IDs keep their own catalog metadata through MergeCatalog.
97+func TestBedrockDiscoveredExactCatalogIDUnchanged(t *testing.T) {
98+ const id = "au.anthropic.claude-sonnet-4-6"
99+ want, err := FindModel("amazon-bedrock", id)
100+ if err != nil {
101+ t.Fatal(err)
102+ }
103+ m, wire := discoveredWireMaxTokens(t, id)
104+ if m.MaxOutput != want.MaxOutput || wire != want.MaxOutput || m.DisplayName != want.DisplayName {
105+ t.Errorf("got MaxOutput=%d wire=%d name=%q, want %d %q", m.MaxOutput, wire, m.DisplayName, want.MaxOutput, want.DisplayName)
106+ }
107+}
@@ -0,0 +1,107 @@
1+package provider
2+
3+import "testing"
4+
5+// discoveredWireMaxTokens runs a discovered ID through the same path as
6+// production (discovery metadata -> MergeCatalog -> FindModel -> the
7+// agent's MaxTokens -> Bedrock buildRequest) and returns the merged model
8+// plus the maxTokens that would be sent on the wire.
9+func discoveredWireMaxTokens(t *testing.T, id string) (Model, int) {
10+ t.Helper()
11+ withLiveModels(t, []Model{bedrockDiscoveredModel(id, "us-east-1")})
12+ m, err := FindModel("amazon-bedrock", id)
13+ if err != nil {
14+ t.Fatalf("FindModel(%q): %v", id, err)
15+ }
16+ c := &bedrockClient{region: "us-east-1"}
17+ req, err := c.buildRequest(Request{Model: id, MaxTokens: m.MaxOutput})
18+ if err != nil {
19+ t.Fatalf("buildRequest(%q): %v", id, err)
20+ }
21+ return m, req.InferenceConfig.MaxTokens
22+}
23+
24+// Discovered inference-profile IDs with no exact catalog entry used to
25+// carry MaxOutput 0, so every request fell back to maxTokens 4096 and
26+// long responses stopped with max_tokens.
27+func TestBedrockDiscoveredProfileInheritsBaseMetadata(t *testing.T) {
28+ base, err := FindModel("amazon-bedrock", "anthropic.claude-sonnet-4-6")
29+ if err != nil {
30+ t.Fatal(err)
31+ }
32+ for _, id := range []string{"apac.anthropic.claude-sonnet-4-6", "us-gov.anthropic.claude-sonnet-4-6"} {
33+ m, wire := discoveredWireMaxTokens(t, id)
34+ if m.MaxOutput != base.MaxOutput || wire != base.MaxOutput {
35+ t.Errorf("%s: MaxOutput=%d wire=%d, want %d", id, m.MaxOutput, wire, base.MaxOutput)
36+ }
37+ if m.ContextWindow != base.ContextWindow || m.Reasoning != base.Reasoning {
38+ t.Errorf("%s: metadata not inherited: %+v", id, m)
39+ }
40+ if m.PriceOutput != base.PriceOutput {
41+ t.Errorf("%s: PriceOutput=%v, want %v", id, m.PriceOutput, base.PriceOutput)
42+ }
43+ }
44+
45+ m, _ := discoveredWireMaxTokens(t, "apac.anthropic.claude-sonnet-4-6")
46+ if m.DisplayName != "Claude Sonnet 4.6 (APAC)" {
47+ t.Errorf("DisplayName = %q", m.DisplayName)
48+ }
49+}
50+
51+// A base model catalogued only under geo-prefixed IDs still donates its
52+// metadata, and the donor's region label is not carried over.
53+func TestBedrockDiscoveredProfileUsesPrefixedCatalogVariant(t *testing.T) {
54+ const id = "apac.anthropic.claude-opus-5"
55+ m, wire := discoveredWireMaxTokens(t, id)
56+ if wire != 128000 || !m.AdaptiveThinking || m.PriceOutput != 25 {
57+ t.Errorf("%s: wire=%d adaptive=%v price=%v, want 128000 adaptive and $25", id, wire, m.AdaptiveThinking, m.PriceOutput)
58+ }
59+ if m.DisplayName != "Claude Opus 5 (APAC)" {
60+ t.Errorf("DisplayName = %q", m.DisplayName)
61+ }
62+}
63+
64+// An uncatalogued Claude ID must not assume a 64k output limit. Opus 4
65+// supports only 32k, and an unknown future model has no verified limit.
66+func TestBedrockDiscoveredUnknownClaudeKeepsFallback(t *testing.T) {
67+ for _, id := range []string{
68+ "anthropic.claude-opus-4-20250514-v1:0",
69+ "us.anthropic.claude-opus-4-20250514-v1:0",
70+ "anthropic.claude-future-9",
71+ } {
72+ m, wire := discoveredWireMaxTokens(t, id)
73+ if m.MaxOutput != 0 || m.ContextWindow != 0 || wire != 4096 {
74+ t.Errorf("%s: MaxOutput=%d ctx=%d wire=%d, want 0/0/4096", id, m.MaxOutput, m.ContextWindow, wire)
75+ }
76+ }
77+}
78+
79+// Legacy Claude and non-Anthropic models cap output well below 64k and
80+// would reject a larger request, so they keep the client fallback.
81+func TestBedrockDiscoveredLegacyAndOtherModelsKeepFallback(t *testing.T) {
82+ for _, id := range []string{
83+ "anthropic.claude-v2:1",
84+ "anthropic.claude-instant-v1",
85+ "us.anthropic.claude-3-5-sonnet-20241022-v2:0",
86+ "anthropic.claude-3-haiku-20240307-v1:0",
87+ "mistral.unknown-model-v1:0",
88+ } {
89+ m, wire := discoveredWireMaxTokens(t, id)
90+ if m.MaxOutput != 0 || wire != 4096 {
91+ t.Errorf("%s: MaxOutput=%d wire=%d, want 0/4096", id, m.MaxOutput, wire)
92+ }
93+ }
94+}
95+
96+// Exact catalog IDs keep their own catalog metadata through MergeCatalog.
97+func TestBedrockDiscoveredExactCatalogIDUnchanged(t *testing.T) {
98+ const id = "au.anthropic.claude-sonnet-4-6"
99+ want, err := FindModel("amazon-bedrock", id)
100+ if err != nil {
101+ t.Fatal(err)
102+ }
103+ m, wire := discoveredWireMaxTokens(t, id)
104+ if m.MaxOutput != want.MaxOutput || wire != want.MaxOutput || m.DisplayName != want.DisplayName {
105+ t.Errorf("got MaxOutput=%d wire=%d name=%q, want %d %q", m.MaxOutput, wire, m.DisplayName, want.MaxOutput, want.DisplayName)
106+ }
107+}

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