mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
* fix: correct Bedrock model ID for Claude Opus 4.6 (#11232) Remove the :0 suffix from the Claude Opus 4.6 model ID to match the correct AWS Bedrock model identifier. The model ID was "anthropic.claude-opus-4-6-v1:0" but should be "anthropic.claude-opus-4-6-v1" per AWS Bedrock documentation. Fixes #11231 Co-authored-by: Roo Code <roomote@roocode.com> * fix: guard against empty-string baseURL in provider constructors (#11233) When the 'custom base URL' checkbox is unchecked in the UI, the setting is set to '' (empty string). Providers that passed this directly to their SDK constructors caused 'Failed to parse URL' errors because the SDK treated '' as a valid but broken base URL override. - gemini.ts: use || undefined (was passing raw option) - openai-native.ts: use || undefined (was passing raw option) - openai.ts: change ?? to || for fallback default - deepseek.ts: change ?? to || for fallback default - moonshot.ts: change ?? to || for fallback default Adds test coverage for Gemini and OpenAI Native constructors verifying empty-string baseURL is coerced to undefined. * 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> * feat: batch consecutive tool calls in chat UI with shared utility (#11245) * feat: group consecutive list_files tool calls into single UI block Consolidate consecutive listFilesTopLevel/listFilesRecursive ask messages into a single 'Roo wants to view multiple directories' block, matching the existing read_file batching pattern. * chore: add missing translation keys for all locales * refactor: consolidate duplicate listFiles batch-handling blocks in ChatRow Merge the separate listFilesTopLevel and listFilesRecursive case blocks into a single combined case with shared batch-detection logic, selecting the icon and translation key based on the tool type. This removes the duplicated isBatchDirRequest check and BatchListFilesPermission render. * feat: batch consecutive file-edit tool calls into single UI block Add edit-file batching in ChatView groupedMessages that consolidates consecutive editedExistingFile, appliedDiff, newFileCreated, insertContent, and searchAndReplace asks into a single BatchDiffApproval block. Move batchDiffs detection in ChatRow above the switch statement so it applies to any file-edit tool type. * refactor: extract batchConsecutive utility, fix batch UI issues - Extract generic batchConsecutive() utility from 3 identical while-loops - Fix React key collisions in BatchListFilesPermission, BatchFilePermission, BatchDiffApproval - Normalize language prop to "shellsession" (was "shell-session" for top-level) - Remove unused _batchedMessages property from synthetic messages - Remove dead didViewMultipleDirectories i18n key from all 18 locale files - Add batch button text for listFilesTopLevel/listFilesRecursive - Add batchConsecutive utility tests (6 cases) * fix: audit improvements for batch tool-call UI - Make batchConsecutive() generic instead of ClineMessage-specific - Add batch-aware button text for edit-file batches ("Save All"/"Deny All") - Add dedicated list-batch/edit-batch i18n keys (stop reusing read-batch) - Add JSON.parse defense-in-depth in all three synthesizers - Fix mixed list_files batch icon to default to FolderTree - Add 6 missing test cases (all-match, immutability, spy, single-dir) * chore: minor type cleanup (out-of-scope housekeeping) - Trim unused recursive/isOutsideWorkspace from DirPermissionItem interface - Remove 4 pre-existing `as any` casts in ChatView.tsx: - window cast → precise inline type - checkpoint bracket access → removed unnecessary casts - condensing message → `as ClineMessage` - debounce cancel → `.clear()` (correct API) - Update BatchListFilesPermission test data to match trimmed interface * i18n: add list-batch and edit-batch translations for all locales * feat: add IPC query handlers for commands, modes, and models (#11279) Add GetCommands, GetModes, and GetModels to the IPC protocol so external clients can fetch slash commands, available modes, and Roo provider models without going through the internal webview message channel. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * feat: add lock toggle to pin API config across all modes in workspace (#11295) * feat: add lock toggle to pin API config across all modes in workspace Add a lock/unlock toggle inside the API config selector popover (next to the settings gear) that, when enabled, applies the selected API configuration to all modes in the current workspace. - Add lockApiConfigAcrossModes to ExtensionState and WebviewMessage types - Store setting in workspaceState (per-workspace, not global) - When locked, activateProviderProfile sets config for all modes - Lock icon in ApiConfigSelector popover bottom bar next to gear - Full i18n: English + 17 locale translations (all mention workspace scope) - 9 new tests: 2 ClineProvider, 2 handler, 5 UI (77 total pass) * refactor: replace write-fan-out with read-time override for lock API config The original lock implementation used setModeConfig() fan-out to write the locked config to ALL modes globally. Since the lock flag lives in workspace- scoped workspaceState but modeApiConfigs are in global secrets, this caused cross-workspace data destruction. Replaced with read-time guards: - handleModeSwitch: early return when lock is on (skip per-mode config load) - createTaskWithHistoryItem: skip mode-based config restoration under lock - activateProviderProfile: removed fan-out block - lockApiConfigAcrossModes handler: simplified to flag + state post only - Fixed pre-existing workspaceState mock gap in ClineProvider.spec.ts and ClineProvider.sticky-profile.spec.ts * fix: validate Gemini thinkingLevel against model capabilities and handle empty streams (#11303) * fix: validate Gemini thinkingLevel against model capabilities and handle empty streams getGeminiReasoning() now validates the selected effort against the model's supportsReasoningEffort array before sending it as thinkingLevel. When a stale settings value (e.g. 'medium' from a different model) is not in the supported set, it falls back to the model's default reasoningEffort. GeminiHandler.createMessage() now tracks whether any text content was yielded during streaming and handles NoOutputGeneratedError gracefully instead of surfacing the cryptic 'No output generated' error. * fix: guard thinkingLevel fallback against 'none' effort and add i18n TODO The array validation fallback in getGeminiReasoning() now only triggers when the selected effort IS a valid Gemini thinking level but not in the model's supported set. Values like 'none' (explicit no-reasoning signal) are no longer overridden by the model default. Also adds a TODO for moving the empty-stream message to i18n. * fix: track tool_call_start in hasContent to avoid false empty-stream warning Tool-only responses (no text) are valid content. Without this, agentic tool-call responses would incorrectly trigger the empty response warning message. * chore(cli): prepare release v0.0.53 (#11425) * feat: add GLM-5 model support to Z.ai provider (#11440) * chore: regenerate pnpm-lock.yaml * fix: resolve type errors and remove AI SDK test contamination * docs: update progress.txt with rebuilt Batch 2 status --------- Co-authored-by: roomote[bot] <219738659+roomote[bot]@users.noreply.github.com> Co-authored-by: Roo Code <roomote@roocode.com> Co-authored-by: daniel-lxs <ricciodaniel98@gmail.com> Co-authored-by: Chris Estreich <cestreich@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
404 lines
12 KiB
TypeScript
404 lines
12 KiB
TypeScript
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 ModelInfo,
|
|
type AnthropicModelId,
|
|
anthropicDefaultModelId,
|
|
anthropicModels,
|
|
ANTHROPIC_DEFAULT_MAX_TOKENS,
|
|
ApiProviderError,
|
|
} from "@roo-code/types"
|
|
import { TelemetryService } from "@roo-code/telemetry"
|
|
|
|
import type { ApiHandlerOptions } from "../../shared/api"
|
|
|
|
import { ApiStream } from "../transform/stream"
|
|
import { getModelParams } from "../transform/model-params"
|
|
import { filterNonAnthropicBlocks } from "../transform/anthropic-filter"
|
|
import { handleProviderError } from "./utils/error-handler"
|
|
|
|
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 readonly providerName = "Anthropic"
|
|
|
|
constructor(options: ApiHandlerOptions) {
|
|
super()
|
|
this.options = options
|
|
|
|
const apiKeyFieldName =
|
|
this.options.anthropicBaseUrl && this.options.anthropicUseAuthToken ? "authToken" : "apiKey"
|
|
|
|
this.client = new Anthropic({
|
|
baseURL: this.options.anthropicBaseUrl || undefined,
|
|
[apiKeyFieldName]: this.options.apiKey,
|
|
})
|
|
}
|
|
|
|
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()
|
|
|
|
// 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
|
|
) {
|
|
betas.push("context-1m-2025-08-07")
|
|
}
|
|
|
|
const nativeToolParams = {
|
|
tools: convertOpenAIToolsToAnthropic(metadata?.tools ?? []),
|
|
tool_choice: convertOpenAIToolChoiceToAnthropic(metadata?.tool_choice, metadata?.parallelToolCalls),
|
|
}
|
|
|
|
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[],
|
|
)
|
|
|
|
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
|
|
}
|
|
}
|
|
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
|
|
}
|
|
}
|
|
|
|
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,
|
|
}
|
|
}
|
|
}
|
|
|
|
getModel() {
|
|
const modelId = this.options.apiModelId
|
|
let id = modelId && modelId in anthropicModels ? (modelId as AnthropicModelId) : anthropicDefaultModelId
|
|
let info: ModelInfo = anthropicModels[id]
|
|
|
|
// If 1M context beta is enabled for supported models, update the model info
|
|
if (
|
|
(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 = {
|
|
...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,
|
|
})
|
|
|
|
// 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 === "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()
|
|
|
|
let message
|
|
try {
|
|
message = await this.client.messages.create({
|
|
model,
|
|
max_tokens: ANTHROPIC_DEFAULT_MAX_TOKENS,
|
|
thinking: undefined,
|
|
temperature,
|
|
messages: [{ role: "user", content: prompt }],
|
|
stream: false,
|
|
})
|
|
} catch (error) {
|
|
TelemetryService.instance.captureException(
|
|
new ApiProviderError(
|
|
error instanceof Error ? error.message : String(error),
|
|
this.providerName,
|
|
model,
|
|
"completePrompt",
|
|
),
|
|
)
|
|
throw error
|
|
}
|
|
|
|
const content = message.content.find(({ type }) => type === "text")
|
|
return content?.type === "text" ? content.text : ""
|
|
}
|
|
}
|