Skip to content

Commit c72dc16

Browse files
committed
feat(bedrock): port robust support
1 parent d2fb21e commit c72dc16

37 files changed

Lines changed: 3929 additions & 318 deletions
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import type { ModelInfo } from "../model.js"
2+
import type { BedrockDiscoveredTarget } from "../providers/bedrock.js"
3+
import {
4+
BEDROCK_1M_CONTEXT_DEFAULT_MODEL_IDS,
5+
BEDROCK_1M_CONTEXT_MODEL_IDS,
6+
BEDROCK_1M_CONTEXT_OPT_IN_MODEL_IDS,
7+
BEDROCK_GLOBAL_INFERENCE_MODEL_IDS,
8+
BEDROCK_NATIVE_1M_CONTEXT_MODEL_IDS,
9+
bedrockModels,
10+
expandBedrockTargetsWith1MVariants,
11+
hasBedrock1MContextIndicator,
12+
resolveBedrockModelInfo,
13+
stripBedrock1MContextSuffix,
14+
} from "../providers/bedrock.js"
15+
16+
describe("Bedrock model catalog", () => {
17+
it("includes Claude Opus 4.7 with flat $5/$25 pricing across its 200K base and 1M tier", () => {
18+
// `bedrockModels` is typed as a giant discriminated union across every model entry,
19+
// so narrowing to `ModelInfo` lets us read the common fields without per-model checks.
20+
const opus47 = bedrockModels["anthropic.claude-opus-4-7" as keyof typeof bedrockModels] as ModelInfo
21+
expect(opus47).toBeDefined()
22+
expect(opus47.contextWindow).toBe(200_000)
23+
expect(opus47.supportsReasoningBudget).toBe(true)
24+
expect(opus47.inputPrice).toBe(5.0)
25+
expect(opus47.outputPrice).toBe(25.0)
26+
27+
// Tier exists only so the dropdown split can offer a 1M variant; pricing is flat.
28+
const tier = opus47.tiers?.[0]
29+
expect(tier).toBeDefined()
30+
expect(tier?.contextWindow).toBe(1_000_000)
31+
expect(tier?.inputPrice).toBe(5.0)
32+
expect(tier?.outputPrice).toBe(25.0)
33+
})
34+
35+
it("flags Opus 4.7 as a native-1M model (so beta flags are NOT sent at runtime)", () => {
36+
expect(BEDROCK_NATIVE_1M_CONTEXT_MODEL_IDS).toContain("anthropic.claude-opus-4-7")
37+
})
38+
39+
it("includes Opus 4.7 in the 1M-capable and global-inference lists", () => {
40+
expect(BEDROCK_1M_CONTEXT_MODEL_IDS).toContain("anthropic.claude-opus-4-7")
41+
expect(BEDROCK_GLOBAL_INFERENCE_MODEL_IDS).toContain("anthropic.claude-opus-4-7")
42+
})
43+
44+
it("makes every 1M-capable model opt-in (empty default list)", () => {
45+
// Previously Sonnet 4.6 and Opus 4.6 auto-advertised 1M; the new dual-variant dropdown
46+
// presents both context tiers explicitly, so no model is auto-flipped anymore.
47+
expect(BEDROCK_1M_CONTEXT_DEFAULT_MODEL_IDS.length).toBe(0)
48+
expect(BEDROCK_1M_CONTEXT_OPT_IN_MODEL_IDS.length).toBe(BEDROCK_1M_CONTEXT_MODEL_IDS.length)
49+
})
50+
51+
it("matches per-model maxTokens to the documented Bedrock caps for current Anthropic models", () => {
52+
// These caps mirror the Anthropic-direct entries in `anthropic.ts`. Bumping them lets the
53+
// reasoning-budget slider extend past the legacy 8K cap (the original bug surfaced by Opus 4.7).
54+
expect((bedrockModels["anthropic.claude-opus-4-7"] as ModelInfo).maxTokens).toBe(128_000)
55+
expect((bedrockModels["anthropic.claude-opus-4-6-v1"] as ModelInfo).maxTokens).toBe(128_000)
56+
expect((bedrockModels["anthropic.claude-opus-4-5-20251101-v1:0"] as ModelInfo).maxTokens).toBe(32_000)
57+
expect((bedrockModels["anthropic.claude-opus-4-1-20250805-v1:0"] as ModelInfo).maxTokens).toBe(32_000)
58+
expect((bedrockModels["anthropic.claude-sonnet-4-6"] as ModelInfo).maxTokens).toBe(64_000)
59+
expect((bedrockModels["anthropic.claude-sonnet-4-5-20250929-v1:0"] as ModelInfo).maxTokens).toBe(64_000)
60+
expect((bedrockModels["anthropic.claude-haiku-4-5-20251001-v1:0"] as ModelInfo).maxTokens).toBe(64_000)
61+
})
62+
63+
it("marks AWS-documented Claude 4.5 models for 1-hour Bedrock prompt cache TTL", () => {
64+
expect((bedrockModels["anthropic.claude-sonnet-4-5-20250929-v1:0"] as ModelInfo).promptCacheTtl).toBe("1h")
65+
expect((bedrockModels["anthropic.claude-haiku-4-5-20251001-v1:0"] as ModelInfo).promptCacheTtl).toBe("1h")
66+
expect((bedrockModels["anthropic.claude-opus-4-5-20251101-v1:0"] as ModelInfo).promptCacheTtl).toBe("1h")
67+
})
68+
})
69+
70+
describe("resolveBedrockModelInfo", () => {
71+
it("prefers the static maxTokens when no override is set", () => {
72+
const { info } = resolveBedrockModelInfo({
73+
baseModelId: "anthropic.claude-opus-4-7",
74+
targetId: "anthropic.claude-opus-4-7",
75+
})
76+
expect(info.maxTokens).toBe(128_000)
77+
})
78+
79+
it("applies maxOutputTokensOverride above the static cap", () => {
80+
const { info } = resolveBedrockModelInfo({
81+
baseModelId: "anthropic.claude-opus-4-7",
82+
targetId: "anthropic.claude-opus-4-7",
83+
maxOutputTokensOverride: 256_000,
84+
})
85+
expect(info.maxTokens).toBe(256_000)
86+
})
87+
88+
it("lets request-time modelMaxTokens still override (lowering for cost control)", () => {
89+
const { info } = resolveBedrockModelInfo({
90+
baseModelId: "anthropic.claude-opus-4-7",
91+
targetId: "anthropic.claude-opus-4-7",
92+
maxOutputTokensOverride: 256_000,
93+
modelMaxTokens: 32_000,
94+
})
95+
expect(info.maxTokens).toBe(32_000)
96+
})
97+
})
98+
99+
describe("expandBedrockTargetsWith1MVariants", () => {
100+
const makeTarget = (overrides: Partial<BedrockDiscoveredTarget> = {}): BedrockDiscoveredTarget => ({
101+
id: "anthropic.claude-opus-4-6-v1",
102+
label: "Claude Opus 4.6",
103+
baseModelId: "anthropic.claude-opus-4-6-v1",
104+
targetKind: "foundation-model",
105+
contextWindow: 200_000,
106+
contextSource: "base",
107+
...overrides,
108+
})
109+
110+
it("emits two entries for a 1M-capable base target: default + :1m variant", () => {
111+
const expanded = expandBedrockTargetsWith1MVariants([makeTarget()])
112+
113+
expect(expanded).toHaveLength(2)
114+
const [base, oneM] = expanded as [BedrockDiscoveredTarget, BedrockDiscoveredTarget]
115+
expect(base.id).toBe("anthropic.claude-opus-4-6-v1")
116+
expect(base.contextWindow).toBe(200_000)
117+
expect(oneM.id).toBe("anthropic.claude-opus-4-6-v1:1m")
118+
expect(oneM.contextWindow).toBe(1_000_000)
119+
expect(oneM.label).toMatch(/1M context/i)
120+
expect(oneM.contextSource).toBe("profile-id")
121+
})
122+
123+
it("also emits two entries when AWS discovery returns a system profile id", () => {
124+
const expanded = expandBedrockTargetsWith1MVariants([
125+
makeTarget({
126+
id: "us.anthropic.claude-opus-4-7",
127+
baseModelId: "anthropic.claude-opus-4-7",
128+
targetKind: "system-profile",
129+
label: "Claude Opus 4.7 (us.anthropic.claude-opus-4-7)",
130+
}),
131+
])
132+
133+
expect(expanded).toHaveLength(2)
134+
const oneM = expanded[1] as BedrockDiscoveredTarget
135+
expect(oneM.id).toBe("us.anthropic.claude-opus-4-7:1m")
136+
expect(hasBedrock1MContextIndicator(oneM.id)).toBe(true)
137+
expect(stripBedrock1MContextSuffix(oneM.id)).toBe("us.anthropic.claude-opus-4-7")
138+
})
139+
140+
it("leaves non-1M-capable targets untouched", () => {
141+
const expanded = expandBedrockTargetsWith1MVariants([
142+
makeTarget({
143+
id: "anthropic.claude-3-5-sonnet-20241022-v2:0",
144+
baseModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
145+
}),
146+
])
147+
148+
expect(expanded).toHaveLength(1)
149+
expect((expanded[0] as BedrockDiscoveredTarget).id).toBe("anthropic.claude-3-5-sonnet-20241022-v2:0")
150+
})
151+
152+
it("does not double-expand a target that already represents the 1M variant", () => {
153+
const expanded = expandBedrockTargetsWith1MVariants([
154+
makeTarget({ id: "anthropic.claude-opus-4-6-v1:1m", contextWindow: 1_000_000 }),
155+
])
156+
157+
expect(expanded).toHaveLength(1)
158+
expect((expanded[0] as BedrockDiscoveredTarget).id).toBe("anthropic.claude-opus-4-6-v1:1m")
159+
})
160+
})

packages/types/src/global-settings.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,11 @@ export const globalSettingsSchema = z.object({
232232
* Tools in this list will be excluded from prompt generation and rejected at execution time.
233233
*/
234234
disabledTools: z.array(toolNamesSchema).optional(),
235+
/**
236+
* Hidden per-model cache of AWS Bedrock models observed to reject strict structured output.
237+
* Map key: Bedrock model id / ARN. Map value: expiry timestamp (ms since epoch).
238+
*/
239+
bedrockStructuredOutputUnsupported: z.record(z.string(), z.number()).optional(),
235240
})
236241

237242
export type GlobalSettings = z.infer<typeof globalSettingsSchema>

packages/types/src/model.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ export const modelInfoSchema = z.object({
9898
outputPrice: z.number().optional(),
9999
cacheWritesPrice: z.number().optional(),
100100
cacheReadsPrice: z.number().optional(),
101+
// Bedrock Converse cachePoint TTL values. Omitted means provider default.
102+
promptCacheTtl: z.enum(["5m", "1h"]).optional(),
101103
longContextPricing: z
102104
.object({
103105
thresholdTokens: z.number(),

packages/types/src/provider-settings.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,11 +230,19 @@ const bedrockSchema = apiModelIdProviderModelSchema.extend({
230230
awsApiKey: z.string().optional(),
231231
awsUseApiKey: z.boolean().optional(),
232232
awsCustomArn: z.string().optional(),
233+
awsBedrockInvokeTarget: z.string().optional(),
234+
awsBedrockTargetKind: z
235+
.enum(["foundation-model", "system-profile", "application-profile", "custom-arn", "prompt-router", "unknown"])
236+
.optional(),
233237
awsModelContextWindow: z.number().optional(),
238+
// Empirically detected (or manually entered) per-config cap on the model's max output tokens.
239+
// Takes precedence over the static `bedrockModels.<id>.maxTokens` table when present.
240+
awsModelMaxOutputTokens: z.number().optional(),
234241
awsBedrockEndpointEnabled: z.boolean().optional(),
235242
awsBedrockEndpoint: z.string().optional(),
236243
awsBedrock1MContext: z.boolean().optional(), // Enable 'context-1m-2025-08-07' beta for 1M context window.
237244
awsBedrockServiceTier: z.enum(["STANDARD", "FLEX", "PRIORITY"]).optional(), // AWS Bedrock service tier selection
245+
awsBedrockStructuredOutput: z.boolean().optional(), // Attempt strict JSON-schema tool validation on Bedrock Converse. Default ON when unset.
238246
})
239247

240248
const vertexSchema = apiModelIdProviderModelSchema.extend({

0 commit comments

Comments
 (0)