mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
refactor: migrate AnthropicVertexHandler to AI SDK (#11345)
Replace @anthropic-ai/vertex-sdk with @ai-sdk/google-vertex/anthropic, using streamText/generateText from the Vercel AI SDK for consistent provider behavior. Changes: - Use createVertexAnthropic from @ai-sdk/google-vertex/anthropic - Use streamText/generateText instead of direct Anthropic API calls - Add AI SDK transform utilities for message/tool conversion - Handle cache control via AI SDK providerOptions - Handle thinking/reasoning via providerOptions.anthropic.thinking - Add thought signature and redacted thinking block tracking - Set isAiSdkProvider() to return true - Remove unused deps: @anthropic-ai/vertex-sdk, google-auth-library - Rewrite tests to mock AI SDK instead of @anthropic-ai/vertex-sdk
This commit is contained in:
parent
571be71005
commit
24039c78b5
4 changed files with 663 additions and 1338 deletions
85
pnpm-lock.yaml
generated
85
pnpm-lock.yaml
generated
|
|
@ -782,9 +782,6 @@ importers:
|
|||
'@anthropic-ai/sdk':
|
||||
specifier: ^0.37.0
|
||||
version: 0.37.0
|
||||
'@anthropic-ai/vertex-sdk':
|
||||
specifier: ^0.7.0
|
||||
version: 0.7.0
|
||||
'@aws-sdk/client-bedrock-runtime':
|
||||
specifier: ^3.922.0
|
||||
version: 3.922.0
|
||||
|
|
@ -872,9 +869,6 @@ importers:
|
|||
global-agent:
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0
|
||||
google-auth-library:
|
||||
specifier: ^9.15.1
|
||||
version: 9.15.1
|
||||
gray-matter:
|
||||
specifier: ^4.0.3
|
||||
version: 4.0.3
|
||||
|
|
@ -1567,9 +1561,6 @@ packages:
|
|||
'@anthropic-ai/sdk@0.37.0':
|
||||
resolution: {integrity: sha512-tHjX2YbkUBwEgg0JZU3EFSSAQPoK4qQR/NFYa8Vtzd5UAyXzZksCw2In69Rml4R/TyHPBfRYaLK35XiOe33pjw==}
|
||||
|
||||
'@anthropic-ai/vertex-sdk@0.7.0':
|
||||
resolution: {integrity: sha512-zNm3hUXgYmYDTyveIxOyxbcnh5VXFkrLo4bSnG6LAfGzW7k3k2iCNDSVKtR9qZrK2BCid7JtVu7jsEKaZ/9dSw==}
|
||||
|
||||
'@asamuzakjp/css-color@3.2.0':
|
||||
resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==}
|
||||
|
||||
|
|
@ -6815,18 +6806,10 @@ packages:
|
|||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||
deprecated: This package is no longer supported.
|
||||
|
||||
gaxios@6.7.1:
|
||||
resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
gaxios@7.1.3:
|
||||
resolution: {integrity: sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
gcp-metadata@6.1.1:
|
||||
resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
gcp-metadata@8.1.2:
|
||||
resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -6947,14 +6930,6 @@ packages:
|
|||
resolution: {integrity: sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
google-auth-library@9.15.1:
|
||||
resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
google-logging-utils@0.0.2:
|
||||
resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
google-logging-utils@1.1.3:
|
||||
resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==}
|
||||
engines: {node: '>=14'}
|
||||
|
|
@ -6973,10 +6948,6 @@ packages:
|
|||
resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==}
|
||||
engines: {node: '>=6.0'}
|
||||
|
||||
gtoken@7.1.0:
|
||||
resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
gtoken@8.0.0:
|
||||
resolution: {integrity: sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -10539,10 +10510,6 @@ packages:
|
|||
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
||||
hasBin: true
|
||||
|
||||
uuid@9.0.1:
|
||||
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
|
||||
hasBin: true
|
||||
|
||||
v8-to-istanbul@9.3.0:
|
||||
resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==}
|
||||
engines: {node: '>=10.12.0'}
|
||||
|
|
@ -11232,14 +11199,6 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
'@anthropic-ai/vertex-sdk@0.7.0':
|
||||
dependencies:
|
||||
'@anthropic-ai/sdk': 0.37.0
|
||||
google-auth-library: 9.15.1
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
|
||||
'@asamuzakjp/css-color@3.2.0':
|
||||
dependencies:
|
||||
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
|
||||
|
|
@ -17221,17 +17180,6 @@ snapshots:
|
|||
strip-ansi: 6.0.1
|
||||
wide-align: 1.1.5
|
||||
|
||||
gaxios@6.7.1:
|
||||
dependencies:
|
||||
extend: 3.0.2
|
||||
https-proxy-agent: 7.0.6
|
||||
is-stream: 2.0.1
|
||||
node-fetch: 2.7.0
|
||||
uuid: 9.0.1
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
|
||||
gaxios@7.1.3:
|
||||
dependencies:
|
||||
extend: 3.0.2
|
||||
|
|
@ -17241,15 +17189,6 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
gcp-metadata@6.1.1:
|
||||
dependencies:
|
||||
gaxios: 6.7.1
|
||||
google-logging-utils: 0.0.2
|
||||
json-bigint: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
|
||||
gcp-metadata@8.1.2:
|
||||
dependencies:
|
||||
gaxios: 7.1.3
|
||||
|
|
@ -17398,20 +17337,6 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
google-auth-library@9.15.1:
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
ecdsa-sig-formatter: 1.0.11
|
||||
gaxios: 6.7.1
|
||||
gcp-metadata: 6.1.1
|
||||
gtoken: 7.1.0
|
||||
jws: 4.0.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
|
||||
google-logging-utils@0.0.2: {}
|
||||
|
||||
google-logging-utils@1.1.3: {}
|
||||
|
||||
gopd@1.2.0: {}
|
||||
|
|
@ -17427,14 +17352,6 @@ snapshots:
|
|||
section-matter: 1.0.0
|
||||
strip-bom-string: 1.0.0
|
||||
|
||||
gtoken@7.1.0:
|
||||
dependencies:
|
||||
gaxios: 6.7.1
|
||||
jws: 4.0.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
|
||||
gtoken@8.0.0:
|
||||
dependencies:
|
||||
gaxios: 7.1.3
|
||||
|
|
@ -21641,8 +21558,6 @@ snapshots:
|
|||
|
||||
uuid@8.3.2: {}
|
||||
|
||||
uuid@9.0.1: {}
|
||||
|
||||
v8-to-istanbul@9.3.0:
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.25
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"
|
||||
import { GoogleAuth, JWTInput } from "google-auth-library"
|
||||
import type { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { createVertexAnthropic } from "@ai-sdk/google-vertex/anthropic"
|
||||
import { streamText, generateText, ToolSet } from "ai"
|
||||
|
||||
import {
|
||||
type ModelInfo,
|
||||
|
|
@ -9,58 +9,78 @@ import {
|
|||
vertexModels,
|
||||
ANTHROPIC_DEFAULT_MAX_TOKENS,
|
||||
VERTEX_1M_CONTEXT_MODEL_IDS,
|
||||
ApiProviderError,
|
||||
} from "@roo-code/types"
|
||||
import { safeJsonParse } from "@roo-code/core"
|
||||
import { TelemetryService } from "@roo-code/telemetry"
|
||||
|
||||
import { ApiHandlerOptions } from "../../shared/api"
|
||||
import type { ApiHandlerOptions } from "../../shared/api"
|
||||
import { shouldUseReasoningBudget } from "../../shared/api"
|
||||
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { addCacheBreakpoints } from "../transform/caching/vertex"
|
||||
import type { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
|
||||
import { getModelParams } from "../transform/model-params"
|
||||
import { filterNonAnthropicBlocks } from "../transform/anthropic-filter"
|
||||
import {
|
||||
convertOpenAIToolsToAnthropic,
|
||||
convertOpenAIToolChoiceToAnthropic,
|
||||
} from "../../core/prompts/tools/native-tools/converters"
|
||||
convertToAiSdkMessages,
|
||||
convertToolsForAiSdk,
|
||||
processAiSdkStreamPart,
|
||||
mapToolChoice,
|
||||
handleAiSdkError,
|
||||
} from "../transform/ai-sdk"
|
||||
import { calculateApiCostAnthropic } from "../../shared/cost"
|
||||
|
||||
import { DEFAULT_HEADERS } from "./constants"
|
||||
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
|
||||
private provider: ReturnType<typeof createVertexAnthropic>
|
||||
private readonly providerName = "Vertex (Anthropic)"
|
||||
private lastThoughtSignature: string | undefined
|
||||
private lastRedactedThinkingBlocks: Array<{ type: "redacted_thinking"; data: string }> = []
|
||||
|
||||
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 })
|
||||
// Build googleAuthOptions based on provided credentials
|
||||
let googleAuthOptions: { credentials?: object; keyFile?: string } | undefined
|
||||
if (options.vertexJsonCredentials) {
|
||||
try {
|
||||
googleAuthOptions = { credentials: JSON.parse(options.vertexJsonCredentials) }
|
||||
} catch {
|
||||
// If JSON parsing fails, ignore and try other auth methods
|
||||
}
|
||||
} else if (options.vertexKeyFile) {
|
||||
googleAuthOptions = { keyFile: options.vertexKeyFile }
|
||||
}
|
||||
|
||||
// Build beta headers for 1M context support
|
||||
const modelId = options.apiModelId
|
||||
const betas: string[] = []
|
||||
|
||||
if (modelId) {
|
||||
const supports1MContext = VERTEX_1M_CONTEXT_MODEL_IDS.includes(
|
||||
modelId as (typeof VERTEX_1M_CONTEXT_MODEL_IDS)[number],
|
||||
)
|
||||
if (supports1MContext && options.vertex1MContext) {
|
||||
betas.push("context-1m-2025-08-07")
|
||||
}
|
||||
}
|
||||
|
||||
this.provider = createVertexAnthropic({
|
||||
project: projectId,
|
||||
location: region,
|
||||
googleAuthOptions,
|
||||
headers: {
|
||||
...DEFAULT_HEADERS,
|
||||
...(betas.length > 0 ? { "anthropic-beta": betas.join(",") } : {}),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
override async *createMessage(
|
||||
|
|
@ -68,16 +88,39 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple
|
|||
messages: Anthropic.Messages.MessageParam[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
let { id, info, temperature, maxTokens, reasoning: thinking, betas } = this.getModel()
|
||||
const modelConfig = this.getModel()
|
||||
|
||||
const { supportsPromptCache } = info
|
||||
// Reset thinking state for this request
|
||||
this.lastThoughtSignature = undefined
|
||||
this.lastRedactedThinkingBlocks = []
|
||||
|
||||
// Filter out non-Anthropic blocks (reasoning, thoughtSignature, etc.) before sending to the API
|
||||
const sanitizedMessages = filterNonAnthropicBlocks(messages)
|
||||
// Convert messages to AI SDK format
|
||||
const aiSdkMessages = convertToAiSdkMessages(messages)
|
||||
|
||||
const nativeToolParams = {
|
||||
tools: convertOpenAIToolsToAnthropic(metadata?.tools ?? []),
|
||||
tool_choice: convertOpenAIToolChoiceToAnthropic(metadata?.tool_choice, metadata?.parallelToolCalls),
|
||||
// Convert tools to AI SDK format
|
||||
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
|
||||
const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
|
||||
|
||||
// Build Anthropic provider options
|
||||
const anthropicProviderOptions: Record<string, unknown> = {}
|
||||
|
||||
// Configure thinking/reasoning if the model supports it
|
||||
const isThinkingEnabled =
|
||||
shouldUseReasoningBudget({ model: modelConfig.info, settings: this.options }) &&
|
||||
modelConfig.reasoning &&
|
||||
modelConfig.reasoningBudget
|
||||
|
||||
if (isThinkingEnabled) {
|
||||
anthropicProviderOptions.thinking = {
|
||||
type: "enabled",
|
||||
budgetTokens: modelConfig.reasoningBudget,
|
||||
}
|
||||
}
|
||||
|
||||
// Forward parallelToolCalls setting
|
||||
// When parallelToolCalls is explicitly false, disable parallel tool use
|
||||
if (metadata?.parallelToolCalls === false) {
|
||||
anthropicProviderOptions.disableParallelToolUse = true
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -93,114 +136,178 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple
|
|||
* 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,
|
||||
const cacheProviderOption = { anthropic: { cacheControl: { type: "ephemeral" as const } } }
|
||||
|
||||
const userMsgIndices = messages.reduce(
|
||||
(acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc),
|
||||
[] as number[],
|
||||
)
|
||||
|
||||
const targetIndices = new Set<number>()
|
||||
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
|
||||
const secondLastUserMsgIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
|
||||
|
||||
if (lastUserMsgIndex >= 0) targetIndices.add(lastUserMsgIndex)
|
||||
if (secondLastUserMsgIndex >= 0) targetIndices.add(secondLastUserMsgIndex)
|
||||
|
||||
if (targetIndices.size > 0) {
|
||||
this.applyCacheControlToAiSdkMessages(messages, aiSdkMessages, targetIndices, cacheProviderOption)
|
||||
}
|
||||
|
||||
// and prompt caching
|
||||
const requestOptions = betas?.length ? { headers: { "anthropic-beta": betas.join(",") } } : undefined
|
||||
// Build streamText request
|
||||
// Cast providerOptions to any to bypass strict JSONObject typing — the AI SDK accepts the correct runtime values
|
||||
const requestOptions: Parameters<typeof streamText>[0] = {
|
||||
model: this.provider(modelConfig.id),
|
||||
system: systemPrompt,
|
||||
...({
|
||||
systemProviderOptions: { anthropic: { cacheControl: { type: "ephemeral" } } },
|
||||
} as Record<string, unknown>),
|
||||
messages: aiSdkMessages,
|
||||
temperature: modelConfig.temperature,
|
||||
maxOutputTokens: modelConfig.maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,
|
||||
tools: aiSdkTools,
|
||||
toolChoice: mapToolChoice(metadata?.tool_choice),
|
||||
...(Object.keys(anthropicProviderOptions).length > 0 && {
|
||||
providerOptions: { anthropic: anthropicProviderOptions } as any,
|
||||
}),
|
||||
}
|
||||
|
||||
const stream = await this.client.messages.create(params, requestOptions)
|
||||
try {
|
||||
const result = streamText(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
|
||||
for await (const part of result.fullStream) {
|
||||
// Capture thinking signature from stream events
|
||||
// The AI SDK's @ai-sdk/anthropic emits the signature as a reasoning-delta
|
||||
// event with providerMetadata.anthropic.signature
|
||||
const partAny = part as any
|
||||
if (partAny.providerMetadata?.anthropic?.signature) {
|
||||
this.lastThoughtSignature = partAny.providerMetadata.anthropic.signature
|
||||
}
|
||||
case "message_delta": {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: 0,
|
||||
outputTokens: chunk.usage!.output_tokens || 0,
|
||||
}
|
||||
|
||||
break
|
||||
// Capture redacted thinking blocks from stream events
|
||||
if (partAny.providerMetadata?.anthropic?.redactedData) {
|
||||
this.lastRedactedThinkingBlocks.push({
|
||||
type: "redacted_thinking",
|
||||
data: partAny.providerMetadata.anthropic.redactedData,
|
||||
})
|
||||
}
|
||||
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" }
|
||||
}
|
||||
for (const chunk of processAiSdkStreamPart(part)) {
|
||||
yield chunk
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// Yield usage metrics at the end, including cache metrics from providerMetadata
|
||||
const usage = await result.usage
|
||||
const providerMetadata = await result.providerMetadata
|
||||
if (usage) {
|
||||
yield this.processUsageMetrics(usage, modelConfig.info, providerMetadata)
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
TelemetryService.instance.captureException(
|
||||
new ApiProviderError(errorMessage, this.providerName, modelConfig.id, "createMessage"),
|
||||
)
|
||||
throw handleAiSdkError(error, this.providerName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process usage metrics from the AI SDK response, including Anthropic's cache metrics.
|
||||
*/
|
||||
private processUsageMetrics(
|
||||
usage: { inputTokens?: number; outputTokens?: number },
|
||||
info: ModelInfo,
|
||||
providerMetadata?: Record<string, Record<string, unknown>>,
|
||||
): ApiStreamUsageChunk {
|
||||
const inputTokens = usage.inputTokens ?? 0
|
||||
const outputTokens = usage.outputTokens ?? 0
|
||||
|
||||
// Extract cache metrics from Anthropic's providerMetadata
|
||||
const anthropicMeta = providerMetadata?.anthropic as
|
||||
| { cacheCreationInputTokens?: number; cacheReadInputTokens?: number }
|
||||
| undefined
|
||||
const cacheWriteTokens = anthropicMeta?.cacheCreationInputTokens ?? 0
|
||||
const cacheReadTokens = anthropicMeta?.cacheReadInputTokens ?? 0
|
||||
|
||||
const { totalCost } = calculateApiCostAnthropic(
|
||||
info,
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cacheWriteTokens,
|
||||
cacheReadTokens,
|
||||
)
|
||||
|
||||
return {
|
||||
type: "usage",
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cacheWriteTokens: cacheWriteTokens > 0 ? cacheWriteTokens : undefined,
|
||||
cacheReadTokens: cacheReadTokens > 0 ? cacheReadTokens : undefined,
|
||||
totalCost,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply cacheControl providerOptions to the correct AI SDK messages by walking
|
||||
* the original Anthropic messages and converted AI SDK messages in parallel.
|
||||
*
|
||||
* convertToAiSdkMessages() can split a single Anthropic user message (containing
|
||||
* tool_results + text) into 2 AI SDK messages (tool role + user role). This method
|
||||
* accounts for that split so cache control lands on the right message.
|
||||
*/
|
||||
private applyCacheControlToAiSdkMessages(
|
||||
originalMessages: Anthropic.Messages.MessageParam[],
|
||||
aiSdkMessages: { role: string; providerOptions?: Record<string, Record<string, unknown>> }[],
|
||||
targetOriginalIndices: Set<number>,
|
||||
cacheProviderOption: Record<string, Record<string, unknown>>,
|
||||
): void {
|
||||
let aiSdkIdx = 0
|
||||
for (let origIdx = 0; origIdx < originalMessages.length; origIdx++) {
|
||||
const origMsg = originalMessages[origIdx]
|
||||
|
||||
if (typeof origMsg.content === "string") {
|
||||
if (targetOriginalIndices.has(origIdx) && aiSdkIdx < aiSdkMessages.length) {
|
||||
aiSdkMessages[aiSdkIdx].providerOptions = {
|
||||
...aiSdkMessages[aiSdkIdx].providerOptions,
|
||||
...cacheProviderOption,
|
||||
}
|
||||
}
|
||||
aiSdkIdx++
|
||||
} else if (origMsg.role === "user") {
|
||||
const hasToolResults = origMsg.content.some((part) => (part as { type: string }).type === "tool_result")
|
||||
const hasNonToolContent = origMsg.content.some(
|
||||
(part) => (part as { type: string }).type === "text" || (part as { type: string }).type === "image",
|
||||
)
|
||||
|
||||
if (hasToolResults && hasNonToolContent) {
|
||||
const userMsgIdx = aiSdkIdx + 1
|
||||
if (targetOriginalIndices.has(origIdx) && userMsgIdx < aiSdkMessages.length) {
|
||||
aiSdkMessages[userMsgIdx].providerOptions = {
|
||||
...aiSdkMessages[userMsgIdx].providerOptions,
|
||||
...cacheProviderOption,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
aiSdkIdx += 2
|
||||
} else if (hasToolResults) {
|
||||
if (targetOriginalIndices.has(origIdx) && aiSdkIdx < aiSdkMessages.length) {
|
||||
aiSdkMessages[aiSdkIdx].providerOptions = {
|
||||
...aiSdkMessages[aiSdkIdx].providerOptions,
|
||||
...cacheProviderOption,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
aiSdkIdx++
|
||||
} else {
|
||||
if (targetOriginalIndices.has(origIdx) && aiSdkIdx < aiSdkMessages.length) {
|
||||
aiSdkMessages[aiSdkIdx].providerOptions = {
|
||||
...aiSdkMessages[aiSdkIdx].providerOptions,
|
||||
...cacheProviderOption,
|
||||
}
|
||||
}
|
||||
aiSdkIdx++
|
||||
}
|
||||
} else {
|
||||
aiSdkIdx++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -239,10 +346,9 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple
|
|||
defaultTemperature: 0,
|
||||
})
|
||||
|
||||
// Build betas array for request headers
|
||||
// Build betas array for request headers (kept for backward compatibility / testing)
|
||||
const betas: string[] = []
|
||||
|
||||
// Add 1M context beta flag if enabled for supported models
|
||||
if (enable1MContext) {
|
||||
betas.push("context-1m-2025-08-07")
|
||||
}
|
||||
|
|
@ -259,46 +365,49 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple
|
|||
}
|
||||
}
|
||||
|
||||
async completePrompt(prompt: string) {
|
||||
async completePrompt(prompt: string): Promise<string> {
|
||||
const { id, temperature } = this.getModel()
|
||||
|
||||
try {
|
||||
let {
|
||||
id,
|
||||
info: { supportsPromptCache },
|
||||
const { text } = await generateText({
|
||||
model: this.provider(id),
|
||||
prompt,
|
||||
maxOutputTokens: ANTHROPIC_DEFAULT_MAX_TOKENS,
|
||||
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 ""
|
||||
return text
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new Error(`Vertex completion error: ${error.message}`)
|
||||
}
|
||||
|
||||
throw error
|
||||
TelemetryService.instance.captureException(
|
||||
new ApiProviderError(
|
||||
error instanceof Error ? error.message : String(error),
|
||||
this.providerName,
|
||||
id,
|
||||
"completePrompt",
|
||||
),
|
||||
)
|
||||
throw handleAiSdkError(error, this.providerName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the thinking signature captured from the last Anthropic response.
|
||||
* Claude models with extended thinking return a cryptographic signature
|
||||
* which must be round-tripped back for multi-turn conversations with tool use.
|
||||
*/
|
||||
getThoughtSignature(): string | undefined {
|
||||
return this.lastThoughtSignature
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns any redacted thinking blocks captured from the last Anthropic response.
|
||||
* Anthropic returns these when safety filters trigger on reasoning content.
|
||||
*/
|
||||
getRedactedThinkingBlocks(): Array<{ type: "redacted_thinking"; data: string }> | undefined {
|
||||
return this.lastRedactedThinkingBlocks.length > 0 ? this.lastRedactedThinkingBlocks : undefined
|
||||
}
|
||||
|
||||
override isAiSdkProvider(): boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -462,7 +462,6 @@
|
|||
"@ai-sdk/openai": "^3.0.26",
|
||||
"@ai-sdk/xai": "^3.0.48",
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
"@anthropic-ai/vertex-sdk": "^0.7.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.922.0",
|
||||
"@aws-sdk/credential-providers": "^3.922.0",
|
||||
"@google/genai": "^1.29.1",
|
||||
|
|
@ -492,7 +491,6 @@
|
|||
"fzf": "^0.5.2",
|
||||
"get-folder-size": "^5.0.0",
|
||||
"global-agent": "^3.0.0",
|
||||
"google-auth-library": "^9.15.1",
|
||||
"gray-matter": "^4.0.3",
|
||||
"i18next": "^25.0.0",
|
||||
"ignore": "^7.0.3",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue