mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
refactor: migrate Anthropic provider to @ai-sdk/anthropic (#11287)
* refactor: migrate Anthropic provider to @ai-sdk/anthropic Replace the raw @anthropic-ai/sdk implementation with @ai-sdk/anthropic (Vercel AI SDK) for consistency with other providers (Bedrock, DeepSeek, Mistral, etc.). Changes: - Replace Anthropic() client with createAnthropic() from @ai-sdk/anthropic - Replace manual stream parsing with streamText() + processAiSdkStreamPart() - Replace client.messages.create() with generateText() for completePrompt() - Use convertToAiSdkMessages() and convertToolsForAiSdk() for format conversion - Handle prompt caching via AI SDK providerOptions (cacheControl on messages) - Handle extended thinking via providerOptions.anthropic.thinking - Add getThoughtSignature() and getRedactedThinkingBlocks() for thinking signature round-tripping (matching Bedrock pattern, improves on original which had a TODO for this) - Add isAiSdkProvider() returning true - Update tests to mock @ai-sdk/anthropic and ai instead of raw SDK * fix: address PR review - remove apiKey fallback and use system+systemProviderOptions pattern
This commit is contained in:
parent
ef2fec9a23
commit
1e0fc89fa1
4 changed files with 694 additions and 838 deletions
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
|
|
@ -749,6 +749,9 @@ importers:
|
|||
'@ai-sdk/amazon-bedrock':
|
||||
specifier: ^4.0.51
|
||||
version: 4.0.51(zod@3.25.76)
|
||||
'@ai-sdk/anthropic':
|
||||
specifier: ^3.0.38
|
||||
version: 3.0.38(zod@3.25.76)
|
||||
'@ai-sdk/baseten':
|
||||
specifier: ^1.0.31
|
||||
version: 1.0.31(zod@3.25.76)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,6 @@
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming"
|
||||
import { CacheControlEphemeral } from "@anthropic-ai/sdk/resources"
|
||||
import OpenAI from "openai"
|
||||
import type { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { createAnthropic } from "@ai-sdk/anthropic"
|
||||
import { streamText, generateText, ToolSet } from "ai"
|
||||
|
||||
import {
|
||||
type ModelInfo,
|
||||
|
|
@ -14,317 +13,277 @@ import {
|
|||
import { TelemetryService } from "@roo-code/telemetry"
|
||||
|
||||
import type { ApiHandlerOptions } from "../../shared/api"
|
||||
import { shouldUseReasoningBudget } from "../../shared/api"
|
||||
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import type { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
|
||||
import { getModelParams } from "../transform/model-params"
|
||||
import { filterNonAnthropicBlocks } from "../transform/anthropic-filter"
|
||||
import { handleProviderError } from "./utils/error-handler"
|
||||
import {
|
||||
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"
|
||||
import { calculateApiCostAnthropic } from "../../shared/cost"
|
||||
import {
|
||||
convertOpenAIToolsToAnthropic,
|
||||
convertOpenAIToolChoiceToAnthropic,
|
||||
} from "../../core/prompts/tools/native-tools/converters"
|
||||
|
||||
export class AnthropicHandler extends BaseProvider implements SingleCompletionHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: Anthropic
|
||||
private provider: ReturnType<typeof createAnthropic>
|
||||
private readonly providerName = "Anthropic"
|
||||
private lastThoughtSignature: string | undefined
|
||||
private lastRedactedThinkingBlocks: Array<{ type: "redacted_thinking"; data: string }> = []
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
super()
|
||||
this.options = options
|
||||
|
||||
const apiKeyFieldName =
|
||||
this.options.anthropicBaseUrl && this.options.anthropicUseAuthToken ? "authToken" : "apiKey"
|
||||
const useAuthToken = Boolean(options.anthropicBaseUrl && options.anthropicUseAuthToken)
|
||||
|
||||
this.client = new Anthropic({
|
||||
baseURL: this.options.anthropicBaseUrl || undefined,
|
||||
[apiKeyFieldName]: this.options.apiKey,
|
||||
})
|
||||
}
|
||||
// Build beta headers for model-specific features
|
||||
const betas: string[] = []
|
||||
const modelId = options.apiModelId
|
||||
|
||||
async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
let stream: AnthropicStream<Anthropic.Messages.RawMessageStreamEvent>
|
||||
const cacheControl: CacheControlEphemeral = { type: "ephemeral" }
|
||||
let {
|
||||
id: modelId,
|
||||
betas = ["fine-grained-tool-streaming-2025-05-14"],
|
||||
maxTokens,
|
||||
temperature,
|
||||
reasoning: thinking,
|
||||
} = this.getModel()
|
||||
if (modelId === "claude-3-7-sonnet-20250219:thinking") {
|
||||
betas.push("output-128k-2025-02-19")
|
||||
}
|
||||
|
||||
// Filter out non-Anthropic blocks (reasoning, thoughtSignature, etc.) before sending to the API
|
||||
const sanitizedMessages = filterNonAnthropicBlocks(messages)
|
||||
|
||||
// Add 1M context beta flag if enabled for supported models (Claude Sonnet 4/4.5, Opus 4.6)
|
||||
if (
|
||||
(modelId === "claude-sonnet-4-20250514" ||
|
||||
modelId === "claude-sonnet-4-5" ||
|
||||
modelId === "claude-opus-4-6") &&
|
||||
this.options.anthropicBeta1MContext
|
||||
options.anthropicBeta1MContext
|
||||
) {
|
||||
betas.push("context-1m-2025-08-07")
|
||||
}
|
||||
|
||||
const nativeToolParams = {
|
||||
tools: convertOpenAIToolsToAnthropic(metadata?.tools ?? []),
|
||||
tool_choice: convertOpenAIToolChoiceToAnthropic(metadata?.tool_choice, metadata?.parallelToolCalls),
|
||||
this.provider = createAnthropic({
|
||||
baseURL: options.anthropicBaseUrl || undefined,
|
||||
...(useAuthToken ? { authToken: options.apiKey } : { apiKey: options.apiKey }),
|
||||
headers: {
|
||||
...DEFAULT_HEADERS,
|
||||
...(betas.length > 0 ? { "anthropic-beta": betas.join(",") } : {}),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
override async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
const modelConfig = this.getModel()
|
||||
|
||||
// Reset thinking state for this request
|
||||
this.lastThoughtSignature = undefined
|
||||
this.lastRedactedThinkingBlocks = []
|
||||
|
||||
// Convert messages to AI SDK format
|
||||
const aiSdkMessages = convertToAiSdkMessages(messages)
|
||||
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
|
||||
switch (modelId) {
|
||||
case "claude-sonnet-4-5":
|
||||
case "claude-sonnet-4-20250514":
|
||||
case "claude-opus-4-6":
|
||||
case "claude-opus-4-5-20251101":
|
||||
case "claude-opus-4-1-20250805":
|
||||
case "claude-opus-4-20250514":
|
||||
case "claude-3-7-sonnet-20250219":
|
||||
case "claude-3-5-sonnet-20241022":
|
||||
case "claude-3-5-haiku-20241022":
|
||||
case "claude-3-opus-20240229":
|
||||
case "claude-haiku-4-5-20251001":
|
||||
case "claude-3-haiku-20240307": {
|
||||
/**
|
||||
* The latest message will be the new user message, one before
|
||||
* will be the assistant message from a previous request, and
|
||||
* the user message before that will be a previously cached user
|
||||
* message. So we need to mark the latest user message as
|
||||
* ephemeral to cache it for the next request, and mark the
|
||||
* second to last user message as ephemeral to let the server
|
||||
* know the last message to retrieve from the cache for the
|
||||
* current request.
|
||||
*/
|
||||
const userMsgIndices = sanitizedMessages.reduce(
|
||||
(acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc),
|
||||
[] as number[],
|
||||
// Forward parallelToolCalls setting
|
||||
// When parallelToolCalls is explicitly false, disable parallel tool use
|
||||
if (metadata?.parallelToolCalls === false) {
|
||||
anthropicProviderOptions.disableParallelToolUse = true
|
||||
}
|
||||
|
||||
// Apply cache control to user messages
|
||||
// Strategy: cache the last 2 user messages (write-to-cache + read-from-cache)
|
||||
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)
|
||||
}
|
||||
|
||||
// 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,
|
||||
}),
|
||||
}
|
||||
|
||||
try {
|
||||
const result = streamText(requestOptions)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Capture redacted thinking blocks from stream events
|
||||
if (partAny.providerMetadata?.anthropic?.redactedData) {
|
||||
this.lastRedactedThinkingBlocks.push({
|
||||
type: "redacted_thinking",
|
||||
data: partAny.providerMetadata.anthropic.redactedData,
|
||||
})
|
||||
}
|
||||
|
||||
for (const chunk of processAiSdkStreamPart(part)) {
|
||||
yield chunk
|
||||
}
|
||||
}
|
||||
|
||||
// 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",
|
||||
)
|
||||
|
||||
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
|
||||
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
|
||||
|
||||
try {
|
||||
stream = await this.client.messages.create(
|
||||
{
|
||||
model: modelId,
|
||||
max_tokens: maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,
|
||||
temperature,
|
||||
thinking,
|
||||
// Setting cache breakpoint for system prompt so new tasks can reuse it.
|
||||
system: [{ text: systemPrompt, type: "text", cache_control: cacheControl }],
|
||||
messages: sanitizedMessages.map((message, index) => {
|
||||
if (index === lastUserMsgIndex || index === secondLastMsgUserIndex) {
|
||||
return {
|
||||
...message,
|
||||
content:
|
||||
typeof message.content === "string"
|
||||
? [{ type: "text", text: message.content, cache_control: cacheControl }]
|
||||
: message.content.map((content, contentIndex) =>
|
||||
contentIndex === message.content.length - 1
|
||||
? { ...content, cache_control: cacheControl }
|
||||
: content,
|
||||
),
|
||||
}
|
||||
}
|
||||
return message
|
||||
}),
|
||||
stream: true,
|
||||
...nativeToolParams,
|
||||
},
|
||||
(() => {
|
||||
// prompt caching: https://x.com/alexalbert__/status/1823751995901272068
|
||||
// https://github.com/anthropics/anthropic-sdk-typescript?tab=readme-ov-file#default-headers
|
||||
// https://github.com/anthropics/anthropic-sdk-typescript/commit/c920b77fc67bd839bfeb6716ceab9d7c9bbe7393
|
||||
|
||||
// Then check for models that support prompt caching
|
||||
switch (modelId) {
|
||||
case "claude-sonnet-4-5":
|
||||
case "claude-sonnet-4-20250514":
|
||||
case "claude-opus-4-6":
|
||||
case "claude-opus-4-5-20251101":
|
||||
case "claude-opus-4-1-20250805":
|
||||
case "claude-opus-4-20250514":
|
||||
case "claude-3-7-sonnet-20250219":
|
||||
case "claude-3-5-sonnet-20241022":
|
||||
case "claude-3-5-haiku-20241022":
|
||||
case "claude-3-opus-20240229":
|
||||
case "claude-haiku-4-5-20251001":
|
||||
case "claude-3-haiku-20240307":
|
||||
betas.push("prompt-caching-2024-07-31")
|
||||
return { headers: { "anthropic-beta": betas.join(",") } }
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
})(),
|
||||
)
|
||||
} catch (error) {
|
||||
TelemetryService.instance.captureException(
|
||||
new ApiProviderError(
|
||||
error instanceof Error ? error.message : String(error),
|
||||
this.providerName,
|
||||
modelId,
|
||||
"createMessage",
|
||||
),
|
||||
)
|
||||
throw error
|
||||
}
|
||||
break
|
||||
}
|
||||
default: {
|
||||
try {
|
||||
stream = (await this.client.messages.create({
|
||||
model: modelId,
|
||||
max_tokens: maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,
|
||||
temperature,
|
||||
system: [{ text: systemPrompt, type: "text" }],
|
||||
messages: sanitizedMessages,
|
||||
stream: true,
|
||||
...nativeToolParams,
|
||||
})) as any
|
||||
} catch (error) {
|
||||
TelemetryService.instance.captureException(
|
||||
new ApiProviderError(
|
||||
error instanceof Error ? error.message : String(error),
|
||||
this.providerName,
|
||||
modelId,
|
||||
"createMessage",
|
||||
),
|
||||
)
|
||||
throw error
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let inputTokens = 0
|
||||
let outputTokens = 0
|
||||
let cacheWriteTokens = 0
|
||||
let cacheReadTokens = 0
|
||||
|
||||
for await (const chunk of stream) {
|
||||
switch (chunk.type) {
|
||||
case "message_start": {
|
||||
// Tells us cache reads/writes/input/output.
|
||||
const {
|
||||
input_tokens = 0,
|
||||
output_tokens = 0,
|
||||
cache_creation_input_tokens,
|
||||
cache_read_input_tokens,
|
||||
} = chunk.message.usage
|
||||
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: input_tokens,
|
||||
outputTokens: output_tokens,
|
||||
cacheWriteTokens: cache_creation_input_tokens || undefined,
|
||||
cacheReadTokens: cache_read_input_tokens || undefined,
|
||||
}
|
||||
|
||||
inputTokens += input_tokens
|
||||
outputTokens += output_tokens
|
||||
cacheWriteTokens += cache_creation_input_tokens || 0
|
||||
cacheReadTokens += cache_read_input_tokens || 0
|
||||
|
||||
break
|
||||
}
|
||||
case "message_delta":
|
||||
// Tells us stop_reason, stop_sequence, and output tokens
|
||||
// along the way and at the end of the message.
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: 0,
|
||||
outputTokens: chunk.usage.output_tokens || 0,
|
||||
}
|
||||
|
||||
break
|
||||
case "message_stop":
|
||||
// No usage data, just an indicator that the message is done.
|
||||
break
|
||||
case "content_block_start":
|
||||
switch (chunk.content_block.type) {
|
||||
case "thinking":
|
||||
// We may receive multiple text blocks, in which
|
||||
// case just insert a line break between them.
|
||||
if (chunk.index > 0) {
|
||||
yield { type: "reasoning", text: "\n" }
|
||||
}
|
||||
|
||||
yield { type: "reasoning", text: chunk.content_block.thinking }
|
||||
break
|
||||
case "text":
|
||||
// We may receive multiple text blocks, in which
|
||||
// case just insert a line break between them.
|
||||
if (chunk.index > 0) {
|
||||
yield { type: "text", text: "\n" }
|
||||
}
|
||||
|
||||
yield { type: "text", text: chunk.content_block.text }
|
||||
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
|
||||
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 "thinking_delta":
|
||||
yield { type: "reasoning", text: chunk.delta.thinking }
|
||||
break
|
||||
case "text_delta":
|
||||
yield { type: "text", text: chunk.delta.text }
|
||||
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.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
|
||||
}
|
||||
}
|
||||
|
||||
if (inputTokens > 0 || outputTokens > 0 || cacheWriteTokens > 0 || cacheReadTokens > 0) {
|
||||
const { totalCost } = calculateApiCostAnthropic(
|
||||
this.getModel().info,
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cacheWriteTokens,
|
||||
cacheReadTokens,
|
||||
)
|
||||
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
totalCost,
|
||||
aiSdkIdx++
|
||||
} else {
|
||||
if (targetOriginalIndices.has(origIdx) && aiSdkIdx < aiSdkMessages.length) {
|
||||
aiSdkMessages[aiSdkIdx].providerOptions = {
|
||||
...aiSdkMessages[aiSdkIdx].providerOptions,
|
||||
...cacheProviderOption,
|
||||
}
|
||||
}
|
||||
aiSdkIdx++
|
||||
}
|
||||
} else {
|
||||
aiSdkIdx++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -339,7 +298,6 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
|
|||
(id === "claude-sonnet-4-20250514" || id === "claude-sonnet-4-5" || id === "claude-opus-4-6") &&
|
||||
this.options.anthropicBeta1MContext
|
||||
) {
|
||||
// Use the tier pricing for 1M context
|
||||
const tier = info.tiers?.[0]
|
||||
if (tier) {
|
||||
info = {
|
||||
|
|
@ -368,37 +326,53 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
|
|||
return {
|
||||
id: id === "claude-3-7-sonnet-20250219:thinking" ? "claude-3-7-sonnet-20250219" : id,
|
||||
info,
|
||||
betas: id === "claude-3-7-sonnet-20250219:thinking" ? ["output-128k-2025-02-19"] : undefined,
|
||||
...params,
|
||||
}
|
||||
}
|
||||
|
||||
async completePrompt(prompt: string) {
|
||||
let { id: model, temperature } = this.getModel()
|
||||
async completePrompt(prompt: string): Promise<string> {
|
||||
const { id, temperature } = this.getModel()
|
||||
|
||||
let message
|
||||
try {
|
||||
message = await this.client.messages.create({
|
||||
model,
|
||||
max_tokens: ANTHROPIC_DEFAULT_MAX_TOKENS,
|
||||
thinking: undefined,
|
||||
const { text } = await generateText({
|
||||
model: this.provider(id),
|
||||
prompt,
|
||||
maxOutputTokens: ANTHROPIC_DEFAULT_MAX_TOKENS,
|
||||
temperature,
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
stream: false,
|
||||
})
|
||||
|
||||
return text
|
||||
} catch (error) {
|
||||
TelemetryService.instance.captureException(
|
||||
new ApiProviderError(
|
||||
error instanceof Error ? error.message : String(error),
|
||||
this.providerName,
|
||||
model,
|
||||
id,
|
||||
"completePrompt",
|
||||
),
|
||||
)
|
||||
throw error
|
||||
throw handleAiSdkError(error, this.providerName)
|
||||
}
|
||||
}
|
||||
|
||||
const content = message.content.find(({ type }) => type === "text")
|
||||
return content?.type === "text" ? content.text : ""
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -451,6 +451,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/amazon-bedrock": "^4.0.51",
|
||||
"@ai-sdk/anthropic": "^3.0.38",
|
||||
"@ai-sdk/baseten": "^1.0.31",
|
||||
"@ai-sdk/deepseek": "^2.0.18",
|
||||
"@ai-sdk/fireworks": "^2.0.32",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue