detached a993eb3

Avoid unverified Bedrock output limits

commit
a993eb30adb9d2f307080b2f5eabce7fc759208d
author
patriceckhart · · 2026-09-29 20:00 UTC
parent
8cb588b
signature
unsigned
3 files · +22 -46
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 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. Claude models that aren't in the catalog at all get a 64k output / 200k context default; other unknown IDs show up with placeholder pricing and default limits 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 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. Claude models that aren't in the catalog at all get a 64k output / 200k context default; other unknown IDs show up with placeholder pricing and default limits 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/discover_bedrock.go +7 -35
@@ -86,16 +86,6 @@ func DiscoverBedrock(ctx context.Context, region string) ([]Model, error) {
8686 return out, nil
8787 }
8888
89-// bedrockAnthropicDefaultMaxOutput is the output budget assumed for a
90-// discovered Claude model with no catalog entry. It matches Claude Haiku
91-// 4.5, the smallest output limit among current Claude models, so it is
92-// safe for any Claude 4+ model and far above the client's 4096 fallback.
93-const bedrockAnthropicDefaultMaxOutput = 64000
94-
95-// bedrockAnthropicDefaultContextWindow is the context window assumed
96-// for an uncatalogued Claude model, the smallest among current models.
97-const bedrockAnthropicDefaultContextWindow = 200000
98-
9989 // bedrockDiscoveredModel builds the Model for a discovered ID. The
10090 // control plane returns IDs only, so limits and capabilities come from
10191 // the static catalog. MergeCatalog already uses an exact catalog match;
@@ -133,7 +123,10 @@ func bedrockDiscoveredModel(id, region string) Model {
133123 if c.DisplayName != "" {
134124 m.DisplayName = c.DisplayName
135125 if cp, _ := bedrockSplitGeoPrefix(c.ID); cp != "" {
136- m.DisplayName = strings.TrimSuffix(m.DisplayName, " ("+strings.ToUpper(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+ }
137130 }
138131 if prefix != "" {
139132 m.DisplayName += " (" + strings.ToUpper(prefix) + ")"
@@ -141,10 +134,9 @@ func bedrockDiscoveredModel(id, region string) Model {
141134 }
142135 return m
143136 }
144- if bedrockIsModernClaude(base) {
145- m.ContextWindow = bedrockAnthropicDefaultContextWindow
146- m.MaxOutput = bedrockAnthropicDefaultMaxOutput
147- }
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.
148140 return m
149141 }
150142
@@ -189,26 +181,6 @@ func bedrockCatalogBase(base string) (Model, bool) {
189181 return variant, found
190182 }
191183
192-// bedrockIsModernClaude reports whether a foundation-model ID is a Claude
193-// model whose output limit is at least bedrockAnthropicDefaultMaxOutput.
194-// Legacy Claude 2 / Instant / 3 / 3.5 models cap output at 4096-8192 and
195-// would reject a 64000-token request, so they keep the client fallback.
196-func bedrockIsModernClaude(base string) bool {
197- if !strings.HasPrefix(base, "anthropic.claude-") {
198- return false
199- }
200- switch {
201- case strings.HasPrefix(base, "anthropic.claude-v"),
202- strings.HasPrefix(base, "anthropic.claude-instant"),
203- strings.HasPrefix(base, "anthropic.claude-3-5-"),
204- strings.HasPrefix(base, "anthropic.claude-3-haiku"),
205- strings.HasPrefix(base, "anthropic.claude-3-sonnet"),
206- strings.HasPrefix(base, "anthropic.claude-3-opus"):
207- return false
208- }
209- return true
210-}
211-
212184 // bedrockListModelIDs enumerates foundation-model IDs and inference-
213185 // profile IDs available in the region. Profile IDs are the geo-prefixed
214186 // forms zot invokes; both are returned so the picker shows every
@@ -86,16 +86,6 @@ func DiscoverBedrock(ctx context.Context, region string) ([]Model, error) {
86 return out, nil86 return out, nil
87 }87 }
88 88
89-// bedrockAnthropicDefaultMaxOutput is the output budget assumed for a
90-// discovered Claude model with no catalog entry. It matches Claude Haiku
91-// 4.5, the smallest output limit among current Claude models, so it is
92-// safe for any Claude 4+ model and far above the client's 4096 fallback.
93-const bedrockAnthropicDefaultMaxOutput = 64000
94-
95-// bedrockAnthropicDefaultContextWindow is the context window assumed
96-// for an uncatalogued Claude model, the smallest among current models.
97-const bedrockAnthropicDefaultContextWindow = 200000
98-
99 // bedrockDiscoveredModel builds the Model for a discovered ID. The89 // bedrockDiscoveredModel builds the Model for a discovered ID. The
100 // control plane returns IDs only, so limits and capabilities come from90 // control plane returns IDs only, so limits and capabilities come from
101 // the static catalog. MergeCatalog already uses an exact catalog match;91 // the static catalog. MergeCatalog already uses an exact catalog match;
@@ -133,7 +123,10 @@ func bedrockDiscoveredModel(id, region string) Model {
133 if c.DisplayName != "" {123 if c.DisplayName != "" {
134 m.DisplayName = c.DisplayName124 m.DisplayName = c.DisplayName
135 if cp, _ := bedrockSplitGeoPrefix(c.ID); cp != "" {125 if cp, _ := bedrockSplitGeoPrefix(c.ID); cp != "" {
136- m.DisplayName = strings.TrimSuffix(m.DisplayName, " ("+strings.ToUpper(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+ }
137 }130 }
138 if prefix != "" {131 if prefix != "" {
139 m.DisplayName += " (" + strings.ToUpper(prefix) + ")"132 m.DisplayName += " (" + strings.ToUpper(prefix) + ")"
@@ -141,10 +134,9 @@ func bedrockDiscoveredModel(id, region string) Model {
141 }134 }
142 return m135 return m
143 }136 }
144- if bedrockIsModernClaude(base) {137+ // Unknown models have no verified output or context limits. Keep the
145- m.ContextWindow = bedrockAnthropicDefaultContextWindow138+ // client's 4096-token fallback rather than guessing from the family:
146- m.MaxOutput = bedrockAnthropicDefaultMaxOutput139+ // even Claude Opus 4 supports only 32k, not the 64k of newer models.
147- }
148 return m140 return m
149 }141 }
150 142
@@ -189,26 +181,6 @@ func bedrockCatalogBase(base string) (Model, bool) {
189 return variant, found181 return variant, found
190 }182 }
191 183
192-// bedrockIsModernClaude reports whether a foundation-model ID is a Claude
193-// model whose output limit is at least bedrockAnthropicDefaultMaxOutput.
194-// Legacy Claude 2 / Instant / 3 / 3.5 models cap output at 4096-8192 and
195-// would reject a 64000-token request, so they keep the client fallback.
196-func bedrockIsModernClaude(base string) bool {
197- if !strings.HasPrefix(base, "anthropic.claude-") {
198- return false
199- }
200- switch {
201- case strings.HasPrefix(base, "anthropic.claude-v"),
202- strings.HasPrefix(base, "anthropic.claude-instant"),
203- strings.HasPrefix(base, "anthropic.claude-3-5-"),
204- strings.HasPrefix(base, "anthropic.claude-3-haiku"),
205- strings.HasPrefix(base, "anthropic.claude-3-sonnet"),
206- strings.HasPrefix(base, "anthropic.claude-3-opus"):
207- return false
208- }
209- return true
210-}
211-
212 // bedrockListModelIDs enumerates foundation-model IDs and inference-184 // bedrockListModelIDs enumerates foundation-model IDs and inference-
213 // profile IDs available in the region. Profile IDs are the geo-prefixed185 // profile IDs available in the region. Profile IDs are the geo-prefixed
214 // forms zot invokes; both are returned so the picker shows every186 // forms zot invokes; both are returned so the picker shows every
M packages/provider/discover_bedrock_metadata_test.go +14 -10
@@ -51,23 +51,27 @@ func TestBedrockDiscoveredProfileInheritsBaseMetadata(t *testing.T) {
5151 // A base model catalogued only under geo-prefixed IDs still donates its
5252 // metadata, and the donor's region label is not carried over.
5353 func TestBedrockDiscoveredProfileUsesPrefixedCatalogVariant(t *testing.T) {
54- const id = "apac.anthropic.claude-opus-5-5"
54+ const id = "apac.anthropic.claude-opus-5"
5555 m, wire := discoveredWireMaxTokens(t, id)
56- if wire != 128000 || !m.AdaptiveThinking {
57- t.Errorf("%s: wire=%d adaptive=%v, want 128000 adaptive", id, wire, m.AdaptiveThinking)
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)
5858 }
59- if m.DisplayName != "Claude Opus 5.5 (APAC)" {
59+ if m.DisplayName != "Claude Opus 5 (APAC)" {
6060 t.Errorf("DisplayName = %q", m.DisplayName)
6161 }
6262 }
6363
64-// Claude models absent from the catalog get Haiku 4.5's 64k output, the
65-// smallest among current Claude models, instead of the 4096 fallback.
66-func TestBedrockDiscoveredUnknownClaudeUses64kFloor(t *testing.T) {
67- for _, id := range []string{"anthropic.claude-future-9", "us.anthropic.claude-future-9-v1:0"} {
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+ } {
6872 m, wire := discoveredWireMaxTokens(t, id)
69- if wire != bedrockAnthropicDefaultMaxOutput || m.ContextWindow != bedrockAnthropicDefaultContextWindow {
70- t.Errorf("%s: wire=%d ctx=%d", id, wire, m.ContextWindow)
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)
7175 }
7276 }
7377 }
@@ -51,23 +51,27 @@ func TestBedrockDiscoveredProfileInheritsBaseMetadata(t *testing.T) {
51 // A base model catalogued only under geo-prefixed IDs still donates its51 // A base model catalogued only under geo-prefixed IDs still donates its
52 // metadata, and the donor's region label is not carried over.52 // metadata, and the donor's region label is not carried over.
53 func TestBedrockDiscoveredProfileUsesPrefixedCatalogVariant(t *testing.T) {53 func TestBedrockDiscoveredProfileUsesPrefixedCatalogVariant(t *testing.T) {
54- const id = "apac.anthropic.claude-opus-5-5"54+ const id = "apac.anthropic.claude-opus-5"
55 m, wire := discoveredWireMaxTokens(t, id)55 m, wire := discoveredWireMaxTokens(t, id)
56- if wire != 128000 || !m.AdaptiveThinking {56+ if wire != 128000 || !m.AdaptiveThinking || m.PriceOutput != 25 {
57- t.Errorf("%s: wire=%d adaptive=%v, want 128000 adaptive", id, wire, m.AdaptiveThinking)57+ t.Errorf("%s: wire=%d adaptive=%v price=%v, want 128000 adaptive and $25", id, wire, m.AdaptiveThinking, m.PriceOutput)
58 }58 }
59- if m.DisplayName != "Claude Opus 5.5 (APAC)" {59+ if m.DisplayName != "Claude Opus 5 (APAC)" {
60 t.Errorf("DisplayName = %q", m.DisplayName)60 t.Errorf("DisplayName = %q", m.DisplayName)
61 }61 }
62 }62 }
63 63
64-// Claude models absent from the catalog get Haiku 4.5's 64k output, the64+// An uncatalogued Claude ID must not assume a 64k output limit. Opus 4
65-// smallest among current Claude models, instead of the 4096 fallback.65+// supports only 32k, and an unknown future model has no verified limit.
66-func TestBedrockDiscoveredUnknownClaudeUses64kFloor(t *testing.T) {66+func TestBedrockDiscoveredUnknownClaudeKeepsFallback(t *testing.T) {
67- for _, id := range []string{"anthropic.claude-future-9", "us.anthropic.claude-future-9-v1:0"} {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+ } {
68 m, wire := discoveredWireMaxTokens(t, id)72 m, wire := discoveredWireMaxTokens(t, id)
69- if wire != bedrockAnthropicDefaultMaxOutput || m.ContextWindow != bedrockAnthropicDefaultContextWindow {73+ if m.MaxOutput != 0 || m.ContextWindow != 0 || wire != 4096 {
70- t.Errorf("%s: wire=%d ctx=%d", id, wire, m.ContextWindow)74+ t.Errorf("%s: MaxOutput=%d ctx=%d wire=%d, want 0/0/4096", id, m.MaxOutput, m.ContextWindow, wire)
71 }75 }
72 }76 }
73 }77 }

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