Skip to content

Commit 8e2ce92

Browse files
committed
feat(vertex): add native tool calling for Claude models on Vertex AI
- Add tool protocol resolution and native tool params to AnthropicVertexHandler - Handle tool_use blocks and input_json_delta in response streaming - Add convertOpenAIToolChoice helper method for tool choice conversion - Add supportsNativeTools and defaultToolProtocol to all Claude models in vertexModels
1 parent e2d1599 commit 8e2ce92

File tree

2 files changed

+117
-7
lines changed

2 files changed

+117
-7
lines changed

packages/types/src/providers/vertex.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,8 @@ export const vertexModels = {
278278
contextWindow: 200_000,
279279
supportsImages: true,
280280
supportsPromptCache: true,
281+
supportsNativeTools: true,
282+
defaultToolProtocol: "native",
281283
inputPrice: 3.0,
282284
outputPrice: 15.0,
283285
cacheWritesPrice: 3.75,
@@ -289,6 +291,8 @@ export const vertexModels = {
289291
contextWindow: 200_000,
290292
supportsImages: true,
291293
supportsPromptCache: true,
294+
supportsNativeTools: true,
295+
defaultToolProtocol: "native",
292296
inputPrice: 3.0,
293297
outputPrice: 15.0,
294298
cacheWritesPrice: 3.75,
@@ -300,6 +304,8 @@ export const vertexModels = {
300304
contextWindow: 200_000,
301305
supportsImages: true,
302306
supportsPromptCache: true,
307+
supportsNativeTools: true,
308+
defaultToolProtocol: "native",
303309
inputPrice: 1.0,
304310
outputPrice: 5.0,
305311
cacheWritesPrice: 1.25,
@@ -311,6 +317,8 @@ export const vertexModels = {
311317
contextWindow: 200_000,
312318
supportsImages: true,
313319
supportsPromptCache: true,
320+
supportsNativeTools: true,
321+
defaultToolProtocol: "native",
314322
inputPrice: 5.0,
315323
outputPrice: 25.0,
316324
cacheWritesPrice: 6.25,
@@ -322,6 +330,8 @@ export const vertexModels = {
322330
contextWindow: 200_000,
323331
supportsImages: true,
324332
supportsPromptCache: true,
333+
supportsNativeTools: true,
334+
defaultToolProtocol: "native",
325335
inputPrice: 15.0,
326336
outputPrice: 75.0,
327337
cacheWritesPrice: 18.75,
@@ -333,6 +343,8 @@ export const vertexModels = {
333343
contextWindow: 200_000,
334344
supportsImages: true,
335345
supportsPromptCache: true,
346+
supportsNativeTools: true,
347+
defaultToolProtocol: "native",
336348
inputPrice: 15.0,
337349
outputPrice: 75.0,
338350
cacheWritesPrice: 18.75,
@@ -343,6 +355,8 @@ export const vertexModels = {
343355
contextWindow: 200_000,
344356
supportsImages: true,
345357
supportsPromptCache: true,
358+
supportsNativeTools: true,
359+
defaultToolProtocol: "native",
346360
inputPrice: 3.0,
347361
outputPrice: 15.0,
348362
cacheWritesPrice: 3.75,
@@ -355,6 +369,8 @@ export const vertexModels = {
355369
contextWindow: 200_000,
356370
supportsImages: true,
357371
supportsPromptCache: true,
372+
supportsNativeTools: true,
373+
defaultToolProtocol: "native",
358374
inputPrice: 3.0,
359375
outputPrice: 15.0,
360376
cacheWritesPrice: 3.75,
@@ -365,6 +381,8 @@ export const vertexModels = {
365381
contextWindow: 200_000,
366382
supportsImages: true,
367383
supportsPromptCache: true,
384+
supportsNativeTools: true,
385+
defaultToolProtocol: "native",
368386
inputPrice: 3.0,
369387
outputPrice: 15.0,
370388
cacheWritesPrice: 3.75,
@@ -375,6 +393,8 @@ export const vertexModels = {
375393
contextWindow: 200_000,
376394
supportsImages: true,
377395
supportsPromptCache: true,
396+
supportsNativeTools: true,
397+
defaultToolProtocol: "native",
378398
inputPrice: 3.0,
379399
outputPrice: 15.0,
380400
cacheWritesPrice: 3.75,
@@ -385,6 +405,8 @@ export const vertexModels = {
385405
contextWindow: 200_000,
386406
supportsImages: false,
387407
supportsPromptCache: true,
408+
supportsNativeTools: true,
409+
defaultToolProtocol: "native",
388410
inputPrice: 1.0,
389411
outputPrice: 5.0,
390412
cacheWritesPrice: 1.25,
@@ -395,6 +417,8 @@ export const vertexModels = {
395417
contextWindow: 200_000,
396418
supportsImages: true,
397419
supportsPromptCache: true,
420+
supportsNativeTools: true,
421+
defaultToolProtocol: "native",
398422
inputPrice: 15.0,
399423
outputPrice: 75.0,
400424
cacheWritesPrice: 18.75,
@@ -405,6 +429,8 @@ export const vertexModels = {
405429
contextWindow: 200_000,
406430
supportsImages: true,
407431
supportsPromptCache: true,
432+
supportsNativeTools: true,
433+
defaultToolProtocol: "native",
408434
inputPrice: 0.25,
409435
outputPrice: 1.25,
410436
cacheWritesPrice: 0.3,

src/api/providers/anthropic-vertex.ts

Lines changed: 91 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import { Anthropic } from "@anthropic-ai/sdk"
22
import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"
33
import { GoogleAuth, JWTInput } from "google-auth-library"
4+
import OpenAI from "openai"
45

56
import {
67
type ModelInfo,
78
type VertexModelId,
89
vertexDefaultModelId,
910
vertexModels,
1011
ANTHROPIC_DEFAULT_MAX_TOKENS,
12+
TOOL_PROTOCOL,
1113
} from "@roo-code/types"
1214

1315
import { ApiHandlerOptions } from "../../shared/api"
@@ -17,6 +19,8 @@ import { ApiStream } from "../transform/stream"
1719
import { addCacheBreakpoints } from "../transform/caching/vertex"
1820
import { getModelParams } from "../transform/model-params"
1921
import { filterNonAnthropicBlocks } from "../transform/anthropic-filter"
22+
import { resolveToolProtocol } from "../../utils/resolveToolProtocol"
23+
import { convertOpenAIToolsToAnthropic } from "../../core/prompts/tools/native-tools/converters"
2024

2125
import { BaseProvider } from "./base-provider"
2226
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
@@ -63,17 +67,30 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple
6367
messages: Anthropic.Messages.MessageParam[],
6468
metadata?: ApiHandlerCreateMessageMetadata,
6569
): ApiStream {
66-
let {
67-
id,
68-
info: { supportsPromptCache },
69-
temperature,
70-
maxTokens,
71-
reasoning: thinking,
72-
} = this.getModel()
70+
let { id, info, temperature, maxTokens, reasoning: thinking } = this.getModel()
71+
72+
const { supportsPromptCache } = info
7373

7474
// Filter out non-Anthropic blocks (reasoning, thoughtSignature, etc.) before sending to the API
7575
const sanitizedMessages = filterNonAnthropicBlocks(messages)
7676

77+
// Enable native tools using resolveToolProtocol (which checks model's defaultToolProtocol)
78+
// This matches the approach used in AnthropicHandler
79+
// Also exclude tools when tool_choice is "none" since that means "don't use tools"
80+
const toolProtocol = resolveToolProtocol(this.options, info, metadata?.toolProtocol)
81+
const shouldIncludeNativeTools =
82+
metadata?.tools &&
83+
metadata.tools.length > 0 &&
84+
toolProtocol === TOOL_PROTOCOL.NATIVE &&
85+
metadata?.tool_choice !== "none"
86+
87+
const nativeToolParams = shouldIncludeNativeTools
88+
? {
89+
tools: convertOpenAIToolsToAnthropic(metadata.tools!),
90+
tool_choice: this.convertOpenAIToolChoice(metadata.tool_choice, metadata.parallelToolCalls),
91+
}
92+
: {}
93+
7794
/**
7895
* Vertex API has specific limitations for prompt caching:
7996
* 1. Maximum of 4 blocks can have cache_control
@@ -98,6 +115,7 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple
98115
: systemPrompt,
99116
messages: supportsPromptCache ? addCacheBreakpoints(sanitizedMessages) : sanitizedMessages,
100117
stream: true,
118+
...nativeToolParams,
101119
}
102120

103121
const stream = await this.client.messages.create(params)
@@ -144,6 +162,17 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple
144162
yield { type: "reasoning", text: (chunk.content_block as any).thinking }
145163
break
146164
}
165+
case "tool_use": {
166+
// Emit initial tool call partial with id and name
167+
yield {
168+
type: "tool_call_partial",
169+
index: chunk.index,
170+
id: chunk.content_block!.id,
171+
name: chunk.content_block!.name,
172+
arguments: undefined,
173+
}
174+
break
175+
}
147176
}
148177

149178
break
@@ -158,12 +187,24 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple
158187
yield { type: "reasoning", text: (chunk.delta as any).thinking }
159188
break
160189
}
190+
case "input_json_delta": {
191+
// Emit tool call partial chunks as arguments stream in
192+
yield {
193+
type: "tool_call_partial",
194+
index: chunk.index,
195+
id: undefined,
196+
name: undefined,
197+
arguments: (chunk.delta as any).partial_json,
198+
}
199+
break
200+
}
161201
}
162202

163203
break
164204
}
165205
case "content_block_stop": {
166206
// Block complete - no action needed for now.
207+
// NativeToolCallParser handles tool call completion
167208
// Note: Signature for multi-turn thinking would require using stream.finalMessage()
168209
// after iteration completes, which requires restructuring the streaming approach.
169210
break
@@ -227,4 +268,47 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple
227268
throw error
228269
}
229270
}
271+
272+
/**
273+
* Converts OpenAI tool_choice to Anthropic ToolChoice format
274+
* @param toolChoice - OpenAI tool_choice parameter
275+
* @param parallelToolCalls - When true, allows parallel tool calls. When false (default), disables parallel tool calls.
276+
*/
277+
private convertOpenAIToolChoice(
278+
toolChoice: OpenAI.Chat.ChatCompletionCreateParams["tool_choice"],
279+
parallelToolCalls?: boolean,
280+
): Anthropic.Messages.MessageCreateParams["tool_choice"] | undefined {
281+
// Anthropic allows parallel tool calls by default. When parallelToolCalls is false or undefined,
282+
// we disable parallel tool use to ensure one tool call at a time.
283+
const disableParallelToolUse = !parallelToolCalls
284+
285+
if (!toolChoice) {
286+
// Default to auto with parallel tool use control
287+
return { type: "auto", disable_parallel_tool_use: disableParallelToolUse }
288+
}
289+
290+
if (typeof toolChoice === "string") {
291+
switch (toolChoice) {
292+
case "none":
293+
return undefined // Anthropic doesn't have "none", just omit tools
294+
case "auto":
295+
return { type: "auto", disable_parallel_tool_use: disableParallelToolUse }
296+
case "required":
297+
return { type: "any", disable_parallel_tool_use: disableParallelToolUse }
298+
default:
299+
return { type: "auto", disable_parallel_tool_use: disableParallelToolUse }
300+
}
301+
}
302+
303+
// Handle object form { type: "function", function: { name: string } }
304+
if (typeof toolChoice === "object" && "function" in toolChoice) {
305+
return {
306+
type: "tool",
307+
name: toolChoice.function.name,
308+
disable_parallel_tool_use: disableParallelToolUse,
309+
}
310+
}
311+
312+
return { type: "auto", disable_parallel_tool_use: disableParallelToolUse }
313+
}
230314
}

0 commit comments

Comments
 (0)