Merge remote-tracking branch 'origin/main' into feat/eval-recommendations

This commit is contained in:
Roo Code 2026-02-13 03:45:55 +00:00
commit 132f7af6ca
234 changed files with 4510 additions and 9543 deletions

View file

@ -5,6 +5,17 @@ All notable changes to the `@roo-code/cli` package will be documented in this fi
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.0.53] - 2026-02-12
### Changed
- **Auto-Approve by Default**: The CLI now auto-approves all actions (tools, commands, browser, MCP) by default. Followup questions auto-select the first suggestion after a 60-second timeout.
- **New `--require-approval` Flag**: Replaced `-y`/`--yes`/`--dangerously-skip-permissions` flags with a new `-a, --require-approval` flag for users who want manual approval prompts before actions execute.
### Fixed
- Spamming the escape key to cancel a running task no longer crashes the cli.
## [0.0.52] - 2026-02-09
### Added
@ -67,7 +78,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Skip onboarding flow when a provider is explicitly specified via `--provider` flag or saved in settings
- Unified permission flags: Combined `-y`, `--yes`, and `--dangerously-skip-permissions` into a single option for Claude Code-like CLI compatibility
- Unified permission flags: Combined approval-skipping flags into a single option for Claude Code-like CLI compatibility
- Improved Roo Code Router authentication flow and error messaging
### Fixed

View file

@ -66,7 +66,7 @@ pnpm --filter @roo-code/cli build
### Interactive Mode (Default)
By default, the CLI prompts for approval before executing actions:
By default, the CLI auto-approves actions and runs in interactive TUI mode:
```bash
export OPENROUTER_API_KEY=sk-or-v1-...
@ -82,24 +82,23 @@ roo -w ~/Documents/my-project
In interactive mode:
- Tool executions prompt for yes/no approval
- Commands prompt for yes/no approval
- Followup questions show suggestions and wait for user input
- Browser and MCP actions prompt for approval
- Tool executions are auto-approved
- Commands are auto-approved
- Followup questions show suggestions with a 60-second timeout, then auto-select the first suggestion
- Browser and MCP actions are auto-approved
### Non-Interactive Mode (`-y`)
### Approval-Required Mode (`--require-approval`)
For automation and scripts, use `-y` to auto-approve all actions:
If you want manual approval prompts, enable approval-required mode:
```bash
roo "Refactor the utils.ts file" -y -w ~/Documents/my-project
roo "Refactor the utils.ts file" --require-approval -w ~/Documents/my-project
```
In non-interactive mode:
In approval-required mode:
- Tool, command, browser, and MCP actions are auto-approved
- Followup questions show a 60-second timeout, then auto-select the first suggestion
- Typing any key cancels the timeout and allows manual input
- Tool, command, browser, and MCP actions prompt for yes/no approval
- Followup questions wait for manual input (no auto-timeout)
### Roo Code Cloud Authentication
@ -147,23 +146,23 @@ Tokens are valid for 90 days. The CLI will prompt you to re-authenticate when yo
## Options
| Option | Description | Default |
| ------------------------------------------- | --------------------------------------------------------------------------------------- | ---------------------------------------- |
| `[prompt]` | Your prompt (positional argument, optional) | None |
| `--prompt-file <path>` | Read prompt from a file instead of command line argument | None |
| `-w, --workspace <path>` | Workspace path to operate in | Current directory |
| `-p, --print` | Print response and exit (non-interactive mode) | `false` |
| `-e, --extension <path>` | Path to the extension bundle directory | Auto-detected |
| `-d, --debug` | Enable debug output (includes detailed debug information, prompts, paths, etc) | `false` |
| `-y, --yes, --dangerously-skip-permissions` | Auto-approve all actions (use with caution) | `false` |
| `-k, --api-key <key>` | API key for the LLM provider | From env var |
| `--provider <provider>` | API provider (roo, anthropic, openai, openrouter, etc.) | `openrouter` (or `roo` if authenticated) |
| `-m, --model <model>` | Model to use | `anthropic/claude-opus-4.6` |
| `--mode <mode>` | Mode to start in (code, architect, ask, debug, etc.) | `code` |
| `-r, --reasoning-effort <effort>` | Reasoning effort level (unspecified, disabled, none, minimal, low, medium, high, xhigh) | `medium` |
| `--ephemeral` | Run without persisting state (uses temporary storage) | `false` |
| `--oneshot` | Exit upon task completion | `false` |
| `--output-format <format>` | Output format with `--print`: `text`, `json`, or `stream-json` | `text` |
| Option | Description | Default |
| --------------------------------- | --------------------------------------------------------------------------------------- | ---------------------------------------- |
| `[prompt]` | Your prompt (positional argument, optional) | None |
| `--prompt-file <path>` | Read prompt from a file instead of command line argument | None |
| `-w, --workspace <path>` | Workspace path to operate in | Current directory |
| `-p, --print` | Print response and exit (non-interactive mode) | `false` |
| `-e, --extension <path>` | Path to the extension bundle directory | Auto-detected |
| `-d, --debug` | Enable debug output (includes detailed debug information, prompts, paths, etc) | `false` |
| `-a, --require-approval` | Require manual approval before actions execute | `false` |
| `-k, --api-key <key>` | API key for the LLM provider | From env var |
| `--provider <provider>` | API provider (roo, anthropic, openai, openrouter, etc.) | `openrouter` (or `roo` if authenticated) |
| `-m, --model <model>` | Model to use | `anthropic/claude-opus-4.6` |
| `--mode <mode>` | Mode to start in (code, architect, ask, debug, etc.) | `code` |
| `-r, --reasoning-effort <effort>` | Reasoning effort level (unspecified, disabled, none, minimal, low, medium, high, xhigh) | `medium` |
| `--ephemeral` | Run without persisting state (uses temporary storage) | `false` |
| `--oneshot` | Exit upon task completion | `false` |
| `--output-format <format>` | Output format with `--print`: `text`, `json`, or `stream-json` | `text` |
## Auth Commands

View file

@ -242,7 +242,8 @@ Routes asks to appropriate handlers:
- Uses type guards: `isIdleAsk()`, `isInteractiveAsk()`, etc.
- Coordinates between `OutputManager` and `PromptManager`
- In non-interactive mode (`-y` flag), auto-approves everything
- By default, the CLI auto-approves tool/command/browser/MCP actions
- In `--require-approval` mode, those actions prompt for manual approval
### OutputManager
@ -320,7 +321,7 @@ if (isInteractiveAsk(ask)) {
Enable with `-d` flag. Logs go to `~/.roo/cli-debug.log`:
```bash
roo -d -y -P "Build something" --no-tui
roo -d -P "Build something" --no-tui
```
View logs:

View file

@ -1,6 +1,6 @@
{
"name": "@roo-code/cli",
"version": "0.0.52",
"version": "0.0.53",
"description": "Roo Code CLI - Run the Roo Code agent from the command line",
"private": true,
"type": "module",
@ -15,7 +15,7 @@
"test": "vitest run",
"build": "tsup",
"build:extension": "pnpm --filter roo-cline bundle",
"dev": "ROO_AUTH_BASE_URL=https://app.roocode.com ROO_SDK_BASE_URL=https://cloud-api.roocode.com ROO_CODE_PROVIDER_URL=https://api.roocode.com/proxy tsx src/index.ts -y",
"dev": "ROO_AUTH_BASE_URL=https://app.roocode.com ROO_SDK_BASE_URL=https://cloud-api.roocode.com ROO_CODE_PROVIDER_URL=https://api.roocode.com/proxy tsx src/index.ts",
"dev:local": "ROO_AUTH_BASE_URL=http://localhost:3000 ROO_SDK_BASE_URL=http://localhost:3001 ROO_CODE_PROVIDER_URL=http://localhost:8080/proxy tsx src/index.ts",
"clean": "rimraf dist .turbo"
},

View file

@ -93,13 +93,6 @@ describe("detectAgentState", () => {
expect(state.requiredAction).toBe("answer")
})
it("should detect waiting for browser_action_launch approval", () => {
const messages = [createMessage({ type: "ask", ask: "browser_action_launch", partial: false })]
const state = detectAgentState(messages)
expect(state.state).toBe(AgentLoopState.WAITING_FOR_INPUT)
expect(state.requiredAction).toBe("approve")
})
it("should detect waiting for use_mcp_server approval", () => {
const messages = [createMessage({ type: "ask", ask: "use_mcp_server", partial: false })]
const state = detectAgentState(messages)
@ -202,7 +195,6 @@ describe("Type Guards", () => {
expect(isInteractiveAsk("tool")).toBe(true)
expect(isInteractiveAsk("command")).toBe(true)
expect(isInteractiveAsk("followup")).toBe(true)
expect(isInteractiveAsk("browser_action_launch")).toBe(true)
expect(isInteractiveAsk("use_mcp_server")).toBe(true)
})

View file

@ -116,7 +116,7 @@ export enum AgentLoopState {
*/
export type RequiredAction =
| "none" // No action needed (running/streaming)
| "approve" // Can approve/reject (tool, command, browser, mcp)
| "approve" // Can approve/reject (tool, command, mcp)
| "answer" // Need to answer a question (followup)
| "retry_or_new_task" // Can retry or start new task (api_req_failed)
| "proceed_or_new_task" // Can proceed or start new task (mistake_limit)
@ -221,7 +221,6 @@ function getRequiredAction(ask: ClineAsk): RequiredAction {
return "answer"
case "command":
case "tool":
case "browser_action_launch":
case "use_mcp_server":
return "approve"
case "command_output":
@ -264,8 +263,6 @@ function getStateDescription(state: AgentLoopState, ask?: ClineAsk): string {
return "Agent wants to execute a command. Approve or reject."
case "tool":
return "Agent wants to perform a file operation. Approve or reject."
case "browser_action_launch":
return "Agent wants to use the browser. Approve or reject."
case "use_mcp_server":
return "Agent wants to use an MCP server. Approve or reject."
default:

View file

@ -244,7 +244,7 @@ export class AskDispatcher {
}
/**
* Handle interactive asks (followup, command, tool, browser_action_launch, use_mcp_server).
* Handle interactive asks (followup, command, tool, use_mcp_server).
* These require user approval or input.
*/
private async handleInteractiveAsk(ts: number, ask: ClineAsk, text: string): Promise<AskHandleResult> {
@ -258,9 +258,6 @@ export class AskDispatcher {
case "tool":
return await this.handleToolApproval(ts, text)
case "browser_action_launch":
return await this.handleBrowserApproval(ts, text)
case "use_mcp_server":
return await this.handleMcpApproval(ts, text)
@ -444,32 +441,6 @@ export class AskDispatcher {
}
}
/**
* Handle browser action approval.
*/
private async handleBrowserApproval(ts: number, text: string): Promise<AskHandleResult> {
this.outputManager.output("\n[browser action request]")
if (text) {
this.outputManager.output(` Action: ${text}`)
}
this.outputManager.markDisplayed(ts, text || "", false)
if (this.nonInteractive) {
// Auto-approved by extension settings
return { handled: true }
}
try {
const approved = await this.promptManager.promptForYesNo("Allow browser action? (y/n): ")
this.sendApprovalResponse(approved)
return { handled: true, response: approved ? "yesButtonClicked" : "noButtonClicked" }
} catch {
this.outputManager.output("[Defaulting to: no]")
this.sendApprovalResponse(false)
return { handled: true, response: "noButtonClicked" }
}
}
/**
* Handle MCP server access approval.
*/

View file

@ -214,7 +214,6 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac
const baseSettings: RooCodeSettings = {
mode: this.options.mode,
commandExecutionTimeout: 30,
browserToolEnabled: false,
enableCheckpoints: false,
...getProviderSettings(this.options.provider, this.options.apiKey, this.options.model),
}
@ -227,7 +226,6 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac
alwaysAllowWrite: true,
alwaysAllowWriteOutsideWorkspace: true,
alwaysAllowWriteProtected: true,
alwaysAllowBrowser: true,
alwaysAllowMcp: true,
alwaysAllowModeSwitch: true,
alwaysAllowSubtasks: true,

View file

@ -258,15 +258,6 @@ export class JsonEventEmitter {
break
}
case "browser_action":
case "browser_action_result":
this.emitEvent({
type: "tool_result",
subtype: "browser",
tool_result: { name: "browser_action", output: msg.text },
})
break
case "mcp_server_response":
this.emitEvent({
type: "tool_result",
@ -336,15 +327,6 @@ export class JsonEventEmitter {
})
break
case "browser_action_launch":
this.emitEvent({
type: "tool_use",
id: msg.ts,
subtype: "browser",
tool_use: { name: "browser_action", input: { raw: msg.text } },
})
break
case "use_mcp_server":
this.emitEvent({
type: "tool_use",

View file

@ -65,8 +65,10 @@ export async function run(promptArg: string | undefined, flagOptions: FlagOption
flagOptions.reasoningEffort || settings.reasoningEffort || DEFAULT_FLAGS.reasoningEffort
const effectiveProvider = flagOptions.provider ?? settings.provider ?? (rooToken ? "roo" : "openrouter")
const effectiveWorkspacePath = flagOptions.workspace ? path.resolve(flagOptions.workspace) : process.cwd()
const effectiveDangerouslySkipPermissions =
flagOptions.yes || flagOptions.dangerouslySkipPermissions || settings.dangerouslySkipPermissions || false
const legacyRequireApprovalFromSettings =
settings.requireApproval ??
(settings.dangerouslySkipPermissions === undefined ? undefined : !settings.dangerouslySkipPermissions)
const effectiveRequireApproval = flagOptions.requireApproval || legacyRequireApprovalFromSettings || false
const effectiveExitOnComplete = flagOptions.print || flagOptions.oneshot || settings.oneshot || false
const extensionHostOptions: ExtensionHostOptions = {
@ -77,7 +79,7 @@ export async function run(promptArg: string | undefined, flagOptions: FlagOption
model: effectiveModel,
workspacePath: effectiveWorkspacePath,
extensionPath: path.resolve(flagOptions.extension || getDefaultExtensionPath(__dirname)),
nonInteractive: effectiveDangerouslySkipPermissions,
nonInteractive: !effectiveRequireApproval,
exitOnError: flagOptions.exitOnError,
ephemeral: flagOptions.ephemeral,
debug: flagOptions.debug,

View file

@ -18,8 +18,7 @@ program
.option("-p, --print", "Print response and exit (non-interactive mode)", false)
.option("-e, --extension <path>", "Path to the extension bundle directory")
.option("-d, --debug", "Enable debug output (includes detailed debug information)", false)
.option("-y, --yes", "Auto-approve all prompts (use with caution)", false)
.option("--dangerously-skip-permissions", "Alias for --yes", false)
.option("-a, --require-approval", "Require manual approval for actions", false)
.option("-k, --api-key <key>", "API key for the LLM provider")
.option("--provider <provider>", "API provider (roo, anthropic, openai, openrouter, etc.)")
.option("-m, --model <model>", "Model to use", DEFAULT_FLAGS.model)

View file

@ -179,20 +179,20 @@ describe("Settings Storage", () => {
expect(loaded.reasoningEffort).toBe("low")
})
it("should support dangerouslySkipPermissions setting", async () => {
await saveSettings({ dangerouslySkipPermissions: true })
it("should support requireApproval setting", async () => {
await saveSettings({ requireApproval: true })
const loaded = await loadSettings()
expect(loaded.dangerouslySkipPermissions).toBe(true)
expect(loaded.requireApproval).toBe(true)
})
it("should support all settings together including dangerouslySkipPermissions", async () => {
it("should support all settings together including requireApproval", async () => {
const allSettings = {
mode: "architect",
provider: "anthropic" as const,
model: "claude-sonnet-4-20250514",
reasoningEffort: "high" as const,
dangerouslySkipPermissions: true,
requireApproval: true,
}
await saveSettings(allSettings)
@ -202,7 +202,7 @@ describe("Settings Storage", () => {
expect(loaded.provider).toBe("anthropic")
expect(loaded.model).toBe("claude-sonnet-4-20250514")
expect(loaded.reasoningEffort).toBe("high")
expect(loaded.dangerouslySkipPermissions).toBe(true)
expect(loaded.requireApproval).toBe(true)
})
it("should support oneshot setting", async () => {
@ -218,7 +218,7 @@ describe("Settings Storage", () => {
provider: "anthropic" as const,
model: "claude-sonnet-4-20250514",
reasoningEffort: "high" as const,
dangerouslySkipPermissions: true,
requireApproval: true,
oneshot: true,
}
@ -229,8 +229,15 @@ describe("Settings Storage", () => {
expect(loaded.provider).toBe("anthropic")
expect(loaded.model).toBe("claude-sonnet-4-20250514")
expect(loaded.reasoningEffort).toBe("high")
expect(loaded.dangerouslySkipPermissions).toBe(true)
expect(loaded.requireApproval).toBe(true)
expect(loaded.oneshot).toBe(true)
})
it("should still load legacy dangerouslySkipPermissions setting", async () => {
await saveSettings({ dangerouslySkipPermissions: true })
const loaded = await loadSettings()
expect(loaded.dangerouslySkipPermissions).toBe(true)
})
})
})

View file

@ -24,8 +24,7 @@ export type FlagOptions = {
print: boolean
extension?: string
debug: boolean
yes: boolean
dangerouslySkipPermissions: boolean
requireApproval: boolean
exitOnError: boolean
apiKey?: string
provider?: SupportedProvider
@ -58,7 +57,9 @@ export interface CliSettings {
model?: string
/** Default reasoning effort level */
reasoningEffort?: ReasoningEffortFlagOptions
/** Auto-approve all prompts (use with caution) */
/** Require manual approval for tools/commands/browser/MCP actions */
requireApproval?: boolean
/** @deprecated Legacy inverse setting kept for backward compatibility */
dangerouslySkipPermissions?: boolean
/** Exit upon task completion */
oneshot?: boolean

View file

@ -10,14 +10,13 @@ import { getToolRenderer } from "./tools/index.js"
/**
* Tool categories for styling
*/
type ToolCategory = "file" | "directory" | "search" | "command" | "browser" | "mode" | "completion" | "other"
type ToolCategory = "file" | "directory" | "search" | "command" | "mode" | "completion" | "other"
function getToolCategory(toolName: string): ToolCategory {
const fileTools = ["readFile", "read_file", "writeToFile", "write_to_file", "applyDiff", "apply_diff"]
const dirTools = ["listFiles", "list_files", "listFilesRecursive", "listFilesTopLevel"]
const searchTools = ["searchFiles", "search_files"]
const commandTools = ["executeCommand", "execute_command"]
const browserTools = ["browserAction", "browser_action"]
const modeTools = ["switchMode", "switch_mode", "newTask", "new_task"]
const completionTools = ["attemptCompletion", "attempt_completion", "askFollowupQuestion", "ask_followup_question"]
@ -25,7 +24,6 @@ function getToolCategory(toolName: string): ToolCategory {
if (dirTools.includes(toolName)) return "directory"
if (searchTools.includes(toolName)) return "search"
if (commandTools.includes(toolName)) return "command"
if (browserTools.includes(toolName)) return "browser"
if (modeTools.includes(toolName)) return "mode"
if (completionTools.includes(toolName)) return "completion"
return "other"
@ -39,7 +37,6 @@ const CATEGORY_COLORS: Record<ToolCategory, string> = {
directory: theme.toolHeader,
search: theme.warningColor,
command: theme.successColor,
browser: theme.focusColor,
mode: theme.userHeader,
completion: theme.successColor,
other: theme.toolHeader,

View file

@ -1,87 +0,0 @@
import { Box, Text } from "ink"
import * as theme from "../../theme.js"
import { Icon } from "../Icon.js"
import type { ToolRendererProps } from "./types.js"
import { getToolDisplayName, getToolIconName } from "./utils.js"
const ACTION_LABELS: Record<string, string> = {
launch: "Launch Browser",
click: "Click",
hover: "Hover",
type: "Type Text",
press: "Press Key",
scroll_down: "Scroll Down",
scroll_up: "Scroll Up",
resize: "Resize Window",
close: "Close Browser",
screenshot: "Take Screenshot",
}
export function BrowserTool({ toolData }: ToolRendererProps) {
const iconName = getToolIconName(toolData.tool)
const displayName = getToolDisplayName(toolData.tool)
const action = toolData.action || ""
const url = toolData.url || ""
const coordinate = toolData.coordinate || ""
const content = toolData.content || "" // May contain text for type action.
const actionLabel = ACTION_LABELS[action] || action
return (
<Box flexDirection="column" paddingX={1}>
{/* Header */}
<Box>
<Icon name={iconName} color={theme.toolHeader} />
<Text bold color={theme.toolHeader}>
{" "}
{displayName}
</Text>
{action && (
<Text color={theme.focusColor} bold>
{" "}
{actionLabel}
</Text>
)}
</Box>
{/* Action details */}
<Box flexDirection="column" marginLeft={2}>
{/* URL for launch action */}
{url && (
<Box>
<Text color={theme.dimText}>url: </Text>
<Text color={theme.text} underline>
{url}
</Text>
</Box>
)}
{/* Coordinates for click/hover actions */}
{coordinate && (
<Box>
<Text color={theme.dimText}>at: </Text>
<Text color={theme.warningColor}>{coordinate}</Text>
</Box>
)}
{/* Text content for type action */}
{content && action === "type" && (
<Box>
<Text color={theme.dimText}>text: </Text>
<Text color={theme.text}>"{content}"</Text>
</Box>
)}
{/* Key for press action */}
{content && action === "press" && (
<Box>
<Text color={theme.dimText}>key: </Text>
<Text color={theme.successColor}>{content}</Text>
</Box>
)}
</Box>
</Box>
)
}

View file

@ -15,7 +15,6 @@ import { FileReadTool } from "./FileReadTool.js"
import { FileWriteTool } from "./FileWriteTool.js"
import { SearchTool } from "./SearchTool.js"
import { CommandTool } from "./CommandTool.js"
import { BrowserTool } from "./BrowserTool.js"
import { ModeTool } from "./ModeTool.js"
import { CompletionTool } from "./CompletionTool.js"
import { GenericTool } from "./GenericTool.js"
@ -32,7 +31,6 @@ export { FileReadTool } from "./FileReadTool.js"
export { FileWriteTool } from "./FileWriteTool.js"
export { SearchTool } from "./SearchTool.js"
export { CommandTool } from "./CommandTool.js"
export { BrowserTool } from "./BrowserTool.js"
export { ModeTool } from "./ModeTool.js"
export { CompletionTool } from "./CompletionTool.js"
export { GenericTool } from "./GenericTool.js"
@ -45,7 +43,6 @@ const CATEGORY_RENDERERS: Record<string, React.FC<ToolRendererProps>> = {
"file-write": FileWriteTool,
search: SearchTool,
command: CommandTool,
browser: BrowserTool,
mode: ModeTool,
completion: CompletionTool,
other: GenericTool,

View file

@ -5,15 +5,7 @@ export interface ToolRendererProps {
rawContent?: string
}
export type ToolCategory =
| "file-read"
| "file-write"
| "search"
| "command"
| "browser"
| "mode"
| "completion"
| "other"
export type ToolCategory = "file-read" | "file-write" | "search" | "command" | "mode" | "completion" | "other"
export function getToolCategory(toolName: string): ToolCategory {
const fileReadTools = ["readFile", "read_file", "skill", "listFilesTopLevel", "listFilesRecursive", "list_files"]
@ -29,7 +21,6 @@ export function getToolCategory(toolName: string): ToolCategory {
const searchTools = ["searchFiles", "search_files", "codebaseSearch", "codebase_search"]
const commandTools = ["execute_command", "executeCommand"]
const browserTools = ["browser_action", "browserAction"]
const modeTools = ["switchMode", "switch_mode", "newTask", "new_task", "finishTask"]
const completionTools = ["attempt_completion", "attemptCompletion", "ask_followup_question", "askFollowupQuestion"]
@ -37,7 +28,6 @@ export function getToolCategory(toolName: string): ToolCategory {
if (fileWriteTools.includes(toolName)) return "file-write"
if (searchTools.includes(toolName)) return "search"
if (commandTools.includes(toolName)) return "command"
if (browserTools.includes(toolName)) return "browser"
if (modeTools.includes(toolName)) return "mode"
if (completionTools.includes(toolName)) return "completion"
return "other"

View file

@ -73,10 +73,6 @@ export function getToolDisplayName(toolName: string): string {
execute_command: "Execute Command",
executeCommand: "Execute Command",
// Browser operations
browser_action: "Browser Action",
browserAction: "Browser Action",
// Mode operations
switchMode: "Switch Mode",
switch_mode: "Switch Mode",
@ -129,10 +125,6 @@ export function getToolIconName(toolName: string): IconName {
execute_command: "terminal",
executeCommand: "terminal",
// Browser operations
browser_action: "browser",
browserAction: "browser",
// Mode operations
switchMode: "switch",
switch_mode: "switch",

View file

@ -40,14 +40,6 @@ export interface ToolData {
/** Command output */
output?: string
// Browser operation fields
/** Browser action type */
action?: string
/** Browser URL */
url?: string
/** Click/hover coordinates */
coordinate?: string
// Batch operation fields
/** Batch file reads */
batchFiles?: Array<{

View file

@ -57,17 +57,6 @@ export function extractToolData(toolInfo: Record<string, unknown>): ToolData {
toolData.output = toolInfo.output as string
}
// Extract browser-related fields
if (toolInfo.action !== undefined) {
toolData.action = toolInfo.action as string
}
if (toolInfo.url !== undefined) {
toolData.url = toolInfo.url as string
}
if (toolInfo.coordinate !== undefined) {
toolData.coordinate = toolInfo.coordinate as string
}
// Extract batch file operations
if (Array.isArray(toolInfo.files)) {
toolData.batchFiles = (toolInfo.files as Array<Record<string, unknown>>).map((f) => ({
@ -165,12 +154,6 @@ export function formatToolOutput(toolInfo: Record<string, unknown>): string {
return `📁 ${listPath || "."}${recursive ? " (recursive)" : ""}`
}
case "browser_action": {
const action = toolInfo.action as string
const url = toolInfo.url as string
return `🌐 ${action || "action"}${url ? `: ${url}` : ""}`
}
case "attempt_completion": {
const result = toolInfo.result as string
if (result) {
@ -248,12 +231,6 @@ export function formatToolAskMessage(toolInfo: Record<string, unknown>): string
return `Apply changes to: ${diffPath || "(no path)"}`
}
case "browser_action": {
const action = toolInfo.action as string
const url = toolInfo.url as string
return `Browser: ${action || "action"}${url ? ` - ${url}` : ""}`
}
default: {
const params = Object.entries(toolInfo)
.filter(([key]) => key !== "tool")

View file

@ -43,7 +43,6 @@ export const runTaskWithCli = async ({ run, task, publish, logger, jobToken }: R
promptSourcePath,
"--workspace",
workspacePath,
"--yes",
"--reasoning-effort",
"disabled",
"--oneshot",

View file

@ -138,8 +138,8 @@ describe("copyRun", () => {
const toolError3 = await createToolError({
runId: sourceRunId,
taskId: null,
toolName: "browser_action",
error: "Browser connection timeout",
toolName: "write_to_file",
error: "Write timeout",
})
sourceToolErrorIds.push(toolError3.id)
@ -234,8 +234,8 @@ describe("copyRun", () => {
expect(taskToolErrors).toHaveLength(2)
expect(runToolErrors).toHaveLength(1)
const browserError = runToolErrors.find((te) => te.toolName === "browser_action")!
expect(browserError.error).toBe("Browser connection timeout")
const writeError = runToolErrors.find((te) => te.toolName === "write_to_file")!
expect(writeError.error).toBe("Write timeout")
await db.delete(schema.toolErrors).where(eq(schema.toolErrors.runId, newRunId))
await db.delete(schema.tasks).where(eq(schema.tasks.runId, newRunId))

View file

@ -1,6 +1,6 @@
{
"name": "@roo-code/types",
"version": "1.110.0",
"version": "1.111.0",
"description": "TypeScript type definitions for Roo Code.",
"publishConfig": {
"access": "public",

View file

@ -487,11 +487,11 @@ describe("userSettingsConfigSchema with llmEnhancedFeaturesEnabled", () => {
describe("organizationDefaultSettingsSchema with disabledTools", () => {
it("should accept disabledTools as an array of valid tool names", () => {
const input: OrganizationDefaultSettings = {
disabledTools: ["execute_command", "browser_action"],
disabledTools: ["execute_command", "write_to_file"],
}
const result = organizationDefaultSettingsSchema.safeParse(input)
expect(result.success).toBe(true)
expect(result.data?.disabledTools).toEqual(["execute_command", "browser_action"])
expect(result.data?.disabledTools).toEqual(["execute_command", "write_to_file"])
})
it("should accept empty disabledTools array", () => {

View file

@ -102,7 +102,6 @@ export const globalSettingsSchema = z.object({
alwaysAllowWriteOutsideWorkspace: z.boolean().optional(),
alwaysAllowWriteProtected: z.boolean().optional(),
writeDelayMs: z.number().min(0).optional(),
alwaysAllowBrowser: z.boolean().optional(),
requestDelaySeconds: z.number().optional(),
alwaysAllowMcp: z.boolean().optional(),
alwaysAllowModeSwitch: z.boolean().optional(),
@ -148,13 +147,6 @@ export const globalSettingsSchema = z.object({
*/
maxDiagnosticMessages: z.number().optional(),
browserToolEnabled: z.boolean().optional(),
browserViewportSize: z.string().optional(),
screenshotQuality: z.number().optional(),
remoteBrowserEnabled: z.boolean().optional(),
remoteBrowserHost: z.string().optional(),
cachedChromeHostUrl: z.string().optional(),
enableCheckpoints: z.boolean().optional(),
checkpointTimeout: z
.number()
@ -338,7 +330,6 @@ export const EVALS_SETTINGS: RooCodeSettings = {
alwaysAllowWriteOutsideWorkspace: false,
alwaysAllowWriteProtected: false,
writeDelayMs: 1000,
alwaysAllowBrowser: true,
requestDelaySeconds: 10,
alwaysAllowMcp: true,
alwaysAllowModeSwitch: true,
@ -351,11 +342,6 @@ export const EVALS_SETTINGS: RooCodeSettings = {
commandTimeoutAllowlist: [],
preventCompletionWithOpenTodos: false,
browserToolEnabled: false,
browserViewportSize: "900x600",
screenshotQuality: 75,
remoteBrowserEnabled: false,
ttsEnabled: false,
ttsSpeed: 1,
soundEnabled: false,

View file

@ -21,7 +21,6 @@ import { z } from "zod"
* - `resume_task`: Confirmation needed to resume a previously paused task
* - `resume_completed_task`: Confirmation needed to resume a task that was already marked as completed
* - `mistake_limit_reached`: Too many errors encountered, needs user guidance on how to proceed
* - `browser_action_launch`: Permission to open or interact with a browser
* - `use_mcp_server`: Permission to use Model Context Protocol (MCP) server functionality
* - `auto_approval_max_req_reached`: Auto-approval limit has been reached, manual approval required
*/
@ -35,7 +34,6 @@ export const clineAsks = [
"resume_task",
"resume_completed_task",
"mistake_limit_reached",
"browser_action_launch",
"use_mcp_server",
"auto_approval_max_req_reached",
] as const
@ -83,13 +81,7 @@ export function isResumableAsk(ask: ClineAsk): ask is ResumableAsk {
* Asks that put the task into an "user interaction required" state.
*/
export const interactiveAsks = [
"followup",
"command",
"tool",
"browser_action_launch",
"use_mcp_server",
] as const satisfies readonly ClineAsk[]
export const interactiveAsks = ["followup", "command", "tool", "use_mcp_server"] as const satisfies readonly ClineAsk[]
export type InteractiveAsk = (typeof interactiveAsks)[number]
@ -138,8 +130,6 @@ export function isNonBlockingAsk(ask: ClineAsk): ask is NonBlockingAsk {
* - `user_feedback_diff`: Diff-formatted feedback from user showing requested changes
* - `command_output`: Output from an executed command
* - `shell_integration_warning`: Warning about shell integration issues or limitations
* - `browser_action`: Action performed in the browser
* - `browser_action_result`: Result of a browser action
* - `mcp_server_request_started`: MCP server request has been initiated
* - `mcp_server_response`: Response received from MCP server
* - `subtask_result`: Result of a completed subtask
@ -167,9 +157,6 @@ export const clineSays = [
"user_feedback_diff",
"command_output",
"shell_integration_warning",
"browser_action",
"browser_action_result",
"browser_session_status",
"mcp_server_request_started",
"mcp_server_response",
"subtask_result",

View file

@ -1,6 +1,6 @@
import { z } from "zod"
import { toolGroupsSchema } from "./tool.js"
import { deprecatedToolGroups, toolGroupsSchema } from "./tool.js"
/**
* GroupOptions
@ -42,7 +42,24 @@ export type GroupEntry = z.infer<typeof groupEntrySchema>
* ModeConfig
*/
const groupEntryArraySchema = z.array(groupEntrySchema).refine(
/**
* Checks if a group entry references a deprecated tool group.
* Handles both string entries ("browser") and tuple entries (["browser", { ... }]).
*/
function isDeprecatedGroupEntry(entry: unknown): boolean {
if (typeof entry === "string") {
return deprecatedToolGroups.includes(entry)
}
if (Array.isArray(entry) && entry.length >= 1 && typeof entry[0] === "string") {
return deprecatedToolGroups.includes(entry[0])
}
return false
}
/**
* Raw schema for validating group entries after deprecated groups are stripped.
*/
const rawGroupEntryArraySchema = z.array(groupEntrySchema).refine(
(groups) => {
const seen = new Set()
@ -61,6 +78,21 @@ const groupEntryArraySchema = z.array(groupEntrySchema).refine(
{ message: "Duplicate groups are not allowed" },
)
/**
* Schema for mode group entries. Preprocesses the input to strip deprecated
* tool groups (e.g., "browser") before validation, ensuring backward compatibility
* with older user configs.
*
* The type assertion to `z.ZodType<GroupEntry[], z.ZodTypeDef, GroupEntry[]>` is
* required because `z.preprocess` erases the input type to `unknown`, which
* propagates through `modeConfigSchema → rooCodeSettingsSchema → createRunSchema`
* and breaks `zodResolver` generic inference in downstream consumers (e.g., web-evals).
*/
export const groupEntryArraySchema = z.preprocess((val) => {
if (!Array.isArray(val)) return val
return val.filter((entry) => !isDeprecatedGroupEntry(entry))
}, rawGroupEntryArraySchema) as z.ZodType<GroupEntry[], z.ZodTypeDef, GroupEntry[]>
export const modeConfigSchema = z.object({
slug: z.string().regex(/^[a-zA-Z0-9-]+$/, "Slug must contain only letters numbers and dashes"),
name: z.string().min(1, "Name is required"),
@ -142,7 +174,7 @@ export const DEFAULT_MODES: readonly ModeConfig[] = [
whenToUse:
"Use this mode when you need to plan, design, or strategize before implementation. Perfect for breaking down complex problems, creating technical specifications, designing system architecture, or brainstorming solutions before coding.",
description: "Plan and design before implementation",
groups: ["read", ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }], "browser", "mcp"],
groups: ["read", ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }], "mcp"],
customInstructions:
"1. Do some information gathering (using provided tools) to get more context about the task.\n\n2. You should also ask the user clarifying questions to get a better understanding of the task.\n\n3. Once you've gained more context about the user's request, break down the task into clear, actionable steps and create a todo list using the `update_todo_list` tool. Each todo item should be:\n - Specific and actionable\n - Listed in logical execution order\n - Focused on a single, well-defined outcome\n - Clear enough that another mode could execute it independently\n\n **Note:** If the `update_todo_list` tool is not available, write the plan to a markdown file (e.g., `plan.md` or `todo.md`) instead.\n\n4. As you gather more information or discover new requirements, update the todo list to reflect the current understanding of what needs to be accomplished.\n\n5. Ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and refine the todo list.\n\n6. Include Mermaid diagrams if they help clarify complex workflows or system architecture. Please avoid using double quotes (\"\") and parentheses () inside square brackets ([]) in Mermaid diagrams, as this can cause parsing errors.\n\n7. Use the switch_mode tool to request that the user switch to another mode to implement the solution.\n\n**IMPORTANT: Focus on creating clear, actionable todo lists rather than lengthy markdown documents. Use the todo list as your primary planning tool to track and organize the work that needs to be done.**\n\n**CRITICAL: Never provide level of effort time estimates (e.g., hours, days, weeks) for tasks. Focus solely on breaking down the work into clear, actionable steps without estimating how long they will take.**\n\nUnless told otherwise, if you want to save a plan file, put it in the /plans directory",
},
@ -154,7 +186,7 @@ export const DEFAULT_MODES: readonly ModeConfig[] = [
whenToUse:
"Use this mode when you need to write, modify, or refactor code. Ideal for implementing features, fixing bugs, creating new files, or making code improvements across any programming language or framework.",
description: "Write, modify, and refactor code",
groups: ["read", "edit", "browser", "command", "mcp"],
groups: ["read", "edit", "command", "mcp"],
},
{
slug: "ask",
@ -164,7 +196,7 @@ export const DEFAULT_MODES: readonly ModeConfig[] = [
whenToUse:
"Use this mode when you need explanations, documentation, or answers to technical questions. Best for understanding concepts, analyzing existing code, getting recommendations, or learning about technologies without making changes.",
description: "Get answers and explanations",
groups: ["read", "browser", "mcp"],
groups: ["read", "mcp"],
customInstructions:
"You can analyze code, explain concepts, and access external resources. Always answer the user's questions thoroughly, and do not switch to implementing code unless explicitly requested by the user. Include Mermaid diagrams when they clarify your response.",
},
@ -176,7 +208,7 @@ export const DEFAULT_MODES: readonly ModeConfig[] = [
whenToUse:
"Use this mode when you're troubleshooting issues, investigating errors, or diagnosing problems. Specialized in systematic debugging, adding logging, analyzing stack traces, and identifying root causes before applying fixes.",
description: "Diagnose and fix software issues",
groups: ["read", "edit", "browser", "command", "mcp"],
groups: ["read", "edit", "command", "mcp"],
customInstructions:
"Reflect on 5-7 different possible sources of the problem, distill those down to 1-2 most likely sources, and then add logs to validate your assumptions. Explicitly ask the user to confirm the diagnosis before fixing the problem.",
},

View file

@ -120,6 +120,21 @@ export const internationalZAiModels = {
description:
"GLM-4.7 is Zhipu's latest model with built-in thinking capabilities enabled by default. It provides enhanced reasoning for complex tasks while maintaining fast response times.",
},
"glm-5": {
maxTokens: 16_384,
contextWindow: 202_752,
supportsImages: false,
supportsPromptCache: true,
supportsReasoningEffort: ["disable", "medium"],
reasoningEffort: "medium",
preserveReasoning: true,
inputPrice: 0.6,
outputPrice: 2.2,
cacheWritesPrice: 0,
cacheReadsPrice: 0.11,
description:
"GLM-5 is Zhipu's next-generation model with a 202k context window and built-in thinking capabilities. It delivers state-of-the-art reasoning, coding, and agentic performance.",
},
"glm-4.7-flash": {
maxTokens: 16_384,
contextWindow: 200_000,
@ -281,6 +296,21 @@ export const mainlandZAiModels = {
description:
"GLM-4.7 is Zhipu's latest model with built-in thinking capabilities enabled by default. It provides enhanced reasoning for complex tasks while maintaining fast response times.",
},
"glm-5": {
maxTokens: 16_384,
contextWindow: 202_752,
supportsImages: false,
supportsPromptCache: true,
supportsReasoningEffort: ["disable", "medium"],
reasoningEffort: "medium",
preserveReasoning: true,
inputPrice: 0.29,
outputPrice: 1.14,
cacheWritesPrice: 0,
cacheReadsPrice: 0.057,
description:
"GLM-5 is Zhipu's next-generation model with a 202k context window and built-in thinking capabilities. It delivers state-of-the-art reasoning, coding, and agentic performance.",
},
"glm-4.7-flash": {
maxTokens: 16_384,
contextWindow: 204_800,

View file

@ -93,8 +93,6 @@ export interface CreateTaskOptions {
consecutiveMistakeLimit?: number
experiments?: Record<string, boolean>
initialTodos?: TodoItem[]
/** Initial status for the task's history item (e.g., "active" for child tasks) */
initialStatus?: "active" | "delegated" | "completed"
/** Whether to start the task loop immediately (default: true).
* When false, the caller must invoke `task.start()` manually. */
startTask?: boolean

View file

@ -102,15 +102,6 @@ export interface Size {
height: number
}
export interface BrowserActionParams {
action: "launch" | "click" | "hover" | "type" | "scroll_down" | "scroll_up" | "resize" | "close" | "screenshot"
url?: string
coordinate?: Coordinate
size?: Size
text?: string
path?: string
}
export interface GenerateImageParams {
prompt: string
path: string

View file

@ -4,10 +4,17 @@ import { z } from "zod"
* ToolGroup
*/
export const toolGroups = ["read", "edit", "browser", "command", "mcp", "modes"] as const
export const toolGroups = ["read", "edit", "command", "mcp", "modes"] as const
export const toolGroupsSchema = z.enum(toolGroups)
/**
* Tool groups that have been removed but may still exist in user config files.
* Used by schema preprocessing to silently strip these before validation,
* preventing errors for users with older configs.
*/
export const deprecatedToolGroups: readonly string[] = ["browser"]
export type ToolGroup = z.infer<typeof toolGroupsSchema>
/**
@ -27,7 +34,6 @@ export const toolNames = [
"apply_patch",
"search_files",
"list_files",
"browser_action",
"use_mcp_tool",
"access_mcp_resource",
"ask_followup_question",

View file

@ -59,9 +59,6 @@ export interface ExtensionMessage {
| "deleteCustomModeCheck"
| "currentCheckpointUpdated"
| "checkpointInitWarning"
| "browserToolEnabled"
| "browserConnectionResult"
| "remoteBrowserEnabled"
| "ttsStart"
| "ttsStop"
| "fileSearchResults"
@ -92,8 +89,6 @@ export interface ExtensionMessage {
| "dismissedUpsells"
| "organizationSwitchResult"
| "interactionRequired"
| "browserSessionUpdate"
| "browserSessionNavigate"
| "customToolsResult"
| "modes"
| "taskWithAggregatedCosts"
@ -180,9 +175,6 @@ export interface ExtensionMessage {
queuedMessages?: QueuedMessage[]
list?: string[] // For dismissedUpsells
organizationId?: string | null // For organizationSwitchResult
browserSessionMessages?: ClineMessage[] // For browser session panel updates
isBrowserSessionActive?: boolean // For browser session panel updates
stepIndex?: number // For browserSessionNavigate: the target step index to display
tools?: SerializedCustomToolDefinition[] // For customToolsResult
modes?: { slug: string; name: string }[] // For modes response
skills?: SkillMetadata[] // For skills response
@ -264,7 +256,6 @@ export type ExtensionState = Pick<
| "alwaysAllowWrite"
| "alwaysAllowWriteOutsideWorkspace"
| "alwaysAllowWriteProtected"
| "alwaysAllowBrowser"
| "alwaysAllowMcp"
| "alwaysAllowModeSwitch"
| "alwaysAllowSubtasks"
@ -275,12 +266,6 @@ export type ExtensionState = Pick<
| "deniedCommands"
| "allowedMaxRequests"
| "allowedMaxCost"
| "browserToolEnabled"
| "browserViewportSize"
| "screenshotQuality"
| "remoteBrowserEnabled"
| "cachedChromeHostUrl"
| "remoteBrowserHost"
| "ttsEnabled"
| "ttsSpeed"
| "soundEnabled"
@ -367,8 +352,6 @@ export type ExtensionState = Pick<
organizationAllowList: OrganizationAllowList
organizationSettingsVersion?: number
isBrowserSessionActive: boolean // Actual browser session state
autoCondenseContext: boolean
autoCondenseContextPercent: number
marketplaceItems?: MarketplaceItem[]
@ -508,8 +491,6 @@ export interface WebviewMessage {
| "deleteMcpServer"
| "codebaseIndexEnabled"
| "telemetrySetting"
| "testBrowserConnection"
| "browserConnectionResult"
| "searchFiles"
| "toggleApiConfigPin"
| "hasOpenedModeSelector"
@ -566,11 +547,6 @@ export interface WebviewMessage {
| "allowedCommands"
| "getTaskWithAggregatedCosts"
| "deniedCommands"
| "killBrowserSession"
| "openBrowserSessionPanel"
| "showBrowserSessionPanelAtStep"
| "refreshBrowserSessionPanel"
| "browserPanelDidLaunch"
| "openDebugApiHistory"
| "openDebugUiHistory"
| "downloadErrorDiagnostics"
@ -852,39 +828,6 @@ export interface ClineSayTool {
skill?: string
}
// Must keep in sync with system prompt.
export const browserActions = [
"launch",
"click",
"hover",
"type",
"press",
"scroll_down",
"scroll_up",
"resize",
"close",
"screenshot",
] as const
export type BrowserAction = (typeof browserActions)[number]
export interface ClineSayBrowserAction {
action: BrowserAction
coordinate?: string
size?: string
text?: string
executedCoordinate?: string
}
export type BrowserActionResult = {
screenshot?: string
logs?: string
currentUrl?: string
currentMousePosition?: string
viewportWidth?: number
viewportHeight?: number
}
export interface ClineAskUseMcpServer {
serverName: string
type: "use_mcp_tool" | "access_mcp_resource"

424
pnpm-lock.yaml generated
View file

@ -805,7 +805,7 @@ importers:
version: 1.14.0(typescript@5.8.3)
'@requesty/ai-sdk':
specifier: ^3.0.0
version: 3.0.0(vite@6.3.5(@types/node@20.17.50)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0))(zod@3.25.76)
version: 3.0.0(vite@6.3.6(@types/node@20.17.50)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0))(zod@3.25.76)
'@roo-code/cloud':
specifier: workspace:^
version: link:../packages/cloud
@ -938,12 +938,6 @@ importers:
ps-tree:
specifier: ^1.2.0
version: 1.2.0
puppeteer-chromium-resolver:
specifier: ^24.0.0
version: 24.0.1
puppeteer-core:
specifier: ^23.4.0
version: 23.11.1
reconnecting-eventsource:
specifier: ^1.6.4
version: 1.6.4
@ -3039,16 +3033,6 @@ packages:
'@protobufjs/utf8@1.1.0':
resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==}
'@puppeteer/browsers@2.10.5':
resolution: {integrity: sha512-eifa0o+i8dERnngJwKrfp3dEq7ia5XFyoqB17S4gK8GhsQE4/P8nxOfQSE0zQHxzzLo/cmF+7+ywEQ7wK7Fb+w==}
engines: {node: '>=18'}
hasBin: true
'@puppeteer/browsers@2.6.1':
resolution: {integrity: sha512-aBSREisdsGH890S2rQqK82qmQYU3uFpSH8wcZWHgHzl3LfzsxAKbLNiAG9mO8v1Y0UICBeClICxPJvyr0rcuxg==}
engines: {node: '>=18'}
hasBin: true
'@qdrant/js-client-rest@1.14.0':
resolution: {integrity: sha512-2sM2g17FSkN2sNCSeAfqxHRr+SPEVnUQLXBjVv/whm4YQ4JjZ53Jiy1iShk95G+xBf3hKBhJdj8itRnor03IYw==}
engines: {node: '>=18.0.0', pnpm: '>=8'}
@ -4362,9 +4346,6 @@ packages:
peerDependencies:
'@testing-library/dom': '>=7.21.4'
'@tootallnate/quickjs-emscripten@0.23.0':
resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==}
'@trpc/client@11.8.1':
resolution: {integrity: sha512-L/SJFGanr9xGABmuDoeXR4xAdHJmsXsiF9OuH+apecJ+8sUITzVT1EPeqp0ebqA6lBhEl5pPfg3rngVhi/h60Q==}
peerDependencies:
@ -4684,9 +4665,6 @@ packages:
'@types/yargs@17.0.33':
resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==}
'@types/yauzl@2.10.3':
resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
'@typescript-eslint/eslint-plugin@8.32.1':
resolution: {integrity: sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@ -4961,9 +4939,6 @@ packages:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
aproba@2.0.0:
resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==}
archiver-utils@2.1.0:
resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==}
engines: {node: '>= 6'}
@ -5040,10 +5015,6 @@ packages:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
ast-types@0.13.4:
resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==}
engines: {node: '>=4'}
async-function@1.0.0:
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
engines: {node: '>= 0.4'}
@ -5137,10 +5108,6 @@ packages:
resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==}
hasBin: true
basic-ftp@5.0.5:
resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==}
engines: {node: '>=10.0.0'}
better-path-resolve@1.0.0:
resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==}
engines: {node: '>=4'}
@ -5374,16 +5341,6 @@ packages:
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
engines: {node: '>=18'}
chromium-bidi@0.11.0:
resolution: {integrity: sha512-6CJWHkNRoyZyjV9Rwv2lYONZf1Xm0IuDyNq97nwSsxxP3wf5Bwy15K5rOvVKMtJ127jJBmxFUanSAOjgFRxgrA==}
peerDependencies:
devtools-protocol: '*'
chromium-bidi@5.1.0:
resolution: {integrity: sha512-9MSRhWRVoRPDG0TgzkHrshFSJJNZzfY5UFqUMuksg7zL1yoZIZ3jLB0YAgHclbiAxPI86pBnwDX1tbzoiV8aFw==}
peerDependencies:
devtools-protocol: '*'
ci-info@2.0.0:
resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==}
@ -5481,10 +5438,6 @@ packages:
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
color-support@1.1.3:
resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==}
hasBin: true
colorette@2.0.20:
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
@ -5544,9 +5497,6 @@ packages:
resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==}
engines: {node: ^14.18.0 || >=16.10.0}
console-control-strings@1.1.0:
resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
content-disposition@1.0.0:
resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==}
engines: {node: '>= 0.6'}
@ -5823,10 +5773,6 @@ packages:
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
engines: {node: '>= 12'}
data-uri-to-buffer@6.0.2:
resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==}
engines: {node: '>= 14'}
data-urls@5.0.0:
resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
engines: {node: '>=18'}
@ -5948,10 +5894,6 @@ packages:
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
engines: {node: '>= 0.4'}
degenerator@5.0.1:
resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==}
engines: {node: '>= 14'}
delaunator@5.0.1:
resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==}
@ -6000,12 +5942,6 @@ packages:
devlop@1.1.0:
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
devtools-protocol@0.0.1367902:
resolution: {integrity: sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==}
devtools-protocol@0.0.1452169:
resolution: {integrity: sha512-FOFDVMGrAUNp0dDKsAU1TorWJUx2JOU1k9xdgBKKJF3IBh/Uhl2yswG5r3TEAOrCiGY2QRp1e6LVDQrCsTKO4g==}
didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
@ -6203,9 +6139,6 @@ packages:
ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
eight-colors@1.3.1:
resolution: {integrity: sha512-7nXPYDeKh6DgJDR/mpt2G7N/hCNSGwwoPVmoI3+4TEwOb07VFN1WMPG0DFf6nMEjrkgdj8Og7l7IaEEk3VE6Zg==}
electron-to-chromium@1.5.152:
resolution: {integrity: sha512-xBOfg/EBaIlVsHipHl2VdTPJRSvErNUaqW8ejTq5OlOlIYx1wOllCHsAvAIrr55jD1IYEfdR86miUEt8H5IeJg==}
@ -6371,11 +6304,6 @@ packages:
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
engines: {node: '>=12'}
escodegen@2.1.0:
resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==}
engines: {node: '>=6.0'}
hasBin: true
eslint-config-prettier@10.1.8:
resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==}
hasBin: true
@ -6570,11 +6498,6 @@ packages:
extendable-error@0.1.7:
resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==}
extract-zip@2.0.1:
resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==}
engines: {node: '>= 10.17.0'}
hasBin: true
fast-csv@4.3.6:
resolution: {integrity: sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==}
engines: {node: '>=10.0.0'}
@ -6811,11 +6734,6 @@ packages:
fzf@0.5.2:
resolution: {integrity: sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==}
gauge@5.0.2:
resolution: {integrity: sha512-pMaFftXPtiGIHCJHdcUUx9Rby/rFT/Kkt3fIIGCs+9PMDIljSyRiqraTlxNtBReJRDfUefpa263RQ3vnp5G/LQ==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
deprecated: This package is no longer supported.
gaxios@7.1.3:
resolution: {integrity: sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==}
engines: {node: '>=18'}
@ -6858,10 +6776,6 @@ packages:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
get-stream@5.2.0:
resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==}
engines: {node: '>=8'}
get-stream@6.0.1:
resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
engines: {node: '>=10'}
@ -6884,10 +6798,6 @@ packages:
get-tsconfig@4.10.1:
resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==}
get-uri@6.0.4:
resolution: {integrity: sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==}
engines: {node: '>= 14'}
github-from-package@0.0.0:
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
@ -6995,9 +6905,6 @@ packages:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'}
has-unicode@2.0.1:
resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==}
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
@ -7230,10 +7137,6 @@ packages:
resolution: {integrity: sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==}
engines: {node: '>=12.22.0'}
ip-address@9.0.5:
resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==}
engines: {node: '>= 12'}
ipaddr.js@1.9.1:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
engines: {node: '>= 0.10'}
@ -7574,9 +7477,6 @@ packages:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
hasBin: true
jsbn@1.1.0:
resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==}
jsdom@26.1.0:
resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==}
engines: {node: '>=18'}
@ -8016,10 +7916,6 @@ packages:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'}
lru-cache@7.18.3:
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
engines: {node: '>=12'}
lucide-react@0.518.0:
resolution: {integrity: sha512-kFg34uQqnVl/7HwAiigxPSpj//43VIVHQbMygQPtS1yT4btMXHCWUipHcgcXHD2pm1Z2nUBA/M+Vnh/YmWXQUw==}
peerDependencies:
@ -8329,9 +8225,6 @@ packages:
resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==}
engines: {node: '>= 18'}
mitt@3.0.1:
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
mkdirp-classic@0.5.3:
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
@ -8411,10 +8304,6 @@ packages:
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
engines: {node: '>= 0.6'}
netmask@2.0.2:
resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==}
engines: {node: '>= 0.4.0'}
next-sitemap@4.2.3:
resolution: {integrity: sha512-vjdCxeDuWDzldhCnyFCQipw5bfpl4HmZA7uoo3GAaYGjGgfL4Cxb1CiztPuWGmS+auYs7/8OekRS8C2cjdAsjQ==}
engines: {node: '>=14.18'}
@ -8701,14 +8590,6 @@ packages:
resolution: {integrity: sha512-lwx6u1CotQYPVju77R+D0vFomni/AqRfqLmqQ8hekklqZ6gAY9rONh7lBQ0uxWMkC2AuX9b2DVAl8To0NyP1JA==}
engines: {node: '>=12'}
pac-proxy-agent@7.2.0:
resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==}
engines: {node: '>= 14'}
pac-resolver@7.0.1:
resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==}
engines: {node: '>= 14'}
package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
@ -9012,10 +8893,6 @@ packages:
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
engines: {node: '>= 0.6.0'}
progress@2.0.3:
resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
engines: {node: '>=0.4.0'}
promise-limit@2.7.0:
resolution: {integrity: sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==}
@ -9046,10 +8923,6 @@ packages:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'}
proxy-agent@6.5.0:
resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==}
engines: {node: '>= 14'}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
@ -9069,17 +8942,6 @@ packages:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
puppeteer-chromium-resolver@24.0.1:
resolution: {integrity: sha512-whu9e5qmnZekCP5hvlYMe7rWe4cU9seCISRlfT0vXGlCsy7psbeXHdGW6QdXrwyadvCTiD1Ft62jPaqia8ZQaA==}
puppeteer-core@23.11.1:
resolution: {integrity: sha512-3HZ2/7hdDKZvZQ7dhhITOUg4/wOrDRjyK2ZBllRB0ZCOi9u0cwq1ACHDjBB+nX+7+kltHjQvBRdeY7+W0T+7Gg==}
engines: {node: '>=18'}
puppeteer-core@24.10.2:
resolution: {integrity: sha512-CnzhOgrZj8DvkDqI+Yx+9or33i3Y9uUYbKyYpP4C13jWwXx/keQ38RMTMmxuLCWQlxjZrOH0Foq7P2fGP7adDQ==}
engines: {node: '>=18'}
qrcode@1.5.4:
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
engines: {node: '>=10.13.0'}
@ -9699,10 +9561,6 @@ packages:
resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==}
engines: {node: '>=18'}
smart-buffer@4.2.0:
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
smol-toml@1.3.4:
resolution: {integrity: sha512-UOPtVuYkzYGee0Bd2Szz8d2G3RfMfJ2t3qVdZUAozZyAk+a0Sxa+QKix0YCwjL/A1RR0ar44nCxaoN9FxdJGwA==}
engines: {node: '>= 18'}
@ -9715,14 +9573,6 @@ packages:
resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==}
engines: {node: '>=10.0.0'}
socks-proxy-agent@8.0.5:
resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==}
engines: {node: '>= 14'}
socks@2.8.4:
resolution: {integrity: sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==}
engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
sonner@2.0.5:
resolution: {integrity: sha512-YwbHQO6cSso3HBXlbCkgrgzDNIhws14r4MO87Ofy+cV2X7ES4pOoAK3+veSmVTvqNx1BWUxlhPmZzP00Crk2aQ==}
peerDependencies:
@ -10314,9 +10164,6 @@ packages:
resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
engines: {node: '>= 0.4'}
typed-query-selector@2.12.0:
resolution: {integrity: sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==}
typed-rest-client@1.8.11:
resolution: {integrity: sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==}
@ -10342,9 +10189,6 @@ packages:
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
engines: {node: '>= 0.4'}
unbzip2-stream@1.4.3:
resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==}
underscore@1.13.7:
resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==}
@ -10805,9 +10649,6 @@ packages:
engines: {node: '>=8'}
hasBin: true
wide-align@1.1.5:
resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==}
widest-line@5.0.0:
resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==}
engines: {node: '>=18'}
@ -12582,7 +12423,7 @@ snapshots:
'@kwsites/file-exists@1.1.1':
dependencies:
debug: 4.4.1
debug: 4.4.1(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
@ -13043,33 +12884,6 @@ snapshots:
'@protobufjs/utf8@1.1.0': {}
'@puppeteer/browsers@2.10.5':
dependencies:
debug: 4.4.1
extract-zip: 2.0.1
progress: 2.0.3
proxy-agent: 6.5.0
semver: 7.7.3
tar-fs: 3.1.1
yargs: 17.7.2
transitivePeerDependencies:
- bare-buffer
- supports-color
'@puppeteer/browsers@2.6.1':
dependencies:
debug: 4.4.1
extract-zip: 2.0.1
progress: 2.0.3
proxy-agent: 6.5.0
semver: 7.7.3
tar-fs: 3.1.1
unbzip2-stream: 1.4.3
yargs: 17.7.2
transitivePeerDependencies:
- bare-buffer
- supports-color
'@qdrant/js-client-rest@1.14.0(typescript@5.8.3)':
dependencies:
'@qdrant/openapi-typescript-fetch': 1.2.6
@ -13819,11 +13633,11 @@ snapshots:
dependencies:
'@redis/client': 5.5.5
'@requesty/ai-sdk@3.0.0(vite@6.3.5(@types/node@20.17.50)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0))(zod@3.25.76)':
'@requesty/ai-sdk@3.0.0(vite@6.3.6(@types/node@20.17.50)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0))(zod@3.25.76)':
dependencies:
'@ai-sdk/provider': 3.0.8
'@ai-sdk/provider-utils': 3.0.20(zod@3.25.76)
vite: 6.3.5(@types/node@20.17.50)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0)
vite: 6.3.6(@types/node@20.17.50)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0)
zod: 3.25.76
'@resvg/resvg-wasm@2.4.0': {}
@ -14449,8 +14263,6 @@ snapshots:
dependencies:
'@testing-library/dom': 10.4.0
'@tootallnate/quickjs-emscripten@0.23.0': {}
'@trpc/client@11.8.1(@trpc/server@11.8.1(typescript@5.8.3))(typescript@5.8.3)':
dependencies:
'@trpc/server': 11.8.1(typescript@5.8.3)
@ -14800,11 +14612,6 @@ snapshots:
dependencies:
'@types/yargs-parser': 21.0.3
'@types/yauzl@2.10.3':
dependencies:
'@types/node': 24.2.1
optional: true
'@typescript-eslint/eslint-plugin@8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)':
dependencies:
'@eslint-community/regexpp': 4.12.1
@ -14971,7 +14778,7 @@ snapshots:
sirv: 3.0.1
tinyglobby: 0.2.14
tinyrainbow: 2.0.0
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.50)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0)
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0)
'@vitest/utils@3.2.4':
dependencies:
@ -15173,8 +14980,6 @@ snapshots:
normalize-path: 3.0.0
picomatch: 2.3.1
aproba@2.0.0: {}
archiver-utils@2.1.0:
dependencies:
glob: 11.1.0
@ -15308,10 +15113,6 @@ snapshots:
assertion-error@2.0.1: {}
ast-types@0.13.4:
dependencies:
tslib: 2.8.1
async-function@1.0.0: {}
async-mutex@0.5.0:
@ -15395,8 +15196,6 @@ snapshots:
baseline-browser-mapping@2.9.19: {}
basic-ftp@5.0.5: {}
better-path-resolve@1.0.0:
dependencies:
is-windows: 1.0.2
@ -15437,7 +15236,7 @@ snapshots:
dependencies:
bytes: 3.1.2
content-type: 1.0.5
debug: 4.4.1
debug: 4.4.1(supports-color@8.1.1)
http-errors: 2.0.0
iconv-lite: 0.6.3
on-finished: 2.4.1
@ -15663,18 +15462,6 @@ snapshots:
chownr@3.0.0: {}
chromium-bidi@0.11.0(devtools-protocol@0.0.1367902):
dependencies:
devtools-protocol: 0.0.1367902
mitt: 3.0.1
zod: 3.25.76
chromium-bidi@5.1.0(devtools-protocol@0.0.1452169):
dependencies:
devtools-protocol: 0.0.1452169
mitt: 3.0.1
zod: 3.25.76
ci-info@2.0.0: {}
ci-info@3.9.0: {}
@ -15778,8 +15565,6 @@ snapshots:
color-name@1.1.4: {}
color-support@1.1.3: {}
colorette@2.0.20: {}
combined-stream@1.0.8:
@ -15825,8 +15610,6 @@ snapshots:
consola@3.4.2: {}
console-control-strings@1.1.0: {}
content-disposition@1.0.0:
dependencies:
safe-buffer: 5.2.1
@ -16130,8 +15913,6 @@ snapshots:
data-uri-to-buffer@4.0.1: {}
data-uri-to-buffer@6.0.2: {}
data-urls@5.0.0:
dependencies:
whatwg-mimetype: 4.0.0
@ -16169,10 +15950,6 @@ snapshots:
dependencies:
ms: 2.1.3
debug@4.4.1:
dependencies:
ms: 2.1.3
debug@4.4.1(supports-color@8.1.1):
dependencies:
ms: 2.1.3
@ -16232,12 +16009,6 @@ snapshots:
has-property-descriptors: 1.0.2
object-keys: 1.1.1
degenerator@5.0.1:
dependencies:
ast-types: 0.13.4
escodegen: 2.1.0
esprima: 4.0.1
delaunator@5.0.1:
dependencies:
robust-predicates: 3.0.2
@ -16269,10 +16040,6 @@ snapshots:
dependencies:
dequal: 2.0.3
devtools-protocol@0.0.1367902: {}
devtools-protocol@0.0.1452169: {}
didyoumean@1.2.2: {}
diff-match-patch@1.0.5: {}
@ -16386,8 +16153,6 @@ snapshots:
ee-first@1.1.1: {}
eight-colors@1.3.1: {}
electron-to-chromium@1.5.152: {}
electron-to-chromium@1.5.283: {}
@ -16622,14 +16387,6 @@ snapshots:
escape-string-regexp@5.0.0: {}
escodegen@2.1.0:
dependencies:
esprima: 4.0.1
estraverse: 5.3.0
esutils: 2.0.3
optionalDependencies:
source-map: 0.6.1
eslint-config-prettier@10.1.8(eslint@9.27.0(jiti@2.4.2)):
dependencies:
eslint: 9.27.0(jiti@2.4.2)
@ -16924,7 +16681,7 @@ snapshots:
content-type: 1.0.5
cookie: 0.7.2
cookie-signature: 1.2.2
debug: 4.4.1
debug: 4.4.1(supports-color@8.1.1)
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
@ -16958,16 +16715,6 @@ snapshots:
extendable-error@0.1.7: {}
extract-zip@2.0.1:
dependencies:
debug: 4.4.1
get-stream: 5.2.0
yauzl: 2.10.0
optionalDependencies:
'@types/yauzl': 2.10.3
transitivePeerDependencies:
- supports-color
fast-csv@4.3.6:
dependencies:
'@fast-csv/format': 4.3.5
@ -17061,7 +16808,7 @@ snapshots:
finalhandler@2.1.0:
dependencies:
debug: 4.4.1
debug: 4.4.1(supports-color@8.1.1)
encodeurl: 2.0.0
escape-html: 1.0.3
on-finished: 2.4.1
@ -17190,17 +16937,6 @@ snapshots:
fzf@0.5.2: {}
gauge@5.0.2:
dependencies:
aproba: 2.0.0
color-support: 1.1.3
console-control-strings: 1.1.0
has-unicode: 2.0.1
signal-exit: 4.1.0
string-width: 4.2.3
strip-ansi: 6.0.1
wide-align: 1.1.5
gaxios@7.1.3:
dependencies:
extend: 3.0.2
@ -17258,10 +16994,6 @@ snapshots:
dunder-proto: 1.0.1
es-object-atoms: 1.1.1
get-stream@5.2.0:
dependencies:
pump: 3.0.2
get-stream@6.0.1: {}
get-stream@8.0.1: {}
@ -17285,14 +17017,6 @@ snapshots:
dependencies:
resolve-pkg-maps: 1.0.0
get-uri@6.0.4:
dependencies:
basic-ftp: 5.0.5
data-uri-to-buffer: 6.0.2
debug: 4.4.1
transitivePeerDependencies:
- supports-color
github-from-package@0.0.0:
optional: true
@ -17404,8 +17128,6 @@ snapshots:
dependencies:
has-symbols: 1.1.0
has-unicode@2.0.1: {}
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
@ -17584,14 +17306,14 @@ snapshots:
http-proxy-agent@7.0.2:
dependencies:
agent-base: 7.1.3
debug: 4.4.1
debug: 4.4.1(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
https-proxy-agent@7.0.6:
dependencies:
agent-base: 7.1.3
debug: 4.4.1
debug: 4.4.1(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
@ -17729,11 +17451,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
ip-address@9.0.5:
dependencies:
jsbn: 1.1.0
sprintf-js: 1.1.3
ipaddr.js@1.9.1: {}
is-alphabetical@1.0.4: {}
@ -18043,8 +17760,6 @@ snapshots:
dependencies:
argparse: 2.0.1
jsbn@1.1.0: {}
jsdom@26.1.0:
dependencies:
cssstyle: 4.4.0
@ -18491,8 +18206,6 @@ snapshots:
dependencies:
yallist: 4.0.0
lru-cache@7.18.3: {}
lucide-react@0.518.0(react@18.3.1):
dependencies:
react: 18.3.1
@ -19046,8 +18759,6 @@ snapshots:
dependencies:
minipass: 7.1.2
mitt@3.0.1: {}
mkdirp-classic@0.5.3:
optional: true
@ -19139,8 +18850,6 @@ snapshots:
negotiator@1.0.0: {}
netmask@2.0.2: {}
next-sitemap@4.2.3(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)):
dependencies:
'@corex/deepmerge': 4.0.43
@ -19465,24 +19174,6 @@ snapshots:
dependencies:
p-timeout: 6.1.4
pac-proxy-agent@7.2.0:
dependencies:
'@tootallnate/quickjs-emscripten': 0.23.0
agent-base: 7.1.3
debug: 4.4.1
get-uri: 6.0.4
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
pac-resolver: 7.0.1
socks-proxy-agent: 8.0.5
transitivePeerDependencies:
- supports-color
pac-resolver@7.0.1:
dependencies:
degenerator: 5.0.1
netmask: 2.0.2
package-json-from-dist@1.0.1: {}
package-manager-detector@0.2.11:
@ -19771,8 +19462,6 @@ snapshots:
process@0.11.10: {}
progress@2.0.3: {}
promise-limit@2.7.0:
optional: true
@ -19818,19 +19507,6 @@ snapshots:
forwarded: 0.2.0
ipaddr.js: 1.9.1
proxy-agent@6.5.0:
dependencies:
agent-base: 7.1.3
debug: 4.4.1
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
lru-cache: 7.18.3
pac-proxy-agent: 7.2.0
proxy-from-env: 1.1.0
socks-proxy-agent: 8.0.5
transitivePeerDependencies:
- supports-color
proxy-from-env@1.1.0: {}
ps-tree@1.2.0:
@ -19841,51 +19517,12 @@ snapshots:
dependencies:
end-of-stream: 1.4.4
once: 1.4.0
optional: true
punycode.js@2.3.1: {}
punycode@2.3.1: {}
puppeteer-chromium-resolver@24.0.1:
dependencies:
'@puppeteer/browsers': 2.10.5
eight-colors: 1.3.1
gauge: 5.0.2
puppeteer-core: 24.10.2
transitivePeerDependencies:
- bare-buffer
- bufferutil
- supports-color
- utf-8-validate
puppeteer-core@23.11.1:
dependencies:
'@puppeteer/browsers': 2.6.1
chromium-bidi: 0.11.0(devtools-protocol@0.0.1367902)
debug: 4.4.1
devtools-protocol: 0.0.1367902
typed-query-selector: 2.12.0
ws: 8.18.2
transitivePeerDependencies:
- bare-buffer
- bufferutil
- supports-color
- utf-8-validate
puppeteer-core@24.10.2:
dependencies:
'@puppeteer/browsers': 2.10.5
chromium-bidi: 5.1.0(devtools-protocol@0.0.1452169)
debug: 4.4.1
devtools-protocol: 0.0.1452169
typed-query-selector: 2.12.0
ws: 8.18.2
transitivePeerDependencies:
- bare-buffer
- bufferutil
- supports-color
- utf-8-validate
qrcode@1.5.4:
dependencies:
dijkstrajs: 1.0.3
@ -20397,7 +20034,7 @@ snapshots:
router@2.2.0:
dependencies:
debug: 4.4.1
debug: 4.4.1(supports-color@8.1.1)
depd: 2.0.0
is-promise: 4.0.0
parseurl: 1.3.3
@ -20514,7 +20151,7 @@ snapshots:
send@1.2.0:
dependencies:
debug: 4.4.1
debug: 4.4.1(supports-color@8.1.1)
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
@ -20686,7 +20323,7 @@ snapshots:
dependencies:
'@kwsites/file-exists': 1.1.1
'@kwsites/promise-deferred': 1.1.1
debug: 4.4.1
debug: 4.4.1(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
@ -20710,8 +20347,6 @@ snapshots:
ansi-styles: 6.2.3
is-fullwidth-code-point: 5.0.0
smart-buffer@4.2.0: {}
smol-toml@1.3.4: {}
socket.io-client@4.8.1:
@ -20732,19 +20367,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
socks-proxy-agent@8.0.5:
dependencies:
agent-base: 7.1.3
debug: 4.4.1
socks: 2.8.4
transitivePeerDependencies:
- supports-color
socks@2.8.4:
dependencies:
ip-address: 9.0.5
smart-buffer: 4.2.0
sonner@2.0.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
react: 18.3.1
@ -21092,6 +20714,7 @@ snapshots:
bare-path: 3.0.0
transitivePeerDependencies:
- bare-buffer
optional: true
tar-stream@2.2.0:
dependencies:
@ -21245,7 +20868,7 @@ snapshots:
cac: 6.7.14
chokidar: 4.0.3
consola: 3.4.2
debug: 4.4.1
debug: 4.4.1(supports-color@8.1.1)
esbuild: 0.25.9
fix-dts-default-cjs-exports: 1.0.1
joycon: 3.1.1
@ -21359,8 +20982,6 @@ snapshots:
possible-typed-array-names: 1.1.0
reflect.getprototypeof: 1.0.10
typed-query-selector@2.12.0: {}
typed-rest-client@1.8.11:
dependencies:
qs: 6.14.0
@ -21390,11 +21011,6 @@ snapshots:
has-symbols: 1.1.0
which-boxed-primitive: 1.1.1
unbzip2-stream@1.4.3:
dependencies:
buffer: 5.7.1
through: 2.3.8
underscore@1.13.7: {}
undici-types@5.26.5: {}
@ -21646,7 +21262,7 @@ snapshots:
vite-node@3.2.4(@types/node@20.17.50)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0):
dependencies:
cac: 6.7.14
debug: 4.4.1
debug: 4.4.1(supports-color@8.1.1)
es-module-lexer: 1.7.0
pathe: 2.0.3
vite: 6.3.6(@types/node@20.17.50)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0)
@ -21813,7 +21429,7 @@ snapshots:
'@vitest/spy': 3.2.4
'@vitest/utils': 3.2.4
chai: 5.2.0
debug: 4.4.1
debug: 4.4.1(supports-color@8.1.1)
expect-type: 1.2.1
magic-string: 0.30.17
pathe: 2.0.3
@ -22070,10 +21686,6 @@ snapshots:
siginfo: 2.0.0
stackback: 0.0.2
wide-align@1.1.5:
dependencies:
string-width: 4.2.3
widest-line@5.0.0:
dependencies:
string-width: 7.2.0

View file

@ -1,28 +1,14 @@
import { parseMentions } from "../core/mentions"
import { UrlContentFetcher } from "../services/browser/UrlContentFetcher"
import { getCommand } from "../services/command/commands"
// Mock the dependencies
vi.mock("../services/command/commands")
vi.mock("../services/browser/UrlContentFetcher")
const MockedUrlContentFetcher = vi.mocked(UrlContentFetcher)
const mockGetCommand = vi.mocked(getCommand)
describe("Command Mentions", () => {
let mockUrlContentFetcher: any
beforeEach(() => {
vi.clearAllMocks()
// Create a mock UrlContentFetcher instance
mockUrlContentFetcher = {
launchBrowser: vi.fn(),
urlToMarkdown: vi.fn(),
closeBrowser: vi.fn(),
}
MockedUrlContentFetcher.mockImplementation(() => mockUrlContentFetcher)
})
// Helper function to call parseMentions with required parameters
@ -30,7 +16,6 @@ describe("Command Mentions", () => {
return parseMentions(
text,
"/test/cwd", // cwd
mockUrlContentFetcher, // urlContentFetcher
undefined, // fileContextTracker
undefined, // rooIgnoreController
false, // showRooIgnoredFiles

View file

@ -38,6 +38,10 @@ vi.mock("../core/task-persistence", async (importOriginal) => {
saveTaskMessages: vi.fn().mockResolvedValue(undefined),
}
})
vi.mock("../core/task-persistence/delegationMeta", () => ({
readDelegationMeta: vi.fn().mockResolvedValue(null),
saveDelegationMeta: vi.fn().mockResolvedValue(undefined),
}))
import { ClineProvider } from "../core/webview/ClineProvider"
import { readTaskMessages } from "../core/task-persistence/taskMessages"
@ -149,6 +153,7 @@ describe("History resume delegation - parent metadata transitions", () => {
overwriteApiConversationHistory: vi.fn().mockResolvedValue(undefined),
}),
updateTaskHistory: vi.fn().mockResolvedValue([]),
log: vi.fn(),
} as unknown as ClineProvider
// Start with existing messages in history
@ -232,6 +237,7 @@ describe("History resume delegation - parent metadata transitions", () => {
overwriteApiConversationHistory: vi.fn().mockResolvedValue(undefined),
}),
updateTaskHistory: vi.fn().mockResolvedValue([]),
log: vi.fn(),
} as unknown as ClineProvider
// Include an assistant message with new_task tool_use to exercise the tool_result path
@ -320,6 +326,7 @@ describe("History resume delegation - parent metadata transitions", () => {
overwriteApiConversationHistory: vi.fn().mockResolvedValue(undefined),
}),
updateTaskHistory: vi.fn().mockResolvedValue([]),
log: vi.fn(),
} as unknown as ClineProvider
// No assistant tool_use in history
@ -555,6 +562,7 @@ describe("History resume delegation - parent metadata transitions", () => {
overwriteApiConversationHistory: vi.fn().mockResolvedValue(undefined),
}),
updateTaskHistory: vi.fn().mockResolvedValue([]),
log: vi.fn(),
} as unknown as ClineProvider
vi.mocked(readTaskMessages).mockResolvedValue([])
@ -754,6 +762,7 @@ describe("History resume delegation - parent metadata transitions", () => {
overwriteApiConversationHistory: vi.fn().mockResolvedValue(undefined),
}),
updateTaskHistory: vi.fn().mockResolvedValue([]),
log: vi.fn(),
} as unknown as ClineProvider
// Mock read failures or empty returns

View file

@ -2,6 +2,12 @@
import { describe, it, expect, vi } from "vitest"
import { RooCodeEventName } from "@roo-code/types"
vi.mock("../core/task-persistence/delegationMeta", () => ({
readDelegationMeta: vi.fn().mockResolvedValue(null),
saveDelegationMeta: vi.fn().mockResolvedValue(undefined),
}))
import { ClineProvider } from "../core/webview/ClineProvider"
describe("ClineProvider.delegateParentAndOpenChild()", () => {
@ -9,7 +15,7 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => {
const providerEmit = vi.fn()
const parentTask = { taskId: "parent-1", emit: vi.fn() } as any
const childStart = vi.fn()
const childStart = vi.fn().mockResolvedValue(undefined)
const updateTaskHistory = vi.fn()
const removeClineFromStack = vi.fn().mockResolvedValue(undefined)
const createTask = vi.fn().mockResolvedValue({ taskId: "child-1", start: childStart })
@ -48,6 +54,7 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => {
updateTaskHistory,
handleModeSwitch,
log: vi.fn(),
contextProxy: { globalStorageUri: { fsPath: "/tmp" } },
} as unknown as ClineProvider
const params = {
@ -63,18 +70,26 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => {
// Invariant: parent closed before child creation
expect(removeClineFromStack).toHaveBeenCalledTimes(1)
// Child task is created with startTask: false and initialStatus: "active"
// Child task is created with startTask: false
expect(createTask).toHaveBeenCalledWith("Do something", undefined, parentTask, {
initialTodos: [],
initialStatus: "active",
startTask: false,
})
// Metadata persistence - parent gets "delegated" status (child status is set at creation via initialStatus)
expect(updateTaskHistory).toHaveBeenCalledTimes(1)
// Metadata persistence - child gets "active" status, parent gets "delegated" status
expect(updateTaskHistory).toHaveBeenCalledTimes(2)
// Parent set to "delegated"
const parentSaved = updateTaskHistory.mock.calls[0][0]
// Child set to "active" (first call)
const childSaved = updateTaskHistory.mock.calls[0][0]
expect(childSaved).toEqual(
expect.objectContaining({
id: "child-1",
status: "active",
}),
)
// Parent set to "delegated" (second call)
const parentSaved = updateTaskHistory.mock.calls[1][0]
expect(parentSaved).toEqual(
expect.objectContaining({
id: "parent-1",
@ -99,7 +114,10 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => {
const callOrder: string[] = []
const parentTask = { taskId: "parent-1", emit: vi.fn() } as any
const childStart = vi.fn(() => callOrder.push("child.start"))
const childStart = vi.fn(() => {
callOrder.push("child.start")
return Promise.resolve()
})
const updateTaskHistory = vi.fn(async () => {
callOrder.push("updateTaskHistory")
@ -130,6 +148,7 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => {
updateTaskHistory,
handleModeSwitch,
log: vi.fn(),
contextProxy: { globalStorageUri: { fsPath: "/tmp" } },
} as unknown as ClineProvider
await (ClineProvider.prototype as any).delegateParentAndOpenChild.call(provider, {
@ -139,7 +158,7 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => {
mode: "code",
})
// Verify ordering: createTask → updateTaskHistory → child.start
expect(callOrder).toEqual(["createTask", "updateTaskHistory", "child.start"])
// Verify ordering: createTask → updateTaskHistory (child) → updateTaskHistory (parent) → child.start
expect(callOrder).toEqual(["createTask", "updateTaskHistory", "updateTaskHistory", "child.start"])
})
})

View file

@ -1,6 +1,12 @@
// npx vitest run __tests__/removeClineFromStack-delegation.spec.ts
import { describe, it, expect, vi } from "vitest"
vi.mock("../core/task-persistence/delegationMeta", () => ({
readDelegationMeta: vi.fn().mockResolvedValue(null),
saveDelegationMeta: vi.fn().mockResolvedValue(undefined),
}))
import { ClineProvider } from "../core/webview/ClineProvider"
describe("ClineProvider.removeClineFromStack() delegation awareness", () => {
@ -38,6 +44,7 @@ describe("ClineProvider.removeClineFromStack() delegation awareness", () => {
log: vi.fn(),
getTaskWithId,
updateTaskHistory,
contextProxy: { globalStorageUri: { fsPath: "/tmp" } },
}
return { provider, childTask, updateTaskHistory, getTaskWithId }
@ -183,6 +190,7 @@ describe("ClineProvider.removeClineFromStack() delegation awareness", () => {
log: vi.fn(),
getTaskWithId: vi.fn(),
updateTaskHistory: vi.fn(),
contextProxy: { globalStorageUri: { fsPath: "/tmp" } },
}
// Should not throw
@ -263,6 +271,7 @@ describe("ClineProvider.removeClineFromStack() delegation awareness", () => {
log: vi.fn(),
getTaskWithId,
updateTaskHistory,
contextProxy: { globalStorageUri: { fsPath: "/tmp" } },
}
// Simulate what delegateParentAndOpenChild does: pop B with skipDelegationRepair

View file

@ -88,6 +88,13 @@ export interface ApiHandlerCreateMessageMetadata {
* Only applies to providers that support function calling restrictions (e.g., Gemini).
*/
allowedFunctionNames?: string[]
/** Provider-specific options for tool definitions (e.g. cache control). */
toolProviderOptions?: Record<string, Record<string, unknown>>
/** Provider-specific options for the system prompt (e.g. cache control).
* Cache-aware providers use this to inject the system prompt as a cached
* system message, since AI SDK v6 does not support providerOptions on the
* `system` string parameter. */
systemProviderOptions?: Record<string, Record<string, unknown>>
}
export interface ApiHandler {

View file

@ -399,7 +399,7 @@ describe("AnthropicHandler", () => {
expect(endChunk).toBeDefined()
})
it("should pass system prompt via system param with systemProviderOptions for cache control", async () => {
it("should pass system prompt via system param when no systemProviderOptions", async () => {
setupStreamTextMock([{ type: "text-delta", text: "test" }])
const stream = handler.createMessage(systemPrompt, [
@ -410,16 +410,37 @@ describe("AnthropicHandler", () => {
// Consume
}
// Verify streamText was called with system + systemProviderOptions (not as a message)
// Without systemProviderOptions, system prompt is passed via the system parameter
const callArgs = mockStreamText.mock.calls[0]![0]
expect(callArgs.system).toBe(systemPrompt)
expect(callArgs.systemProviderOptions).toEqual({
anthropic: { cacheControl: { type: "ephemeral" } },
})
// System prompt should NOT be in the messages array
const systemMessages = callArgs.messages.filter((m: any) => m.role === "system")
expect(systemMessages).toHaveLength(0)
})
it("should inject system prompt as cached system message when systemProviderOptions provided", async () => {
setupStreamTextMock([{ type: "text-delta", text: "test" }])
const cacheOpts = { anthropic: { cacheControl: { type: "ephemeral" } } }
const stream = handler.createMessage(
systemPrompt,
[{ role: "user", content: [{ type: "text" as const, text: "test" }] }],
{ taskId: "test-task", systemProviderOptions: cacheOpts },
)
for await (const _chunk of stream) {
// Consume
}
// With systemProviderOptions, system prompt is injected as messages[0]
const callArgs = mockStreamText.mock.calls[0]![0]
expect(callArgs.system).toBeUndefined()
// System prompt should be the first message with providerOptions
const systemMessages = callArgs.messages.filter((m: any) => m.role === "system")
expect(systemMessages).toHaveLength(1)
expect(systemMessages[0].content).toBe(systemPrompt)
expect(systemMessages[0].providerOptions).toEqual(cacheOpts)
})
})
describe("completePrompt", () => {

View file

@ -1279,4 +1279,165 @@ describe("AwsBedrockHandler", () => {
expect(mockCaptureException).toHaveBeenCalled()
})
})
describe("AI SDK v6 usage field paths", () => {
const systemPrompt = "You are a helpful assistant"
const messages: RooMessage[] = [
{
role: "user",
content: "Hello",
},
]
function setupStream(usage: Record<string, unknown>, providerMetadata: Record<string, unknown> = {}) {
async function* mockFullStream() {
yield { type: "text-delta", text: "reply" }
}
mockStreamText.mockReturnValue({
fullStream: mockFullStream(),
usage: Promise.resolve(usage),
providerMetadata: Promise.resolve(providerMetadata),
})
}
describe("cache tokens", () => {
it("should read cache tokens from v6 top-level cachedInputTokens", async () => {
setupStream({ inputTokens: 100, outputTokens: 50, cachedInputTokens: 30 })
const generator = handler.createMessage(systemPrompt, messages)
const chunks: unknown[] = []
for await (const chunk of generator) {
chunks.push(chunk)
}
const usageChunk = chunks.find((c: any) => c.type === "usage") as any
expect(usageChunk).toBeDefined()
expect(usageChunk.cacheReadTokens).toBe(30)
})
it("should read cache tokens from v6 inputTokenDetails.cacheReadTokens", async () => {
setupStream({
inputTokens: 100,
outputTokens: 50,
inputTokenDetails: { cacheReadTokens: 25 },
})
const generator = handler.createMessage(systemPrompt, messages)
const chunks: unknown[] = []
for await (const chunk of generator) {
chunks.push(chunk)
}
const usageChunk = chunks.find((c: any) => c.type === "usage") as any
expect(usageChunk).toBeDefined()
expect(usageChunk.cacheReadTokens).toBe(25)
})
it("should prefer v6 top-level cachedInputTokens over providerMetadata.bedrock", async () => {
setupStream(
{ inputTokens: 100, outputTokens: 50, cachedInputTokens: 30 },
{ bedrock: { usage: { cacheReadInputTokens: 20 } } },
)
const generator = handler.createMessage(systemPrompt, messages)
const chunks: unknown[] = []
for await (const chunk of generator) {
chunks.push(chunk)
}
const usageChunk = chunks.find((c: any) => c.type === "usage") as any
expect(usageChunk).toBeDefined()
expect(usageChunk.cacheReadTokens).toBe(30)
})
it("should fall back to providerMetadata.bedrock.usage.cacheReadInputTokens", async () => {
setupStream(
{ inputTokens: 100, outputTokens: 50 },
{ bedrock: { usage: { cacheReadInputTokens: 20 } } },
)
const generator = handler.createMessage(systemPrompt, messages)
const chunks: unknown[] = []
for await (const chunk of generator) {
chunks.push(chunk)
}
const usageChunk = chunks.find((c: any) => c.type === "usage") as any
expect(usageChunk).toBeDefined()
expect(usageChunk.cacheReadTokens).toBe(20)
})
it("should read cacheWriteTokens from v6 inputTokenDetails.cacheWriteTokens", async () => {
setupStream({
inputTokens: 100,
outputTokens: 50,
inputTokenDetails: { cacheWriteTokens: 15 },
})
const generator = handler.createMessage(systemPrompt, messages)
const chunks: unknown[] = []
for await (const chunk of generator) {
chunks.push(chunk)
}
const usageChunk = chunks.find((c: any) => c.type === "usage") as any
expect(usageChunk).toBeDefined()
expect(usageChunk.cacheWriteTokens).toBe(15)
})
})
describe("reasoning tokens", () => {
it("should read reasoning tokens from v6 top-level reasoningTokens", async () => {
setupStream({ inputTokens: 100, outputTokens: 50, reasoningTokens: 40 })
const generator = handler.createMessage(systemPrompt, messages)
const chunks: unknown[] = []
for await (const chunk of generator) {
chunks.push(chunk)
}
const usageChunk = chunks.find((c: any) => c.type === "usage") as any
expect(usageChunk).toBeDefined()
expect(usageChunk.reasoningTokens).toBe(40)
})
it("should read reasoning tokens from v6 outputTokenDetails.reasoningTokens", async () => {
setupStream({
inputTokens: 100,
outputTokens: 50,
outputTokenDetails: { reasoningTokens: 35 },
})
const generator = handler.createMessage(systemPrompt, messages)
const chunks: unknown[] = []
for await (const chunk of generator) {
chunks.push(chunk)
}
const usageChunk = chunks.find((c: any) => c.type === "usage") as any
expect(usageChunk).toBeDefined()
expect(usageChunk.reasoningTokens).toBe(35)
})
it("should prefer v6 top-level reasoningTokens over outputTokenDetails", async () => {
setupStream({
inputTokens: 100,
outputTokens: 50,
reasoningTokens: 40,
outputTokenDetails: { reasoningTokens: 15 },
})
const generator = handler.createMessage(systemPrompt, messages)
const chunks: unknown[] = []
for await (const chunk of generator) {
chunks.push(chunk)
}
const usageChunk = chunks.find((c: any) => c.type === "usage") as any
expect(usageChunk).toBeDefined()
expect(usageChunk.reasoningTokens).toBe(40)
})
})
})
})

View file

@ -472,4 +472,168 @@ describe("GeminiHandler", () => {
expect(mockCaptureException).toHaveBeenCalled()
})
})
describe("AI SDK v6 usage field paths", () => {
const mockMessages: RooMessage[] = [
{
role: "user",
content: "Hello",
},
]
const systemPrompt = "You are a helpful assistant"
function setupStream(usage: Record<string, unknown>) {
const mockFullStream = (async function* () {
yield { type: "text-delta", text: "reply" }
})()
mockStreamText.mockReturnValue({
fullStream: mockFullStream,
usage: Promise.resolve(usage),
providerMetadata: Promise.resolve({}),
})
}
describe("cache tokens", () => {
it("should read cache tokens from v6 top-level cachedInputTokens", async () => {
setupStream({ inputTokens: 100, outputTokens: 50, cachedInputTokens: 30 })
const stream = handler.createMessage(systemPrompt, mockMessages)
const chunks = []
for await (const chunk of stream) {
chunks.push(chunk)
}
const usageChunk = chunks.find((c) => c.type === "usage")
expect(usageChunk).toBeDefined()
expect(usageChunk!.cacheReadTokens).toBe(30)
})
it("should read cache tokens from v6 inputTokenDetails.cacheReadTokens", async () => {
setupStream({
inputTokens: 100,
outputTokens: 50,
inputTokenDetails: { cacheReadTokens: 25 },
})
const stream = handler.createMessage(systemPrompt, mockMessages)
const chunks = []
for await (const chunk of stream) {
chunks.push(chunk)
}
const usageChunk = chunks.find((c) => c.type === "usage")
expect(usageChunk).toBeDefined()
expect(usageChunk!.cacheReadTokens).toBe(25)
})
it("should prefer v6 top-level cachedInputTokens over legacy details", async () => {
setupStream({
inputTokens: 100,
outputTokens: 50,
cachedInputTokens: 30,
details: { cachedInputTokens: 20 },
})
const stream = handler.createMessage(systemPrompt, mockMessages)
const chunks = []
for await (const chunk of stream) {
chunks.push(chunk)
}
const usageChunk = chunks.find((c) => c.type === "usage")
expect(usageChunk).toBeDefined()
expect(usageChunk!.cacheReadTokens).toBe(30)
})
it("should fall back to legacy details.cachedInputTokens", async () => {
setupStream({
inputTokens: 100,
outputTokens: 50,
details: { cachedInputTokens: 20 },
})
const stream = handler.createMessage(systemPrompt, mockMessages)
const chunks = []
for await (const chunk of stream) {
chunks.push(chunk)
}
const usageChunk = chunks.find((c) => c.type === "usage")
expect(usageChunk).toBeDefined()
expect(usageChunk!.cacheReadTokens).toBe(20)
})
})
describe("reasoning tokens", () => {
it("should read reasoning tokens from v6 top-level reasoningTokens", async () => {
setupStream({ inputTokens: 100, outputTokens: 50, reasoningTokens: 40 })
const stream = handler.createMessage(systemPrompt, mockMessages)
const chunks = []
for await (const chunk of stream) {
chunks.push(chunk)
}
const usageChunk = chunks.find((c) => c.type === "usage")
expect(usageChunk).toBeDefined()
expect(usageChunk!.reasoningTokens).toBe(40)
})
it("should read reasoning tokens from v6 outputTokenDetails.reasoningTokens", async () => {
setupStream({
inputTokens: 100,
outputTokens: 50,
outputTokenDetails: { reasoningTokens: 35 },
})
const stream = handler.createMessage(systemPrompt, mockMessages)
const chunks = []
for await (const chunk of stream) {
chunks.push(chunk)
}
const usageChunk = chunks.find((c) => c.type === "usage")
expect(usageChunk).toBeDefined()
expect(usageChunk!.reasoningTokens).toBe(35)
})
it("should prefer v6 top-level reasoningTokens over legacy details", async () => {
setupStream({
inputTokens: 100,
outputTokens: 50,
reasoningTokens: 40,
details: { reasoningTokens: 15 },
})
const stream = handler.createMessage(systemPrompt, mockMessages)
const chunks = []
for await (const chunk of stream) {
chunks.push(chunk)
}
const usageChunk = chunks.find((c) => c.type === "usage")
expect(usageChunk).toBeDefined()
expect(usageChunk!.reasoningTokens).toBe(40)
})
it("should fall back to legacy details.reasoningTokens", async () => {
setupStream({
inputTokens: 100,
outputTokens: 50,
details: { reasoningTokens: 15 },
})
const stream = handler.createMessage(systemPrompt, mockMessages)
const chunks = []
for await (const chunk of stream) {
chunks.push(chunk)
}
const usageChunk = chunks.find((c) => c.type === "usage")
expect(usageChunk).toBeDefined()
expect(usageChunk!.reasoningTokens).toBe(15)
})
})
})
})

View file

@ -338,16 +338,12 @@ describe("MiniMaxHandler", () => {
expect(mockMergeEnvironmentDetailsForMiniMax).toHaveBeenCalledWith(messages)
const callArgs = mockStreamText.mock.calls[0]?.[0]
// Cache control is now applied centrally in Task.ts, not per-provider
expect(callArgs.messages).toEqual(
expect.arrayContaining([
expect.objectContaining({
role: "user",
content: [{ type: "text", text: "Merged message" }],
providerOptions: {
anthropic: {
cacheControl: { type: "ephemeral" },
},
},
}),
]),
)

View file

@ -84,7 +84,13 @@ describe("NativeOllamaHandler", () => {
expect(results).toHaveLength(3)
expect(results[0]).toEqual({ type: "text", text: "Hello" })
expect(results[1]).toEqual({ type: "text", text: " world" })
expect(results[2]).toEqual({ type: "usage", inputTokens: 10, outputTokens: 2 })
expect(results[2]).toEqual({
type: "usage",
inputTokens: 10,
outputTokens: 2,
totalInputTokens: 10,
totalOutputTokens: 2,
})
})
it("should not include providerOptions by default (no num_ctx)", async () => {

View file

@ -353,4 +353,201 @@ describe("OpenAiNativeHandler - usage metrics", () => {
expect(callArgs.providerOptions.openai.promptCacheRetention).toBeUndefined()
})
})
describe("AI SDK v6 usage field paths", () => {
describe("cache tokens", () => {
it("should read cache tokens from v6 top-level cachedInputTokens", async () => {
async function* mockFullStream() {
yield { type: "text-delta", text: "Test" }
}
mockStreamText.mockReturnValue({
fullStream: mockFullStream(),
usage: Promise.resolve({
inputTokens: 100,
outputTokens: 50,
cachedInputTokens: 30,
}),
providerMetadata: Promise.resolve({}),
content: Promise.resolve([]),
})
const stream = handler.createMessage(systemPrompt, messages)
const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}
const usageChunks = chunks.filter((c) => c.type === "usage")
expect(usageChunks).toHaveLength(1)
expect(usageChunks[0].cacheReadTokens).toBe(30)
})
it("should read cache tokens from v6 inputTokenDetails.cacheReadTokens", async () => {
async function* mockFullStream() {
yield { type: "text-delta", text: "Test" }
}
mockStreamText.mockReturnValue({
fullStream: mockFullStream(),
usage: Promise.resolve({
inputTokens: 100,
outputTokens: 50,
inputTokenDetails: { cacheReadTokens: 25 },
}),
providerMetadata: Promise.resolve({}),
content: Promise.resolve([]),
})
const stream = handler.createMessage(systemPrompt, messages)
const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}
const usageChunks = chunks.filter((c) => c.type === "usage")
expect(usageChunks).toHaveLength(1)
expect(usageChunks[0].cacheReadTokens).toBe(25)
})
it("should prefer v6 top-level cachedInputTokens over legacy details", async () => {
async function* mockFullStream() {
yield { type: "text-delta", text: "Test" }
}
mockStreamText.mockReturnValue({
fullStream: mockFullStream(),
usage: Promise.resolve({
inputTokens: 100,
outputTokens: 50,
cachedInputTokens: 30,
details: { cachedInputTokens: 20 },
}),
providerMetadata: Promise.resolve({}),
content: Promise.resolve([]),
})
const stream = handler.createMessage(systemPrompt, messages)
const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}
const usageChunks = chunks.filter((c) => c.type === "usage")
expect(usageChunks).toHaveLength(1)
expect(usageChunks[0].cacheReadTokens).toBe(30)
})
it("should read cacheWriteTokens from v6 inputTokenDetails.cacheWriteTokens", async () => {
async function* mockFullStream() {
yield { type: "text-delta", text: "Test" }
}
mockStreamText.mockReturnValue({
fullStream: mockFullStream(),
usage: Promise.resolve({
inputTokens: 100,
outputTokens: 50,
inputTokenDetails: { cacheWriteTokens: 15 },
}),
providerMetadata: Promise.resolve({}),
content: Promise.resolve([]),
})
const stream = handler.createMessage(systemPrompt, messages)
const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}
const usageChunks = chunks.filter((c) => c.type === "usage")
expect(usageChunks).toHaveLength(1)
expect(usageChunks[0].cacheWriteTokens).toBe(15)
})
})
describe("reasoning tokens", () => {
it("should read reasoning tokens from v6 top-level reasoningTokens", async () => {
async function* mockFullStream() {
yield { type: "text-delta", text: "Test" }
}
mockStreamText.mockReturnValue({
fullStream: mockFullStream(),
usage: Promise.resolve({
inputTokens: 100,
outputTokens: 50,
reasoningTokens: 40,
}),
providerMetadata: Promise.resolve({}),
content: Promise.resolve([]),
})
const stream = handler.createMessage(systemPrompt, messages)
const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}
const usageChunks = chunks.filter((c) => c.type === "usage")
expect(usageChunks).toHaveLength(1)
expect(usageChunks[0].reasoningTokens).toBe(40)
})
it("should read reasoning tokens from v6 outputTokenDetails.reasoningTokens", async () => {
async function* mockFullStream() {
yield { type: "text-delta", text: "Test" }
}
mockStreamText.mockReturnValue({
fullStream: mockFullStream(),
usage: Promise.resolve({
inputTokens: 100,
outputTokens: 50,
outputTokenDetails: { reasoningTokens: 35 },
}),
providerMetadata: Promise.resolve({}),
content: Promise.resolve([]),
})
const stream = handler.createMessage(systemPrompt, messages)
const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}
const usageChunks = chunks.filter((c) => c.type === "usage")
expect(usageChunks).toHaveLength(1)
expect(usageChunks[0].reasoningTokens).toBe(35)
})
it("should prefer v6 top-level reasoningTokens over legacy details", async () => {
async function* mockFullStream() {
yield { type: "text-delta", text: "Test" }
}
mockStreamText.mockReturnValue({
fullStream: mockFullStream(),
usage: Promise.resolve({
inputTokens: 100,
outputTokens: 50,
reasoningTokens: 40,
details: { reasoningTokens: 15 },
}),
providerMetadata: Promise.resolve({}),
content: Promise.resolve([]),
})
const stream = handler.createMessage(systemPrompt, messages)
const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}
const usageChunks = chunks.filter((c) => c.type === "usage")
expect(usageChunks).toHaveLength(1)
expect(usageChunks[0].reasoningTokens).toBe(40)
})
})
})
})

View file

@ -98,6 +98,8 @@ describe("OpenAiHandler with usage tracking fix", () => {
type: "usage",
inputTokens: 10,
outputTokens: 5,
totalInputTokens: 10,
totalOutputTokens: 5,
})
const lastChunk = chunks[chunks.length - 1]
@ -133,6 +135,8 @@ describe("OpenAiHandler with usage tracking fix", () => {
type: "usage",
inputTokens: 10,
outputTokens: 5,
totalInputTokens: 10,
totalOutputTokens: 5,
})
})
@ -229,5 +233,202 @@ describe("OpenAiHandler with usage tracking fix", () => {
}),
)
})
describe("AI SDK v6 usage field paths", () => {
describe("cache tokens", () => {
it("should read cache tokens from v6 top-level cachedInputTokens when providerMetadata is empty", async () => {
async function* mockFullStream() {
yield { type: "text-delta", text: "Test response" }
}
mockStreamText.mockReturnValueOnce({
fullStream: mockFullStream(),
usage: Promise.resolve({
inputTokens: 100,
outputTokens: 50,
cachedInputTokens: 30,
}),
providerMetadata: Promise.resolve(undefined),
})
const stream = handler.createMessage(systemPrompt, messages)
const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}
const usageChunks = chunks.filter((chunk) => chunk.type === "usage")
expect(usageChunks).toHaveLength(1)
expect(usageChunks[0].cacheReadTokens).toBe(30)
})
it("should read cache tokens from v6 inputTokenDetails.cacheReadTokens when providerMetadata is empty", async () => {
async function* mockFullStream() {
yield { type: "text-delta", text: "Test response" }
}
mockStreamText.mockReturnValueOnce({
fullStream: mockFullStream(),
usage: Promise.resolve({
inputTokens: 100,
outputTokens: 50,
inputTokenDetails: { cacheReadTokens: 25 },
}),
providerMetadata: Promise.resolve(undefined),
})
const stream = handler.createMessage(systemPrompt, messages)
const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}
const usageChunks = chunks.filter((chunk) => chunk.type === "usage")
expect(usageChunks).toHaveLength(1)
expect(usageChunks[0].cacheReadTokens).toBe(25)
})
it("should prefer providerMetadata.openai.cachedPromptTokens over v6 top-level", async () => {
async function* mockFullStream() {
yield { type: "text-delta", text: "Test response" }
}
mockStreamText.mockReturnValueOnce({
fullStream: mockFullStream(),
usage: Promise.resolve({
inputTokens: 100,
outputTokens: 50,
cachedInputTokens: 30,
}),
providerMetadata: Promise.resolve({
openai: {
cachedPromptTokens: 80,
},
}),
})
const stream = handler.createMessage(systemPrompt, messages)
const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}
const usageChunks = chunks.filter((chunk) => chunk.type === "usage")
expect(usageChunks).toHaveLength(1)
expect(usageChunks[0].cacheReadTokens).toBe(80)
})
it("should prefer v6 top-level cachedInputTokens over legacy details when providerMetadata is empty", async () => {
async function* mockFullStream() {
yield { type: "text-delta", text: "Test response" }
}
mockStreamText.mockReturnValueOnce({
fullStream: mockFullStream(),
usage: Promise.resolve({
inputTokens: 100,
outputTokens: 50,
cachedInputTokens: 30,
details: { cachedInputTokens: 20 },
}),
providerMetadata: Promise.resolve(undefined),
})
const stream = handler.createMessage(systemPrompt, messages)
const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}
const usageChunks = chunks.filter((chunk) => chunk.type === "usage")
expect(usageChunks).toHaveLength(1)
expect(usageChunks[0].cacheReadTokens).toBe(30)
})
})
describe("reasoning tokens", () => {
it("should read reasoning tokens from v6 top-level reasoningTokens when providerMetadata is empty", async () => {
async function* mockFullStream() {
yield { type: "text-delta", text: "Test response" }
}
mockStreamText.mockReturnValueOnce({
fullStream: mockFullStream(),
usage: Promise.resolve({
inputTokens: 100,
outputTokens: 50,
reasoningTokens: 40,
}),
providerMetadata: Promise.resolve(undefined),
})
const stream = handler.createMessage(systemPrompt, messages)
const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}
const usageChunks = chunks.filter((chunk) => chunk.type === "usage")
expect(usageChunks).toHaveLength(1)
expect(usageChunks[0].reasoningTokens).toBe(40)
})
it("should read reasoning tokens from v6 outputTokenDetails.reasoningTokens when providerMetadata is empty", async () => {
async function* mockFullStream() {
yield { type: "text-delta", text: "Test response" }
}
mockStreamText.mockReturnValueOnce({
fullStream: mockFullStream(),
usage: Promise.resolve({
inputTokens: 100,
outputTokens: 50,
outputTokenDetails: { reasoningTokens: 35 },
}),
providerMetadata: Promise.resolve(undefined),
})
const stream = handler.createMessage(systemPrompt, messages)
const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}
const usageChunks = chunks.filter((chunk) => chunk.type === "usage")
expect(usageChunks).toHaveLength(1)
expect(usageChunks[0].reasoningTokens).toBe(35)
})
it("should prefer providerMetadata.openai.reasoningTokens over v6 top-level", async () => {
async function* mockFullStream() {
yield { type: "text-delta", text: "Test response" }
}
mockStreamText.mockReturnValueOnce({
fullStream: mockFullStream(),
usage: Promise.resolve({
inputTokens: 100,
outputTokens: 50,
reasoningTokens: 40,
}),
providerMetadata: Promise.resolve({
openai: {
reasoningTokens: 20,
},
}),
})
const stream = handler.createMessage(systemPrompt, messages)
const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}
const usageChunks = chunks.filter((chunk) => chunk.type === "usage")
expect(usageChunks).toHaveLength(1)
expect(usageChunks[0].reasoningTokens).toBe(20)
})
})
})
})
})

View file

@ -178,6 +178,8 @@ describe("RequestyHandler", () => {
cacheReadTokens: 2,
reasoningTokens: undefined,
totalCost: expect.any(Number),
totalInputTokens: 10,
totalOutputTokens: 20,
})
})

View file

@ -105,7 +105,12 @@ function createMockStreamResult(options?: {
toolCallParts?: Array<{ type: string; id?: string; toolName?: string; delta?: string }>
inputTokens?: number
outputTokens?: number
providerMetadata?: Record<string, any>
providerMetadata?: Record<string, unknown>
usage?: {
cachedInputTokens?: number
inputTokenDetails?: { cacheReadTokens?: number; cacheWriteTokens?: number }
details?: { cachedInputTokens?: number }
}
}) {
const {
textChunks = ["Test response"],
@ -114,6 +119,7 @@ function createMockStreamResult(options?: {
inputTokens = 10,
outputTokens = 5,
providerMetadata = undefined,
usage = undefined,
} = options ?? {}
const fullStream = (async function* () {
@ -130,7 +136,7 @@ function createMockStreamResult(options?: {
return {
fullStream,
usage: Promise.resolve({ inputTokens, outputTokens }),
usage: Promise.resolve({ inputTokens, outputTokens, ...usage }),
providerMetadata: Promise.resolve(providerMetadata),
}
}
@ -767,6 +773,65 @@ describe("RooHandler", () => {
expect(usageChunk).toBeDefined()
expect(usageChunk.cacheWriteTokens).toBe(20)
expect(usageChunk.cacheReadTokens).toBe(30)
expect(usageChunk.totalInputTokens).toBe(100)
})
it("should fall back to anthropic metadata when roo metadata is missing", async () => {
mockStreamText.mockReturnValue(
createMockStreamResult({
inputTokens: 120,
outputTokens: 40,
providerMetadata: {
anthropic: {
cacheCreationInputTokens: 25,
usage: {
cache_read_input_tokens: 35,
},
},
},
}),
)
const stream = handler.createMessage(systemPrompt, messages)
const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}
const usageChunk = chunks.find((c) => c.type === "usage")
expect(usageChunk).toBeDefined()
expect(usageChunk.inputTokens).toBe(120)
expect(usageChunk.cacheWriteTokens).toBe(25)
expect(usageChunk.cacheReadTokens).toBe(35)
expect(usageChunk.totalInputTokens).toBe(120)
})
it("should fall back to AI SDK usage cache fields when provider metadata is missing", async () => {
mockStreamText.mockReturnValue(
createMockStreamResult({
inputTokens: 140,
outputTokens: 30,
usage: {
cachedInputTokens: 22,
inputTokenDetails: {
cacheWriteTokens: 11,
},
},
}),
)
const stream = handler.createMessage(systemPrompt, messages)
const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}
const usageChunk = chunks.find((c) => c.type === "usage")
expect(usageChunk).toBeDefined()
expect(usageChunk.inputTokens).toBe(140)
expect(usageChunk.cacheWriteTokens).toBe(11)
expect(usageChunk.cacheReadTokens).toBe(22)
expect(usageChunk.totalInputTokens).toBe(140)
})
})

View file

@ -191,6 +191,8 @@ describe("VercelAiGatewayHandler", () => {
cacheWriteTokens: 2,
cacheReadTokens: 3,
totalCost: 0.005,
totalInputTokens: 10,
totalOutputTokens: 5,
})
})
@ -281,6 +283,8 @@ describe("VercelAiGatewayHandler", () => {
cacheWriteTokens: 2,
cacheReadTokens: 3,
totalCost: 0.005,
totalInputTokens: 10,
totalOutputTokens: 5,
})
})

View file

@ -121,6 +121,22 @@ describe("ZAiHandler", () => {
expect(model.info.preserveReasoning).toBe(true)
})
it("should return GLM-5 international model with thinking support", () => {
const testModelId: InternationalZAiModelId = "glm-5"
const handlerWithModel = new ZAiHandler({
apiModelId: testModelId,
zaiApiKey: "test-zai-api-key",
zaiApiLine: "international_coding",
})
const model = handlerWithModel.getModel()
expect(model.id).toBe(testModelId)
expect(model.info).toEqual(internationalZAiModels[testModelId])
expect(model.info.contextWindow).toBe(202_752)
expect(model.info.supportsReasoningEffort).toEqual(["disable", "medium"])
expect(model.info.reasoningEffort).toBe("medium")
expect(model.info.preserveReasoning).toBe(true)
})
it("should return GLM-4.5v international model with vision support", () => {
const testModelId: InternationalZAiModelId = "glm-4.5v"
const handlerWithModel = new ZAiHandler({
@ -203,6 +219,22 @@ describe("ZAiHandler", () => {
expect(model.info.reasoningEffort).toBe("medium")
expect(model.info.preserveReasoning).toBe(true)
})
it("should return GLM-5 China model with thinking support", () => {
const testModelId: MainlandZAiModelId = "glm-5"
const handlerWithModel = new ZAiHandler({
apiModelId: testModelId,
zaiApiKey: "test-zai-api-key",
zaiApiLine: "china_coding",
})
const model = handlerWithModel.getModel()
expect(model.id).toBe(testModelId)
expect(model.info).toEqual(mainlandZAiModels[testModelId])
expect(model.info.contextWindow).toBe(202_752)
expect(model.info.supportsReasoningEffort).toEqual(["disable", "medium"])
expect(model.info.reasoningEffort).toBe("medium")
expect(model.info.preserveReasoning).toBe(true)
})
})
describe("International API", () => {
@ -508,6 +540,74 @@ describe("ZAiHandler", () => {
})
})
describe("GLM-5 Thinking Mode", () => {
it("should enable thinking by default for GLM-5 (default reasoningEffort is medium)", async () => {
const handlerWithModel = new ZAiHandler({
apiModelId: "glm-5",
zaiApiKey: "test-zai-api-key",
zaiApiLine: "international_coding",
})
async function* mockFullStream() {
yield { type: "text-delta", text: "response" }
}
mockStreamText.mockReturnValue({
fullStream: mockFullStream(),
usage: Promise.resolve({ inputTokens: 0, outputTokens: 0 }),
})
const stream = handlerWithModel.createMessage("system prompt", [])
for await (const _chunk of stream) {
// drain
}
expect(mockStreamText).toHaveBeenCalledWith(
expect.objectContaining({
providerOptions: {
zhipu: {
thinking: { type: "enabled" },
},
},
}),
)
})
it("should disable thinking for GLM-5 when reasoningEffort is set to disable", async () => {
const handlerWithModel = new ZAiHandler({
apiModelId: "glm-5",
zaiApiKey: "test-zai-api-key",
zaiApiLine: "international_coding",
enableReasoningEffort: true,
reasoningEffort: "disable",
})
async function* mockFullStream() {
yield { type: "text-delta", text: "response" }
}
mockStreamText.mockReturnValue({
fullStream: mockFullStream(),
usage: Promise.resolve({ inputTokens: 0, outputTokens: 0 }),
})
const stream = handlerWithModel.createMessage("system prompt", [])
for await (const _chunk of stream) {
// drain
}
expect(mockStreamText).toHaveBeenCalledWith(
expect.objectContaining({
providerOptions: {
zhipu: {
thinking: { type: "disabled" },
},
},
}),
)
})
})
describe("completePrompt", () => {
it("should complete a prompt using generateText", async () => {
mockGenerateText.mockResolvedValue({

View file

@ -26,6 +26,7 @@ import {
handleAiSdkError,
yieldResponseMessage,
} from "../transform/ai-sdk"
import { applyToolCacheOptions, applySystemPromptCaching } from "../transform/cache-breakpoints"
import { calculateApiCostAnthropic } from "../../shared/cost"
import { DEFAULT_HEADERS } from "./constants"
@ -96,6 +97,7 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple
// Convert tools to AI SDK format
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
applyToolCacheOptions(aiSdkTools as Parameters<typeof applyToolCacheOptions>[0], metadata?.toolProviderOptions)
// Build Anthropic provider options
const anthropicProviderOptions: Record<string, unknown> = {}
@ -119,45 +121,18 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple
anthropicProviderOptions.disableParallelToolUse = true
}
/**
* 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 cacheProviderOption = { anthropic: { cacheControl: { type: "ephemeral" as const } } }
const userMsgIndices = messages.reduce(
(acc, msg, index) => ("role" in msg && msg.role === "user" ? [...acc, index] : acc),
[] as number[],
// Breakpoint 1: System prompt caching — inject as cached system message
const effectiveSystemPrompt = applySystemPromptCaching(
systemPrompt,
aiSdkMessages,
metadata?.systemProviderOptions,
)
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 as ModelMessage[], 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>),
system: effectiveSystemPrompt,
messages: aiSdkMessages,
temperature: modelConfig.temperature,
maxOutputTokens: modelConfig.maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,
@ -216,12 +191,19 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple
const inputTokens = usage.inputTokens ?? 0
const outputTokens = usage.outputTokens ?? 0
// Extract cache metrics from Anthropic's providerMetadata
// Extract cache metrics from Anthropic's providerMetadata.
// In @ai-sdk/anthropic v3.0.38+, cacheReadInputTokens may only exist at
// usage.cache_read_input_tokens rather than the top-level property.
const anthropicMeta = providerMetadata?.anthropic as
| { cacheCreationInputTokens?: number; cacheReadInputTokens?: number }
| {
cacheCreationInputTokens?: number
cacheReadInputTokens?: number
usage?: { cache_read_input_tokens?: number }
}
| undefined
const cacheWriteTokens = anthropicMeta?.cacheCreationInputTokens ?? 0
const cacheReadTokens = anthropicMeta?.cacheReadInputTokens ?? 0
const cacheReadTokens =
anthropicMeta?.cacheReadInputTokens ?? anthropicMeta?.usage?.cache_read_input_tokens ?? 0
const { totalCost } = calculateApiCostAnthropic(
info,
@ -238,29 +220,9 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple
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(
aiSdkMessages: { role: string; providerOptions?: Record<string, Record<string, unknown>> }[],
targetIndices: Set<number>,
cacheProviderOption: Record<string, Record<string, unknown>>,
): void {
for (const idx of targetIndices) {
if (idx >= 0 && idx < aiSdkMessages.length) {
aiSdkMessages[idx].providerOptions = {
...aiSdkMessages[idx].providerOptions,
...cacheProviderOption,
}
}
// Anthropic: inputTokens is non-cached only; total = input + cache write + cache read
totalInputTokens: inputTokens + (cacheWriteTokens ?? 0) + (cacheReadTokens ?? 0),
totalOutputTokens: outputTokens,
}
}

View file

@ -24,6 +24,7 @@ import {
handleAiSdkError,
yieldResponseMessage,
} from "../transform/ai-sdk"
import { applyToolCacheOptions, applySystemPromptCaching } from "../transform/cache-breakpoints"
import { calculateApiCostAnthropic } from "../../shared/cost"
import { DEFAULT_HEADERS } from "./constants"
@ -82,6 +83,7 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
// Convert tools to AI SDK format
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
applyToolCacheOptions(aiSdkTools as Parameters<typeof applyToolCacheOptions>[0], metadata?.toolProviderOptions)
// Build Anthropic provider options
const anthropicProviderOptions: Record<string, unknown> = {}
@ -105,34 +107,20 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
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) => ("role" in msg && msg.role === "user" ? [...acc, index] : acc),
[] as number[],
// Breakpoint 1: System prompt caching — inject as cached system message
// AI SDK v6 does not support providerOptions on the system string parameter,
// so cache-aware providers convert it to a system message with providerOptions.
const effectiveSystemPrompt = applySystemPromptCaching(
systemPrompt,
aiSdkMessages,
metadata?.systemProviderOptions,
)
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 as ModelMessage[], 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>),
system: effectiveSystemPrompt,
messages: aiSdkMessages,
temperature: modelConfig.temperature,
maxOutputTokens: modelConfig.maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,
@ -191,12 +179,19 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
const inputTokens = usage.inputTokens ?? 0
const outputTokens = usage.outputTokens ?? 0
// Extract cache metrics from Anthropic's providerMetadata
// Extract cache metrics from Anthropic's providerMetadata.
// In @ai-sdk/anthropic v3.0.38+, cacheReadInputTokens may only exist at
// usage.cache_read_input_tokens rather than the top-level property.
const anthropicMeta = providerMetadata?.anthropic as
| { cacheCreationInputTokens?: number; cacheReadInputTokens?: number }
| {
cacheCreationInputTokens?: number
cacheReadInputTokens?: number
usage?: { cache_read_input_tokens?: number }
}
| undefined
const cacheWriteTokens = anthropicMeta?.cacheCreationInputTokens ?? 0
const cacheReadTokens = anthropicMeta?.cacheReadInputTokens ?? 0
const cacheReadTokens =
anthropicMeta?.cacheReadInputTokens ?? anthropicMeta?.usage?.cache_read_input_tokens ?? 0
const { totalCost } = calculateApiCostAnthropic(
info,
@ -213,29 +208,9 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
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(
aiSdkMessages: { role: string; providerOptions?: Record<string, Record<string, unknown>> }[],
targetIndices: Set<number>,
cacheProviderOption: Record<string, Record<string, unknown>>,
): void {
for (const idx of targetIndices) {
if (idx >= 0 && idx < aiSdkMessages.length) {
aiSdkMessages[idx].providerOptions = {
...aiSdkMessages[idx].providerOptions,
...cacheProviderOption,
}
}
// Anthropic: inputTokens is non-cached only; total = input + cache write + cache read
totalInputTokens: inputTokens + (cacheWriteTokens ?? 0) + (cacheReadTokens ?? 0),
totalOutputTokens: outputTokens,
}
}

View file

@ -13,6 +13,7 @@ import {
mapToolChoice,
handleAiSdkError,
} from "../transform/ai-sdk"
import { applyToolCacheOptions } from "../transform/cache-breakpoints"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import { getModelParams } from "../transform/model-params"
@ -88,6 +89,12 @@ export class AzureHandler extends BaseProvider implements SingleCompletionHandle
usage: {
inputTokens?: number
outputTokens?: number
totalInputTokens?: number
totalOutputTokens?: number
cachedInputTokens?: number
reasoningTokens?: number
inputTokenDetails?: { cacheReadTokens?: number; cacheWriteTokens?: number }
outputTokenDetails?: { reasoningTokens?: number }
details?: {
cachedInputTokens?: number
reasoningTokens?: number
@ -100,19 +107,28 @@ export class AzureHandler extends BaseProvider implements SingleCompletionHandle
}
},
): ApiStreamUsageChunk {
// Extract cache metrics from Azure's providerMetadata if available
const cacheReadTokens = providerMetadata?.azure?.promptCacheHitTokens ?? usage.details?.cachedInputTokens
// Extract cache metrics from Azure's providerMetadata, then v6 fields, then legacy
const cacheReadTokens =
providerMetadata?.azure?.promptCacheHitTokens ??
usage.cachedInputTokens ??
usage.inputTokenDetails?.cacheReadTokens ??
usage.details?.cachedInputTokens
// Azure uses OpenAI-compatible caching which does not report cache write tokens separately;
// promptCacheMissTokens represents tokens NOT found in cache (processed from scratch), not tokens written to cache.
const cacheWriteTokens = undefined
const cacheWriteTokens = usage.inputTokenDetails?.cacheWriteTokens
const inputTokens = usage.inputTokens || 0
const outputTokens = usage.outputTokens || 0
return {
type: "usage",
inputTokens: usage.inputTokens || 0,
outputTokens: usage.outputTokens || 0,
inputTokens,
outputTokens,
cacheReadTokens,
cacheWriteTokens,
reasoningTokens: usage.details?.reasoningTokens,
reasoningTokens:
usage.reasoningTokens ?? usage.outputTokenDetails?.reasoningTokens ?? usage.details?.reasoningTokens,
totalInputTokens: inputTokens,
totalOutputTokens: outputTokens,
}
}
@ -144,11 +160,12 @@ export class AzureHandler extends BaseProvider implements SingleCompletionHandle
// Convert tools to OpenAI format first, then to AI SDK format
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
applyToolCacheOptions(aiSdkTools as Parameters<typeof applyToolCacheOptions>[0], metadata?.toolProviderOptions)
// Build the request options
const requestOptions: Parameters<typeof streamText>[0] = {
model: languageModel,
system: systemPrompt,
system: systemPrompt || undefined,
messages: aiSdkMessages,
temperature: this.options.modelTemperature ?? temperature ?? AZURE_DEFAULT_TEMPERATURE,
maxOutputTokens: this.getMaxOutputTokens(),

View file

@ -13,6 +13,7 @@ import {
mapToolChoice,
handleAiSdkError,
} from "../transform/ai-sdk"
import { applyToolCacheOptions } from "../transform/cache-breakpoints"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import { getModelParams } from "../transform/model-params"
@ -69,16 +70,29 @@ export class BasetenHandler extends BaseProvider implements SingleCompletionHand
protected processUsageMetrics(usage: {
inputTokens?: number
outputTokens?: number
totalInputTokens?: number
totalOutputTokens?: number
cachedInputTokens?: number
reasoningTokens?: number
inputTokenDetails?: { cacheReadTokens?: number; cacheWriteTokens?: number }
outputTokenDetails?: { reasoningTokens?: number }
details?: {
cachedInputTokens?: number
reasoningTokens?: number
}
}): ApiStreamUsageChunk {
const inputTokens = usage.inputTokens || 0
const outputTokens = usage.outputTokens || 0
return {
type: "usage",
inputTokens: usage.inputTokens || 0,
outputTokens: usage.outputTokens || 0,
reasoningTokens: usage.details?.reasoningTokens,
inputTokens,
outputTokens,
cacheReadTokens:
usage.cachedInputTokens ?? usage.inputTokenDetails?.cacheReadTokens ?? usage.details?.cachedInputTokens,
reasoningTokens:
usage.reasoningTokens ?? usage.outputTokenDetails?.reasoningTokens ?? usage.details?.reasoningTokens,
totalInputTokens: inputTokens,
totalOutputTokens: outputTokens,
}
}
@ -105,10 +119,11 @@ export class BasetenHandler extends BaseProvider implements SingleCompletionHand
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
applyToolCacheOptions(aiSdkTools as Parameters<typeof applyToolCacheOptions>[0], metadata?.toolProviderOptions)
const requestOptions: Parameters<typeof streamText>[0] = {
model: languageModel,
system: systemPrompt,
system: systemPrompt || undefined,
messages: aiSdkMessages,
temperature: this.options.modelTemperature ?? temperature ?? BASETEN_DEFAULT_TEMPERATURE,
maxOutputTokens: this.getMaxOutputTokens(),

View file

@ -32,6 +32,7 @@ import {
handleAiSdkError,
yieldResponseMessage,
} from "../transform/ai-sdk"
import { applyToolCacheOptions, applySystemPromptCaching } from "../transform/cache-breakpoints"
import { getModelParams } from "../transform/model-params"
import { shouldUseReasoningBudget } from "../../shared/api"
import { BaseProvider } from "./base-provider"
@ -210,6 +211,7 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
// Convert tools to AI SDK format
let openAiTools = this.convertToolsForOpenAI(metadata?.tools)
const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
applyToolCacheOptions(aiSdkTools as Parameters<typeof applyToolCacheOptions>[0], metadata?.toolProviderOptions)
const toolChoice = mapToolChoice(metadata?.tool_choice)
// Build provider options for reasoning, betas, etc.
@ -251,65 +253,34 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
}
}
// Prompt caching: use AI SDK's cachePoint mechanism
// The AI SDK's @ai-sdk/amazon-bedrock supports cachePoint in providerOptions per message.
//
// Strategy: Bedrock allows up to 4 cache checkpoints. We use them as:
// 1. System prompt (via systemProviderOptions below)
// 2-4. Up to 3 user messages in the conversation history
//
// For the message cache points, we target the last 2 user messages (matching
// Anthropic's strategy: write-to-cache + read-from-cache) PLUS an earlier "anchor"
// user message near the middle of the conversation. This anchor ensures the 20-block
// lookback window has a stable cache entry to hit, covering all assistant/tool messages
// between the anchor and the recent messages.
//
// We identify targets in the ORIGINAL Anthropic messages (before AI SDK conversion)
// because convertToAiSdkMessages() splits user messages containing tool_results into
// separate "tool" + "user" role messages, which would skew naive counting.
// Prompt caching — only apply cache annotations when caching is enabled.
// This avoids the need to strip annotations after the fact, and keeps
// Bedrock decoupled from knowledge of what Task.ts stamps universally.
const usePromptCache = Boolean(this.options.awsUsePromptCache && this.supportsAwsPromptCache(modelConfig))
if (usePromptCache) {
const cachePointOption = { bedrock: { cachePoint: { type: "default" as const } } }
// Breakpoint 1: System prompt caching — only when Bedrock prompt cache is enabled
const effectiveSystemPrompt = usePromptCache
? applySystemPromptCaching(systemPrompt, aiSdkMessages, metadata?.systemProviderOptions)
: systemPrompt || undefined
// Find all user message indices in the original (pre-conversion) message array.
const originalUserIndices = filteredMessages.reduce<number[]>(
(acc, msg, idx) => ("role" in msg && msg.role === "user" ? [...acc, idx] : acc),
[],
)
// Select up to 3 user messages for cache points (system prompt uses the 4th):
// - Last user message: write to cache for next request
// - Second-to-last user message: read from cache for current request
// - An "anchor" message earlier in the conversation for 20-block window coverage
const targetOriginalIndices = new Set<number>()
const numUserMsgs = originalUserIndices.length
if (numUserMsgs >= 1) {
// Always cache the last user message
targetOriginalIndices.add(originalUserIndices[numUserMsgs - 1])
}
if (numUserMsgs >= 2) {
// Cache the second-to-last user message
targetOriginalIndices.add(originalUserIndices[numUserMsgs - 2])
}
if (numUserMsgs >= 5) {
// Add an anchor cache point roughly in the first third of user messages.
// This ensures that the 20-block lookback from the second-to-last breakpoint
// can find a stable cache entry, covering all the assistant and tool messages
// in the middle of the conversation. We pick the user message at ~1/3 position.
const anchorIdx = Math.floor(numUserMsgs / 3)
// Only add if it's not already one of the last-2 targets
if (!targetOriginalIndices.has(originalUserIndices[anchorIdx])) {
targetOriginalIndices.add(originalUserIndices[anchorIdx])
// Strip non-Bedrock cache annotations from messages when caching is disabled,
// and strip Bedrock-specific annotations when caching is disabled.
if (!usePromptCache) {
for (const msg of aiSdkMessages) {
if (msg.providerOptions?.bedrock) {
const { bedrock: _, ...rest } = msg.providerOptions
msg.providerOptions = Object.keys(rest).length > 0 ? rest : undefined
}
}
// Apply cachePoint to the correct AI SDK messages by walking both arrays in parallel.
// A single original user message with tool_results becomes [tool-role msg, user-role msg]
// in the AI SDK array, while a plain user message becomes [user-role msg].
if (targetOriginalIndices.size > 0) {
this.applyCachePointsToAiSdkMessages(aiSdkMessages, targetOriginalIndices, cachePointOption)
// Also strip cache annotations from tool definitions
if (aiSdkTools) {
for (const key of Object.keys(aiSdkTools)) {
const tool = aiSdkTools[key] as { providerOptions?: Record<string, Record<string, unknown>> }
if (tool.providerOptions?.bedrock) {
const { bedrock: _, ...rest } = tool.providerOptions
tool.providerOptions = Object.keys(rest).length > 0 ? rest : undefined
}
}
}
}
@ -317,10 +288,7 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
// 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,
...(usePromptCache && {
systemProviderOptions: { bedrock: { cachePoint: { type: "default" } } } as Record<string, unknown>,
}),
system: effectiveSystemPrompt,
messages: aiSdkMessages,
temperature: modelConfig.temperature ?? (this.options.modelTemperature as number),
maxOutputTokens: modelConfig.maxTokens || (modelConfig.info.maxTokens as number),
@ -383,7 +351,20 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
* Process usage metrics from the AI SDK response.
*/
private processUsageMetrics(
usage: { inputTokens?: number; outputTokens?: number },
usage: {
inputTokens?: number
outputTokens?: number
totalInputTokens?: number
totalOutputTokens?: number
cachedInputTokens?: number
reasoningTokens?: number
inputTokenDetails?: { cacheReadTokens?: number; cacheWriteTokens?: number }
outputTokenDetails?: { reasoningTokens?: number }
details?: {
cachedInputTokens?: number
reasoningTokens?: number
}
},
info: ModelInfo,
providerMetadata?: Record<string, Record<string, unknown>>,
): ApiStreamUsageChunk {
@ -392,8 +373,7 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
// The AI SDK exposes reasoningTokens as a top-level field on usage, and also
// under outputTokenDetails.reasoningTokens — there is no .details property.
const reasoningTokens =
(usage as any).reasoningTokens ?? (usage as any).outputTokenDetails?.reasoningTokens ?? 0
const reasoningTokens = usage.reasoningTokens ?? usage.outputTokenDetails?.reasoningTokens ?? 0
// Extract cache metrics primarily from usage (AI SDK standard locations),
// falling back to providerMetadata.bedrock.usage for provider-specific fields.
@ -401,12 +381,11 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
| { cacheReadInputTokens?: number; cacheWriteInputTokens?: number }
| undefined
const cacheReadTokens =
(usage as any).inputTokenDetails?.cacheReadTokens ??
(usage as any).cachedInputTokens ??
usage.cachedInputTokens ??
usage.inputTokenDetails?.cacheReadTokens ??
bedrockUsage?.cacheReadInputTokens ??
0
const cacheWriteTokens =
(usage as any).inputTokenDetails?.cacheWriteTokens ?? bedrockUsage?.cacheWriteInputTokens ?? 0
const cacheWriteTokens = usage.inputTokenDetails?.cacheWriteTokens ?? bedrockUsage?.cacheWriteInputTokens ?? 0
// For prompt routers, the AI SDK surfaces the invoked model ID in
// providerMetadata.bedrock.trace.promptRouter.invokedModelId.
@ -449,6 +428,9 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
reasoningTokens,
info: costInfo,
}),
// AI SDK normalizes inputTokens to total (OpenAI convention) for Bedrock
totalInputTokens: inputTokens,
totalOutputTokens: outputTokens,
}
}
@ -706,29 +688,6 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
)
}
/**
* Apply cachePoint 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 points land on the right message.
*/
private applyCachePointsToAiSdkMessages(
aiSdkMessages: { role: string; providerOptions?: Record<string, Record<string, unknown>> }[],
targetIndices: Set<number>,
cachePointOption: Record<string, Record<string, unknown>>,
): void {
for (const idx of targetIndices) {
if (idx >= 0 && idx < aiSdkMessages.length) {
aiSdkMessages[idx].providerOptions = {
...aiSdkMessages[idx].providerOptions,
...cachePointOption,
}
}
}
}
/************************************************************************************
*
* AMAZON REGIONS

View file

@ -13,6 +13,7 @@ import {
mapToolChoice,
handleAiSdkError,
} from "../transform/ai-sdk"
import { applyToolCacheOptions } from "../transform/cache-breakpoints"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import { getModelParams } from "../transform/model-params"
@ -70,6 +71,12 @@ export class DeepSeekHandler extends BaseProvider implements SingleCompletionHan
usage: {
inputTokens?: number
outputTokens?: number
totalInputTokens?: number
totalOutputTokens?: number
cachedInputTokens?: number
reasoningTokens?: number
inputTokenDetails?: { cacheReadTokens?: number; cacheWriteTokens?: number }
outputTokenDetails?: { reasoningTokens?: number }
details?: {
cachedInputTokens?: number
reasoningTokens?: number
@ -82,17 +89,27 @@ export class DeepSeekHandler extends BaseProvider implements SingleCompletionHan
}
},
): ApiStreamUsageChunk {
// Extract cache metrics from DeepSeek's providerMetadata
const cacheReadTokens = providerMetadata?.deepseek?.promptCacheHitTokens ?? usage.details?.cachedInputTokens
const cacheWriteTokens = providerMetadata?.deepseek?.promptCacheMissTokens
// Extract cache metrics from DeepSeek's providerMetadata, then v6 fields, then legacy
const cacheReadTokens =
providerMetadata?.deepseek?.promptCacheHitTokens ??
usage.cachedInputTokens ??
usage.inputTokenDetails?.cacheReadTokens ??
usage.details?.cachedInputTokens
const cacheWriteTokens =
providerMetadata?.deepseek?.promptCacheMissTokens ?? usage.inputTokenDetails?.cacheWriteTokens
const inputTokens = usage.inputTokens || 0
const outputTokens = usage.outputTokens || 0
return {
type: "usage",
inputTokens: usage.inputTokens || 0,
outputTokens: usage.outputTokens || 0,
inputTokens,
outputTokens,
cacheReadTokens,
cacheWriteTokens,
reasoningTokens: usage.details?.reasoningTokens,
reasoningTokens:
usage.reasoningTokens ?? usage.outputTokenDetails?.reasoningTokens ?? usage.details?.reasoningTokens,
totalInputTokens: inputTokens,
totalOutputTokens: outputTokens,
}
}
@ -122,11 +139,12 @@ export class DeepSeekHandler extends BaseProvider implements SingleCompletionHan
// Convert tools to OpenAI format first, then to AI SDK format
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
applyToolCacheOptions(aiSdkTools as Parameters<typeof applyToolCacheOptions>[0], metadata?.toolProviderOptions)
// Build the request options
const requestOptions: Parameters<typeof streamText>[0] = {
model: languageModel,
system: systemPrompt,
system: systemPrompt || undefined,
messages: aiSdkMessages,
temperature: this.options.modelTemperature ?? temperature ?? DEEP_SEEK_DEFAULT_TEMPERATURE,
maxOutputTokens: this.getMaxOutputTokens(),

View file

@ -13,6 +13,7 @@ import {
mapToolChoice,
handleAiSdkError,
} from "../transform/ai-sdk"
import { applyToolCacheOptions } from "../transform/cache-breakpoints"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import { getModelParams } from "../transform/model-params"
@ -71,6 +72,12 @@ export class FireworksHandler extends BaseProvider implements SingleCompletionHa
usage: {
inputTokens?: number
outputTokens?: number
totalInputTokens?: number
totalOutputTokens?: number
cachedInputTokens?: number
reasoningTokens?: number
inputTokenDetails?: { cacheReadTokens?: number; cacheWriteTokens?: number }
outputTokenDetails?: { reasoningTokens?: number }
details?: {
cachedInputTokens?: number
reasoningTokens?: number
@ -83,17 +90,27 @@ export class FireworksHandler extends BaseProvider implements SingleCompletionHa
}
},
): ApiStreamUsageChunk {
// Extract cache metrics from Fireworks' providerMetadata if available
const cacheReadTokens = providerMetadata?.fireworks?.promptCacheHitTokens ?? usage.details?.cachedInputTokens
const cacheWriteTokens = providerMetadata?.fireworks?.promptCacheMissTokens
// Extract cache metrics from Fireworks' providerMetadata, then v6 fields, then legacy
const cacheReadTokens =
providerMetadata?.fireworks?.promptCacheHitTokens ??
usage.cachedInputTokens ??
usage.inputTokenDetails?.cacheReadTokens ??
usage.details?.cachedInputTokens
const cacheWriteTokens =
providerMetadata?.fireworks?.promptCacheMissTokens ?? usage.inputTokenDetails?.cacheWriteTokens
const inputTokens = usage.inputTokens || 0
const outputTokens = usage.outputTokens || 0
return {
type: "usage",
inputTokens: usage.inputTokens || 0,
outputTokens: usage.outputTokens || 0,
inputTokens,
outputTokens,
cacheReadTokens,
cacheWriteTokens,
reasoningTokens: usage.details?.reasoningTokens,
reasoningTokens:
usage.reasoningTokens ?? usage.outputTokenDetails?.reasoningTokens ?? usage.details?.reasoningTokens,
totalInputTokens: inputTokens,
totalOutputTokens: outputTokens,
}
}
@ -122,11 +139,12 @@ export class FireworksHandler extends BaseProvider implements SingleCompletionHa
// Convert tools to OpenAI format first, then to AI SDK format
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
applyToolCacheOptions(aiSdkTools as Parameters<typeof applyToolCacheOptions>[0], metadata?.toolProviderOptions)
// Build the request options
const requestOptions: Parameters<typeof streamText>[0] = {
model: languageModel,
system: systemPrompt,
system: systemPrompt || undefined,
messages: aiSdkMessages,
temperature: this.options.modelTemperature ?? temperature ?? FIREWORKS_DEFAULT_TEMPERATURE,
maxOutputTokens: this.getMaxOutputTokens(),

View file

@ -21,6 +21,7 @@ import {
handleAiSdkError,
yieldResponseMessage,
} from "../transform/ai-sdk"
import { applyToolCacheOptions } from "../transform/cache-breakpoints"
import { t } from "i18next"
import type { ApiStream, ApiStreamUsageChunk, GroundingSource } from "../transform/stream"
import { getModelParams } from "../transform/model-params"
@ -103,6 +104,7 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
}
const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
applyToolCacheOptions(aiSdkTools as Parameters<typeof applyToolCacheOptions>[0], metadata?.toolProviderOptions)
// Build tool choice - use 'required' when allowedFunctionNames restricts available tools
const toolChoice =
@ -113,7 +115,7 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
// Build the request options
const requestOptions: Parameters<typeof streamText>[0] = {
model: this.provider(modelId),
system: systemInstruction,
system: systemInstruction || undefined,
messages: aiSdkMessages,
temperature: temperatureConfig,
maxOutputTokens,
@ -246,6 +248,12 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
usage: {
inputTokens?: number
outputTokens?: number
totalInputTokens?: number
totalOutputTokens?: number
cachedInputTokens?: number
reasoningTokens?: number
inputTokenDetails?: { cacheReadTokens?: number; cacheWriteTokens?: number }
outputTokenDetails?: { reasoningTokens?: number }
details?: {
cachedInputTokens?: number
reasoningTokens?: number
@ -256,8 +264,10 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
): ApiStreamUsageChunk {
const inputTokens = usage.inputTokens || 0
const outputTokens = usage.outputTokens || 0
const cacheReadTokens = usage.details?.cachedInputTokens
const reasoningTokens = usage.details?.reasoningTokens
const cacheReadTokens =
usage.cachedInputTokens ?? usage.inputTokenDetails?.cacheReadTokens ?? usage.details?.cachedInputTokens
const reasoningTokens =
usage.reasoningTokens ?? usage.outputTokenDetails?.reasoningTokens ?? usage.details?.reasoningTokens
return {
type: "usage",
@ -272,6 +282,9 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
cacheReadTokens,
reasoningTokens,
}),
// Gemini: inputTokens is already total
totalInputTokens: inputTokens,
totalOutputTokens: outputTokens,
}
}

View file

@ -96,18 +96,30 @@ export class LiteLLMHandler extends OpenAICompatibleHandler implements SingleCom
protected override processUsageMetrics(usage: {
inputTokens?: number
outputTokens?: number
totalInputTokens?: number
totalOutputTokens?: number
cachedInputTokens?: number
reasoningTokens?: number
inputTokenDetails?: { cacheReadTokens?: number; cacheWriteTokens?: number }
outputTokenDetails?: { reasoningTokens?: number }
details?: {
cachedInputTokens?: number
reasoningTokens?: number
}
raw?: Record<string, unknown>
}): ApiStreamUsageChunk {
const inputTokens = usage.inputTokens || 0
const outputTokens = usage.outputTokens || 0
return {
type: "usage",
inputTokens: usage.inputTokens || 0,
outputTokens: usage.outputTokens || 0,
cacheReadTokens: usage.details?.cachedInputTokens,
reasoningTokens: usage.details?.reasoningTokens,
inputTokens,
outputTokens,
cacheReadTokens:
usage.cachedInputTokens ?? usage.inputTokenDetails?.cacheReadTokens ?? usage.details?.cachedInputTokens,
reasoningTokens:
usage.reasoningTokens ?? usage.outputTokenDetails?.reasoningTokens ?? usage.details?.reasoningTokens,
totalInputTokens: inputTokens,
totalOutputTokens: outputTokens,
}
}
}

View file

@ -20,6 +20,7 @@ import {
mapToolChoice,
handleAiSdkError,
} from "../transform/ai-sdk"
import { applyToolCacheOptions } from "../transform/cache-breakpoints"
import { ApiStream } from "../transform/stream"
import { OpenAICompatibleHandler, OpenAICompatibleConfig } from "./openai-compatible"
@ -68,10 +69,11 @@ export class LmStudioHandler extends OpenAICompatibleHandler implements SingleCo
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
applyToolCacheOptions(aiSdkTools as Parameters<typeof applyToolCacheOptions>[0], metadata?.toolProviderOptions)
const requestOptions: Parameters<typeof streamText>[0] = {
model: languageModel,
system: systemPrompt,
system: systemPrompt || undefined,
messages: aiSdkMessages,
temperature: model.temperature ?? this.config.temperature ?? LMSTUDIO_DEFAULT_TEMPERATURE,
maxOutputTokens: this.getMaxOutputTokens(),

View file

@ -16,6 +16,7 @@ import {
handleAiSdkError,
yieldResponseMessage,
} from "../transform/ai-sdk"
import { applyToolCacheOptions } from "../transform/cache-breakpoints"
import { calculateApiCostAnthropic } from "../../shared/cost"
import { DEFAULT_HEADERS } from "./constants"
@ -75,6 +76,7 @@ export class MiniMaxHandler extends BaseProvider implements SingleCompletionHand
const aiSdkMessages = mergedMessages as ModelMessage[]
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
applyToolCacheOptions(aiSdkTools as Parameters<typeof applyToolCacheOptions>[0], metadata?.toolProviderOptions)
const anthropicProviderOptions: Record<string, unknown> = {}
@ -89,29 +91,9 @@ export class MiniMaxHandler extends BaseProvider implements SingleCompletionHand
anthropicProviderOptions.disableParallelToolUse = true
}
const cacheProviderOption = { anthropic: { cacheControl: { type: "ephemeral" as const } } }
const userMsgIndices = mergedMessages.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(aiSdkMessages, targetIndices, cacheProviderOption)
}
const requestOptions = {
model: this.client(modelConfig.id),
system: systemPrompt,
...({
systemProviderOptions: { anthropic: { cacheControl: { type: "ephemeral" } } },
} as Record<string, unknown>),
system: systemPrompt || undefined,
messages: aiSdkMessages,
temperature: modelParams.temperature,
maxOutputTokens: modelParams.maxTokens ?? modelConfig.info.maxTokens,
@ -184,21 +166,9 @@ export class MiniMaxHandler extends BaseProvider implements SingleCompletionHand
cacheWriteTokens: cacheWriteTokens > 0 ? cacheWriteTokens : undefined,
cacheReadTokens: cacheReadTokens > 0 ? cacheReadTokens : undefined,
totalCost,
}
}
private applyCacheControlToAiSdkMessages(
aiSdkMessages: { role: string; providerOptions?: Record<string, Record<string, unknown>> }[],
targetIndices: Set<number>,
cacheProviderOption: Record<string, Record<string, unknown>>,
): void {
for (const idx of targetIndices) {
if (idx >= 0 && idx < aiSdkMessages.length) {
aiSdkMessages[idx].providerOptions = {
...aiSdkMessages[idx].providerOptions,
...cacheProviderOption,
}
}
// MiniMax uses Anthropic SDK: inputTokens is non-cached only
totalInputTokens: inputTokens + (cacheWriteTokens ?? 0) + (cacheReadTokens ?? 0),
totalOutputTokens: outputTokens,
}
}

View file

@ -13,6 +13,7 @@ import {
import type { ApiHandlerOptions } from "../../shared/api"
import { convertToAiSdkMessages, convertToolsForAiSdk, consumeAiSdkStream, handleAiSdkError } from "../transform/ai-sdk"
import { applyToolCacheOptions } from "../transform/cache-breakpoints"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import { getModelParams } from "../transform/model-params"
@ -76,17 +77,29 @@ export class MistralHandler extends BaseProvider implements SingleCompletionHand
protected processUsageMetrics(usage: {
inputTokens?: number
outputTokens?: number
totalInputTokens?: number
totalOutputTokens?: number
cachedInputTokens?: number
reasoningTokens?: number
inputTokenDetails?: { cacheReadTokens?: number; cacheWriteTokens?: number }
outputTokenDetails?: { reasoningTokens?: number }
details?: {
cachedInputTokens?: number
reasoningTokens?: number
}
}): ApiStreamUsageChunk {
const inputTokens = usage.inputTokens || 0
const outputTokens = usage.outputTokens || 0
return {
type: "usage",
inputTokens: usage.inputTokens || 0,
outputTokens: usage.outputTokens || 0,
cacheReadTokens: usage.details?.cachedInputTokens,
reasoningTokens: usage.details?.reasoningTokens,
inputTokens,
outputTokens,
cacheReadTokens:
usage.cachedInputTokens ?? usage.inputTokenDetails?.cacheReadTokens ?? usage.details?.cachedInputTokens,
reasoningTokens:
usage.reasoningTokens ?? usage.outputTokenDetails?.reasoningTokens ?? usage.details?.reasoningTokens,
totalInputTokens: inputTokens,
totalOutputTokens: outputTokens,
}
}
@ -149,12 +162,13 @@ export class MistralHandler extends BaseProvider implements SingleCompletionHand
// Convert tools to OpenAI format first, then to AI SDK format
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
applyToolCacheOptions(aiSdkTools as Parameters<typeof applyToolCacheOptions>[0], metadata?.toolProviderOptions)
// Build the request options
// Use MISTRAL_DEFAULT_TEMPERATURE (1) as fallback to match original behavior
const requestOptions: Parameters<typeof streamText>[0] = {
model: languageModel,
system: systemPrompt,
system: systemPrompt || undefined,
messages: aiSdkMessages,
temperature: this.options.modelTemperature ?? MISTRAL_DEFAULT_TEMPERATURE,
maxOutputTokens: this.getMaxOutputTokens(),

View file

@ -46,6 +46,12 @@ export class MoonshotHandler extends OpenAICompatibleHandler {
protected override processUsageMetrics(usage: {
inputTokens?: number
outputTokens?: number
totalInputTokens?: number
totalOutputTokens?: number
cachedInputTokens?: number
reasoningTokens?: number
inputTokenDetails?: { cacheReadTokens?: number; cacheWriteTokens?: number }
outputTokenDetails?: { reasoningTokens?: number }
details?: {
cachedInputTokens?: number
reasoningTokens?: number
@ -55,12 +61,20 @@ export class MoonshotHandler extends OpenAICompatibleHandler {
// Moonshot uses cached_tokens at the top level of raw usage data
const rawUsage = usage.raw as { cached_tokens?: number } | undefined
const inputTokens = usage.inputTokens || 0
const outputTokens = usage.outputTokens || 0
return {
type: "usage",
inputTokens: usage.inputTokens || 0,
outputTokens: usage.outputTokens || 0,
cacheWriteTokens: 0,
cacheReadTokens: rawUsage?.cached_tokens ?? usage.details?.cachedInputTokens,
inputTokens,
outputTokens,
cacheWriteTokens: usage.inputTokenDetails?.cacheWriteTokens ?? 0,
cacheReadTokens:
rawUsage?.cached_tokens ??
usage.cachedInputTokens ??
usage.inputTokenDetails?.cacheReadTokens ??
usage.details?.cachedInputTokens,
totalInputTokens: inputTokens,
totalOutputTokens: outputTokens,
}
}

View file

@ -14,6 +14,7 @@ import {
handleAiSdkError,
yieldResponseMessage,
} from "../transform/ai-sdk"
import { applyToolCacheOptions } from "../transform/cache-breakpoints"
import { ApiStream } from "../transform/stream"
import { BaseProvider } from "./base-provider"
@ -99,12 +100,13 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
applyToolCacheOptions(aiSdkTools as Parameters<typeof applyToolCacheOptions>[0], metadata?.toolProviderOptions)
const providerOptions = this.buildProviderOptions(useR1Format)
const requestOptions: Parameters<typeof streamText>[0] = {
model: languageModel,
system: systemPrompt,
system: systemPrompt || undefined,
messages: aiSdkMessages,
temperature,
tools: aiSdkTools,
@ -123,10 +125,14 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio
const usage = await result.usage
if (usage) {
const inputTokens = usage.inputTokens || 0
const outputTokens = usage.outputTokens || 0
yield {
type: "usage",
inputTokens: usage.inputTokens || 0,
outputTokens: usage.outputTokens || 0,
inputTokens,
outputTokens,
totalInputTokens: inputTokens,
totalOutputTokens: outputTokens,
}
}

View file

@ -252,14 +252,27 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
if (usage) {
const inputTokens = usage.inputTokens || 0
const outputTokens = usage.outputTokens || 0
const details = (usage as any).details as
| { cachedInputTokens?: number; reasoningTokens?: number }
| undefined
const cacheReadTokens = details?.cachedInputTokens ?? 0
const typedUsage = usage as {
inputTokens?: number
outputTokens?: number
cachedInputTokens?: number
reasoningTokens?: number
inputTokenDetails?: { cacheReadTokens?: number; cacheWriteTokens?: number }
outputTokenDetails?: { reasoningTokens?: number }
details?: { cachedInputTokens?: number; reasoningTokens?: number }
}
const cacheReadTokens =
typedUsage.cachedInputTokens ??
typedUsage.inputTokenDetails?.cacheReadTokens ??
typedUsage.details?.cachedInputTokens ??
0
// The OpenAI Responses API does not report cache write tokens separately;
// only cached (read) tokens are available via usage.details.cachedInputTokens.
const cacheWriteTokens = 0
const reasoningTokens = details?.reasoningTokens
const cacheWriteTokens = typedUsage.inputTokenDetails?.cacheWriteTokens ?? 0
const reasoningTokens =
typedUsage.reasoningTokens ??
typedUsage.outputTokenDetails?.reasoningTokens ??
typedUsage.details?.reasoningTokens
yield {
type: "usage",
@ -269,6 +282,8 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
cacheReadTokens: cacheReadTokens || undefined,
...(typeof reasoningTokens === "number" ? { reasoningTokens } : {}),
totalCost: 0, // Subscription-based pricing
totalInputTokens: inputTokens,
totalOutputTokens: outputTokens,
}
}
} catch (usageError) {

View file

@ -20,6 +20,7 @@ import {
handleAiSdkError,
} from "../transform/ai-sdk"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import { applyToolCacheOptions } from "../transform/cache-breakpoints"
import { DEFAULT_HEADERS } from "./constants"
import { BaseProvider } from "./base-provider"
@ -95,18 +96,40 @@ export abstract class OpenAICompatibleHandler extends BaseProvider implements Si
protected processUsageMetrics(usage: {
inputTokens?: number
outputTokens?: number
totalInputTokens?: number
totalOutputTokens?: number
cachedInputTokens?: number
reasoningTokens?: number
inputTokenDetails?: {
cacheReadTokens?: number
cacheWriteTokens?: number
noCacheTokens?: number
}
outputTokenDetails?: {
reasoningTokens?: number
}
details?: {
cachedInputTokens?: number
reasoningTokens?: number
}
raw?: Record<string, unknown>
}): ApiStreamUsageChunk {
const inputTokens = usage.inputTokens || 0
const outputTokens = usage.outputTokens || 0
return {
type: "usage",
inputTokens: usage.inputTokens || 0,
outputTokens: usage.outputTokens || 0,
cacheReadTokens: usage.details?.cachedInputTokens,
reasoningTokens: usage.details?.reasoningTokens,
inputTokens,
outputTokens,
// P1: AI SDK v6 top-level
// P2: AI SDK v6 structured (LanguageModelInputTokenDetails)
// P3: Legacy AI SDK standard (usage.details)
cacheReadTokens:
usage.cachedInputTokens ?? usage.inputTokenDetails?.cacheReadTokens ?? usage.details?.cachedInputTokens,
cacheWriteTokens: usage.inputTokenDetails?.cacheWriteTokens,
reasoningTokens:
usage.reasoningTokens ?? usage.outputTokenDetails?.reasoningTokens ?? usage.details?.reasoningTokens,
totalInputTokens: inputTokens,
totalOutputTokens: outputTokens,
}
}
@ -137,11 +160,12 @@ export abstract class OpenAICompatibleHandler extends BaseProvider implements Si
// Convert tools to OpenAI format first, then to AI SDK format
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
applyToolCacheOptions(aiSdkTools as Parameters<typeof applyToolCacheOptions>[0], metadata?.toolProviderOptions)
// Build the request options
const requestOptions: Parameters<typeof streamText>[0] = {
model: languageModel,
system: systemPrompt,
system: systemPrompt || undefined,
messages: aiSdkMessages,
temperature: model.temperature ?? this.config.temperature ?? 0,
maxOutputTokens: this.getMaxOutputTokens(),

View file

@ -26,6 +26,7 @@ import {
mapToolChoice,
handleAiSdkError,
} from "../transform/ai-sdk"
import { applyToolCacheOptions } from "../transform/cache-breakpoints"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import { getModelParams } from "../transform/model-params"
@ -344,6 +345,12 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
usage: {
inputTokens?: number
outputTokens?: number
totalInputTokens?: number
totalOutputTokens?: number
cachedInputTokens?: number
reasoningTokens?: number
inputTokenDetails?: { cacheReadTokens?: number; cacheWriteTokens?: number }
outputTokenDetails?: { reasoningTokens?: number }
details?: {
cachedInputTokens?: number
reasoningTokens?: number
@ -355,11 +362,13 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
const inputTokens = usage.inputTokens || 0
const outputTokens = usage.outputTokens || 0
const cacheReadTokens = usage.details?.cachedInputTokens ?? 0
const cacheReadTokens =
usage.cachedInputTokens ?? usage.inputTokenDetails?.cacheReadTokens ?? usage.details?.cachedInputTokens ?? 0
// The OpenAI Responses API does not report cache write tokens separately;
// only cached (read) tokens are available via usage.details.cachedInputTokens.
const cacheWriteTokens = 0
const reasoningTokens = usage.details?.reasoningTokens
const cacheWriteTokens = usage.inputTokenDetails?.cacheWriteTokens ?? 0
const reasoningTokens =
usage.reasoningTokens ?? usage.outputTokenDetails?.reasoningTokens ?? usage.details?.reasoningTokens
const effectiveTier =
this.lastServiceTier || (this.options.openAiNativeServiceTier as ServiceTier | undefined) || undefined
@ -381,6 +390,9 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
cacheReadTokens: cacheReadTokens || undefined,
...(typeof reasoningTokens === "number" ? { reasoningTokens } : {}),
totalCost,
// OpenAI: inputTokens is already total
totalInputTokens: inputTokens,
totalOutputTokens: outputTokens,
}
}
@ -434,6 +446,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
applyToolCacheOptions(aiSdkTools as Parameters<typeof applyToolCacheOptions>[0], metadata?.toolProviderOptions)
const taskId = metadata?.taskId
const userAgent = `roo-code/${Package.version} (${os.platform()} ${os.release()}; ${os.arch()}) node/${process.version.slice(1)}`

View file

@ -24,6 +24,7 @@ import {
handleAiSdkError,
yieldResponseMessage,
} from "../transform/ai-sdk"
import { applyToolCacheOptions } from "../transform/cache-breakpoints"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import { getModelParams } from "../transform/model-params"
@ -110,6 +111,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
applyToolCacheOptions(aiSdkTools as Parameters<typeof applyToolCacheOptions>[0], metadata?.toolProviderOptions)
let effectiveSystemPrompt: string | undefined = systemPrompt
let effectiveTemperature: number | undefined =
@ -141,7 +143,9 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
if (deepseekReasoner) {
effectiveSystemPrompt = undefined
aiSdkMessages.unshift({ role: "user", content: systemPrompt })
if (systemPrompt) {
aiSdkMessages.unshift({ role: "user", content: systemPrompt })
}
}
if (this.options.openAiStreamingEnabled ?? true) {
@ -181,7 +185,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
): ApiStream {
const result = streamText({
model: languageModel,
system: systemPrompt,
system: systemPrompt || undefined,
messages,
temperature,
maxOutputTokens: this.getMaxOutputTokens(),
@ -253,7 +257,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
try {
const { text, toolCalls, usage, providerMetadata } = await generateText({
model: languageModel,
system: systemPrompt,
system: systemPrompt || undefined,
messages,
temperature,
maxOutputTokens: this.getMaxOutputTokens(),
@ -290,6 +294,12 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
usage: {
inputTokens?: number
outputTokens?: number
totalInputTokens?: number
totalOutputTokens?: number
cachedInputTokens?: number
reasoningTokens?: number
inputTokenDetails?: { cacheReadTokens?: number; cacheWriteTokens?: number }
outputTokenDetails?: { reasoningTokens?: number }
details?: {
cachedInputTokens?: number
reasoningTokens?: number
@ -304,16 +314,28 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
},
): ApiStreamUsageChunk {
// Extract cache and reasoning metrics from OpenAI's providerMetadata when available,
// falling back to usage.details for standard AI SDK fields.
const cacheReadTokens = providerMetadata?.openai?.cachedPromptTokens ?? usage.details?.cachedInputTokens
const reasoningTokens = providerMetadata?.openai?.reasoningTokens ?? usage.details?.reasoningTokens
// then v6 fields, then legacy usage.details.
const cacheReadTokens =
providerMetadata?.openai?.cachedPromptTokens ??
usage.cachedInputTokens ??
usage.inputTokenDetails?.cacheReadTokens ??
usage.details?.cachedInputTokens
const reasoningTokens =
providerMetadata?.openai?.reasoningTokens ??
usage.reasoningTokens ??
usage.outputTokenDetails?.reasoningTokens ??
usage.details?.reasoningTokens
const inputTokens = usage.inputTokens || 0
const outputTokens = usage.outputTokens || 0
return {
type: "usage",
inputTokens: usage.inputTokens || 0,
outputTokens: usage.outputTokens || 0,
inputTokens,
outputTokens,
cacheReadTokens,
reasoningTokens,
totalInputTokens: inputTokens,
totalOutputTokens: outputTokens,
}
}

View file

@ -23,6 +23,7 @@ import {
processAiSdkStreamPart,
yieldResponseMessage,
} from "../transform/ai-sdk"
import { applyToolCacheOptions, applySystemPromptCaching } from "../transform/cache-breakpoints"
import { BaseProvider } from "./base-provider"
import { getModels, getModelsFromCache } from "./fetchers/modelCache"
@ -125,6 +126,9 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
...(cacheReadTokens > 0 ? { cacheReadTokens } : {}),
...(typeof reasoningTokens === "number" && reasoningTokens > 0 ? { reasoningTokens } : {}),
totalCost,
// OpenRouter uses OpenAI convention: inputTokens is already total
totalInputTokens: inputTokens,
totalOutputTokens: outputTokens,
}
}
@ -153,6 +157,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
const openrouter = this.createOpenRouterProvider({ reasoning, headers })
const tools = convertToolsForAiSdk(metadata?.tools)
applyToolCacheOptions(tools as Parameters<typeof applyToolCacheOptions>[0], metadata?.toolProviderOptions)
const providerOptions:
| {
@ -174,10 +179,18 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
}
: undefined
// Breakpoint 1: System prompt caching — inject as cached system message
// OpenRouter routes to Anthropic models that benefit from cache annotations
const effectiveSystemPrompt = applySystemPromptCaching(
systemPrompt,
aiSdkMessages,
metadata?.systemProviderOptions,
)
try {
const result = streamText({
model: openrouter.chat(modelId),
system: systemPrompt,
system: effectiveSystemPrompt,
messages: aiSdkMessages,
maxOutputTokens: maxTokens && maxTokens > 0 ? maxTokens : undefined,
temperature,

View file

@ -14,6 +14,7 @@ import {
mapToolChoice,
handleAiSdkError,
} from "../transform/ai-sdk"
import { applyToolCacheOptions, applySystemPromptCaching } from "../transform/cache-breakpoints"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import { getModelParams } from "../transform/model-params"
@ -140,6 +141,12 @@ export class RequestyHandler extends BaseProvider implements SingleCompletionHan
usage: {
inputTokens?: number
outputTokens?: number
totalInputTokens?: number
totalOutputTokens?: number
cachedInputTokens?: number
reasoningTokens?: number
inputTokenDetails?: { cacheReadTokens?: number; cacheWriteTokens?: number }
outputTokenDetails?: { reasoningTokens?: number }
details?: {
cachedInputTokens?: number
reasoningTokens?: number
@ -150,8 +157,14 @@ export class RequestyHandler extends BaseProvider implements SingleCompletionHan
): ApiStreamUsageChunk {
const inputTokens = usage.inputTokens || 0
const outputTokens = usage.outputTokens || 0
const cacheWriteTokens = providerMetadata?.requesty?.usage?.cachingTokens ?? 0
const cacheReadTokens = providerMetadata?.requesty?.usage?.cachedTokens ?? usage.details?.cachedInputTokens ?? 0
const cacheWriteTokens =
providerMetadata?.requesty?.usage?.cachingTokens ?? usage.inputTokenDetails?.cacheWriteTokens ?? 0
const cacheReadTokens =
providerMetadata?.requesty?.usage?.cachedTokens ??
usage.cachedInputTokens ??
usage.inputTokenDetails?.cacheReadTokens ??
usage.details?.cachedInputTokens ??
0
const { totalCost } = modelInfo
? calculateApiCostOpenAI(modelInfo, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens)
@ -163,8 +176,11 @@ export class RequestyHandler extends BaseProvider implements SingleCompletionHan
outputTokens,
cacheWriteTokens,
cacheReadTokens,
reasoningTokens: usage.details?.reasoningTokens,
reasoningTokens:
usage.reasoningTokens ?? usage.outputTokenDetails?.reasoningTokens ?? usage.details?.reasoningTokens,
totalCost,
totalInputTokens: inputTokens,
totalOutputTokens: outputTokens,
}
}
@ -183,12 +199,21 @@ export class RequestyHandler extends BaseProvider implements SingleCompletionHan
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
applyToolCacheOptions(aiSdkTools as Parameters<typeof applyToolCacheOptions>[0], metadata?.toolProviderOptions)
const requestyOptions = this.getRequestyProviderOptions(metadata)
// Breakpoint 1: System prompt caching — inject as cached system message
// Requesty routes to Anthropic models that benefit from cache annotations
const effectiveSystemPrompt = applySystemPromptCaching(
systemPrompt,
aiSdkMessages,
metadata?.systemProviderOptions,
)
const requestOptions: Parameters<typeof streamText>[0] = {
model: languageModel,
system: systemPrompt,
system: effectiveSystemPrompt,
messages: aiSdkMessages,
temperature: this.options.modelTemperature ?? temperature ?? 0,
maxOutputTokens: this.getMaxOutputTokens(),

View file

@ -17,6 +17,7 @@ import {
mapToolChoice,
yieldResponseMessage,
} from "../transform/ai-sdk"
import { applyToolCacheOptions } from "../transform/cache-breakpoints"
import type { RooReasoningParams } from "../transform/reasoning"
import { getRooReasoning } from "../transform/reasoning"
@ -27,6 +28,40 @@ import { generateImageWithProvider, generateImageWithImagesApi, ImageGenerationR
import { t } from "../../i18n"
import type { RooMessage } from "../../core/task-persistence/rooMessage"
type RooProviderMetadata = {
cost?: number
cache_creation_input_tokens?: number
cache_read_input_tokens?: number
cached_tokens?: number
}
type AnthropicProviderMetadata = {
cacheCreationInputTokens?: number
cacheReadInputTokens?: number
usage?: {
cache_read_input_tokens?: number
}
}
type GatewayProviderMetadata = {
cost?: number
cache_creation_input_tokens?: number
cached_tokens?: number
}
type UsageWithCache = {
inputTokens?: number
outputTokens?: number
cachedInputTokens?: number
inputTokenDetails?: {
cacheReadTokens?: number
cacheWriteTokens?: number
}
details?: {
cachedInputTokens?: number
}
}
function getSessionToken(): string {
const token = CloudService.hasInstance() ? CloudService.instance.authService?.getSessionToken() : undefined
return token ?? "unauthenticated"
@ -93,6 +128,8 @@ export class RooHandler extends BaseProvider implements SingleCompletionHandler
messages: RooMessage[],
metadata?: ApiHandlerCreateMessageMetadata,
): ApiStream {
const firstNumber = (...values: Array<number | undefined>) => values.find((value) => typeof value === "number")
const model = this.getModel()
const { id: modelId, info } = model
@ -122,13 +159,14 @@ export class RooHandler extends BaseProvider implements SingleCompletionHandler
// RooMessage[] is already AI SDK-compatible, cast directly
const aiSdkMessages = messages as ModelMessage[]
const tools = convertToolsForAiSdk(this.convertToolsForOpenAI(metadata?.tools))
applyToolCacheOptions(tools as Parameters<typeof applyToolCacheOptions>[0], metadata?.toolProviderOptions)
let lastStreamError: string | undefined
try {
const result = streamText({
model: provider(modelId),
system: systemPrompt,
system: systemPrompt || undefined,
messages: aiSdkMessages,
maxOutputTokens: maxTokens && maxTokens > 0 ? maxTokens : undefined,
temperature,
@ -146,18 +184,40 @@ export class RooHandler extends BaseProvider implements SingleCompletionHandler
}
// Check provider metadata for usage details
const providerMetadata =
(await result.providerMetadata) ?? (await (result as any).experimental_providerMetadata)
const rooMeta = providerMetadata?.roo as Record<string, any> | undefined
const providerMetadata = (await result.providerMetadata) ?? undefined
const experimentalProviderMetadata = await (
result as { experimental_providerMetadata?: Promise<Record<string, unknown> | undefined> }
).experimental_providerMetadata
const metadataWithFallback = providerMetadata ?? experimentalProviderMetadata
const rooMeta = metadataWithFallback?.roo as RooProviderMetadata | undefined
const anthropicMeta = metadataWithFallback?.anthropic as AnthropicProviderMetadata | undefined
const gatewayMeta = metadataWithFallback?.gateway as GatewayProviderMetadata | undefined
// Process usage with protocol-aware normalization
const usage = await result.usage
const usage = (await result.usage) as UsageWithCache
const promptTokens = usage.inputTokens ?? 0
const completionTokens = usage.outputTokens ?? 0
// Extract cache tokens from provider metadata
const cacheCreation = (rooMeta?.cache_creation_input_tokens as number) ?? 0
const cacheRead = (rooMeta?.cache_read_input_tokens as number) ?? (rooMeta?.cached_tokens as number) ?? 0
// Extract cache tokens with priority chain (no double counting):
// Roo metadata -> Anthropic metadata -> Gateway metadata -> AI SDK usage -> legacy usage.details -> 0
const cacheCreation =
firstNumber(
rooMeta?.cache_creation_input_tokens,
anthropicMeta?.cacheCreationInputTokens,
gatewayMeta?.cache_creation_input_tokens,
usage.inputTokenDetails?.cacheWriteTokens,
) ?? 0
const cacheRead =
firstNumber(
rooMeta?.cache_read_input_tokens,
rooMeta?.cached_tokens,
anthropicMeta?.cacheReadInputTokens,
anthropicMeta?.usage?.cache_read_input_tokens,
gatewayMeta?.cached_tokens,
usage.cachedInputTokens,
usage.inputTokenDetails?.cacheReadTokens,
usage.details?.cachedInputTokens,
) ?? 0
// Protocol-aware token normalization:
// - OpenAI protocol expects TOTAL input tokens (cached + non-cached)
@ -168,7 +228,7 @@ export class RooHandler extends BaseProvider implements SingleCompletionHandler
// Cost: prefer server-side cost, fall back to client-side calculation
const isFreeModel = info.isFree === true
const serverCost = rooMeta?.cost as number | undefined
const serverCost = firstNumber(rooMeta?.cost, gatewayMeta?.cost)
const { totalCost: calculatedCost } = calculateApiCostOpenAI(
info,
promptTokens,
@ -185,6 +245,9 @@ export class RooHandler extends BaseProvider implements SingleCompletionHandler
cacheWriteTokens: cacheCreation,
cacheReadTokens: cacheRead,
totalCost,
// Roo: promptTokens is always the server-reported total regardless of protocol normalization
totalInputTokens: promptTokens,
totalOutputTokens: completionTokens,
}
yield* yieldResponseMessage(result)

View file

@ -14,6 +14,7 @@ import {
handleAiSdkError,
flattenAiSdkMessagesToStringContent,
} from "../transform/ai-sdk"
import { applyToolCacheOptions } from "../transform/cache-breakpoints"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import { getModelParams } from "../transform/model-params"
@ -72,6 +73,12 @@ export class SambaNovaHandler extends BaseProvider implements SingleCompletionHa
usage: {
inputTokens?: number
outputTokens?: number
totalInputTokens?: number
totalOutputTokens?: number
cachedInputTokens?: number
reasoningTokens?: number
inputTokenDetails?: { cacheReadTokens?: number; cacheWriteTokens?: number }
outputTokenDetails?: { reasoningTokens?: number }
details?: {
cachedInputTokens?: number
reasoningTokens?: number
@ -84,17 +91,27 @@ export class SambaNovaHandler extends BaseProvider implements SingleCompletionHa
}
},
): ApiStreamUsageChunk {
// Extract cache metrics from SambaNova's providerMetadata if available
const cacheReadTokens = providerMetadata?.sambanova?.promptCacheHitTokens ?? usage.details?.cachedInputTokens
const cacheWriteTokens = providerMetadata?.sambanova?.promptCacheMissTokens
// Extract cache metrics from SambaNova's providerMetadata, then v6 fields, then legacy
const cacheReadTokens =
providerMetadata?.sambanova?.promptCacheHitTokens ??
usage.cachedInputTokens ??
usage.inputTokenDetails?.cacheReadTokens ??
usage.details?.cachedInputTokens
const cacheWriteTokens =
providerMetadata?.sambanova?.promptCacheMissTokens ?? usage.inputTokenDetails?.cacheWriteTokens
const inputTokens = usage.inputTokens || 0
const outputTokens = usage.outputTokens || 0
return {
type: "usage",
inputTokens: usage.inputTokens || 0,
outputTokens: usage.outputTokens || 0,
inputTokens,
outputTokens,
cacheReadTokens,
cacheWriteTokens,
reasoningTokens: usage.details?.reasoningTokens,
reasoningTokens:
usage.reasoningTokens ?? usage.outputTokenDetails?.reasoningTokens ?? usage.details?.reasoningTokens,
totalInputTokens: inputTokens,
totalOutputTokens: outputTokens,
}
}
@ -125,11 +142,12 @@ export class SambaNovaHandler extends BaseProvider implements SingleCompletionHa
// Convert tools to OpenAI format first, then to AI SDK format
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
applyToolCacheOptions(aiSdkTools as Parameters<typeof applyToolCacheOptions>[0], metadata?.toolProviderOptions)
// Build the request options
const requestOptions: Parameters<typeof streamText>[0] = {
model: languageModel,
system: systemPrompt,
system: systemPrompt || undefined,
messages: aiSdkMessages,
temperature: this.options.modelTemperature ?? temperature ?? SAMBANOVA_DEFAULT_TEMPERATURE,
maxOutputTokens: this.getMaxOutputTokens(),

View file

@ -19,6 +19,7 @@ import {
handleAiSdkError,
yieldResponseMessage,
} from "../transform/ai-sdk"
import { applyToolCacheOptions } from "../transform/cache-breakpoints"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import { DEFAULT_HEADERS } from "./constants"
@ -85,6 +86,12 @@ export class VercelAiGatewayHandler extends BaseProvider implements SingleComple
usage: {
inputTokens?: number
outputTokens?: number
totalInputTokens?: number
totalOutputTokens?: number
cachedInputTokens?: number
reasoningTokens?: number
inputTokenDetails?: { cacheReadTokens?: number; cacheWriteTokens?: number }
outputTokenDetails?: { reasoningTokens?: number }
details?: {
cachedInputTokens?: number
reasoningTokens?: number
@ -94,17 +101,29 @@ export class VercelAiGatewayHandler extends BaseProvider implements SingleComple
): ApiStreamUsageChunk {
const gatewayMeta = providerMetadata?.gateway as Record<string, unknown> | undefined
const cacheWriteTokens = (gatewayMeta?.cache_creation_input_tokens as number) ?? undefined
const cacheReadTokens = usage.details?.cachedInputTokens ?? (gatewayMeta?.cached_tokens as number) ?? undefined
const cacheWriteTokens =
(gatewayMeta?.cache_creation_input_tokens as number) ??
usage.inputTokenDetails?.cacheWriteTokens ??
undefined
const cacheReadTokens =
usage.cachedInputTokens ??
usage.inputTokenDetails?.cacheReadTokens ??
usage.details?.cachedInputTokens ??
(gatewayMeta?.cached_tokens as number) ??
undefined
const totalCost = (gatewayMeta?.cost as number) ?? 0
const inputTokens = usage.inputTokens || 0
const outputTokens = usage.outputTokens || 0
return {
type: "usage",
inputTokens: usage.inputTokens || 0,
outputTokens: usage.outputTokens || 0,
inputTokens,
outputTokens,
cacheWriteTokens,
cacheReadTokens,
totalCost,
totalInputTokens: inputTokens,
totalOutputTokens: outputTokens,
}
}
@ -120,6 +139,7 @@ export class VercelAiGatewayHandler extends BaseProvider implements SingleComple
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
applyToolCacheOptions(aiSdkTools as Parameters<typeof applyToolCacheOptions>[0], metadata?.toolProviderOptions)
const temperature = this.supportsTemperature(modelId)
? (this.options.modelTemperature ?? VERCEL_AI_GATEWAY_DEFAULT_TEMPERATURE)
@ -127,7 +147,7 @@ export class VercelAiGatewayHandler extends BaseProvider implements SingleComple
const result = streamText({
model: languageModel,
system: systemPrompt,
system: systemPrompt || undefined,
messages: aiSdkMessages,
temperature,
maxOutputTokens: info.maxTokens ?? undefined,

View file

@ -21,6 +21,7 @@ import {
handleAiSdkError,
yieldResponseMessage,
} from "../transform/ai-sdk"
import { applyToolCacheOptions } from "../transform/cache-breakpoints"
import { t } from "i18next"
import type { ApiStream, ApiStreamUsageChunk, GroundingSource } from "../transform/stream"
import { getModelParams } from "../transform/model-params"
@ -117,6 +118,7 @@ export class VertexHandler extends BaseProvider implements SingleCompletionHandl
}
const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
applyToolCacheOptions(aiSdkTools as Parameters<typeof applyToolCacheOptions>[0], metadata?.toolProviderOptions)
// Build tool choice - use 'required' when allowedFunctionNames restricts available tools
const toolChoice =
@ -127,7 +129,7 @@ export class VertexHandler extends BaseProvider implements SingleCompletionHandl
// Build the request options
const requestOptions: Parameters<typeof streamText>[0] = {
model: this.provider(modelId),
system: systemInstruction,
system: systemInstruction || undefined,
messages: aiSdkMessages,
temperature: temperatureConfig,
maxOutputTokens,
@ -227,6 +229,12 @@ export class VertexHandler extends BaseProvider implements SingleCompletionHandl
usage: {
inputTokens?: number
outputTokens?: number
totalInputTokens?: number
totalOutputTokens?: number
cachedInputTokens?: number
reasoningTokens?: number
inputTokenDetails?: { cacheReadTokens?: number; cacheWriteTokens?: number }
outputTokenDetails?: { reasoningTokens?: number }
details?: {
cachedInputTokens?: number
reasoningTokens?: number
@ -237,8 +245,10 @@ export class VertexHandler extends BaseProvider implements SingleCompletionHandl
): ApiStreamUsageChunk {
const inputTokens = usage.inputTokens || 0
const outputTokens = usage.outputTokens || 0
const cacheReadTokens = usage.details?.cachedInputTokens
const reasoningTokens = usage.details?.reasoningTokens
const cacheReadTokens =
usage.cachedInputTokens ?? usage.inputTokenDetails?.cacheReadTokens ?? usage.details?.cachedInputTokens
const reasoningTokens =
usage.reasoningTokens ?? usage.outputTokenDetails?.reasoningTokens ?? usage.details?.reasoningTokens
return {
type: "usage",
@ -253,6 +263,9 @@ export class VertexHandler extends BaseProvider implements SingleCompletionHandl
cacheReadTokens,
reasoningTokens,
}),
// Vertex: inputTokens is already total
totalInputTokens: inputTokens,
totalOutputTokens: outputTokens,
}
}

View file

@ -475,6 +475,8 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan
type: "usage",
inputTokens: totalInputTokens,
outputTokens: totalOutputTokens,
totalInputTokens,
totalOutputTokens,
}
} catch (error: unknown) {
this.ensureCleanState()

View file

@ -13,6 +13,7 @@ import {
mapToolChoice,
handleAiSdkError,
} from "../transform/ai-sdk"
import { applyToolCacheOptions } from "../transform/cache-breakpoints"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import { getModelParams } from "../transform/model-params"
@ -81,6 +82,12 @@ export class XAIHandler extends BaseProvider implements SingleCompletionHandler
usage: {
inputTokens?: number
outputTokens?: number
totalInputTokens?: number
totalOutputTokens?: number
cachedInputTokens?: number
reasoningTokens?: number
inputTokenDetails?: { cacheReadTokens?: number; cacheWriteTokens?: number }
outputTokenDetails?: { reasoningTokens?: number }
details?: {
cachedInputTokens?: number
reasoningTokens?: number
@ -92,17 +99,25 @@ export class XAIHandler extends BaseProvider implements SingleCompletionHandler
}
},
): ApiStreamUsageChunk {
// Extract cache metrics from xAI's providerMetadata if available
// xAI supports prompt caching through prompt_tokens_details.cached_tokens
const cacheReadTokens = providerMetadata?.xai?.cachedPromptTokens ?? usage.details?.cachedInputTokens
// Extract cache metrics from xAI's providerMetadata, then v6 fields, then legacy
const cacheReadTokens =
providerMetadata?.xai?.cachedPromptTokens ??
usage.cachedInputTokens ??
usage.inputTokenDetails?.cacheReadTokens ??
usage.details?.cachedInputTokens
const inputTokens = usage.inputTokens || 0
const outputTokens = usage.outputTokens || 0
return {
type: "usage",
inputTokens: usage.inputTokens || 0,
outputTokens: usage.outputTokens || 0,
inputTokens,
outputTokens,
cacheReadTokens,
cacheWriteTokens: undefined, // xAI doesn't report cache write tokens separately
reasoningTokens: usage.details?.reasoningTokens,
cacheWriteTokens: usage.inputTokenDetails?.cacheWriteTokens, // xAI doesn't typically report cache write tokens
reasoningTokens:
usage.reasoningTokens ?? usage.outputTokenDetails?.reasoningTokens ?? usage.details?.reasoningTokens,
totalInputTokens: inputTokens,
totalOutputTokens: outputTokens,
}
}
@ -131,11 +146,12 @@ export class XAIHandler extends BaseProvider implements SingleCompletionHandler
// Convert tools to OpenAI format first, then to AI SDK format
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
applyToolCacheOptions(aiSdkTools as Parameters<typeof applyToolCacheOptions>[0], metadata?.toolProviderOptions)
// Build the request options
const requestOptions: Parameters<typeof streamText>[0] = {
model: languageModel,
system: systemPrompt,
system: systemPrompt || undefined,
messages: aiSdkMessages,
temperature: this.options.modelTemperature ?? temperature ?? XAI_DEFAULT_TEMPERATURE,
maxOutputTokens: this.getMaxOutputTokens(),

View file

@ -21,6 +21,7 @@ import {
mapToolChoice,
handleAiSdkError,
} from "../transform/ai-sdk"
import { applyToolCacheOptions } from "../transform/cache-breakpoints"
import { ApiStream } from "../transform/stream"
import { getModelParams } from "../transform/model-params"
@ -102,10 +103,11 @@ export class ZAiHandler extends BaseProvider implements SingleCompletionHandler
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
applyToolCacheOptions(aiSdkTools as Parameters<typeof applyToolCacheOptions>[0], metadata?.toolProviderOptions)
const requestOptions: Parameters<typeof streamText>[0] = {
model: languageModel,
system: systemPrompt,
system: systemPrompt || undefined,
messages: aiSdkMessages,
temperature: this.options.modelTemperature ?? temperature ?? ZAI_DEFAULT_TEMPERATURE,
maxOutputTokens: this.getMaxOutputTokens(),
@ -113,8 +115,8 @@ export class ZAiHandler extends BaseProvider implements SingleCompletionHandler
toolChoice: mapToolChoice(metadata?.tool_choice),
}
// GLM-4.7 thinking mode: pass thinking parameter via providerOptions
const isThinkingModel = modelId === "glm-4.7" && Array.isArray(info.supportsReasoningEffort)
// Thinking mode: pass thinking parameter via providerOptions for models that support it (e.g. GLM-4.7, GLM-5)
const isThinkingModel = Array.isArray(info.supportsReasoningEffort)
if (isThinkingModel) {
const useReasoning = shouldUseReasoningEffort({ model: info, settings: this.options })

View file

@ -0,0 +1,274 @@
import {
applyCacheBreakpoints,
applyToolCacheOptions,
applySystemPromptCaching,
UNIVERSAL_CACHE_OPTIONS,
} from "../cache-breakpoints"
type TestMessage = { role: string; providerOptions?: Record<string, Record<string, unknown>> }
describe("UNIVERSAL_CACHE_OPTIONS", () => {
it("includes anthropic namespace with ephemeral cacheControl", () => {
expect(UNIVERSAL_CACHE_OPTIONS.anthropic).toEqual({ cacheControl: { type: "ephemeral" } })
})
it("includes bedrock namespace with default cachePoint", () => {
expect(UNIVERSAL_CACHE_OPTIONS.bedrock).toEqual({ cachePoint: { type: "default" } })
})
})
describe("applyCacheBreakpoints", () => {
it("is a no-op for empty message array", () => {
const messages: TestMessage[] = []
applyCacheBreakpoints(messages)
expect(messages).toEqual([])
})
it("is a no-op when all messages are assistant or system", () => {
const messages: TestMessage[] = [{ role: "system" }, { role: "assistant" }, { role: "assistant" }]
applyCacheBreakpoints(messages)
expect(messages[0].providerOptions).toBeUndefined()
expect(messages[1].providerOptions).toBeUndefined()
expect(messages[2].providerOptions).toBeUndefined()
})
it("places 1 breakpoint on a single user message", () => {
const messages: TestMessage[] = [{ role: "user" }]
applyCacheBreakpoints(messages)
expect(messages[0].providerOptions).toEqual(UNIVERSAL_CACHE_OPTIONS)
})
it("places 1 breakpoint on a single tool message", () => {
const messages: TestMessage[] = [{ role: "tool" }]
applyCacheBreakpoints(messages)
expect(messages[0].providerOptions).toEqual(UNIVERSAL_CACHE_OPTIONS)
})
it("places 2 breakpoints on 2 user messages", () => {
const messages: TestMessage[] = [{ role: "user" }, { role: "user" }]
applyCacheBreakpoints(messages)
expect(messages[0].providerOptions).toEqual(UNIVERSAL_CACHE_OPTIONS)
expect(messages[1].providerOptions).toEqual(UNIVERSAL_CACHE_OPTIONS)
})
it("places 2 breakpoints on 2 tool messages", () => {
const messages: TestMessage[] = [{ role: "tool" }, { role: "tool" }]
applyCacheBreakpoints(messages)
expect(messages[0].providerOptions).toEqual(UNIVERSAL_CACHE_OPTIONS)
expect(messages[1].providerOptions).toEqual(UNIVERSAL_CACHE_OPTIONS)
})
it("targets last 2 non-assistant messages in a mixed conversation", () => {
const messages: TestMessage[] = [
{ role: "user" },
{ role: "assistant" },
{ role: "user" },
{ role: "assistant" },
{ role: "tool" },
]
applyCacheBreakpoints(messages)
// Last 2 non-assistant: index 2 (user) and index 4 (tool)
expect(messages[0].providerOptions).toBeUndefined()
expect(messages[1].providerOptions).toBeUndefined()
expect(messages[2].providerOptions).toEqual(UNIVERSAL_CACHE_OPTIONS)
expect(messages[3].providerOptions).toBeUndefined()
expect(messages[4].providerOptions).toEqual(UNIVERSAL_CACHE_OPTIONS)
})
it("targets indices 3 and 5 in [user, assistant, tool, user, assistant, tool]", () => {
const messages: TestMessage[] = [
{ role: "user" },
{ role: "assistant" },
{ role: "tool" },
{ role: "user" },
{ role: "assistant" },
{ role: "tool" },
]
applyCacheBreakpoints(messages)
expect(messages[0].providerOptions).toBeUndefined()
expect(messages[1].providerOptions).toBeUndefined()
expect(messages[2].providerOptions).toBeUndefined()
expect(messages[3].providerOptions).toEqual(UNIVERSAL_CACHE_OPTIONS)
expect(messages[4].providerOptions).toBeUndefined()
expect(messages[5].providerOptions).toEqual(UNIVERSAL_CACHE_OPTIONS)
})
it("never targets system messages", () => {
const messages: TestMessage[] = [{ role: "system" }, { role: "user" }, { role: "assistant" }, { role: "user" }]
applyCacheBreakpoints(messages)
expect(messages[0].providerOptions).toBeUndefined()
expect(messages[1].providerOptions).toEqual(UNIVERSAL_CACHE_OPTIONS)
expect(messages[3].providerOptions).toEqual(UNIVERSAL_CACHE_OPTIONS)
})
it("never targets assistant messages", () => {
const messages: TestMessage[] = [
{ role: "user" },
{ role: "assistant" },
{ role: "assistant" },
{ role: "user" },
]
applyCacheBreakpoints(messages)
expect(messages[1].providerOptions).toBeUndefined()
expect(messages[2].providerOptions).toBeUndefined()
})
it("preserves existing providerOptions via spread", () => {
const messages: TestMessage[] = [
{
role: "user",
providerOptions: {
openai: { customField: "keep-me" },
},
},
]
applyCacheBreakpoints(messages)
expect(messages[0].providerOptions).toEqual({
openai: { customField: "keep-me" },
...UNIVERSAL_CACHE_OPTIONS,
})
})
it("adds anchor breakpoint at ~1/3 with useAnchor and enough messages", () => {
// 6 non-assistant messages (indices 0-5 in nonAssistantIndices)
// Anchor at floor(6/3) = index 2 in nonAssistantIndices -> messages index 4
// Last 2: indices 10 and 8
const messages: TestMessage[] = [
{ role: "user" }, // 0 - nonAssistant[0]
{ role: "assistant" }, // 1
{ role: "user" }, // 2 - nonAssistant[1]
{ role: "assistant" }, // 3
{ role: "user" }, // 4 - nonAssistant[2] <- anchor (floor(6/3)=2)
{ role: "assistant" }, // 5
{ role: "user" }, // 6 - nonAssistant[3]
{ role: "assistant" }, // 7
{ role: "user" }, // 8 - nonAssistant[4] <- last 2
{ role: "assistant" }, // 9
{ role: "user" }, // 10 - nonAssistant[5] <- last 2
]
applyCacheBreakpoints(messages, { useAnchor: true })
// Should have 3 breakpoints: indices 4, 8, 10
expect(messages[4].providerOptions).toEqual(UNIVERSAL_CACHE_OPTIONS)
expect(messages[8].providerOptions).toEqual(UNIVERSAL_CACHE_OPTIONS)
expect(messages[10].providerOptions).toEqual(UNIVERSAL_CACHE_OPTIONS)
// Others should NOT have breakpoints
expect(messages[0].providerOptions).toBeUndefined()
expect(messages[2].providerOptions).toBeUndefined()
expect(messages[6].providerOptions).toBeUndefined()
})
it("does not add anchor when below anchorThreshold", () => {
const messages: TestMessage[] = [
{ role: "user" },
{ role: "assistant" },
{ role: "user" },
{ role: "assistant" },
{ role: "user" },
]
// 3 non-assistant messages, below default threshold of 5
applyCacheBreakpoints(messages, { useAnchor: true })
// Last 2 only: indices 2 and 4
expect(messages[0].providerOptions).toBeUndefined()
expect(messages[2].providerOptions).toEqual(UNIVERSAL_CACHE_OPTIONS)
expect(messages[4].providerOptions).toEqual(UNIVERSAL_CACHE_OPTIONS)
})
it("universal options include both anthropic and bedrock namespaces", () => {
const messages: TestMessage[] = [{ role: "user" }]
applyCacheBreakpoints(messages)
expect(messages[0].providerOptions).toHaveProperty("anthropic")
expect(messages[0].providerOptions).toHaveProperty("bedrock")
})
})
describe("applyToolCacheOptions", () => {
it("should apply cache options only to the last tool to conserve breakpoints", () => {
const tools: Record<
string,
{ providerOptions?: Record<string, Record<string, unknown>>; [key: string]: unknown }
> = {
tool1: { description: "test", parameters: {} },
tool2: { description: "test2", parameters: {}, providerOptions: { existing: { key: "value" } } },
}
const cacheOptions = { anthropic: { cacheControl: { type: "ephemeral" } } }
applyToolCacheOptions(tools, cacheOptions)
// Only the last tool (tool2) should receive cache options
expect(tools.tool1.providerOptions).toBeUndefined()
expect(tools.tool2.providerOptions).toEqual({
existing: { key: "value" },
anthropic: { cacheControl: { type: "ephemeral" } },
})
})
it("should handle undefined tools", () => {
expect(() =>
applyToolCacheOptions(undefined, { anthropic: { cacheControl: { type: "ephemeral" } } }),
).not.toThrow()
})
it("should handle undefined cacheOptions", () => {
const tools: Record<
string,
{ providerOptions?: Record<string, Record<string, unknown>>; [key: string]: unknown }
> = {
tool1: { description: "test", parameters: {} },
}
applyToolCacheOptions(tools, undefined)
expect(tools.tool1.providerOptions).toBeUndefined()
})
it("should handle empty tools object", () => {
const tools: Record<
string,
{ providerOptions?: Record<string, Record<string, unknown>>; [key: string]: unknown }
> = {}
applyToolCacheOptions(tools, { anthropic: { cacheControl: { type: "ephemeral" } } })
expect(tools).toEqual({})
})
})
describe("applySystemPromptCaching", () => {
it("injects system prompt as cached system message and returns undefined", () => {
const messages: TestMessage[] = [{ role: "user" }]
const result = applySystemPromptCaching("You are helpful", messages, UNIVERSAL_CACHE_OPTIONS)
expect(result).toBeUndefined()
expect(messages).toHaveLength(2)
expect(messages[0]).toEqual({
role: "system",
content: "You are helpful",
providerOptions: UNIVERSAL_CACHE_OPTIONS,
})
})
it("returns undefined (no system prompt) when systemPrompt is empty string", () => {
const messages: TestMessage[] = [{ role: "user" }]
const result = applySystemPromptCaching("", messages, UNIVERSAL_CACHE_OPTIONS)
expect(result).toBeUndefined()
expect(messages).toHaveLength(1) // no message injected
})
it("returns undefined when systemPrompt is undefined", () => {
const messages: TestMessage[] = [{ role: "user" }]
const result = applySystemPromptCaching(undefined, messages, UNIVERSAL_CACHE_OPTIONS)
expect(result).toBeUndefined()
expect(messages).toHaveLength(1) // no message injected
})
it("returns systemPrompt unchanged when cacheOptions is undefined", () => {
const messages: TestMessage[] = [{ role: "user" }]
const result = applySystemPromptCaching("You are helpful", messages, undefined)
expect(result).toBe("You are helpful")
expect(messages).toHaveLength(1) // no message injected
})
it("prepends system message before existing messages", () => {
const messages: TestMessage[] = [{ role: "user" }, { role: "assistant" }, { role: "user" }]
applySystemPromptCaching("System prompt", messages, UNIVERSAL_CACHE_OPTIONS)
expect(messages).toHaveLength(4)
expect(messages[0].role).toBe("system")
expect(messages[1].role).toBe("user")
})
})

View file

@ -0,0 +1,126 @@
/**
* Universal cache breakpoint options contains ALL provider namespaces.
* AI SDK's `providerOptions` are namespaced: each provider ignores keys
* that don't match its namespace, so it's safe to include all of them.
*/
export const UNIVERSAL_CACHE_OPTIONS: Record<string, Record<string, unknown>> = {
anthropic: { cacheControl: { type: "ephemeral" } },
bedrock: { cachePoint: { type: "default" } },
}
/**
* Optional targeting configuration for cache breakpoint placement.
*/
export interface CacheBreakpointTargeting {
/** Maximum number of message breakpoints to place. Default: 2 */
maxBreakpoints?: number
/** Whether to add an anchor breakpoint at ~1/3 through the conversation. Default: false */
useAnchor?: boolean
/** Minimum number of non-assistant messages before placing an anchor. Default: 5 */
anchorThreshold?: number
}
/**
* Apply cache breakpoints to AI SDK messages with ALL provider namespaces.
*
* 4-breakpoint strategy:
* 1. System prompt passed as first message in messages[] with providerOptions
* 2. Tool definitions handled externally via `toolProviderOptions` in `streamText()`
* 3-4. Last 2 non-assistant messages this function handles these
*
* @param messages - The AI SDK message array (mutated in place)
* @param targeting - Optional targeting options (defaults: 2 breakpoints, no anchor)
*/
export function applyCacheBreakpoints(
messages: { role: string; providerOptions?: Record<string, Record<string, unknown>> }[],
targeting: CacheBreakpointTargeting = {},
): void {
const { maxBreakpoints = 2, useAnchor = false, anchorThreshold = 5 } = targeting
// 1. Collect non-assistant message indices (user | tool roles)
const nonAssistantIndices: number[] = []
for (let i = 0; i < messages.length; i++) {
if (messages[i].role !== "assistant" && messages[i].role !== "system") {
nonAssistantIndices.push(i)
}
}
if (nonAssistantIndices.length === 0) return
// 2. Target last N non-assistant messages
const targetIndices = new Set<number>()
for (let j = 0; j < maxBreakpoints && j < nonAssistantIndices.length; j++) {
targetIndices.add(nonAssistantIndices[nonAssistantIndices.length - 1 - j])
}
// 3. Optional anchor at ~1/3 point
if (useAnchor && nonAssistantIndices.length >= anchorThreshold) {
const anchorIdx = Math.floor(nonAssistantIndices.length / 3)
targetIndices.add(nonAssistantIndices[anchorIdx])
}
// 4. Apply UNIVERSAL cache options to targeted messages
for (const idx of targetIndices) {
if (idx >= 0 && idx < messages.length) {
messages[idx].providerOptions = {
...messages[idx].providerOptions,
...UNIVERSAL_CACHE_OPTIONS,
}
}
}
}
/**
* Apply system prompt caching by injecting the system prompt as a cached
* system message at the front of the messages array.
*
* AI SDK v6 does not support `providerOptions` on the `system` string
* parameter. Cache-aware providers call this helper to convert the system
* prompt into a system message with `providerOptions` for cache control.
*
* Returns the effective system prompt to pass to `streamText()`:
* - `undefined` when caching was applied (prompt is now in messages[0])
* - the original `systemPrompt` when no caching options were provided
*
* @param systemPrompt - The system prompt string
* @param messages - The AI SDK message array (mutated in place)
* @param cacheOptions - Provider-specific cache options (e.g. UNIVERSAL_CACHE_OPTIONS)
*/
export function applySystemPromptCaching(
systemPrompt: string | undefined,
messages: { role: string; content?: unknown; providerOptions?: Record<string, Record<string, unknown>> }[],
cacheOptions: Record<string, Record<string, unknown>> | undefined,
): string | undefined {
if (!systemPrompt || !cacheOptions) {
return systemPrompt || undefined
}
messages.unshift({
role: "system",
content: systemPrompt,
providerOptions: cacheOptions,
})
// Tell the caller not to also pass the system prompt via the `system:` parameter
return undefined
}
/**
* Apply provider-specific cache options to AI SDK tool definitions.
* Breakpoint 2 of 4: tool definitions.
*/
export function applyToolCacheOptions(
tools:
| Record<string, { providerOptions?: Record<string, Record<string, unknown>>; [key: string]: unknown }>
| undefined,
cacheOptions: Record<string, Record<string, unknown>> | undefined,
): void {
if (!tools || !cacheOptions) return
const keys = Object.keys(tools)
if (keys.length === 0) return
// Only stamp the LAST tool to conserve cache breakpoints (max 4 shared across
// messages and tools). Stamping every tool wastes breakpoints — the provider
// silently drops all but the first few.
const lastKey = keys[keys.length - 1]
tools[lastKey].providerOptions = { ...tools[lastKey].providerOptions, ...cacheOptions }
}

View file

@ -66,6 +66,10 @@ export interface ApiStreamUsageChunk {
cacheReadTokens?: number
reasoningTokens?: number
totalCost?: number
/** Total input tokens including cache read/write tokens. Each provider computes this directly. */
totalInputTokens?: number
/** Total output tokens. Each provider computes this directly. */
totalOutputTokens?: number
}
export interface ApiStreamGroundingChunk {

View file

@ -490,19 +490,6 @@ export class NativeToolCallParser {
}
break
case "browser_action":
if (partialArgs.action !== undefined) {
nativeArgs = {
action: partialArgs.action,
url: partialArgs.url,
coordinate: partialArgs.coordinate,
size: partialArgs.size,
text: partialArgs.text,
path: partialArgs.path,
}
}
break
case "codebase_search":
if (partialArgs.query !== undefined) {
nativeArgs = {
@ -838,19 +825,6 @@ export class NativeToolCallParser {
}
break
case "browser_action":
if (args.action !== undefined) {
nativeArgs = {
action: args.action,
url: args.url,
coordinate: args.coordinate,
size: args.size,
text: args.text,
path: args.path,
} as NativeArgsFor<TName>
}
break
case "codebase_search":
if (args.query !== undefined) {
nativeArgs = {

View file

@ -246,7 +246,7 @@ describe("NativeToolCallParser", () => {
name: "read_file" as const,
arguments: JSON.stringify({
files: JSON.stringify([
{ path: "src/services/browser/browserDiscovery.ts" },
{ path: "src/services/example/service.ts" },
{ path: "src/services/mcp/McpServerManager.ts" },
]),
}),
@ -264,7 +264,7 @@ describe("NativeToolCallParser", () => {
}
expect(nativeArgs._legacyFormat).toBe(true)
expect(nativeArgs.files).toHaveLength(2)
expect(nativeArgs.files[0].path).toBe("src/services/browser/browserDiscovery.ts")
expect(nativeArgs.files[0].path).toBe("src/services/example/service.ts")
expect(nativeArgs.files[1].path).toBe("src/services/mcp/McpServerManager.ts")
}
})

View file

@ -60,9 +60,6 @@ describe("presentAssistantMessage - Custom Tool Recording", () => {
api: {
getModel: () => ({ id: "test-model", info: {} }),
},
browserSession: {
closeBrowser: vi.fn().mockResolvedValue(undefined),
},
recordToolUsage: vi.fn(),
recordToolError: vi.fn(),
toolRepetitionDetector: {

View file

@ -46,9 +46,6 @@ describe("presentAssistantMessage - Image Handling in Native Tool Calling", () =
api: {
getModel: () => ({ id: "test-model", info: {} }),
},
browserSession: {
closeBrowser: vi.fn().mockResolvedValue(undefined),
},
recordToolUsage: vi.fn(),
toolRepetitionDetector: {
check: vi.fn().mockReturnValue({ allowExecution: true }),

View file

@ -41,9 +41,6 @@ describe("presentAssistantMessage - Unknown Tool Handling", () => {
api: {
getModel: () => ({ id: "test-model", info: {} }),
},
browserSession: {
closeBrowser: vi.fn().mockResolvedValue(undefined),
},
recordToolUsage: vi.fn(),
recordToolError: vi.fn(),
toolRepetitionDetector: {

View file

@ -24,7 +24,6 @@ import { searchReplaceTool } from "../tools/SearchReplaceTool"
import { editFileTool } from "../tools/EditFileTool"
import { applyPatchTool } from "../tools/ApplyPatchTool"
import { searchFilesTool } from "../tools/SearchFilesTool"
import { browserActionTool } from "../tools/BrowserActionTool"
import { executeCommandTool } from "../tools/ExecuteCommandTool"
import { useMcpToolTool } from "../tools/UseMcpToolTool"
import { accessMcpResourceTool } from "../tools/accessMcpResourceTool"
@ -61,7 +60,7 @@ import { sanitizeToolUseId } from "../../utils/tool-id"
*/
export async function presentAssistantMessage(cline: Task) {
if (cline.abort) {
if (cline.abort || cline.abandoned) {
throw new Error(`[Task#presentAssistantMessage] task ${cline.taskId}.${cline.instanceId} aborted`)
}
@ -358,8 +357,6 @@ export async function presentAssistantMessage(cline: Task) {
return `[${block.name}]`
case "list_files":
return `[${block.name} for '${block.params.path}']`
case "browser_action":
return `[${block.name} for '${block.params.action}']`
case "use_mcp_tool":
return `[${block.name} for '${block.params.server_name}']`
case "access_mcp_resource":
@ -559,34 +556,6 @@ export async function presentAssistantMessage(cline: Task) {
pushToolResult(formatResponse.toolError(errorString))
}
// Keep browser open during an active session so other tools can run.
// Session is active if we've seen any browser_action_result and the last browser_action is not "close".
try {
const messages = cline.clineMessages || []
const hasStarted = messages.some((m: any) => m.say === "browser_action_result")
let isClosed = false
for (let i = messages.length - 1; i >= 0; i--) {
const m = messages[i]
if (m.say === "browser_action") {
try {
const act = JSON.parse(m.text || "{}")
isClosed = act.action === "close"
} catch {}
break
}
}
const sessionActive = hasStarted && !isClosed
// Only auto-close when no active browser session is present, and this isn't a browser_action
if (!sessionActive && block.name !== "browser_action") {
await cline.browserSession.closeBrowser()
}
} catch {
// On any unexpected error, fall back to conservative behavior
if (block.name !== "browser_action") {
await cline.browserSession.closeBrowser()
}
}
if (!block.partial) {
// Check if this is a custom tool - if so, record as "custom_tool" (like MCP tools)
const isCustomTool = stateExperiments?.customTools && customToolRegistry.has(block.name)
@ -798,15 +767,6 @@ export async function presentAssistantMessage(cline: Task) {
pushToolResult,
})
break
case "browser_action":
await browserActionTool(
cline,
block as ToolUse<"browser_action">,
askApproval,
handleError,
pushToolResult,
)
break
case "execute_command":
await executeCommandTool.handle(cline, block as ToolUse<"execute_command">, {
askApproval,
@ -978,6 +938,15 @@ export async function presentAssistantMessage(cline: Task) {
// locked.
cline.presentAssistantMessageLocked = false
// Early exit if task was aborted/abandoned during tool execution (e.g., new_task delegation).
// Prevents unhandled promise rejections from recursive calls hitting the abort check.
if (cline.abort || cline.abandoned) {
if (cline.didCompleteReadingStream) {
cline.userMessageContentReady = true
}
return
}
// NOTE: When tool is rejected, iterator stream is interrupted and it waits
// for `userMessageContentReady` to be true. Future calls to present will
// skip execution since `didRejectTool` and iterate until `contentIndex` is
@ -1005,7 +974,11 @@ export async function presentAssistantMessage(cline: Task) {
if (cline.currentStreamingContentIndex < cline.assistantMessageContent.length) {
// There are already more content blocks to stream, so we'll call
// this function ourselves.
presentAssistantMessage(cline)
presentAssistantMessage(cline).catch((err) => {
if (!cline.abort) {
console.error("[presentAssistantMessage] Unhandled error:", err)
}
})
return
} else {
// CRITICAL FIX: If we're out of bounds and the stream is complete, set userMessageContentReady
@ -1018,7 +991,11 @@ export async function presentAssistantMessage(cline: Task) {
// Block is partial, but the read stream may have finished.
if (cline.presentAssistantMessageHasPendingUpdates) {
presentAssistantMessage(cline)
presentAssistantMessage(cline).catch((err) => {
if (!cline.abort) {
console.error("[presentAssistantMessage] Unhandled error:", err)
}
})
}
}

View file

@ -13,11 +13,10 @@ import { isWriteToolAction, isReadOnlyToolAction } from "./tools"
import { isMcpToolAlwaysAllowed } from "./mcp"
import { getCommandDecision } from "./commands"
// We have 10 different actions that can be auto-approved.
// We have auto-approval actions for different categories.
export type AutoApprovalState =
| "alwaysAllowReadOnly"
| "alwaysAllowWrite"
| "alwaysAllowBrowser"
| "alwaysAllowMcp"
| "alwaysAllowModeSwitch"
| "alwaysAllowSubtasks"
@ -90,10 +89,6 @@ export async function checkAutoApproval({
}
}
if (ask === "browser_action_launch") {
return state.alwaysAllowBrowser === true ? { decision: "approve" } : { decision: "ask" }
}
if (ask === "use_mcp_server") {
if (!text) {
return { decision: "ask" }

View file

@ -227,11 +227,7 @@ describe("CustomModesManager - YAML Edge Cases", () => {
slug: "test-mode",
name: "Test Mode",
roleDefinition: "Test role",
groups: [
"read",
["edit", { fileRegex: "\\.md$", description: "Markdown files only" }],
"browser",
],
groups: ["read", ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }]],
},
],
})
@ -245,20 +241,19 @@ describe("CustomModesManager - YAML Edge Cases", () => {
// Should successfully parse the complex fileRegex syntax
expect(modes).toHaveLength(1)
expect(modes[0].groups).toHaveLength(3)
expect(modes[0].groups).toHaveLength(2)
expect(modes[0].groups[1]).toEqual(["edit", { fileRegex: "\\.md$", description: "Markdown files only" }])
})
it("should handle invalid fileRegex syntax with clear error", async () => {
// This YAML has invalid structure that might cause parsing issues
const invalidYaml = `customModes:
- slug: "test-mode"
name: "Test Mode"
roleDefinition: "Test role"
groups:
- read
- ["edit", { fileRegex: "\\.md$" }] # This line has invalid YAML syntax
- browser`
- slug: "test-mode"
name: "Test Mode"
roleDefinition: "Test role"
groups:
- read
- ["edit", { fileRegex: "\\.md$" }] # This line has invalid YAML syntax`
mockFsReadFile({
[mockRoomodes]: invalidYaml,
@ -433,13 +428,6 @@ describe("CustomModesManager - YAML Edge Cases", () => {
description: "Markdown files with \u2018special\u2019 chars",
},
],
[
"browser",
{
fileRegex: "\\.html?$",
description: "HTML files\u00A0only",
},
],
],
},
],
@ -462,13 +450,6 @@ describe("CustomModesManager - YAML Edge Cases", () => {
description: "Markdown files with 'special' chars",
},
])
expect(modes[0].groups[2]).toEqual([
"browser",
{
fileRegex: "\\.html?$",
description: "HTML files only",
},
])
})
})
})

View file

@ -130,7 +130,7 @@ describe("CustomModesSettings", () => {
customModes: [
{
...validMode,
groups: ["read", "edit", "browser"] as const,
groups: ["read", "edit"] as const,
},
],
}
@ -168,4 +168,41 @@ describe("CustomModesSettings", () => {
expect(settings.customModes[0].customInstructions).toBeDefined()
})
})
describe("deprecated tool group migration", () => {
it("should strip deprecated 'browser' group when validating custom modes settings", () => {
const result = customModesSettingsSchema.parse({
customModes: [
{
slug: "test-mode",
name: "Test Mode",
roleDefinition: "Test role",
groups: ["read", "browser", "edit"],
},
],
})
expect(result.customModes[0].groups).toEqual(["read", "edit"])
})
it("should strip deprecated 'browser' from multiple modes in settings", () => {
const result = customModesSettingsSchema.parse({
customModes: [
{
slug: "mode-a",
name: "Mode A",
roleDefinition: "Role A",
groups: ["read", "browser"],
},
{
slug: "mode-b",
name: "Mode B",
roleDefinition: "Role B",
groups: ["browser", "edit", "command"],
},
],
})
expect(result.customModes[0].groups).toEqual(["read"])
expect(result.customModes[1].groups).toEqual(["edit", "command"])
})
})
})

View file

@ -26,7 +26,7 @@ describe("CustomModeSchema", () => {
slug: "test",
name: "Test Mode",
roleDefinition: "Test role definition",
groups: ["read", "edit", "browser"] as const,
groups: ["read", "edit"] as const,
} satisfies ModeConfig
expect(() => validateCustomMode(validMode)).not.toThrow()
@ -121,18 +121,14 @@ describe("CustomModeSchema", () => {
slug: "markdown-editor",
name: "Markdown Editor",
roleDefinition: "Markdown editing mode",
groups: ["read", ["edit", { fileRegex: "\\.md$" }], "browser"],
groups: ["read", ["edit", { fileRegex: "\\.md$" }]],
}
const modeWithDescription = {
slug: "docs-editor",
name: "Documentation Editor",
roleDefinition: "Documentation editing mode",
groups: [
"read",
["edit", { fileRegex: "\\.(md|txt)$", description: "Documentation files only" }],
"browser",
],
groups: ["read", ["edit", { fileRegex: "\\.(md|txt)$", description: "Documentation files only" }]],
}
expect(() => modeConfigSchema.parse(modeWithJustRegex)).not.toThrow()
@ -195,7 +191,7 @@ describe("CustomModeSchema", () => {
test("accepts multiple groups", () => {
const mode = {
...validBaseMode,
groups: ["read", "edit", "browser"] as const,
groups: ["read", "edit"] as const,
} satisfies ModeConfig
expect(() => modeConfigSchema.parse(mode)).not.toThrow()
@ -204,7 +200,7 @@ describe("CustomModeSchema", () => {
test("accepts all available groups", () => {
const mode = {
...validBaseMode,
groups: ["read", "edit", "browser", "command", "mcp"] as const,
groups: ["read", "edit", "command", "mcp"] as const,
} satisfies ModeConfig
expect(() => modeConfigSchema.parse(mode)).not.toThrow()
@ -252,4 +248,46 @@ describe("CustomModeSchema", () => {
expect(() => modeConfigSchema.parse(modeWithUndefined)).toThrow()
})
})
describe("deprecated tool group migration", () => {
it("should strip deprecated 'browser' string group from mode config", () => {
const result = modeConfigSchema.parse({
slug: "test-mode",
name: "Test Mode",
roleDefinition: "Test role",
groups: ["read", "browser", "edit"],
})
expect(result.groups).toEqual(["read", "edit"])
})
it("should strip deprecated 'browser' tuple group from mode config", () => {
const result = modeConfigSchema.parse({
slug: "test-mode",
name: "Test Mode",
roleDefinition: "Test role",
groups: ["read", ["browser", { fileRegex: ".*", description: "test" }], "edit"],
})
expect(result.groups).toEqual(["read", "edit"])
})
it("should handle mode config where all groups are deprecated", () => {
const result = modeConfigSchema.parse({
slug: "test-mode",
name: "Test Mode",
roleDefinition: "Test role",
groups: ["browser"],
})
expect(result.groups).toEqual([])
})
it("should still reject other invalid group names", () => {
const result = modeConfigSchema.safeParse({
slug: "test-mode",
name: "Test Mode",
roleDefinition: "Test role",
groups: ["read", "nonexistent"],
})
expect(result.success).toBe(false)
})
})
})

View file

@ -117,10 +117,6 @@ describe("getEnvironmentDetails", () => {
deref: vi.fn().mockReturnValue(mockProvider),
[Symbol.toStringTag]: "WeakRef",
} as unknown as WeakRef<ClineProvider>,
browserSession: {
isSessionActive: vi.fn().mockReturnValue(false),
getViewportSize: vi.fn().mockReturnValue({ width: 900, height: 600 }),
} as any,
}
// Mock other dependencies.
@ -448,18 +444,4 @@ describe("getEnvironmentDetails", () => {
expect(getGitStatus).toHaveBeenCalledWith(mockCwd, 5)
})
it("should NOT include Browser Session Status when inactive", async () => {
const result = await getEnvironmentDetails(mockCline as Task)
expect(result).not.toContain("# Browser Session Status")
})
it("should include Browser Session Status with current viewport when active", async () => {
;(mockCline.browserSession as any).isSessionActive = vi.fn().mockReturnValue(true)
;(mockCline.browserSession as any).getViewportSize = vi.fn().mockReturnValue({ width: 1280, height: 720 })
const result = await getEnvironmentDetails(mockCline as Task)
expect(result).toContain("Active - A browser session is currently open and ready for browser_action commands")
expect(result).toContain("Current viewport size: 1280x720 pixels.")
})
})

View file

@ -226,35 +226,6 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo
details += `<name>${modeDetails.name}</name>\n`
details += `<model>${modelId}</model>\n`
// Add browser session status - Only show when active to prevent cluttering context
const isBrowserActive = cline.browserSession.isSessionActive()
if (isBrowserActive) {
// Build viewport info for status (prefer actual viewport if available, else fallback to configured setting)
const configuredViewport = (state?.browserViewportSize as string | undefined) ?? "900x600"
let configuredWidth: number | undefined
let configuredHeight: number | undefined
if (configuredViewport.includes("x")) {
const parts = configuredViewport.split("x").map((v) => Number(v))
configuredWidth = parts[0]
configuredHeight = parts[1]
}
let actualWidth: number | undefined
let actualHeight: number | undefined
const vp = cline.browserSession.getViewportSize?.()
if (vp) {
actualWidth = vp.width
actualHeight = vp.height
}
const width = actualWidth ?? configuredWidth
const height = actualHeight ?? configuredHeight
const viewportInfo = width && height ? `\nCurrent viewport size: ${width}x${height} pixels.` : ""
details += `\n# Browser Session Status\nActive - A browser session is currently open and ready for browser_action commands${viewportInfo}\n`
}
if (includeFileDetails) {
details += `\n\n# Current Workspace Directory (${cline.cwd.toPosix()}) Files\n`
const isDesktop = arePathsEqual(cline.cwd, path.join(os.homedir(), "Desktop"))

View file

@ -3,7 +3,6 @@
import * as vscode from "vscode"
import { parseMentions } from "../index"
import { UrlContentFetcher } from "../../../services/browser/UrlContentFetcher"
// Mock vscode
vi.mock("vscode", () => ({
@ -17,143 +16,15 @@ vi.mock("../../../i18n", () => ({
t: vi.fn((key: string) => key),
}))
describe("parseMentions - URL error handling", () => {
let mockUrlContentFetcher: UrlContentFetcher
let consoleErrorSpy: any
describe("parseMentions - URL mention handling", () => {
beforeEach(() => {
vi.clearAllMocks()
consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
mockUrlContentFetcher = {
launchBrowser: vi.fn(),
urlToMarkdown: vi.fn(),
closeBrowser: vi.fn(),
} as any
})
it("should handle timeout errors with appropriate message", async () => {
const timeoutError = new Error("Navigation timeout of 30000 ms exceeded")
vi.mocked(mockUrlContentFetcher.urlToMarkdown).mockRejectedValue(timeoutError)
it("should replace URL mentions with quoted URL reference", async () => {
const result = await parseMentions("Check @https://example.com", "/test")
const result = await parseMentions("Check @https://example.com", "/test", mockUrlContentFetcher)
expect(consoleErrorSpy).toHaveBeenCalledWith("Error fetching URL https://example.com:", timeoutError)
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url")
expect(result.text).toContain("Error fetching content: Navigation timeout of 30000 ms exceeded")
})
it("should handle DNS resolution errors", async () => {
const dnsError = new Error("net::ERR_NAME_NOT_RESOLVED")
vi.mocked(mockUrlContentFetcher.urlToMarkdown).mockRejectedValue(dnsError)
const result = await parseMentions("Check @https://nonexistent.example", "/test", mockUrlContentFetcher)
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url")
expect(result.text).toContain("Error fetching content: net::ERR_NAME_NOT_RESOLVED")
})
it("should handle network disconnection errors", async () => {
const networkError = new Error("net::ERR_INTERNET_DISCONNECTED")
vi.mocked(mockUrlContentFetcher.urlToMarkdown).mockRejectedValue(networkError)
const result = await parseMentions("Check @https://example.com", "/test", mockUrlContentFetcher)
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url")
expect(result.text).toContain("Error fetching content: net::ERR_INTERNET_DISCONNECTED")
})
it("should handle 403 Forbidden errors", async () => {
const forbiddenError = new Error("403 Forbidden")
vi.mocked(mockUrlContentFetcher.urlToMarkdown).mockRejectedValue(forbiddenError)
const result = await parseMentions("Check @https://example.com", "/test", mockUrlContentFetcher)
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url")
expect(result.text).toContain("Error fetching content: 403 Forbidden")
})
it("should handle 404 Not Found errors", async () => {
const notFoundError = new Error("404 Not Found")
vi.mocked(mockUrlContentFetcher.urlToMarkdown).mockRejectedValue(notFoundError)
const result = await parseMentions("Check @https://example.com/missing", "/test", mockUrlContentFetcher)
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url")
expect(result.text).toContain("Error fetching content: 404 Not Found")
})
it("should handle generic errors with fallback message", async () => {
const genericError = new Error("Some unexpected error")
vi.mocked(mockUrlContentFetcher.urlToMarkdown).mockRejectedValue(genericError)
const result = await parseMentions("Check @https://example.com", "/test", mockUrlContentFetcher)
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url")
expect(result.text).toContain("Error fetching content: Some unexpected error")
})
it("should handle non-Error objects thrown", async () => {
const nonErrorObject = { code: "UNKNOWN", details: "Something went wrong" }
vi.mocked(mockUrlContentFetcher.urlToMarkdown).mockRejectedValue(nonErrorObject)
const result = await parseMentions("Check @https://example.com", "/test", mockUrlContentFetcher)
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url")
expect(result.text).toContain("Error fetching content:")
})
it("should handle browser launch errors correctly", async () => {
const launchError = new Error("Failed to launch browser")
vi.mocked(mockUrlContentFetcher.launchBrowser).mockRejectedValue(launchError)
const result = await parseMentions("Check @https://example.com", "/test", mockUrlContentFetcher)
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
"Error fetching content for https://example.com: Failed to launch browser",
)
expect(result.text).toContain("Error fetching content: Failed to launch browser")
// Should not attempt to fetch URL if browser launch failed
expect(mockUrlContentFetcher.urlToMarkdown).not.toHaveBeenCalled()
})
it("should handle browser launch errors without message property", async () => {
const launchError = "String error"
vi.mocked(mockUrlContentFetcher.launchBrowser).mockRejectedValue(launchError)
const result = await parseMentions("Check @https://example.com", "/test", mockUrlContentFetcher)
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
"Error fetching content for https://example.com: String error",
)
expect(result.text).toContain("Error fetching content: String error")
})
it("should successfully fetch URL content when no errors occur", async () => {
vi.mocked(mockUrlContentFetcher.urlToMarkdown).mockResolvedValue("# Example Content\n\nThis is the content.")
const result = await parseMentions("Check @https://example.com", "/test", mockUrlContentFetcher)
expect(vscode.window.showErrorMessage).not.toHaveBeenCalled()
expect(result.text).toContain('<url_content url="https://example.com">')
expect(result.text).toContain("# Example Content\n\nThis is the content.")
expect(result.text).toContain("</url_content>")
})
it("should handle multiple URLs with mixed success and failure", async () => {
vi.mocked(mockUrlContentFetcher.urlToMarkdown)
.mockResolvedValueOnce("# First Site")
.mockRejectedValueOnce(new Error("timeout"))
const result = await parseMentions(
"Check @https://example1.com and @https://example2.com",
"/test",
mockUrlContentFetcher,
)
expect(result.text).toContain('<url_content url="https://example1.com">')
expect(result.text).toContain("# First Site")
expect(result.text).toContain('<url_content url="https://example2.com">')
expect(result.text).toContain("Error fetching content: timeout")
// URL mentions are now replaced with a quoted reference (no fetching)
expect(result.text).toContain("'https://example.com'")
})
})

View file

@ -2,7 +2,6 @@
import { processUserContentMentions } from "../processUserContentMentions"
import { parseMentions } from "../index"
import { UrlContentFetcher } from "../../../services/browser/UrlContentFetcher"
import { FileContextTracker } from "../../context-tracking/FileContextTracker"
// Mock the parseMentions function
@ -11,14 +10,12 @@ vi.mock("../index", () => ({
}))
describe("processUserContentMentions", () => {
let mockUrlContentFetcher: UrlContentFetcher
let mockFileContextTracker: FileContextTracker
let mockRooIgnoreController: any
beforeEach(() => {
vi.clearAllMocks()
mockUrlContentFetcher = {} as UrlContentFetcher
mockFileContextTracker = {} as FileContextTracker
mockRooIgnoreController = {}
@ -42,7 +39,6 @@ describe("processUserContentMentions", () => {
const result = await processUserContentMentions({
userContent,
cwd: "/test",
urlContentFetcher: mockUrlContentFetcher,
fileContextTracker: mockFileContextTracker,
})
@ -65,7 +61,6 @@ describe("processUserContentMentions", () => {
const result = await processUserContentMentions({
userContent,
cwd: "/test",
urlContentFetcher: mockUrlContentFetcher,
fileContextTracker: mockFileContextTracker,
})
@ -74,6 +69,78 @@ describe("processUserContentMentions", () => {
expect(result.mode).toBeUndefined()
})
it("should process tool_result blocks with string content", async () => {
const userContent = [
{
type: "tool_result" as const,
tool_use_id: "123",
content: "<user_message>Tool feedback</user_message>",
},
]
const result = await processUserContentMentions({
userContent,
cwd: "/test",
fileContextTracker: mockFileContextTracker,
})
expect(parseMentions).toHaveBeenCalled()
// String content is now converted to array format to support content blocks
expect(result.content[0]).toEqual({
type: "tool_result",
tool_use_id: "123",
content: [
{
type: "text",
text: "parsed: <user_message>Tool feedback</user_message>",
},
],
})
expect(result.mode).toBeUndefined()
})
it("should process tool_result blocks with array content", async () => {
const userContent = [
{
type: "tool_result" as const,
tool_use_id: "123",
content: [
{
type: "text" as const,
text: "<user_message>Array task</user_message>",
},
{
type: "text" as const,
text: "Regular text",
},
],
},
]
const result = await processUserContentMentions({
userContent,
cwd: "/test",
fileContextTracker: mockFileContextTracker,
})
expect(parseMentions).toHaveBeenCalledTimes(1)
expect(result.content[0]).toEqual({
type: "tool_result",
tool_use_id: "123",
content: [
{
type: "text",
text: "parsed: <user_message>Array task</user_message>",
},
{
type: "text",
text: "Regular text",
},
],
})
expect(result.mode).toBeUndefined()
})
it("should handle mixed content types (text + image)", async () => {
const userContent = [
{
@ -90,7 +157,6 @@ describe("processUserContentMentions", () => {
const result = await processUserContentMentions({
userContent: userContent as any,
cwd: "/test",
urlContentFetcher: mockUrlContentFetcher,
fileContextTracker: mockFileContextTracker,
})
@ -117,14 +183,12 @@ describe("processUserContentMentions", () => {
await processUserContentMentions({
userContent,
cwd: "/test",
urlContentFetcher: mockUrlContentFetcher,
fileContextTracker: mockFileContextTracker,
})
expect(parseMentions).toHaveBeenCalledWith(
"<user_message>Test default</user_message>",
"/test",
mockUrlContentFetcher,
mockFileContextTracker,
undefined,
false, // showRooIgnoredFiles should default to false
@ -144,7 +208,6 @@ describe("processUserContentMentions", () => {
await processUserContentMentions({
userContent,
cwd: "/test",
urlContentFetcher: mockUrlContentFetcher,
fileContextTracker: mockFileContextTracker,
showRooIgnoredFiles: false,
})
@ -152,7 +215,6 @@ describe("processUserContentMentions", () => {
expect(parseMentions).toHaveBeenCalledWith(
"<user_message>Test explicit false</user_message>",
"/test",
mockUrlContentFetcher,
mockFileContextTracker,
undefined,
false,
@ -181,7 +243,6 @@ describe("processUserContentMentions", () => {
const result = await processUserContentMentions({
userContent,
cwd: "/test",
urlContentFetcher: mockUrlContentFetcher,
fileContextTracker: mockFileContextTracker,
})
@ -195,5 +256,88 @@ describe("processUserContentMentions", () => {
text: "command help",
})
})
it("should include slash command content in tool_result string content", async () => {
vi.mocked(parseMentions).mockResolvedValueOnce({
text: "parsed tool output",
slashCommandHelp: "command help",
mode: undefined,
contentBlocks: [],
})
const userContent = [
{
type: "tool_result" as const,
tool_use_id: "123",
content: "<user_message>Tool output</user_message>",
},
]
const result = await processUserContentMentions({
userContent,
cwd: "/test",
fileContextTracker: mockFileContextTracker,
})
expect(result.content).toHaveLength(1)
expect(result.content[0]).toEqual({
type: "tool_result",
tool_use_id: "123",
content: [
{
type: "text",
text: "parsed tool output",
},
{
type: "text",
text: "command help",
},
],
})
})
it("should include slash command content in tool_result array content", async () => {
vi.mocked(parseMentions).mockResolvedValueOnce({
text: "parsed array item",
slashCommandHelp: "command help",
mode: undefined,
contentBlocks: [],
})
const userContent = [
{
type: "tool_result" as const,
tool_use_id: "123",
content: [
{
type: "text" as const,
text: "<user_message>Array item</user_message>",
},
],
},
]
const result = await processUserContentMentions({
userContent,
cwd: "/test",
fileContextTracker: mockFileContextTracker,
})
expect(result.content).toHaveLength(1)
expect(result.content[0]).toEqual({
type: "tool_result",
tool_use_id: "123",
content: [
{
type: "text",
text: "parsed array item",
},
{
type: "text",
text: "command help",
},
],
})
})
})
})

View file

@ -13,42 +13,11 @@ import { extractTextFromFileWithMetadata, type ExtractTextResult } from "../../i
import { diagnosticsToProblemsString } from "../../integrations/diagnostics"
import { DEFAULT_LINE_LIMIT } from "../prompts/tools/native-tools/read_file"
import { UrlContentFetcher } from "../../services/browser/UrlContentFetcher"
import { FileContextTracker } from "../context-tracking/FileContextTracker"
import { RooIgnoreController } from "../ignore/RooIgnoreController"
import { getCommand, type Command } from "../../services/command/commands"
import { t } from "../../i18n"
function getUrlErrorMessage(error: unknown): string {
const errorMessage = error instanceof Error ? error.message : String(error)
// Check for common error patterns and return appropriate message
if (errorMessage.includes("timeout")) {
return t("common:errors.url_timeout")
}
if (errorMessage.includes("net::ERR_NAME_NOT_RESOLVED")) {
return t("common:errors.url_not_found")
}
if (errorMessage.includes("net::ERR_INTERNET_DISCONNECTED")) {
return t("common:errors.no_internet")
}
if (errorMessage.includes("net::ERR_ABORTED")) {
return t("common:errors.url_request_aborted")
}
if (errorMessage.includes("403") || errorMessage.includes("Forbidden")) {
return t("common:errors.url_forbidden")
}
if (errorMessage.includes("404") || errorMessage.includes("Not Found")) {
return t("common:errors.url_page_not_found")
}
// Default error message
return t("common:errors.url_fetch_failed", { error: errorMessage })
}
export async function openMention(cwd: string, mention?: string): Promise<void> {
if (!mention) {
return
@ -128,7 +97,6 @@ ${result.content}`
export async function parseMentions(
text: string,
cwd: string,
urlContentFetcher: UrlContentFetcher,
fileContextTracker?: FileContextTracker,
rooIgnoreController?: RooIgnoreController,
showRooIgnoredFiles: boolean = false,
@ -180,8 +148,7 @@ export async function parseMentions(
parsedText = parsedText.replace(mentionRegexGlobal, (match, mention) => {
mentions.add(mention)
if (mention.startsWith("http")) {
// Keep old style for URLs (still XML-based)
return `'${mention}' (see below for site content)`
return `'${mention}'`
} else if (mention.startsWith("/")) {
// Clean path reference - no "see below" since we format like tool results
const mentionPath = mention.slice(1)
@ -198,49 +165,8 @@ export async function parseMentions(
return match
})
const urlMention = Array.from(mentions).find((mention) => mention.startsWith("http"))
let launchBrowserError: Error | undefined
if (urlMention) {
try {
await urlContentFetcher.launchBrowser()
} catch (error) {
launchBrowserError = error
const errorMessage = error instanceof Error ? error.message : String(error)
vscode.window.showErrorMessage(`Error fetching content for ${urlMention}: ${errorMessage}`)
}
}
for (const mention of mentions) {
if (mention.startsWith("http")) {
let result: string
if (launchBrowserError) {
const errorMessage =
launchBrowserError instanceof Error ? launchBrowserError.message : String(launchBrowserError)
result = `Error fetching content: ${errorMessage}`
} else {
try {
const markdown = await urlContentFetcher.urlToMarkdown(mention)
result = markdown
} catch (error) {
console.error(`Error fetching URL ${mention}:`, error)
// Get raw error message for AI
const rawErrorMessage = error instanceof Error ? error.message : String(error)
// Get localized error message for UI notification
const localizedErrorMessage = getUrlErrorMessage(error)
vscode.window.showErrorMessage(
t("common:errors.url_fetch_error_with_url", { url: mention, error: localizedErrorMessage }),
)
// Send raw error message to AI model
result = `Error fetching content: ${rawErrorMessage}`
}
}
// URLs still use XML format (appended to text for backwards compat)
parsedText += `\n\n<url_content url="${mention}">\n${result}\n</url_content>`
} else if (mention.startsWith("/")) {
if (mention.startsWith("/")) {
const mentionPath = mention.slice(1)
try {
const fileResult = await getFileOrFolderContentWithMetadata(
@ -305,14 +231,6 @@ export async function parseMentions(
}
}
if (urlMention) {
try {
await urlContentFetcher.closeBrowser()
} catch (error) {
console.error(`Error closing browser: ${error.message}`)
}
}
return {
text: parsedText,
contentBlocks,

View file

@ -1,10 +1,9 @@
import type { TextPart, ImagePart } from "../task-persistence/rooMessage"
import type { TextPart, ImagePart, LegacyToolResultBlock } from "../task-persistence/rooMessage"
import { parseMentions, ParseMentionsResult, MentionContentBlock } from "./index"
import { UrlContentFetcher } from "../../services/browser/UrlContentFetcher"
import { FileContextTracker } from "../context-tracking/FileContextTracker"
export interface ProcessUserContentMentionsResult {
content: Array<TextPart | ImagePart>
content: Array<TextPart | ImagePart | LegacyToolResultBlock>
mode?: string // Mode from the first slash command that has one
}
@ -30,16 +29,14 @@ function contentBlocksToTextParts(contentBlocks: MentionContentBlock[]): TextPar
export async function processUserContentMentions({
userContent,
cwd,
urlContentFetcher,
fileContextTracker,
rooIgnoreController,
showRooIgnoredFiles = false,
includeDiagnosticMessages = true,
maxDiagnosticMessages = 50,
}: {
userContent: Array<TextPart | ImagePart>
userContent: Array<TextPart | ImagePart | LegacyToolResultBlock>
cwd: string
urlContentFetcher: UrlContentFetcher
fileContextTracker: FileContextTracker
rooIgnoreController?: any
showRooIgnoredFiles?: boolean
@ -61,7 +58,6 @@ export async function processUserContentMentions({
const result = await parseMentions(
block.text,
cwd,
urlContentFetcher,
fileContextTracker,
rooIgnoreController,
showRooIgnoredFiles,
@ -98,6 +94,106 @@ export async function processUserContentMentions({
return blocks
}
return block
} else if (block.type === "tool_result") {
if (typeof block.content === "string") {
if (shouldProcessMentions(block.content)) {
const result = await parseMentions(
block.content,
cwd,
fileContextTracker,
rooIgnoreController,
showRooIgnoredFiles,
includeDiagnosticMessages,
maxDiagnosticMessages,
)
// Capture the first mode found
if (!commandMode && result.mode) {
commandMode = result.mode
}
// Build content array with file blocks included
const contentParts: Array<{ type: "text"; text: string }> = [
{
type: "text" as const,
text: result.text,
},
]
// Add file/folder content blocks
for (const contentBlock of result.contentBlocks) {
contentParts.push({
type: "text" as const,
text: contentBlock.content,
})
}
if (result.slashCommandHelp) {
contentParts.push({
type: "text" as const,
text: result.slashCommandHelp,
})
}
return {
...block,
content: contentParts,
}
}
return block
} else if (Array.isArray(block.content)) {
const parsedContent = (
await Promise.all(
block.content.map(async (contentBlock) => {
if (contentBlock.type === "text" && shouldProcessMentions(contentBlock.text)) {
const result = await parseMentions(
contentBlock.text,
cwd,
fileContextTracker,
rooIgnoreController,
showRooIgnoredFiles,
includeDiagnosticMessages,
maxDiagnosticMessages,
)
// Capture the first mode found
if (!commandMode && result.mode) {
commandMode = result.mode
}
// Build blocks array with file content
const blocks: Array<{ type: "text"; text: string }> = [
{
...contentBlock,
text: result.text,
},
]
// Add file/folder content blocks
for (const cb of result.contentBlocks) {
blocks.push({
type: "text" as const,
text: cb.content,
})
}
if (result.slashCommandHelp) {
blocks.push({
type: "text" as const,
text: result.slashCommandHelp,
})
}
return blocks
}
return contentBlock
}),
)
).flat()
return { ...block, content: parsedContent }
}
return block
}
@ -108,5 +204,5 @@ export async function processUserContentMentions({
)
).flat()
return { content: content as Array<TextPart | ImagePart>, mode: commandMode }
return { content: content as Array<TextPart | ImagePart | LegacyToolResultBlock>, mode: commandMode }
}

View file

@ -205,7 +205,6 @@ describe("addCustomInstructions", () => {
false, // supportsImages
undefined, // mcpHub
undefined, // diffStrategy
undefined, // browserViewportSize
"architect", // mode
undefined, // customModePrompts
undefined, // customModes
@ -226,7 +225,6 @@ describe("addCustomInstructions", () => {
false, // supportsImages
undefined, // mcpHub
undefined, // diffStrategy
undefined, // browserViewportSize
"ask", // mode
undefined, // customModePrompts
undefined, // customModes
@ -249,7 +247,6 @@ describe("addCustomInstructions", () => {
false, // supportsImages
mockMcpHub, // mcpHub
undefined, // diffStrategy
undefined, // browserViewportSize
defaultModeSlug, // mode
undefined, // customModePrompts
undefined, // customModes,

View file

@ -220,7 +220,6 @@ describe("SYSTEM_PROMPT", () => {
false, // supportsImages
undefined, // mcpHub
undefined, // diffStrategy
undefined, // browserViewportSize
defaultModeSlug, // mode
undefined, // customModePrompts
undefined, // customModes
@ -233,26 +232,6 @@ describe("SYSTEM_PROMPT", () => {
expect(prompt).toMatchFileSnapshot("./__snapshots__/system-prompt/consistent-system-prompt.snap")
})
it("should include browser actions when supportsImages is true", async () => {
const prompt = await SYSTEM_PROMPT(
mockContext,
"/test/path",
true, // supportsImages
undefined, // mcpHub
undefined, // diffStrategy
"1280x800", // browserViewportSize
defaultModeSlug, // mode
undefined, // customModePrompts
undefined, // customModes,
undefined, // globalCustomInstructions
experiments,
undefined, // language
undefined, // rooIgnoreInstructions
)
expect(prompt).toMatchFileSnapshot("./__snapshots__/system-prompt/with-computer-use-support.snap")
})
it("should include MCP server info when mcpHub is provided", async () => {
mockMcpHub = createMockMcpHub(true)
@ -262,7 +241,6 @@ describe("SYSTEM_PROMPT", () => {
false,
mockMcpHub, // mcpHub
undefined, // diffStrategy
undefined, // browserViewportSize
defaultModeSlug, // mode
undefined, // customModePrompts
undefined, // customModes,
@ -282,7 +260,6 @@ describe("SYSTEM_PROMPT", () => {
false,
undefined, // explicitly undefined mcpHub
undefined, // diffStrategy
undefined, // browserViewportSize
defaultModeSlug, // mode
undefined, // customModePrompts
undefined, // customModes,
@ -295,26 +272,6 @@ describe("SYSTEM_PROMPT", () => {
expect(prompt).toMatchFileSnapshot("./__snapshots__/system-prompt/with-undefined-mcp-hub.snap")
})
it("should handle different browser viewport sizes", async () => {
const prompt = await SYSTEM_PROMPT(
mockContext,
"/test/path",
false,
undefined, // mcpHub
undefined, // diffStrategy
"900x600", // different viewport size
defaultModeSlug, // mode
undefined, // customModePrompts
undefined, // customModes,
undefined, // globalCustomInstructions
experiments,
undefined, // language
undefined, // rooIgnoreInstructions
)
expect(prompt).toMatchFileSnapshot("./__snapshots__/system-prompt/with-different-viewport-size.snap")
})
it("should include vscode language in custom instructions", async () => {
// Mock vscode.env.language
const vscode = vi.mocked(await import("vscode")) as any
@ -349,7 +306,6 @@ describe("SYSTEM_PROMPT", () => {
false,
undefined, // mcpHub
undefined, // diffStrategy
undefined, // browserViewportSize
defaultModeSlug, // mode
undefined, // customModePrompts
undefined, // customModes
@ -407,7 +363,6 @@ describe("SYSTEM_PROMPT", () => {
false,
undefined, // mcpHub
undefined, // diffStrategy
undefined, // browserViewportSize
"custom-mode", // mode
undefined, // customModePrompts
customModes, // customModes
@ -442,7 +397,6 @@ describe("SYSTEM_PROMPT", () => {
false,
undefined, // mcpHub
undefined, // diffStrategy
undefined, // browserViewportSize
defaultModeSlug as Mode, // mode
customModePrompts, // customModePrompts
undefined, // customModes
@ -472,7 +426,6 @@ describe("SYSTEM_PROMPT", () => {
false,
undefined, // mcpHub
undefined, // diffStrategy
undefined, // browserViewportSize
defaultModeSlug as Mode, // mode
customModePrompts, // customModePrompts
undefined, // customModes
@ -499,7 +452,6 @@ describe("SYSTEM_PROMPT", () => {
false,
undefined, // mcpHub
undefined, // diffStrategy
undefined, // browserViewportSize
defaultModeSlug, // mode
undefined, // customModePrompts
undefined, // customModes
@ -528,7 +480,6 @@ describe("SYSTEM_PROMPT", () => {
false,
undefined, // mcpHub
undefined, // diffStrategy
undefined, // browserViewportSize
defaultModeSlug, // mode
undefined, // customModePrompts
undefined, // customModes
@ -557,7 +508,6 @@ describe("SYSTEM_PROMPT", () => {
false,
undefined, // mcpHub
undefined, // diffStrategy
undefined, // browserViewportSize
defaultModeSlug, // mode
undefined, // customModePrompts
undefined, // customModes
@ -586,7 +536,6 @@ describe("SYSTEM_PROMPT", () => {
false,
undefined, // mcpHub
undefined, // diffStrategy
undefined, // browserViewportSize
defaultModeSlug, // mode
undefined, // customModePrompts
undefined, // customModes

View file

@ -45,7 +45,6 @@ async function generatePrompt(
mode: Mode,
mcpHub?: McpHub,
diffStrategy?: DiffStrategy,
browserViewportSize?: string,
promptComponent?: PromptComponent,
customModeConfigs?: ModeConfig[],
globalCustomInstructions?: string,
@ -116,7 +115,6 @@ export const SYSTEM_PROMPT = async (
supportsComputerUse: boolean,
mcpHub?: McpHub,
diffStrategy?: DiffStrategy,
browserViewportSize?: string,
mode: Mode = defaultModeSlug,
customModePrompts?: CustomModePrompts,
customModes?: ModeConfig[],
@ -146,7 +144,6 @@ export const SYSTEM_PROMPT = async (
currentMode.slug,
mcpHub,
diffStrategy,
browserViewportSize,
promptComponent,
customModes,
globalCustomInstructions,

View file

@ -20,21 +20,19 @@ describe("filterNativeToolsForMode - disabledTools", () => {
makeTool("execute_command"),
makeTool("read_file"),
makeTool("write_to_file"),
makeTool("browser_action"),
makeTool("apply_diff"),
makeTool("edit"),
]
it("removes tools listed in settings.disabledTools", () => {
const settings = {
disabledTools: ["execute_command", "browser_action"],
disabledTools: ["execute_command"],
}
const result = filterNativeToolsForMode(nativeTools, "code", undefined, undefined, undefined, settings)
const resultNames = result.map((t) => (t as any).function.name)
expect(resultNames).not.toContain("execute_command")
expect(resultNames).not.toContain("browser_action")
expect(resultNames).toContain("read_file")
expect(resultNames).toContain("write_to_file")
expect(resultNames).toContain("apply_diff")
@ -51,7 +49,6 @@ describe("filterNativeToolsForMode - disabledTools", () => {
expect(resultNames).toContain("execute_command")
expect(resultNames).toContain("read_file")
expect(resultNames).toContain("write_to_file")
expect(resultNames).toContain("browser_action")
expect(resultNames).toContain("apply_diff")
})
@ -67,7 +64,6 @@ describe("filterNativeToolsForMode - disabledTools", () => {
it("combines disabledTools with other setting-based exclusions", () => {
const settings = {
browserToolEnabled: false,
disabledTools: ["execute_command"],
}
@ -75,7 +71,6 @@ describe("filterNativeToolsForMode - disabledTools", () => {
const resultNames = result.map((t) => (t as any).function.name)
expect(resultNames).not.toContain("execute_command")
expect(resultNames).not.toContain("browser_action")
expect(resultNames).toContain("read_file")
})

View file

@ -291,11 +291,6 @@ export function filterNativeToolsForMode(
allowedToolNames.delete("run_slash_command")
}
// Conditionally exclude browser_action if disabled in settings
if (settings?.browserToolEnabled === false) {
allowedToolNames.delete("browser_action")
}
// Remove tools that are explicitly disabled via the disabledTools setting
if (settings?.disabledTools?.length) {
for (const toolName of settings.disabledTools) {
@ -387,11 +382,6 @@ export function isToolAllowedInMode(
return true
}
// Check for browser_action being disabled by user settings
if (toolName === "browser_action" && settings?.browserToolEnabled === false) {
return false
}
// Check if the tool is allowed by the mode's groups
// Resolve to canonical name and check that single value
const canonicalTool = resolveToolAlias(toolName)

Some files were not shown because too many files have changed in this diff Show more