Skip to content

Commit 60f1331

Browse files
committed
Added thinking support for Claude 3.7 via Vertex AI
1 parent 82b282b commit 60f1331

File tree

6 files changed

+385
-18
lines changed

6 files changed

+385
-18
lines changed

package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"displayName": "Roo Code (prev. Roo Cline)",
44
"description": "An AI-powered autonomous coding agent that lives in your editor.",
55
"publisher": "RooVeterinaryInc",
6-
"version": "3.7.4",
6+
"version": "3.7.5",
77
"icon": "assets/icons/rocket.png",
88
"galleryBanner": {
99
"color": "#617A91",
@@ -305,7 +305,7 @@
305305
"dependencies": {
306306
"@anthropic-ai/bedrock-sdk": "^0.10.2",
307307
"@anthropic-ai/sdk": "^0.37.0",
308-
"@anthropic-ai/vertex-sdk": "^0.4.1",
308+
"@anthropic-ai/vertex-sdk": "^0.7.0",
309309
"@aws-sdk/client-bedrock-runtime": "^3.706.0",
310310
"@google/generative-ai": "^0.18.0",
311311
"@mistralai/mistralai": "^1.3.6",

src/api/providers/__tests__/vertex.test.ts

Lines changed: 203 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ jest.mock("@anthropic-ai/vertex-sdk", () => ({
2626
async *[Symbol.asyncIterator]() {
2727
yield {
2828
type: "message_start",
29+
index: 0,
2930
message: {
3031
usage: {
3132
input_tokens: 10,
@@ -35,6 +36,15 @@ jest.mock("@anthropic-ai/vertex-sdk", () => ({
3536
}
3637
yield {
3738
type: "content_block_start",
39+
index: 0,
40+
content_block: {
41+
type: "thinking",
42+
thinking: "Thinking test",
43+
},
44+
}
45+
yield {
46+
type: "content_block_start",
47+
index: 1,
3848
content_block: {
3949
type: "text",
4050
text: "Test response",
@@ -53,6 +63,7 @@ describe("VertexHandler", () => {
5363
beforeEach(() => {
5464
handler = new VertexHandler({
5565
apiModelId: "claude-3-5-sonnet-v2@20241022",
66+
anthropicThinking: undefined,
5667
vertexProjectId: "test-project",
5768
vertexRegion: "us-central1",
5869
})
@@ -124,7 +135,7 @@ describe("VertexHandler", () => {
124135
},
125136
}
126137

127-
const mockCreate = jest.fn().mockResolvedValue(asyncIterator)
138+
const mockCreate = jest.fn().mockImplementation(() => asyncIterator)
128139
;(handler["client"].messages as any).create = mockCreate
129140

130141
const stream = handler.createMessage(systemPrompt, mockMessages)
@@ -160,10 +171,168 @@ describe("VertexHandler", () => {
160171
temperature: 0,
161172
system: systemPrompt,
162173
messages: mockMessages,
174+
thinking: undefined,
163175
stream: true,
164176
})
165177
})
166178

179+
it("should include thinking configuration for supported models", async () => {
180+
// Create a handler with the thinking-capable model
181+
const thinkingHandler = new VertexHandler({
182+
apiModelId: "claude-3-7-sonnet@20250219",
183+
anthropicThinking: 16384, // Set thinking budget
184+
vertexProjectId: "test-project",
185+
vertexRegion: "us-central1",
186+
})
187+
188+
const mockCreate = jest.fn().mockResolvedValue({
189+
async *[Symbol.asyncIterator]() {
190+
yield { type: "message_start", message: { usage: { input_tokens: 10, output_tokens: 0 } } }
191+
},
192+
usage: {},
193+
})
194+
;(thinkingHandler["client"].messages as any).create = mockCreate
195+
196+
const stream = thinkingHandler.createMessage(systemPrompt, mockMessages)
197+
for await (const _ of stream) {
198+
// Just consuming the stream
199+
}
200+
201+
// Verify that mock was called
202+
expect(mockCreate).toHaveBeenCalled()
203+
204+
// Get the actual call arguments
205+
const callArgs = mockCreate.mock.calls[0][0]
206+
207+
// Verify specific properties instead of the entire object
208+
expect(callArgs.model).toBe("claude-3-7-sonnet@20250219")
209+
expect(callArgs.max_tokens).toBe(17408) // thinking.budget_tokens (16384) + 1024
210+
expect(callArgs.temperature).toBe(1.0)
211+
expect(callArgs.system).toBe(systemPrompt)
212+
expect(callArgs.messages).toEqual(mockMessages)
213+
expect(callArgs.stream).toBe(true)
214+
expect(callArgs.thinking).toEqual({ type: "enabled", budget_tokens: 16384 })
215+
})
216+
217+
it("should explicitly disable thinking if the option is undefined", async () => {
218+
// Create a handler with the thinking-capable model but no thinking budget
219+
const thinkingHandler = new VertexHandler({
220+
apiModelId: "claude-3-7-sonnet@20250219",
221+
anthropicThinking: undefined,
222+
vertexProjectId: "test-project",
223+
vertexRegion: "us-central1",
224+
})
225+
226+
const mockCreate = jest.fn().mockResolvedValue({
227+
async *[Symbol.asyncIterator]() {
228+
yield { type: "message_start", message: { usage: { input_tokens: 10, output_tokens: 0 } } }
229+
},
230+
usage: {},
231+
})
232+
;(thinkingHandler["client"].messages as any).create = mockCreate
233+
234+
const stream = thinkingHandler.createMessage(systemPrompt, mockMessages)
235+
for await (const _ of stream) {
236+
// Just consuming the stream
237+
}
238+
239+
// Verify that mock was called
240+
expect(mockCreate).toHaveBeenCalled()
241+
242+
// Get the actual call arguments
243+
const callArgs = mockCreate.mock.calls[0][0]
244+
245+
// Verify specific properties instead of the entire object
246+
expect(callArgs.model).toBe("claude-3-7-sonnet@20250219")
247+
expect(callArgs.max_tokens).toBe(8192) // Default max_tokens when thinking is disabled
248+
expect(callArgs.temperature).toBe(1.0)
249+
expect(callArgs.system).toBe(systemPrompt)
250+
expect(callArgs.messages).toEqual(mockMessages)
251+
expect(callArgs.stream).toBe(true)
252+
expect(callArgs.thinking).toEqual({ type: "disabled" })
253+
})
254+
255+
it("should handle thinking content blocks", async () => {
256+
const mockStream = [
257+
{
258+
type: "message_start",
259+
message: {
260+
usage: {
261+
input_tokens: 10,
262+
output_tokens: 0,
263+
},
264+
},
265+
},
266+
{
267+
type: "content_block_start",
268+
index: 0,
269+
content_block: {
270+
type: "thinking",
271+
thinking: "Let me reason through this step by step",
272+
},
273+
},
274+
{
275+
type: "content_block_delta",
276+
delta: {
277+
type: "thinking_delta",
278+
thinking: ". First, I need to understand...",
279+
},
280+
},
281+
{
282+
type: "content_block_start",
283+
index: 1,
284+
content_block: {
285+
type: "text",
286+
text: "Based on my analysis,",
287+
},
288+
},
289+
{
290+
type: "content_block_delta",
291+
delta: {
292+
type: "text_delta",
293+
text: " here is my response.",
294+
},
295+
},
296+
{
297+
type: "message_delta",
298+
usage: {},
299+
},
300+
]
301+
302+
const asyncIterator = {
303+
async *[Symbol.asyncIterator]() {
304+
for (const chunk of mockStream) {
305+
yield chunk
306+
}
307+
},
308+
}
309+
310+
const mockCreate = jest.fn().mockResolvedValue(asyncIterator)
311+
;(handler["client"].messages as any).create = mockCreate
312+
313+
const stream = handler.createMessage(systemPrompt, mockMessages)
314+
const chunks = []
315+
316+
for await (const chunk of stream) {
317+
chunks.push(chunk)
318+
}
319+
320+
// The actual chunks may vary depending on how the stream is handled
321+
// So instead of checking the exact count, we'll check the specific chunks we care about
322+
expect(chunks[0]).toEqual({ type: "usage", inputTokens: 10, outputTokens: 0 })
323+
324+
// Check that reasoning and text chunks exist in the output
325+
expect(
326+
chunks.some(
327+
(chunk) => chunk.type === "reasoning" && chunk.text === "Let me reason through this step by step",
328+
),
329+
).toBeTruthy()
330+
expect(
331+
chunks.some((chunk) => chunk.type === "reasoning" && chunk.text === ". First, I need to understand..."),
332+
).toBeTruthy()
333+
expect(chunks.some((chunk) => chunk.type === "text" && chunk.text === "Based on my analysis,")).toBeTruthy()
334+
})
335+
167336
it("should handle multiple content blocks with line breaks", async () => {
168337
const mockStream = [
169338
{
@@ -242,6 +411,7 @@ describe("VertexHandler", () => {
242411
temperature: 0,
243412
messages: [{ role: "user", content: "Test prompt" }],
244413
stream: false,
414+
thinking: undefined,
245415
})
246416
})
247417

@@ -265,6 +435,38 @@ describe("VertexHandler", () => {
265435
expect(result).toBe("")
266436
})
267437

438+
it("should include thinking configuration for supported models", async () => {
439+
// Create a handler with the thinking-capable model
440+
const thinkingHandler = new VertexHandler({
441+
apiModelId: "claude-3-7-sonnet@20250219",
442+
anthropicThinking: 16384,
443+
vertexProjectId: "test-project",
444+
vertexRegion: "us-central1",
445+
})
446+
447+
const mockCreate = jest.fn().mockResolvedValue({
448+
content: [{ type: "text", text: "Test response with thinking" }],
449+
usage: {},
450+
})
451+
;(thinkingHandler["client"].messages as any).create = mockCreate
452+
453+
await thinkingHandler.completePrompt("Test prompt")
454+
455+
// Verify that mock was called
456+
expect(mockCreate).toHaveBeenCalled()
457+
458+
// Get the actual call arguments
459+
const callArgs = mockCreate.mock.calls[0][0]
460+
461+
// Verify specific properties instead of the entire object
462+
expect(callArgs.model).toBe("claude-3-7-sonnet@20250219")
463+
expect(callArgs.max_tokens).toBe(17408) // thinking.budget_tokens (16384) + 1024
464+
expect(callArgs.temperature).toBe(1.0)
465+
expect(callArgs.thinking).toEqual({ type: "enabled", budget_tokens: 16384 })
466+
expect(callArgs.messages).toEqual([{ role: "user", content: "Test prompt" }])
467+
expect(callArgs.stream).toBe(false)
468+
})
469+
268470
it("should handle empty response", async () => {
269471
const mockCreate = jest.fn().mockResolvedValue({
270472
content: [{ type: "text", text: "" }],

0 commit comments

Comments
 (0)