mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
Merge remote-tracking branch 'origin/main' into feat/eval-recommendations
This commit is contained in:
commit
132f7af6ca
234 changed files with 4510 additions and 9543 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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<{
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ export const runTaskWithCli = async ({ run, task, publish, logger, jobToken }: R
|
|||
promptSourcePath,
|
||||
"--workspace",
|
||||
workspacePath,
|
||||
"--yes",
|
||||
"--reasoning-effort",
|
||||
"disabled",
|
||||
"--oneshot",
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
424
pnpm-lock.yaml
generated
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
]),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -178,6 +178,8 @@ describe("RequestyHandler", () => {
|
|||
cacheReadTokens: 2,
|
||||
reasoningTokens: undefined,
|
||||
totalCost: expect.any(Number),
|
||||
totalInputTokens: 10,
|
||||
totalOutputTokens: 20,
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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)}`
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -475,6 +475,8 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan
|
|||
type: "usage",
|
||||
inputTokens: totalInputTokens,
|
||||
outputTokens: totalOutputTokens,
|
||||
totalInputTokens,
|
||||
totalOutputTokens,
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
this.ensureCleanState()
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
274
src/api/transform/__tests__/cache-breakpoints.spec.ts
Normal file
274
src/api/transform/__tests__/cache-breakpoints.spec.ts
Normal 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")
|
||||
})
|
||||
})
|
||||
126
src/api/transform/cache-breakpoints.ts
Normal file
126
src/api/transform/cache-breakpoints.ts
Normal 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 }
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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'")
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue