Roo-Code/src/api/providers/anthropic-vertex.ts
roomote[bot] 0e5407aa76
fix: make defaultTemperature required in getModelParams to prevent silent temperature overrides (#11218)
* fix: DeepSeek temperature defaulting to 0 instead of 0.3

Pass defaultTemperature: DEEP_SEEK_DEFAULT_TEMPERATURE to getModelParams() in
DeepSeekHandler.getModel() to ensure the correct default temperature (0.3)
is used when no user configuration is provided.

Closes #11194

* refactor: make defaultTemperature required in getModelParams

Make the defaultTemperature parameter required in getModelParams() instead
of defaulting to 0. This prevents providers with their own non-zero default
temperature (like DeepSeek's 0.3) from being silently overridden by the
implicit 0 default.

Every provider now explicitly declares its temperature default, making the
temperature resolution chain clear:
  user setting → model default → provider default

---------

Co-authored-by: Roo Code <roomote@roocode.com>
Co-authored-by: daniel-lxs <ricciodaniel98@gmail.com>
2026-02-06 18:38:31 -05:00

304 lines
8.8 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,
defaultTemperature: 0,
})
// 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
}
}
}