Skip to content

Commit a6c2c58

Browse files
committed
feat: enable Claude 4.5 global inference profile support for AWS Bedrock
- Add detection for Claude Sonnet 4.5 models in custom ARNs - Enable thinking tokens and 1M context options for Claude 4.5 global inference profiles - Update UI to show options for custom ARNs matching Claude 4.5 patterns - Add comprehensive tests for global inference profile support Fixes #8541
1 parent 5a3f911 commit a6c2c58

File tree

4 files changed

+324
-7
lines changed

4 files changed

+324
-7
lines changed
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
// npx vitest run src/api/providers/__tests__/bedrock-global-inference.spec.ts
2+
3+
import { AwsBedrockHandler } from "../bedrock"
4+
import { BedrockRuntimeClient, ConverseStreamCommand } from "@aws-sdk/client-bedrock-runtime"
5+
import { logger } from "../../../utils/logging"
6+
import type { ProviderSettings } from "@roo-code/types"
7+
8+
// Mock AWS SDK modules
9+
vitest.mock("@aws-sdk/client-bedrock-runtime", () => {
10+
const mockSend = vi.fn().mockResolvedValue({
11+
stream: (async function* () {
12+
yield {
13+
contentBlockStart: {
14+
start: { text: "Test response" },
15+
},
16+
}
17+
yield {
18+
contentBlockDelta: {
19+
delta: { text: " from Claude" },
20+
},
21+
}
22+
yield {
23+
messageStop: {},
24+
}
25+
})(),
26+
})
27+
28+
return {
29+
BedrockRuntimeClient: vi.fn().mockImplementation(() => ({
30+
send: mockSend,
31+
})),
32+
ConverseStreamCommand: vi.fn(),
33+
ConverseCommand: vi.fn(),
34+
}
35+
})
36+
37+
vitest.mock("../../../utils/logging")
38+
39+
describe("AwsBedrockHandler - Global Inference Profile Support", () => {
40+
let handler: AwsBedrockHandler
41+
let mockSend: any
42+
43+
beforeEach(() => {
44+
vi.clearAllMocks()
45+
mockSend = vi.fn().mockResolvedValue({
46+
stream: (async function* () {
47+
yield {
48+
contentBlockStart: {
49+
start: { text: "Test response" },
50+
},
51+
}
52+
yield {
53+
contentBlockDelta: {
54+
delta: { text: " from Claude" },
55+
},
56+
}
57+
yield {
58+
messageStop: {},
59+
}
60+
})(),
61+
})
62+
;(BedrockRuntimeClient as any).mockImplementation(() => ({
63+
send: mockSend,
64+
}))
65+
})
66+
67+
describe("Global Inference Profile ARN Support", () => {
68+
it("should detect Claude Sonnet 4.5 global inference profile ARN", () => {
69+
const options: ProviderSettings = {
70+
apiProvider: "bedrock",
71+
awsRegion: "us-east-1",
72+
awsCustomArn:
73+
"arn:aws:bedrock:us-east-1:148761681080:inference-profile/global.anthropic.claude-sonnet-4-5-20250929-v1:0",
74+
awsAccessKey: "test-key",
75+
awsSecretKey: "test-secret",
76+
}
77+
78+
handler = new AwsBedrockHandler(options)
79+
const model = handler.getModel()
80+
81+
// Should recognize the ARN and provide appropriate model info
82+
expect(model.id).toBe(
83+
"arn:aws:bedrock:us-east-1:148761681080:inference-profile/global.anthropic.claude-sonnet-4-5-20250929-v1:0",
84+
)
85+
expect(model.info).toBeDefined()
86+
expect(model.info.supportsReasoningBudget).toBe(true)
87+
expect(model.info.supportsPromptCache).toBe(true)
88+
expect(model.info.supportsImages).toBe(true)
89+
})
90+
91+
it("should enable 1M context for global inference profile when awsBedrock1MContext is true", async () => {
92+
const options: ProviderSettings = {
93+
apiProvider: "bedrock",
94+
awsRegion: "us-east-1",
95+
awsCustomArn:
96+
"arn:aws:bedrock:us-east-1:148761681080:inference-profile/global.anthropic.claude-sonnet-4-5-20250929-v1:0",
97+
awsBedrock1MContext: true,
98+
awsAccessKey: "test-key",
99+
awsSecretKey: "test-secret",
100+
}
101+
102+
handler = new AwsBedrockHandler(options)
103+
104+
const messages = [{ role: "user" as const, content: "Test message" }]
105+
const stream = handler.createMessage("System prompt", messages)
106+
107+
// Consume the stream
108+
const chunks = []
109+
for await (const chunk of stream) {
110+
chunks.push(chunk)
111+
}
112+
113+
// Check that the command was called
114+
expect(mockSend).toHaveBeenCalled()
115+
expect(ConverseStreamCommand).toHaveBeenCalled()
116+
117+
// Get the payload from the ConverseStreamCommand constructor
118+
const commandPayload = (ConverseStreamCommand as any).mock.calls[0][0]
119+
expect(commandPayload).toBeDefined()
120+
expect(commandPayload.additionalModelRequestFields).toBeDefined()
121+
expect(commandPayload.additionalModelRequestFields.anthropic_beta).toContain("context-1m-2025-08-07")
122+
})
123+
124+
it("should enable thinking/reasoning for global inference profile", async () => {
125+
const options: ProviderSettings = {
126+
apiProvider: "bedrock",
127+
awsRegion: "us-east-1",
128+
awsCustomArn:
129+
"arn:aws:bedrock:us-east-1:148761681080:inference-profile/global.anthropic.claude-sonnet-4-5-20250929-v1:0",
130+
enableReasoningEffort: true,
131+
awsAccessKey: "test-key",
132+
awsSecretKey: "test-secret",
133+
}
134+
135+
handler = new AwsBedrockHandler(options)
136+
137+
const messages = [{ role: "user" as const, content: "Test message" }]
138+
const metadata = {
139+
taskId: "test-task-id",
140+
thinking: {
141+
enabled: true,
142+
maxThinkingTokens: 8192,
143+
},
144+
}
145+
146+
const stream = handler.createMessage("System prompt", messages, metadata)
147+
148+
// Consume the stream
149+
const chunks = []
150+
for await (const chunk of stream) {
151+
chunks.push(chunk)
152+
}
153+
154+
// Check that thinking was enabled
155+
expect(logger.info).toHaveBeenCalledWith(
156+
expect.stringContaining("Extended thinking enabled"),
157+
expect.objectContaining({
158+
ctx: "bedrock",
159+
thinking: expect.objectContaining({
160+
type: "enabled",
161+
budget_tokens: 8192,
162+
}),
163+
}),
164+
)
165+
})
166+
167+
it("should handle various Claude 4.5 ARN patterns", () => {
168+
const testCases = [
169+
"arn:aws:bedrock:us-east-1:148761681080:inference-profile/global.anthropic.claude-sonnet-4-5-20250929-v1:0",
170+
"arn:aws:bedrock:eu-west-1:123456789012:inference-profile/anthropic.claude-sonnet-4-5-20250929-v1:0",
171+
"arn:aws:bedrock:ap-southeast-1:987654321098:foundation-model/anthropic.claude-sonnet-4.5-v1:0",
172+
]
173+
174+
testCases.forEach((arn) => {
175+
const options: ProviderSettings = {
176+
apiProvider: "bedrock",
177+
awsRegion: "us-east-1",
178+
awsCustomArn: arn,
179+
awsAccessKey: "test-key",
180+
awsSecretKey: "test-secret",
181+
}
182+
183+
handler = new AwsBedrockHandler(options)
184+
const model = handler.getModel()
185+
186+
expect(model.info.supportsReasoningBudget).toBe(true)
187+
expect(model.info.supportsPromptCache).toBe(true)
188+
})
189+
})
190+
191+
it("should not enable thinking for non-Claude-4.5 custom ARNs", () => {
192+
const options: ProviderSettings = {
193+
apiProvider: "bedrock",
194+
awsRegion: "us-east-1",
195+
awsCustomArn:
196+
"arn:aws:bedrock:us-east-1:123456789012:foundation-model/anthropic.claude-3-haiku-20240307-v1:0",
197+
awsAccessKey: "test-key",
198+
awsSecretKey: "test-secret",
199+
}
200+
201+
handler = new AwsBedrockHandler(options)
202+
const model = handler.getModel()
203+
204+
// Should not have reasoning budget support for non-Claude-4.5 models
205+
expect(model.info.supportsReasoningBudget).toBeFalsy()
206+
})
207+
})
208+
209+
describe("ARN Parsing with Global Inference Profile", () => {
210+
it("should correctly parse global inference profile ARN", () => {
211+
const handler = new AwsBedrockHandler({
212+
apiProvider: "bedrock",
213+
awsRegion: "us-east-1",
214+
awsAccessKey: "test-key",
215+
awsSecretKey: "test-secret",
216+
})
217+
218+
const parseArn = (handler as any).parseArn.bind(handler)
219+
const result = parseArn(
220+
"arn:aws:bedrock:us-east-1:148761681080:inference-profile/global.anthropic.claude-sonnet-4-5-20250929-v1:0",
221+
)
222+
223+
expect(result.isValid).toBe(true)
224+
expect(result.region).toBe("us-east-1")
225+
expect(result.modelType).toBe("inference-profile")
226+
expect(result.modelId).toContain("anthropic.claude-sonnet-4-5-20250929")
227+
})
228+
})
229+
})

src/api/providers/bedrock.ts

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -252,9 +252,34 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
252252
}
253253

254254
// Helper to guess model info from custom modelId string if not in bedrockModels
255-
private guessModelInfoFromId(modelId: string): Partial<ModelInfo> {
255+
private guessModelInfoFromId(modelId: string | undefined): Partial<ModelInfo> {
256+
// Handle undefined or empty modelId
257+
if (!modelId) {
258+
return {
259+
maxTokens: BEDROCK_MAX_TOKENS,
260+
contextWindow: BEDROCK_DEFAULT_CONTEXT,
261+
supportsImages: false,
262+
supportsPromptCache: false,
263+
}
264+
}
256265
// Define a mapping for model ID patterns and their configurations
257266
const modelConfigMap: Record<string, Partial<ModelInfo>> = {
267+
// Claude 4.5 Sonnet models (including global inference profile)
268+
"claude-sonnet-4-5": {
269+
maxTokens: 8192,
270+
contextWindow: 200_000,
271+
supportsImages: true,
272+
supportsPromptCache: true,
273+
supportsReasoningBudget: true,
274+
},
275+
// Claude 4 Sonnet models
276+
"claude-sonnet-4": {
277+
maxTokens: 8192,
278+
contextWindow: 200_000,
279+
supportsImages: true,
280+
supportsPromptCache: true,
281+
supportsReasoningBudget: true,
282+
},
258283
"claude-4": {
259284
maxTokens: 8192,
260285
contextWindow: 200_000,
@@ -266,6 +291,7 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
266291
contextWindow: 200_000,
267292
supportsImages: true,
268293
supportsPromptCache: true,
294+
supportsReasoningBudget: true,
269295
},
270296
"claude-3-5": {
271297
maxTokens: 8192,
@@ -376,8 +402,10 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
376402
// Check if 1M context is enabled for Claude Sonnet 4
377403
// Use parseBaseModelId to handle cross-region inference prefixes
378404
const baseModelId = this.parseBaseModelId(modelConfig.id)
379-
const is1MContextEnabled =
380-
BEDROCK_1M_CONTEXT_MODEL_IDS.includes(baseModelId as any) && this.options.awsBedrock1MContext
405+
// Check if it's a known model ID or if it's a custom ARN that matches Claude 4.5 pattern
406+
const isEligibleFor1MContext =
407+
BEDROCK_1M_CONTEXT_MODEL_IDS.includes(baseModelId as any) || this.isClaudeSonnet45Model(modelConfig.id)
408+
const is1MContextEnabled = isEligibleFor1MContext && this.options.awsBedrock1MContext
381409

382410
// Add anthropic_beta for 1M context to additionalModelRequestFields
383411
if (is1MContextEnabled) {
@@ -889,6 +917,18 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
889917
return modelId
890918
}
891919

920+
// Helper method to check if a model ID represents a Claude Sonnet 4.5 model
921+
private isClaudeSonnet45Model(modelId: string): boolean {
922+
const id = modelId.toLowerCase()
923+
// Check for various Claude 4.5 patterns including global inference profile
924+
return (
925+
id.includes("claude-sonnet-4-5") ||
926+
id.includes("claude-sonnet-4.5") ||
927+
// Specific check for the global inference profile ARN mentioned in the issue
928+
id.includes("global.anthropic.claude-sonnet-4-5-20250929")
929+
)
930+
}
931+
892932
//Prompt Router responses come back in a different sequence and the model used is in the response and must be fetched by name
893933
getModelById(modelId: string, modelType?: string): { id: BedrockModelId | string; info: ModelInfo } {
894934
// Try to find the model in bedrockModels

webview-ui/src/components/settings/providers/Bedrock.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,25 @@ export const Bedrock = ({ apiConfiguration, setApiConfigurationField, selectedMo
1919
const { t } = useAppTranslation()
2020
const [awsEndpointSelected, setAwsEndpointSelected] = useState(!!apiConfiguration?.awsBedrockEndpointEnabled)
2121

22+
// Helper function to check if a model ID or ARN represents a Claude Sonnet 4.5 model
23+
const isClaudeSonnet45Model = (modelId: string): boolean => {
24+
if (!modelId) return false
25+
const id = modelId.toLowerCase()
26+
return (
27+
id.includes("claude-sonnet-4-5") ||
28+
id.includes("claude-sonnet-4.5") ||
29+
// Specific check for the global inference profile ARN mentioned in the issue
30+
id.includes("global.anthropic.claude-sonnet-4-5-20250929")
31+
)
32+
}
33+
2234
// Check if the selected model supports 1M context (Claude Sonnet 4 / 4.5)
35+
// This includes both known model IDs and custom ARNs that match Claude 4.5 patterns
2336
const supports1MContextBeta =
24-
!!apiConfiguration?.apiModelId && BEDROCK_1M_CONTEXT_MODEL_IDS.includes(apiConfiguration.apiModelId as any)
37+
(!!apiConfiguration?.apiModelId && BEDROCK_1M_CONTEXT_MODEL_IDS.includes(apiConfiguration.apiModelId as any)) ||
38+
(apiConfiguration?.apiModelId === "custom-arn" &&
39+
apiConfiguration?.awsCustomArn &&
40+
isClaudeSonnet45Model(apiConfiguration.awsCustomArn))
2541

2642
// Update the endpoint enabled state when the configuration changes
2743
useEffect(() => {

webview-ui/src/components/ui/hooks/useSelectedModel.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,12 +192,44 @@ function getSelectedModel({
192192
const id = apiConfiguration.apiModelId ?? bedrockDefaultModelId
193193
const baseInfo = bedrockModels[id as keyof typeof bedrockModels]
194194

195+
// Helper function to check if a model ID or ARN represents a Claude Sonnet 4.5 model
196+
const isClaudeSonnet45Model = (modelId: string): boolean => {
197+
if (!modelId) return false
198+
const lowerId = modelId.toLowerCase()
199+
return (
200+
lowerId.includes("claude-sonnet-4-5") ||
201+
lowerId.includes("claude-sonnet-4.5") ||
202+
// Specific check for the global inference profile ARN
203+
lowerId.includes("global.anthropic.claude-sonnet-4-5-20250929")
204+
)
205+
}
206+
195207
// Special case for custom ARN.
196208
if (id === "custom-arn") {
197-
return {
198-
id,
199-
info: { maxTokens: 5000, contextWindow: 128_000, supportsPromptCache: false, supportsImages: true },
209+
const customArn = apiConfiguration.awsCustomArn || ""
210+
const isClaudeSonnet45 = isClaudeSonnet45Model(customArn)
211+
212+
// Base info for custom ARNs
213+
let info: ModelInfo = {
214+
maxTokens: 5000,
215+
contextWindow: 128_000,
216+
supportsPromptCache: false,
217+
supportsImages: true,
200218
}
219+
220+
// If it's a Claude Sonnet 4.5 model, add thinking support and better defaults
221+
if (isClaudeSonnet45) {
222+
info = {
223+
maxTokens: 8192,
224+
contextWindow: apiConfiguration.awsBedrock1MContext ? 1_000_000 : 200_000,
225+
supportsImages: true,
226+
supportsPromptCache: true,
227+
supportsReasoningBudget: true,
228+
supportsComputerUse: true,
229+
}
230+
}
231+
232+
return { id, info }
201233
}
202234

203235
// Apply 1M context for Claude Sonnet 4 / 4.5 when enabled

0 commit comments

Comments
 (0)