Skip to content

Commit 3d794b9

Browse files
committed
Implement extended reasoning support in AwsBedrockHandler and add corresponding tests
1 parent 395f55b commit 3d794b9

File tree

4 files changed

+454
-17
lines changed

4 files changed

+454
-17
lines changed

packages/types/src/providers/bedrock.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export const bedrockModels = {
7373
supportsImages: true,
7474
supportsComputerUse: true,
7575
supportsPromptCache: true,
76+
supportsReasoningBudget: true,
7677
inputPrice: 3.0,
7778
outputPrice: 15.0,
7879
cacheWritesPrice: 3.75,
@@ -87,6 +88,7 @@ export const bedrockModels = {
8788
supportsImages: true,
8889
supportsComputerUse: true,
8990
supportsPromptCache: true,
91+
supportsReasoningBudget: true,
9092
inputPrice: 15.0,
9193
outputPrice: 75.0,
9294
cacheWritesPrice: 18.75,
@@ -101,6 +103,7 @@ export const bedrockModels = {
101103
supportsImages: true,
102104
supportsComputerUse: true,
103105
supportsPromptCache: true,
106+
supportsReasoningBudget: true,
104107
inputPrice: 3.0,
105108
outputPrice: 15.0,
106109
cacheWritesPrice: 3.75,
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
import { AwsBedrockHandler } from "../bedrock"
2+
import { BedrockRuntimeClient } from "@aws-sdk/client-bedrock-runtime"
3+
4+
// Mock the AWS SDK
5+
jest.mock("@aws-sdk/client-bedrock-runtime")
6+
jest.mock("@aws-sdk/credential-providers")
7+
8+
describe("AwsBedrockHandler - Extended Thinking/Reasoning", () => {
9+
let mockClient: jest.Mocked<BedrockRuntimeClient>
10+
let mockSend: jest.Mock
11+
12+
beforeEach(() => {
13+
jest.clearAllMocks()
14+
mockSend = jest.fn()
15+
mockClient = {
16+
send: mockSend,
17+
config: { region: "us-east-1" },
18+
} as any
19+
;(BedrockRuntimeClient as jest.Mock).mockImplementation(() => mockClient)
20+
})
21+
22+
describe("Extended Thinking Configuration", () => {
23+
it("should NOT include thinking configuration by default", async () => {
24+
const handler = new AwsBedrockHandler({
25+
apiModelId: "anthropic.claude-3-7-sonnet-20250219-v1:0",
26+
awsAccessKey: "test-key",
27+
awsSecretKey: "test-secret",
28+
awsRegion: "us-east-1",
29+
// enableReasoningEffort is NOT set, so reasoning should be disabled
30+
})
31+
32+
// Mock the stream response
33+
mockSend.mockResolvedValueOnce({
34+
stream: (async function* () {
35+
yield { messageStart: { role: "assistant" } }
36+
yield { contentBlockStart: { start: { text: "Hello" } } }
37+
yield { messageStop: {} }
38+
})(),
39+
})
40+
41+
const messages = [{ role: "user" as const, content: "Test message" }]
42+
const stream = handler.createMessage("System prompt", messages)
43+
44+
// Consume the stream
45+
for await (const _chunk of stream) {
46+
// Just consume
47+
}
48+
49+
// Verify the command was called
50+
expect(mockSend).toHaveBeenCalledTimes(1)
51+
const command = mockSend.mock.calls[0][0]
52+
const payload = command.input
53+
54+
// Verify thinking is NOT included
55+
expect(payload.anthropic_version).toBeUndefined()
56+
expect(payload.additionalModelRequestFields).toBeUndefined()
57+
expect(payload.inferenceConfig.temperature).toBeDefined()
58+
expect(payload.inferenceConfig.topP).toBeDefined()
59+
})
60+
61+
it("should include thinking configuration when explicitly enabled", async () => {
62+
const handler = new AwsBedrockHandler({
63+
apiModelId: "anthropic.claude-3-7-sonnet-20250219-v1:0",
64+
awsAccessKey: "test-key",
65+
awsSecretKey: "test-secret",
66+
awsRegion: "us-east-1",
67+
enableReasoningEffort: true, // Explicitly enable reasoning
68+
modelMaxThinkingTokens: 5000, // Set thinking tokens
69+
})
70+
71+
// Mock the stream response
72+
mockSend.mockResolvedValueOnce({
73+
stream: (async function* () {
74+
yield { messageStart: { role: "assistant" } }
75+
yield { contentBlockStart: { contentBlock: { type: "thinking", thinking: "Let me think..." } } }
76+
yield { contentBlockDelta: { delta: { type: "thinking_delta", thinking: " about this." } } }
77+
yield { contentBlockStart: { start: { text: "Here's my answer" } } }
78+
yield { messageStop: {} }
79+
})(),
80+
})
81+
82+
const messages = [{ role: "user" as const, content: "Test message" }]
83+
const stream = handler.createMessage("System prompt", messages)
84+
85+
// Consume the stream
86+
const chunks = []
87+
for await (const chunk of stream) {
88+
chunks.push(chunk)
89+
}
90+
91+
// Verify the command was called
92+
expect(mockSend).toHaveBeenCalledTimes(1)
93+
const command = mockSend.mock.calls[0][0]
94+
const payload = command.input
95+
96+
// Verify thinking IS included
97+
expect(payload.anthropic_version).toBe("bedrock-20250514")
98+
expect(payload.additionalModelRequestFields).toEqual({
99+
thinking: {
100+
type: "enabled",
101+
budget_tokens: 5000,
102+
},
103+
})
104+
// Temperature and topP should be removed when thinking is enabled
105+
expect(payload.inferenceConfig.temperature).toBeUndefined()
106+
expect(payload.inferenceConfig.topP).toBeUndefined()
107+
108+
// Verify thinking chunks were properly handled
109+
const reasoningChunks = chunks.filter((c) => c.type === "reasoning")
110+
expect(reasoningChunks).toHaveLength(2)
111+
expect(reasoningChunks[0].text).toBe("Let me think...")
112+
expect(reasoningChunks[1].text).toBe(" about this.")
113+
})
114+
115+
it("should NOT enable thinking for non-supported models even if requested", async () => {
116+
const handler = new AwsBedrockHandler({
117+
apiModelId: "anthropic.claude-3-haiku-20240307-v1:0", // This model doesn't support reasoning
118+
awsAccessKey: "test-key",
119+
awsSecretKey: "test-secret",
120+
awsRegion: "us-east-1",
121+
enableReasoningEffort: true, // Try to enable reasoning
122+
modelMaxThinkingTokens: 5000,
123+
})
124+
125+
// Mock the stream response
126+
mockSend.mockResolvedValueOnce({
127+
stream: (async function* () {
128+
yield { messageStart: { role: "assistant" } }
129+
yield { contentBlockStart: { start: { text: "Hello" } } }
130+
yield { messageStop: {} }
131+
})(),
132+
})
133+
134+
const messages = [{ role: "user" as const, content: "Test message" }]
135+
const stream = handler.createMessage("System prompt", messages)
136+
137+
// Consume the stream
138+
for await (const _chunk of stream) {
139+
// Just consume
140+
}
141+
142+
// Verify the command was called
143+
expect(mockSend).toHaveBeenCalledTimes(1)
144+
const command = mockSend.mock.calls[0][0]
145+
const payload = command.input
146+
147+
// Verify thinking is NOT included because model doesn't support it
148+
expect(payload.anthropic_version).toBeUndefined()
149+
expect(payload.additionalModelRequestFields).toBeUndefined()
150+
})
151+
152+
it("should handle thinking stream events correctly", async () => {
153+
const handler = new AwsBedrockHandler({
154+
apiModelId: "anthropic.claude-sonnet-4-20250514-v1:0",
155+
awsAccessKey: "test-key",
156+
awsSecretKey: "test-secret",
157+
awsRegion: "us-east-1",
158+
enableReasoningEffort: true,
159+
modelMaxThinkingTokens: 8000,
160+
})
161+
162+
// Mock the stream response with various thinking events
163+
mockSend.mockResolvedValueOnce({
164+
stream: (async function* () {
165+
yield { messageStart: { role: "assistant" } }
166+
// Thinking block start
167+
yield {
168+
contentBlockStart: { contentBlock: { type: "thinking", thinking: "Analyzing the request..." } },
169+
}
170+
// Thinking deltas
171+
yield {
172+
contentBlockDelta: { delta: { type: "thinking_delta", thinking: "\nThis seems complex." } },
173+
}
174+
yield {
175+
contentBlockDelta: { delta: { type: "thinking_delta", thinking: "\nLet me break it down." } },
176+
}
177+
// Signature delta (part of thinking)
178+
yield {
179+
contentBlockDelta: { delta: { type: "signature_delta", signature: "\n[Signature: ABC123]" } },
180+
}
181+
// Regular text response
182+
yield { contentBlockStart: { start: { text: "Based on my analysis" } } }
183+
yield { contentBlockDelta: { delta: { text: ", here's the answer." } } }
184+
yield { messageStop: {} }
185+
})(),
186+
})
187+
188+
const messages = [{ role: "user" as const, content: "Complex question" }]
189+
const stream = handler.createMessage("System prompt", messages)
190+
191+
// Collect all chunks
192+
const chunks = []
193+
for await (const chunk of stream) {
194+
chunks.push(chunk)
195+
}
196+
197+
// Verify reasoning chunks
198+
const reasoningChunks = chunks.filter((c) => c.type === "reasoning")
199+
expect(reasoningChunks).toHaveLength(4)
200+
expect(reasoningChunks.map((c) => c.text).join("")).toBe(
201+
"Analyzing the request...\nThis seems complex.\nLet me break it down.\n[Signature: ABC123]",
202+
)
203+
204+
// Verify text chunks
205+
const textChunks = chunks.filter((c) => c.type === "text")
206+
expect(textChunks).toHaveLength(2)
207+
expect(textChunks.map((c) => c.text).join("")).toBe("Based on my analysis, here's the answer.")
208+
})
209+
})
210+
211+
describe("Error Handling for Extended Thinking", () => {
212+
it("should provide helpful error message for thinking-related errors", async () => {
213+
const handler = new AwsBedrockHandler({
214+
apiModelId: "anthropic.claude-3-7-sonnet-20250219-v1:0",
215+
awsAccessKey: "test-key",
216+
awsSecretKey: "test-secret",
217+
awsRegion: "us-east-1",
218+
enableReasoningEffort: true,
219+
modelMaxThinkingTokens: 5000,
220+
})
221+
222+
// Mock an error response
223+
const error = new Error("ValidationException: additionalModelRequestFields.thinking is not supported")
224+
mockSend.mockRejectedValueOnce(error)
225+
226+
const messages = [{ role: "user" as const, content: "Test message" }]
227+
const stream = handler.createMessage("System prompt", messages)
228+
229+
// Collect error chunks
230+
const chunks = []
231+
try {
232+
for await (const chunk of stream) {
233+
chunks.push(chunk)
234+
}
235+
} catch (e) {
236+
// Expected to throw
237+
}
238+
239+
// Should have error chunks before throwing
240+
expect(chunks).toHaveLength(2)
241+
expect(chunks[0].type).toBe("text")
242+
if (chunks[0].type === "text") {
243+
expect(chunks[0].text).toContain("Extended thinking/reasoning is not supported")
244+
}
245+
expect(chunks[1].type).toBe("usage")
246+
})
247+
})
248+
})

0 commit comments

Comments
 (0)