Roo-Code/src/api/providers/anthropic-vertex.ts
Hannes Rudolph 8de9337e63
chore: remove XML tool calling support (#10841)
Co-authored-by: daniel-lxs <ricciodaniel98@gmail.com>
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
2026-01-20 20:25:08 -05:00

298 lines
8.7 KiB
TypeScript

import { Anthropic } from "@anthropic-ai/sdk"
import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"
import { GoogleAuth, JWTInput } from "google-auth-library"
import {
type ModelInfo,
type VertexModelId,
vertexDefaultModelId,
vertexModels,
ANTHROPIC_DEFAULT_MAX_TOKENS,
VERTEX_1M_CONTEXT_MODEL_IDS,
} from "@roo-code/types"
import { safeJsonParse } from "@roo-code/core"
import { ApiHandlerOptions } from "../../shared/api"
import { ApiStream } from "../transform/stream"
import { addCacheBreakpoints } from "../transform/caching/vertex"
import { getModelParams } from "../transform/model-params"
import { filterNonAnthropicBlocks } from "../transform/anthropic-filter"
import {
convertOpenAIToolsToAnthropic,
convertOpenAIToolChoiceToAnthropic,
} from "../../core/prompts/tools/native-tools/converters"
import { BaseProvider } from "./base-provider"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
// https://docs.anthropic.com/en/api/claude-on-vertex-ai
export class AnthropicVertexHandler extends BaseProvider implements SingleCompletionHandler {
protected options: ApiHandlerOptions
private client: AnthropicVertex
constructor(options: ApiHandlerOptions) {
super()
this.options = options
// https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#regions
const projectId = this.options.vertexProjectId ?? "not-provided"
const region = this.options.vertexRegion ?? "us-east5"
if (this.options.vertexJsonCredentials) {
this.client = new AnthropicVertex({
projectId,
region,
googleAuth: new GoogleAuth({
scopes: ["https://www.googleapis.com/auth/cloud-platform"],
credentials: safeJsonParse<JWTInput>(this.options.vertexJsonCredentials, undefined),
}),
})
} else if (this.options.vertexKeyFile) {
this.client = new AnthropicVertex({
projectId,
region,
googleAuth: new GoogleAuth({
scopes: ["https://www.googleapis.com/auth/cloud-platform"],
keyFile: this.options.vertexKeyFile,
}),
})
} else {
this.client = new AnthropicVertex({ projectId, region })
}
}
override async *createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
metadata?: ApiHandlerCreateMessageMetadata,
): ApiStream {
let { id, info, temperature, maxTokens, reasoning: thinking, betas } = this.getModel()
const { supportsPromptCache } = info
// Filter out non-Anthropic blocks (reasoning, thoughtSignature, etc.) before sending to the API
const sanitizedMessages = filterNonAnthropicBlocks(messages)
const nativeToolParams = {
tools: convertOpenAIToolsToAnthropic(metadata?.tools ?? []),
tool_choice: convertOpenAIToolChoiceToAnthropic(metadata?.tool_choice, metadata?.parallelToolCalls),
}
/**
* Vertex API has specific limitations for prompt caching:
* 1. Maximum of 4 blocks can have cache_control
* 2. Only text blocks can be cached (images and other content types cannot)
* 3. Cache control can only be applied to user messages, not assistant messages
*
* Our caching strategy:
* - Cache the system prompt (1 block)
* - Cache the last text block of the second-to-last user message (1 block)
* - Cache the last text block of the last user message (1 block)
* This ensures we stay under the 4-block limit while maintaining effective caching
* for the most relevant context.
*/
const params: Anthropic.Messages.MessageCreateParamsStreaming = {
model: id,
max_tokens: maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,
temperature,
thinking,
// Cache the system prompt if caching is enabled.
system: supportsPromptCache
? [{ text: systemPrompt, type: "text" as const, cache_control: { type: "ephemeral" } }]
: systemPrompt,
messages: supportsPromptCache ? addCacheBreakpoints(sanitizedMessages) : sanitizedMessages,
stream: true,
...nativeToolParams,
}
// and prompt caching
const requestOptions = betas?.length ? { headers: { "anthropic-beta": betas.join(",") } } : undefined
const stream = await this.client.messages.create(params, requestOptions)
for await (const chunk of stream) {
switch (chunk.type) {
case "message_start": {
const usage = chunk.message!.usage
yield {
type: "usage",
inputTokens: usage.input_tokens || 0,
outputTokens: usage.output_tokens || 0,
cacheWriteTokens: usage.cache_creation_input_tokens || undefined,
cacheReadTokens: usage.cache_read_input_tokens || undefined,
}
break
}
case "message_delta": {
yield {
type: "usage",
inputTokens: 0,
outputTokens: chunk.usage!.output_tokens || 0,
}
break
}
case "content_block_start": {
switch (chunk.content_block!.type) {
case "text": {
if (chunk.index! > 0) {
yield { type: "text", text: "\n" }
}
yield { type: "text", text: chunk.content_block!.text }
break
}
case "thinking": {
if (chunk.index! > 0) {
yield { type: "reasoning", text: "\n" }
}
yield { type: "reasoning", text: (chunk.content_block as any).thinking }
break
}
case "tool_use": {
// Emit initial tool call partial with id and name
yield {
type: "tool_call_partial",
index: chunk.index,
id: chunk.content_block!.id,
name: chunk.content_block!.name,
arguments: undefined,
}
break
}
}
break
}
case "content_block_delta": {
switch (chunk.delta!.type) {
case "text_delta": {
yield { type: "text", text: chunk.delta!.text }
break
}
case "thinking_delta": {
yield { type: "reasoning", text: (chunk.delta as any).thinking }
break
}
case "input_json_delta": {
// Emit tool call partial chunks as arguments stream in
yield {
type: "tool_call_partial",
index: chunk.index,
id: undefined,
name: undefined,
arguments: (chunk.delta as any).partial_json,
}
break
}
}
break
}
case "content_block_stop": {
// Block complete - no action needed for now.
// NativeToolCallParser handles tool call completion
// Note: Signature for multi-turn thinking would require using stream.finalMessage()
// after iteration completes, which requires restructuring the streaming approach.
break
}
}
}
}
getModel() {
const modelId = this.options.apiModelId
let id = modelId && modelId in vertexModels ? (modelId as VertexModelId) : vertexDefaultModelId
let info: ModelInfo = vertexModels[id]
// Check if 1M context beta should be enabled for supported models
const supports1MContext = VERTEX_1M_CONTEXT_MODEL_IDS.includes(
id as (typeof VERTEX_1M_CONTEXT_MODEL_IDS)[number],
)
const enable1MContext = supports1MContext && this.options.vertex1MContext
// If 1M context beta is enabled, update the model info with tier pricing
if (enable1MContext) {
const tier = info.tiers?.[0]
if (tier) {
info = {
...info,
contextWindow: tier.contextWindow,
inputPrice: tier.inputPrice,
outputPrice: tier.outputPrice,
cacheWritesPrice: tier.cacheWritesPrice,
cacheReadsPrice: tier.cacheReadsPrice,
}
}
}
const params = getModelParams({ format: "anthropic", modelId: id, model: info, settings: this.options })
// Build betas array for request headers
const betas: string[] = []
// Add 1M context beta flag if enabled for supported models
if (enable1MContext) {
betas.push("context-1m-2025-08-07")
}
// The `:thinking` suffix indicates that the model is a "Hybrid"
// reasoning model and that reasoning is required to be enabled.
// The actual model ID honored by Anthropic's API does not have this
// suffix.
return {
id: id.endsWith(":thinking") ? id.replace(":thinking", "") : id,
info,
betas: betas.length > 0 ? betas : undefined,
...params,
}
}
async completePrompt(prompt: string) {
try {
let {
id,
info: { supportsPromptCache },
temperature,
maxTokens = ANTHROPIC_DEFAULT_MAX_TOKENS,
reasoning: thinking,
} = this.getModel()
const params: Anthropic.Messages.MessageCreateParamsNonStreaming = {
model: id,
max_tokens: maxTokens,
temperature,
thinking,
messages: [
{
role: "user",
content: supportsPromptCache
? [{ type: "text" as const, text: prompt, cache_control: { type: "ephemeral" } }]
: prompt,
},
],
stream: false,
}
const response = await this.client.messages.create(params)
const content = response.content[0]
if (content.type === "text") {
return content.text
}
return ""
} catch (error) {
if (error instanceof Error) {
throw new Error(`Vertex completion error: ${error.message}`)
}
throw error
}
}
}