Jelajahi Sumber

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
Daniel 4 hari lalu
induk
melakukan
24039c78b5

+ 0 - 85
pnpm-lock.yaml

@@ -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/[email protected]':
     resolution: {integrity: sha512-tHjX2YbkUBwEgg0JZU3EFSSAQPoK4qQR/NFYa8Vtzd5UAyXzZksCw2In69Rml4R/TyHPBfRYaLK35XiOe33pjw==}
 
-  '@anthropic-ai/[email protected]':
-    resolution: {integrity: sha512-zNm3hUXgYmYDTyveIxOyxbcnh5VXFkrLo4bSnG6LAfGzW7k3k2iCNDSVKtR9qZrK2BCid7JtVu7jsEKaZ/9dSw==}
-
   '@asamuzakjp/[email protected]':
     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.
 
-  [email protected]:
-    resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==}
-    engines: {node: '>=14'}
-
   [email protected]:
     resolution: {integrity: sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==}
     engines: {node: '>=18'}
 
-  [email protected]:
-    resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==}
-    engines: {node: '>=14'}
-
   [email protected]:
     resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==}
     engines: {node: '>=18'}
@@ -6947,14 +6930,6 @@ packages:
     resolution: {integrity: sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==}
     engines: {node: '>=18'}
 
-  [email protected]:
-    resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==}
-    engines: {node: '>=14'}
-
-  [email protected]:
-    resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==}
-    engines: {node: '>=14'}
-
   [email protected]:
     resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==}
     engines: {node: '>=14'}
@@ -6973,10 +6948,6 @@ packages:
     resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==}
     engines: {node: '>=6.0'}
 
-  [email protected]:
-    resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==}
-    engines: {node: '>=14.0.0'}
-
   [email protected]:
     resolution: {integrity: sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==}
     engines: {node: '>=18'}
@@ -10539,10 +10510,6 @@ packages:
     resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
     hasBin: true
 
-  [email protected]:
-    resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
-    hasBin: true
-
   [email protected]:
     resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==}
     engines: {node: '>=10.12.0'}
@@ -11232,14 +11199,6 @@ snapshots:
     transitivePeerDependencies:
       - encoding
 
-  '@anthropic-ai/[email protected]':
-    dependencies:
-      '@anthropic-ai/sdk': 0.37.0
-      google-auth-library: 9.15.1
-    transitivePeerDependencies:
-      - encoding
-      - supports-color
-
   '@asamuzakjp/[email protected]':
     dependencies:
       '@csstools/css-calc': 2.1.4(@csstools/[email protected](@csstools/[email protected]))(@csstools/[email protected])
@@ -17221,17 +17180,6 @@ snapshots:
       strip-ansi: 6.0.1
       wide-align: 1.1.5
 
-  [email protected]:
-    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
-
   [email protected]:
     dependencies:
       extend: 3.0.2
@@ -17241,15 +17189,6 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  [email protected]:
-    dependencies:
-      gaxios: 6.7.1
-      google-logging-utils: 0.0.2
-      json-bigint: 1.0.0
-    transitivePeerDependencies:
-      - encoding
-      - supports-color
-
   [email protected]:
     dependencies:
       gaxios: 7.1.3
@@ -17398,20 +17337,6 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  [email protected]:
-    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
-
-  [email protected]: {}
-
   [email protected]: {}
 
   [email protected]: {}
@@ -17427,14 +17352,6 @@ snapshots:
       section-matter: 1.0.0
       strip-bom-string: 1.0.0
 
-  [email protected]:
-    dependencies:
-      gaxios: 6.7.1
-      jws: 4.0.0
-    transitivePeerDependencies:
-      - encoding
-      - supports-color
-
   [email protected]:
     dependencies:
       gaxios: 7.1.3
@@ -21641,8 +21558,6 @@ snapshots:
 
   [email protected]: {}
 
-  [email protected]: {}
-
   [email protected]:
     dependencies:
       '@jridgewell/trace-mapping': 0.3.25

File diff ditekan karena terlalu besar
+ 320 - 670
src/api/providers/__tests__/anthropic-vertex.spec.ts


+ 279 - 170
src/api/providers/anthropic-vertex.ts

@@ -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()
+
+		// 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> = {}
 
-		const { supportsPromptCache } = info
+		// Configure thinking/reasoning if the model supports it
+		const isThinkingEnabled =
+			shouldUseReasoningBudget({ model: modelConfig.info, settings: this.options }) &&
+			modelConfig.reasoning &&
+			modelConfig.reasoningBudget
 
-		// Filter out non-Anthropic blocks (reasoning, thoughtSignature, etc.) before sending to the API
-		const sanitizedMessages = filterNonAnthropicBlocks(messages)
+		if (isThinkingEnabled) {
+			anthropicProviderOptions.thinking = {
+				type: "enabled",
+				budgetTokens: modelConfig.reasoningBudget,
+			}
+		}
 
-		const nativeToolParams = {
-			tools: convertOpenAIToolsToAnthropic(metadata?.tools ?? []),
-			tool_choice: convertOpenAIToolChoiceToAnthropic(metadata?.tool_choice, metadata?.parallelToolCalls),
+		// 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[],
+		)
 
-		// and prompt caching
-		const requestOptions = betas?.length ? { headers: { "anthropic-beta": betas.join(",") } } : undefined
+		const targetIndices = new Set<number>()
+		const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
+		const secondLastUserMsgIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
 
-		const stream = await this.client.messages.create(params, requestOptions)
+		if (lastUserMsgIndex >= 0) targetIndices.add(lastUserMsgIndex)
+		if (secondLastUserMsgIndex >= 0) targetIndices.add(secondLastUserMsgIndex)
 
-		for await (const chunk of stream) {
-			switch (chunk.type) {
-				case "message_start": {
-					const usage = chunk.message!.usage
+		if (targetIndices.size > 0) {
+			this.applyCacheControlToAiSdkMessages(messages, aiSdkMessages, targetIndices, cacheProviderOption)
+		}
 
-					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,
-					}
+		// 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,
+			}),
+		}
 
-					break
+		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
 				}
-				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" }
-							}
 
-							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
-						}
-					}
+				for (const chunk of processAiSdkStreamPart(part)) {
+					yield chunk
+				}
+			}
 
-					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,
+					}
 				}
-				case "content_block_delta": {
-					switch (chunk.delta!.type) {
-						case "text_delta": {
-							yield { type: "text", text: chunk.delta!.text }
-							break
+				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,
 						}
-						case "thinking_delta": {
-							yield { type: "reasoning", text: (chunk.delta as any).thinking }
-							break
+					}
+					aiSdkIdx += 2
+				} else if (hasToolResults) {
+					if (targetOriginalIndices.has(origIdx) && aiSdkIdx < aiSdkMessages.length) {
+						aiSdkMessages[aiSdkIdx].providerOptions = {
+							...aiSdkMessages[aiSdkIdx].providerOptions,
+							...cacheProviderOption,
 						}
-						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++
+				} else {
+					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 {
+				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) {
-		try {
-			let {
-				id,
-				info: { supportsPromptCache },
-				temperature,
-				maxTokens = ANTHROPIC_DEFAULT_MAX_TOKENS,
-				reasoning: thinking,
-			} = this.getModel()
+	async completePrompt(prompt: string): Promise<string> {
+		const { id, temperature } = this.getModel()
 
-			const params: Anthropic.Messages.MessageCreateParamsNonStreaming = {
-				model: id,
-				max_tokens: maxTokens,
+		try {
+			const { text } = await generateText({
+				model: this.provider(id),
+				prompt,
+				maxOutputTokens: ANTHROPIC_DEFAULT_MAX_TOKENS,
 				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]
+			return text
+		} catch (error) {
+			TelemetryService.instance.captureException(
+				new ApiProviderError(
+					error instanceof Error ? error.message : String(error),
+					this.providerName,
+					id,
+					"completePrompt",
+				),
+			)
+			throw handleAiSdkError(error, this.providerName)
+		}
+	}
 
-			if (content.type === "text") {
-				return 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
+	}
 
-			return ""
-		} catch (error) {
-			if (error instanceof Error) {
-				throw new Error(`Vertex completion error: ${error.message}`)
-			}
+	/**
+	 * 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
+	}
 
-			throw error
-		}
+	override isAiSdkProvider(): boolean {
+		return true
 	}
 }

+ 0 - 2
src/package.json

@@ -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",

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini