Track edit_file variant tool usage in analytics

This commit is contained in:
Hannes Rudolph 2025-12-21 17:50:03 -07:00
parent 0126cb0b15
commit 33f3e402e5
24 changed files with 506 additions and 414 deletions

View file

@ -15,8 +15,8 @@ export const geminiModels = {
supportsPromptCache: true,
supportsReasoningEffort: ["low", "high"],
reasoningEffort: "low",
includedTools: ["write_file", "edit_file"],
excludedTools: ["apply_diff"],
editToolVariant: "gemini",
includedTools: ["write_file"],
supportsTemperature: true,
defaultTemperature: 1,
inputPrice: 4.0,
@ -43,8 +43,8 @@ export const geminiModels = {
supportsPromptCache: true,
supportsReasoningEffort: ["minimal", "low", "medium", "high"],
reasoningEffort: "medium",
includedTools: ["write_file", "edit_file"],
excludedTools: ["apply_diff"],
editToolVariant: "gemini",
includedTools: ["write_file"],
supportsTemperature: true,
defaultTemperature: 1,
inputPrice: 0.3,
@ -60,8 +60,8 @@ export const geminiModels = {
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
includedTools: ["write_file", "edit_file"],
excludedTools: ["apply_diff"],
editToolVariant: "gemini",
includedTools: ["write_file"],
inputPrice: 2.5, // This is the pricing for prompts above 200k tokens.
outputPrice: 15,
cacheReadsPrice: 0.625,
@ -91,8 +91,8 @@ export const geminiModels = {
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
includedTools: ["write_file", "edit_file"],
excludedTools: ["apply_diff"],
editToolVariant: "gemini",
includedTools: ["write_file"],
inputPrice: 2.5, // This is the pricing for prompts above 200k tokens.
outputPrice: 15,
cacheReadsPrice: 0.625,
@ -121,8 +121,8 @@ export const geminiModels = {
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
includedTools: ["write_file", "edit_file"],
excludedTools: ["apply_diff"],
editToolVariant: "gemini",
includedTools: ["write_file"],
inputPrice: 2.5, // This is the pricing for prompts above 200k tokens.
outputPrice: 15,
cacheReadsPrice: 0.625,
@ -149,8 +149,8 @@ export const geminiModels = {
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
includedTools: ["write_file", "edit_file"],
excludedTools: ["apply_diff"],
editToolVariant: "gemini",
includedTools: ["write_file"],
inputPrice: 2.5, // This is the pricing for prompts above 200k tokens.
outputPrice: 15,
cacheReadsPrice: 0.625,
@ -181,8 +181,8 @@ export const geminiModels = {
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
includedTools: ["write_file", "edit_file"],
excludedTools: ["apply_diff"],
editToolVariant: "gemini",
includedTools: ["write_file"],
inputPrice: 0.3,
outputPrice: 2.5,
cacheReadsPrice: 0.075,
@ -197,8 +197,8 @@ export const geminiModels = {
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
includedTools: ["write_file", "edit_file"],
excludedTools: ["apply_diff"],
editToolVariant: "gemini",
includedTools: ["write_file"],
inputPrice: 0.3,
outputPrice: 2.5,
cacheReadsPrice: 0.075,
@ -213,8 +213,8 @@ export const geminiModels = {
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
includedTools: ["write_file", "edit_file"],
excludedTools: ["apply_diff"],
editToolVariant: "gemini",
includedTools: ["write_file"],
inputPrice: 0.3,
outputPrice: 2.5,
cacheReadsPrice: 0.075,
@ -231,8 +231,8 @@ export const geminiModels = {
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
includedTools: ["write_file", "edit_file"],
excludedTools: ["apply_diff"],
editToolVariant: "gemini",
includedTools: ["write_file"],
inputPrice: 0.1,
outputPrice: 0.4,
cacheReadsPrice: 0.025,
@ -247,8 +247,8 @@ export const geminiModels = {
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
includedTools: ["write_file", "edit_file"],
excludedTools: ["apply_diff"],
editToolVariant: "gemini",
includedTools: ["write_file"],
inputPrice: 0.1,
outputPrice: 0.4,
cacheReadsPrice: 0.025,

View file

@ -15,8 +15,7 @@ export const minimaxModels = {
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["search_and_replace"],
excludedTools: ["apply_diff"],
editToolVariant: "anthropic",
preserveReasoning: true,
inputPrice: 0.3,
outputPrice: 1.2,
@ -32,8 +31,7 @@ export const minimaxModels = {
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["search_and_replace"],
excludedTools: ["apply_diff"],
editToolVariant: "anthropic",
preserveReasoning: true,
inputPrice: 0.3,
outputPrice: 1.2,
@ -49,8 +47,7 @@ export const minimaxModels = {
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["search_and_replace"],
excludedTools: ["apply_diff"],
editToolVariant: "anthropic",
preserveReasoning: true,
inputPrice: 0.3,
outputPrice: 1.2,

View file

@ -11,8 +11,8 @@ export const openAiNativeModels = {
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
editToolVariant: "codex",
excludedTools: ["write_to_file"],
supportsImages: true,
supportsPromptCache: true,
promptCacheRetention: "24h",
@ -31,8 +31,8 @@ export const openAiNativeModels = {
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
editToolVariant: "codex",
excludedTools: ["write_to_file"],
supportsImages: true,
supportsPromptCache: true,
promptCacheRetention: "24h",
@ -54,8 +54,8 @@ export const openAiNativeModels = {
contextWindow: 128_000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
editToolVariant: "codex",
excludedTools: ["write_to_file"],
supportsImages: true,
supportsPromptCache: true,
inputPrice: 1.75,
@ -68,8 +68,8 @@ export const openAiNativeModels = {
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
editToolVariant: "codex",
excludedTools: ["write_to_file"],
supportsImages: true,
supportsPromptCache: true,
promptCacheRetention: "24h",
@ -91,8 +91,8 @@ export const openAiNativeModels = {
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
editToolVariant: "codex",
excludedTools: ["write_to_file"],
supportsImages: true,
supportsPromptCache: true,
promptCacheRetention: "24h",
@ -110,8 +110,8 @@ export const openAiNativeModels = {
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
editToolVariant: "codex",
excludedTools: ["write_to_file"],
supportsImages: true,
supportsPromptCache: true,
promptCacheRetention: "24h",
@ -128,8 +128,8 @@ export const openAiNativeModels = {
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
editToolVariant: "codex",
excludedTools: ["write_to_file"],
supportsImages: true,
supportsPromptCache: true,
supportsReasoningEffort: ["minimal", "low", "medium", "high"],
@ -150,8 +150,8 @@ export const openAiNativeModels = {
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
editToolVariant: "codex",
excludedTools: ["write_to_file"],
supportsImages: true,
supportsPromptCache: true,
supportsReasoningEffort: ["minimal", "low", "medium", "high"],
@ -172,8 +172,8 @@ export const openAiNativeModels = {
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
editToolVariant: "codex",
excludedTools: ["write_to_file"],
supportsImages: true,
supportsPromptCache: true,
supportsReasoningEffort: ["low", "medium", "high"],
@ -190,8 +190,8 @@ export const openAiNativeModels = {
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
editToolVariant: "codex",
excludedTools: ["write_to_file"],
supportsImages: true,
supportsPromptCache: true,
supportsReasoningEffort: ["minimal", "low", "medium", "high"],
@ -209,8 +209,8 @@ export const openAiNativeModels = {
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
editToolVariant: "codex",
excludedTools: ["write_to_file"],
supportsImages: true,
supportsPromptCache: true,
inputPrice: 1.25,
@ -223,8 +223,8 @@ export const openAiNativeModels = {
contextWindow: 1_047_576,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
editToolVariant: "codex",
excludedTools: ["write_to_file"],
supportsImages: true,
supportsPromptCache: true,
inputPrice: 2,
@ -240,8 +240,8 @@ export const openAiNativeModels = {
contextWindow: 1_047_576,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
editToolVariant: "codex",
excludedTools: ["write_to_file"],
supportsImages: true,
supportsPromptCache: true,
inputPrice: 0.4,
@ -257,8 +257,8 @@ export const openAiNativeModels = {
contextWindow: 1_047_576,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
editToolVariant: "codex",
excludedTools: ["write_to_file"],
supportsImages: true,
supportsPromptCache: true,
inputPrice: 0.1,
@ -483,8 +483,8 @@ export const openAiNativeModels = {
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
editToolVariant: "codex",
excludedTools: ["write_to_file"],
supportsImages: true,
supportsPromptCache: true,
supportsReasoningEffort: ["minimal", "low", "medium", "high"],
@ -505,8 +505,8 @@ export const openAiNativeModels = {
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
editToolVariant: "codex",
excludedTools: ["write_to_file"],
supportsImages: true,
supportsPromptCache: true,
supportsReasoningEffort: ["minimal", "low", "medium", "high"],
@ -527,8 +527,8 @@ export const openAiNativeModels = {
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
editToolVariant: "codex",
excludedTools: ["write_to_file"],
supportsImages: true,
supportsPromptCache: true,
supportsReasoningEffort: ["minimal", "low", "medium", "high"],

View file

@ -15,8 +15,8 @@ export const vertexModels = {
supportsPromptCache: true,
supportsReasoningEffort: ["low", "high"],
reasoningEffort: "low",
includedTools: ["write_file", "edit_file"],
excludedTools: ["apply_diff"],
editToolVariant: "gemini",
includedTools: ["write_file"],
supportsTemperature: true,
defaultTemperature: 1,
inputPrice: 4.0,
@ -43,8 +43,8 @@ export const vertexModels = {
supportsPromptCache: true,
supportsReasoningEffort: ["minimal", "low", "medium", "high"],
reasoningEffort: "medium",
includedTools: ["write_file", "edit_file"],
excludedTools: ["apply_diff"],
editToolVariant: "gemini",
includedTools: ["write_file"],
supportsTemperature: true,
defaultTemperature: 1,
inputPrice: 0.3,
@ -59,8 +59,8 @@ export const vertexModels = {
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
includedTools: ["write_file", "edit_file"],
excludedTools: ["apply_diff"],
editToolVariant: "gemini",
includedTools: ["write_file"],
inputPrice: 0.15,
outputPrice: 3.5,
maxThinkingTokens: 24_576,
@ -74,8 +74,8 @@ export const vertexModels = {
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
includedTools: ["write_file", "edit_file"],
excludedTools: ["apply_diff"],
editToolVariant: "gemini",
includedTools: ["write_file"],
inputPrice: 0.15,
outputPrice: 0.6,
},
@ -86,8 +86,8 @@ export const vertexModels = {
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
includedTools: ["write_file", "edit_file"],
excludedTools: ["apply_diff"],
editToolVariant: "gemini",
includedTools: ["write_file"],
inputPrice: 0.3,
outputPrice: 2.5,
cacheReadsPrice: 0.075,
@ -102,8 +102,8 @@ export const vertexModels = {
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: false,
includedTools: ["write_file", "edit_file"],
excludedTools: ["apply_diff"],
editToolVariant: "gemini",
includedTools: ["write_file"],
inputPrice: 0.15,
outputPrice: 3.5,
maxThinkingTokens: 24_576,
@ -117,8 +117,8 @@ export const vertexModels = {
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: false,
includedTools: ["write_file", "edit_file"],
excludedTools: ["apply_diff"],
editToolVariant: "gemini",
includedTools: ["write_file"],
inputPrice: 0.15,
outputPrice: 0.6,
},
@ -129,8 +129,8 @@ export const vertexModels = {
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
includedTools: ["write_file", "edit_file"],
excludedTools: ["apply_diff"],
editToolVariant: "gemini",
includedTools: ["write_file"],
inputPrice: 2.5,
outputPrice: 15,
},
@ -141,8 +141,8 @@ export const vertexModels = {
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
includedTools: ["write_file", "edit_file"],
excludedTools: ["apply_diff"],
editToolVariant: "gemini",
includedTools: ["write_file"],
inputPrice: 2.5,
outputPrice: 15,
},
@ -153,8 +153,8 @@ export const vertexModels = {
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
includedTools: ["write_file", "edit_file"],
excludedTools: ["apply_diff"],
editToolVariant: "gemini",
includedTools: ["write_file"],
inputPrice: 2.5,
outputPrice: 15,
maxThinkingTokens: 32_768,
@ -167,8 +167,8 @@ export const vertexModels = {
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
includedTools: ["write_file", "edit_file"],
excludedTools: ["apply_diff"],
editToolVariant: "gemini",
includedTools: ["write_file"],
inputPrice: 2.5,
outputPrice: 15,
maxThinkingTokens: 32_768,
@ -196,8 +196,8 @@ export const vertexModels = {
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: false,
includedTools: ["write_file", "edit_file"],
excludedTools: ["apply_diff"],
editToolVariant: "gemini",
includedTools: ["write_file"],
inputPrice: 0,
outputPrice: 0,
},
@ -208,8 +208,8 @@ export const vertexModels = {
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: false,
includedTools: ["write_file", "edit_file"],
excludedTools: ["apply_diff"],
editToolVariant: "gemini",
includedTools: ["write_file"],
inputPrice: 0,
outputPrice: 0,
},
@ -220,8 +220,8 @@ export const vertexModels = {
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
includedTools: ["write_file", "edit_file"],
excludedTools: ["apply_diff"],
editToolVariant: "gemini",
includedTools: ["write_file"],
inputPrice: 0.15,
outputPrice: 0.6,
},
@ -232,8 +232,8 @@ export const vertexModels = {
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: false,
includedTools: ["write_file", "edit_file"],
excludedTools: ["apply_diff"],
editToolVariant: "gemini",
includedTools: ["write_file"],
inputPrice: 0.075,
outputPrice: 0.3,
},
@ -244,8 +244,8 @@ export const vertexModels = {
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: false,
includedTools: ["write_file", "edit_file"],
excludedTools: ["apply_diff"],
editToolVariant: "gemini",
includedTools: ["write_file"],
inputPrice: 0,
outputPrice: 0,
},
@ -256,8 +256,8 @@ export const vertexModels = {
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
includedTools: ["write_file", "edit_file"],
excludedTools: ["apply_diff"],
editToolVariant: "gemini",
includedTools: ["write_file"],
inputPrice: 0.075,
outputPrice: 0.3,
},
@ -268,8 +268,8 @@ export const vertexModels = {
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: false,
includedTools: ["write_file", "edit_file"],
excludedTools: ["apply_diff"],
editToolVariant: "gemini",
includedTools: ["write_file"],
inputPrice: 1.25,
outputPrice: 5,
},
@ -463,8 +463,8 @@ export const vertexModels = {
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
includedTools: ["write_file", "edit_file"],
excludedTools: ["apply_diff"],
editToolVariant: "gemini",
includedTools: ["write_file"],
inputPrice: 0.1,
outputPrice: 0.4,
cacheReadsPrice: 0.025,

View file

@ -18,8 +18,7 @@ export const xaiModels = {
cacheWritesPrice: 0.02,
cacheReadsPrice: 0.02,
description: "xAI's Grok Code Fast model with 256K context window",
includedTools: ["search_replace"],
excludedTools: ["apply_diff"],
editToolVariant: "grok",
},
"grok-4-1-fast-reasoning": {
maxTokens: 65_536,
@ -34,8 +33,7 @@ export const xaiModels = {
cacheReadsPrice: 0.05,
description:
"xAI's Grok 4.1 Fast model with 2M context window, optimized for high-performance agentic tool calling with reasoning",
includedTools: ["search_replace"],
excludedTools: ["apply_diff"],
editToolVariant: "grok",
},
"grok-4-1-fast-non-reasoning": {
maxTokens: 65_536,
@ -50,8 +48,7 @@ export const xaiModels = {
cacheReadsPrice: 0.05,
description:
"xAI's Grok 4.1 Fast model with 2M context window, optimized for high-performance agentic tool calling",
includedTools: ["search_replace"],
excludedTools: ["apply_diff"],
editToolVariant: "grok",
},
"grok-4-fast-reasoning": {
maxTokens: 65_536,
@ -66,8 +63,7 @@ export const xaiModels = {
cacheReadsPrice: 0.05,
description:
"xAI's Grok 4 Fast model with 2M context window, optimized for high-performance agentic tool calling with reasoning",
includedTools: ["search_replace"],
excludedTools: ["apply_diff"],
editToolVariant: "grok",
},
"grok-4-fast-non-reasoning": {
maxTokens: 65_536,
@ -82,8 +78,7 @@ export const xaiModels = {
cacheReadsPrice: 0.05,
description:
"xAI's Grok 4 Fast model with 2M context window, optimized for high-performance agentic tool calling",
includedTools: ["search_replace"],
excludedTools: ["apply_diff"],
editToolVariant: "grok",
},
"grok-4-0709": {
maxTokens: 8192,
@ -97,8 +92,7 @@ export const xaiModels = {
cacheWritesPrice: 0.75,
cacheReadsPrice: 0.75,
description: "xAI's Grok-4 model with 256K context window",
includedTools: ["search_replace"],
excludedTools: ["apply_diff"],
editToolVariant: "grok",
},
"grok-3-mini": {
maxTokens: 8192,
@ -114,8 +108,7 @@ export const xaiModels = {
description: "xAI's Grok-3 mini model with 128K context window",
supportsReasoningEffort: ["low", "high"],
reasoningEffort: "low",
includedTools: ["search_replace"],
excludedTools: ["apply_diff"],
editToolVariant: "grok",
},
"grok-3": {
maxTokens: 8192,
@ -129,7 +122,6 @@ export const xaiModels = {
cacheWritesPrice: 0.75,
cacheReadsPrice: 0.75,
description: "xAI's Grok-3 model with 128K context window",
includedTools: ["search_replace"],
excludedTools: ["apply_diff"],
editToolVariant: "grok",
},
} as const satisfies Record<string, ModelInfo>

View file

@ -30,13 +30,11 @@ export const toolNames = [
"execute_command",
"read_file",
"write_to_file",
// Legacy edit tool names (deprecated, use edit_file_* variants)
// apply_diff is kept for XML protocol backward compatibility
"apply_diff",
"search_and_replace",
"search_replace",
// Unified edit tool for native protocol (variant selected via modelInfo.editToolVariant)
"edit_file",
"apply_patch",
// New edit tool variant names
// Internal edit tool variant names (used by native protocol, presented to LLM as "edit_file")
"edit_file_roo",
"edit_file_anthropic",
"edit_file_grok",

View file

@ -161,7 +161,7 @@ describe("OpenRouterHandler", () => {
expect(result.temperature).toBe(0)
})
it("adds excludedTools and includedTools for OpenAI models", async () => {
it("sets editToolVariant to codex for OpenAI models", async () => {
const handler = new OpenRouterHandler({
openRouterApiKey: "test-key",
openRouterModelId: "openai/gpt-4o",
@ -169,12 +169,10 @@ describe("OpenRouterHandler", () => {
const result = await handler.fetchModel()
expect(result.id).toBe("openai/gpt-4o")
expect(result.info.excludedTools).toContain("apply_diff")
expect(result.info.excludedTools).toContain("write_to_file")
expect(result.info.includedTools).toContain("apply_patch")
expect(result.info.editToolVariant).toBe("codex")
})
it("merges excludedTools and includedTools with existing values for OpenAI models", async () => {
it("sets editToolVariant to codex for OpenAI models while preserving existing excludedTools/includedTools", async () => {
const handler = new OpenRouterHandler({
openRouterApiKey: "test-key",
openRouterModelId: "openai/o1",
@ -182,18 +180,14 @@ describe("OpenRouterHandler", () => {
const result = await handler.fetchModel()
expect(result.id).toBe("openai/o1")
// Should have the new exclusions
expect(result.info.excludedTools).toContain("apply_diff")
expect(result.info.excludedTools).toContain("write_to_file")
// Should preserve existing exclusions
// Should have editToolVariant set
expect(result.info.editToolVariant).toBe("codex")
// Should preserve existing exclusions/inclusions from model info
expect(result.info.excludedTools).toContain("existing_excluded")
// Should have the new inclusions
expect(result.info.includedTools).toContain("apply_patch")
// Should preserve existing inclusions
expect(result.info.includedTools).toContain("existing_included")
})
it("does not add excludedTools or includedTools for non-OpenAI models", async () => {
it("does not set editToolVariant for non-OpenAI models", async () => {
const handler = new OpenRouterHandler({
openRouterApiKey: "test-key",
openRouterModelId: "anthropic/claude-sonnet-4",
@ -201,9 +195,8 @@ describe("OpenRouterHandler", () => {
const result = await handler.fetchModel()
expect(result.id).toBe("anthropic/claude-sonnet-4")
// Should NOT have the tool exclusions/inclusions
expect(result.info.excludedTools).toBeUndefined()
expect(result.info.includedTools).toBeUndefined()
// Should NOT have editToolVariant set
expect(result.info.editToolVariant).toBeUndefined()
})
})

View file

@ -741,8 +741,8 @@ describe("getRooModels", () => {
output: "0.0002",
},
settings: {
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
editToolVariant: "codex",
excludedTools: ["write_to_file"],
reasoningEffort: "high",
},
},
@ -756,8 +756,8 @@ describe("getRooModels", () => {
const models = await getRooModels(baseUrl, apiKey)
expect(models["test/model-with-settings"].includedTools).toEqual(["apply_patch"])
expect(models["test/model-with-settings"].excludedTools).toEqual(["apply_diff", "write_to_file"])
expect(models["test/model-with-settings"].editToolVariant).toBe("codex")
expect(models["test/model-with-settings"].excludedTools).toEqual(["write_to_file"])
expect(models["test/model-with-settings"].reasoningEffort).toBe("high")
})
@ -824,14 +824,14 @@ describe("getRooModels", () => {
},
// Plain settings for backward compatibility with old clients
settings: {
includedTools: ["apply_patch"],
editToolVariant: "codex",
excludedTools: ["write_to_file"],
},
// Versioned settings keyed by version number (low version - always met)
versionedSettings: {
"1.0.0": {
includedTools: ["apply_patch", "search_replace"],
excludedTools: ["apply_diff", "write_to_file"],
editToolVariant: "grok",
excludedTools: ["write_to_file", "browser_action"],
},
},
},
@ -846,8 +846,8 @@ describe("getRooModels", () => {
const models = await getRooModels(baseUrl, apiKey)
// Versioned settings should be used instead of plain settings
expect(models["test/versioned-model"].includedTools).toEqual(["apply_patch", "search_replace"])
expect(models["test/versioned-model"].excludedTools).toEqual(["apply_diff", "write_to_file"])
expect(models["test/versioned-model"].editToolVariant).toBe("grok")
expect(models["test/versioned-model"].excludedTools).toEqual(["write_to_file", "browser_action"])
})
it("should use plain settings when no versioned settings version matches", async () => {
@ -870,12 +870,12 @@ describe("getRooModels", () => {
output: "0.0002",
},
settings: {
includedTools: ["apply_patch"],
editToolVariant: "codex",
},
// Versioned settings keyed by very high version - never met
versionedSettings: {
"99.0.0": {
includedTools: ["apply_patch", "search_replace"],
editToolVariant: "grok",
},
},
},
@ -890,7 +890,7 @@ describe("getRooModels", () => {
const models = await getRooModels(baseUrl, apiKey)
// Should use plain settings since no versioned settings match current version
expect(models["test/old-version-model"].includedTools).toEqual(["apply_patch"])
expect(models["test/old-version-model"].editToolVariant).toBe("codex")
})
it("should handle model with only versionedSettings and no plain settings", async () => {

View file

@ -3,12 +3,13 @@ import type { ModelInfo } from "@roo-code/types"
/**
* Apply tool preferences for models accessed through dynamic routers (OpenRouter, Requesty).
*
* Different model families perform better with specific tools:
* - OpenAI models: Better results with apply_patch instead of apply_diff/write_to_file
* - Gemini models: Higher quality results with write_file and edit_file
* Different model families perform better with specific edit tool schemas:
* - OpenAI models: Better results with the codex (patch) format
* - Gemini models: Higher quality results with the gemini (search/replace) format
* - xAI/Grok models: Better results with the grok format
*
* This function modifies the model info to apply these preferences consistently
* across all dynamic router providers.
* across all dynamic router providers via `editToolVariant`.
*
* @param modelId The model identifier (e.g., "openai/gpt-4", "google/gemini-2.5-pro")
* @param info The original model info object
@ -17,23 +18,32 @@ import type { ModelInfo } from "@roo-code/types"
export function applyRouterToolPreferences(modelId: string, info: ModelInfo): ModelInfo {
let result = info
// For OpenAI models via routers, exclude write_to_file and apply_diff, and include apply_patch
// For OpenAI models via routers, use codex variant and exclude write_to_file
// This matches the behavior of the native OpenAI provider
if (modelId.includes("openai")) {
result = {
...result,
excludedTools: [...new Set([...(result.excludedTools || []), "apply_diff", "write_to_file"])],
includedTools: [...new Set([...(result.includedTools || []), "apply_patch"])],
editToolVariant: result.editToolVariant ?? "codex",
excludedTools: [...new Set([...(result.excludedTools || []), "write_to_file"])],
}
}
// For Gemini models via routers, include write_file and edit_file
// For Gemini models via routers, use gemini variant
// This matches the behavior of the native Gemini provider
if (modelId.includes("gemini")) {
result = {
...result,
excludedTools: [...new Set([...(result.excludedTools || []), "apply_diff"])],
includedTools: [...new Set([...(result.includedTools || []), "write_file", "edit_file"])],
editToolVariant: result.editToolVariant ?? "gemini",
includedTools: [...new Set([...(result.includedTools || []), "write_file"])],
}
}
// For xAI/Grok models via routers, use grok variant
// This matches the behavior of the native xAI provider
if (modelId.includes("grok") || modelId.includes("xai")) {
result = {
...result,
editToolVariant: result.editToolVariant ?? "grok",
}
}

View file

@ -497,43 +497,36 @@ export class NativeToolCallParser {
}
break
case "apply_patch":
if (partialArgs.patch !== undefined) {
case "edit_file":
// Handle unified edit_file with variant-aware argument detection
// Detect variant based on which arguments are present:
// - roo variant: { path, diff }
// - anthropic variant: { path, edits: [{ old_text, new_text }] }
// - grok/gemini variant: { file_path, old_string, new_string }
// - codex variant: { patch }
if (partialArgs.diff !== undefined) {
// Roo variant (apply_diff format)
nativeArgs = {
patch: partialArgs.patch,
path: partialArgs.path,
diff: partialArgs.diff,
}
}
break
case "search_replace":
if (
partialArgs.file_path !== undefined ||
partialArgs.old_string !== undefined ||
partialArgs.new_string !== undefined
) {
nativeArgs = {
file_path: partialArgs.file_path,
old_string: partialArgs.old_string,
new_string: partialArgs.new_string,
}
}
break
case "search_and_replace":
if (partialArgs.path !== undefined || partialArgs.edits !== undefined) {
} else if (partialArgs.edits !== undefined) {
// Anthropic variant (search_and_replace format)
nativeArgs = {
path: partialArgs.path,
edits: partialArgs.edits,
}
}
break
case "edit_file":
if (
} else if (partialArgs.patch !== undefined) {
// Codex variant (apply_patch format)
nativeArgs = {
patch: partialArgs.patch,
}
} else if (
partialArgs.file_path !== undefined ||
partialArgs.old_string !== undefined ||
partialArgs.new_string !== undefined
) {
// Grok/Gemini variant (search_replace/edit_file format)
nativeArgs = {
file_path: partialArgs.file_path,
old_string: partialArgs.old_string,
@ -660,15 +653,6 @@ export class NativeToolCallParser {
}
break
case "search_and_replace":
if (args.path !== undefined && args.edits !== undefined && Array.isArray(args.edits)) {
nativeArgs = {
path: args.path,
edits: args.edits,
} as NativeArgsFor<TName>
}
break
case "ask_followup_question":
if (args.question !== undefined && args.follow_up !== undefined) {
nativeArgs = {
@ -782,34 +766,36 @@ export class NativeToolCallParser {
}
break
case "apply_patch":
if (args.patch !== undefined) {
case "edit_file":
// Handle unified edit_file with variant-aware argument detection
// Detect variant based on which arguments are present:
// - roo variant: { path, diff }
// - anthropic variant: { path, edits: [{ old_text, new_text }] }
// - grok/gemini variant: { file_path, old_string, new_string }
// - codex variant: { patch }
if (args.diff !== undefined && args.path !== undefined) {
// Roo variant (apply_diff format)
nativeArgs = {
path: args.path,
diff: args.diff,
} as NativeArgsFor<TName>
} else if (args.edits !== undefined && Array.isArray(args.edits) && args.path !== undefined) {
// Anthropic variant (search_and_replace format)
nativeArgs = {
path: args.path,
edits: args.edits,
} as NativeArgsFor<TName>
} else if (args.patch !== undefined) {
// Codex variant (apply_patch format)
nativeArgs = {
patch: args.patch,
} as NativeArgsFor<TName>
}
break
case "search_replace":
if (
args.file_path !== undefined &&
args.old_string !== undefined &&
args.new_string !== undefined
) {
nativeArgs = {
file_path: args.file_path,
old_string: args.old_string,
new_string: args.new_string,
} as NativeArgsFor<TName>
}
break
case "edit_file":
if (
} else if (
args.file_path !== undefined &&
args.old_string !== undefined &&
args.new_string !== undefined
) {
// Grok/Gemini variant (search_replace/edit_file format)
nativeArgs = {
file_path: args.file_path,
old_string: args.old_string,

View file

@ -0,0 +1,144 @@
// npx vitest run core/assistant-message/__tests__/presentAssistantMessage-tool-usage-variants.spec.ts
import { describe, it, expect, beforeEach, vi } from "vitest"
vi.mock("../../tools/validateToolUse", () => ({
validateToolUse: vi.fn(),
}))
vi.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureToolUsage: vi.fn(),
captureConsecutiveMistakeError: vi.fn(),
captureException: vi.fn(),
},
},
}))
const { mockEditToolHandle } = vi.hoisted(() => ({
mockEditToolHandle: vi.fn(async (_task: unknown, _toolUse: unknown, callbacks: any) => {
callbacks.pushToolResult("ok")
}),
}))
vi.mock("../../tools/EditFileRooTool", () => ({
editFileRooTool: { handle: mockEditToolHandle },
}))
vi.mock("../../tools/EditFileAnthropicTool", () => ({
editFileAnthropicTool: { handle: mockEditToolHandle },
}))
vi.mock("../../tools/EditFileGrokTool", () => ({
editFileGrokTool: { handle: mockEditToolHandle },
}))
vi.mock("../../tools/EditFileGeminiTool", () => ({
editFileGeminiTool: { handle: mockEditToolHandle },
}))
vi.mock("../../tools/EditFileCodexTool", () => ({
editFileCodexTool: { handle: mockEditToolHandle },
}))
// Import AFTER mocks
import { TelemetryService } from "@roo-code/telemetry"
import { presentAssistantMessage } from "../presentAssistantMessage"
describe("presentAssistantMessage - tool usage analytics names", () => {
let mockTask: any
beforeEach(() => {
vi.clearAllMocks()
mockTask = {
taskId: "test-task-id",
instanceId: "test-instance",
abort: false,
presentAssistantMessageLocked: false,
presentAssistantMessageHasPendingUpdates: false,
currentStreamingContentIndex: 0,
assistantMessageContent: [],
userMessageContent: [],
didCompleteReadingStream: false,
didRejectTool: false,
didAlreadyUseTool: false,
diffEnabled: false,
consecutiveMistakeCount: 0,
consecutiveMistakeLimit: 3,
clineMessages: [],
currentStreamingDidCheckpoint: false,
checkpointSave: vi.fn().mockResolvedValue(undefined),
apiConfiguration: { apiProvider: "openai" },
api: {
getModel: () => ({ id: "test-model", info: { editToolVariant: "roo" } }),
},
browserSession: {
closeBrowser: vi.fn().mockResolvedValue(undefined),
},
recordToolUsage: vi.fn(),
recordToolError: vi.fn(),
toolRepetitionDetector: {
check: vi.fn().mockReturnValue({ allowExecution: true }),
},
providerRef: {
deref: () => ({
getState: vi.fn().mockResolvedValue({ mode: "code", customModes: [], experiments: {} }),
}),
},
say: vi.fn().mockResolvedValue(undefined),
ask: vi.fn().mockResolvedValue({ response: "yesButtonClicked" }),
}
})
it.each([
["roo", "edit_file_roo"],
["anthropic", "edit_file_anthropic"],
["grok", "edit_file_grok"],
["gemini", "edit_file_gemini"],
["codex", "edit_file_codex"],
] as const)("records derived analytics tool name for edit_file (%s)", async (variant, expected) => {
mockTask.api.getModel = () => ({ id: "test-model", info: { editToolVariant: variant } })
mockTask.assistantMessageContent = [
{
type: "tool_use",
id: "tool_call_123",
name: "edit_file",
params: { path: "file.txt" },
partial: false,
},
]
await presentAssistantMessage(mockTask)
expect(mockTask.recordToolUsage).toHaveBeenCalledWith(expected)
expect(TelemetryService.instance.captureToolUsage).toHaveBeenCalledWith(
mockTask.taskId,
expected,
expect.any(String),
)
})
it("does not change other tool names", async () => {
mockTask.assistantMessageContent = [
{
type: "tool_use",
id: "tool_call_456",
name: "read_file",
params: { path: "file.txt" },
partial: false,
},
]
await presentAssistantMessage(mockTask)
expect(mockTask.recordToolUsage).toHaveBeenCalledWith("read_file")
expect(TelemetryService.instance.captureToolUsage).toHaveBeenCalledWith(
mockTask.taskId,
"read_file",
expect.any(String),
)
})
})

View file

@ -22,10 +22,10 @@ import { readFileTool } from "../tools/ReadFileTool"
import { TOOL_PROTOCOL } from "@roo-code/types"
import { writeToFileTool } from "../tools/WriteToFileTool"
import { applyDiffTool } from "../tools/MultiApplyDiffTool"
import { searchAndReplaceTool } from "../tools/SearchAndReplaceTool"
import { searchReplaceTool } from "../tools/SearchReplaceTool"
import { editFileTool } from "../tools/EditFileTool"
import { applyPatchTool } from "../tools/ApplyPatchTool"
import { editFileAnthropicTool } from "../tools/EditFileAnthropicTool"
import { editFileGrokTool } from "../tools/EditFileGrokTool"
import { editFileGeminiTool } from "../tools/EditFileGeminiTool"
import { editFileCodexTool } from "../tools/EditFileCodexTool"
import { searchFilesTool } from "../tools/SearchFilesTool"
import { browserActionTool } from "../tools/BrowserActionTool"
import { executeCommandTool } from "../tools/ExecuteCommandTool"
@ -38,12 +38,28 @@ import { newTaskTool } from "../tools/NewTaskTool"
import { updateTodoListTool } from "../tools/UpdateTodoListTool"
import { runSlashCommandTool } from "../tools/RunSlashCommandTool"
import { generateImageTool } from "../tools/GenerateImageTool"
import { applyDiffTool as applyDiffToolClass } from "../tools/ApplyDiffTool"
import { editFileRooTool } from "../tools/EditFileRooTool"
import { validateToolUse } from "../tools/validateToolUse"
import { codebaseSearchTool } from "../tools/CodebaseSearchTool"
import { formatResponse } from "../prompts/responses"
function getAnalyticsToolNameForToolUse(toolName: ToolName, editToolVariant: EditToolVariant): ToolName {
if (toolName !== "edit_file") {
return toolName
}
const analyticsToolNameByVariant = {
roo: "edit_file_roo",
anthropic: "edit_file_anthropic",
grok: "edit_file_grok",
gemini: "edit_file_gemini",
codex: "edit_file_codex",
} satisfies Record<EditToolVariant, ToolName>
return analyticsToolNameByVariant[editToolVariant]
}
/**
* Processes and presents assistant message content to the user interface.
*
@ -395,10 +411,6 @@ export async function presentAssistantMessage(cline: Task) {
return `[${block.name} for '${block.params.regex}'${
block.params.file_pattern ? ` in '${block.params.file_pattern}'` : ""
}]`
case "search_and_replace":
return `[${block.name} for '${block.params.path}']`
case "search_replace":
return `[${block.name} for '${block.params.file_path}']`
case "edit_file": {
// Unified edit_file tool - path location depends on variant
// Gemini/Grok variants use file_path, Roo/Anthropic use path
@ -412,8 +424,6 @@ export async function presentAssistantMessage(cline: Task) {
}
return `[${block.name}]`
}
case "apply_patch":
return `[${block.name}]`
case "list_files":
return `[${block.name} for '${block.params.path}']`
case "browser_action":
@ -706,11 +716,22 @@ export async function presentAssistantMessage(cline: Task) {
}
if (!block.partial) {
// Determine the tool name for analytics
let toolNameForAnalytics: ToolName = block.name
// Check if this is a custom tool - if so, record as "custom_tool" (like MCP tools)
const isCustomTool = stateExperiments?.customTools && customToolRegistry.has(block.name)
const recordName = isCustomTool ? "custom_tool" : block.name
cline.recordToolUsage(recordName)
TelemetryService.instance.captureToolUsage(cline.taskId, recordName, toolProtocol)
if (isCustomTool) {
toolNameForAnalytics = "custom_tool"
} else if (toolNameForAnalytics === "edit_file") {
// Map edit_file to specific variant for analytics
const modelInfo = cline.api.getModel()
const editToolVariant: EditToolVariant = modelInfo?.info?.editToolVariant ?? "roo"
toolNameForAnalytics = getAnalyticsToolNameForToolUse(toolNameForAnalytics, editToolVariant)
}
cline.recordToolUsage(toolNameForAnalytics)
TelemetryService.instance.captureToolUsage(cline.taskId, toolNameForAnalytics, toolProtocol)
}
// Validate tool use before execution - ONLY for complete (non-partial) blocks.
@ -840,7 +861,7 @@ export async function presentAssistantMessage(cline: Task) {
// Check if this tool call came from native protocol by checking for ID
// Native calls always have IDs, XML calls never do
if (toolProtocol === TOOL_PROTOCOL.NATIVE) {
await applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, {
await editFileRooTool.handle(cline, block as ToolUse<"edit_file_roo">, {
askApproval,
handleError,
pushToolResult,
@ -865,7 +886,7 @@ export async function presentAssistantMessage(cline: Task) {
if (isMultiFileApplyDiffEnabled) {
await applyDiffTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
} else {
await applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, {
await editFileRooTool.handle(cline, block as ToolUse<"edit_file_roo">, {
askApproval,
handleError,
pushToolResult,
@ -875,26 +896,6 @@ export async function presentAssistantMessage(cline: Task) {
}
break
}
case "search_and_replace":
await checkpointSaveAndMark(cline)
await searchAndReplaceTool.handle(cline, block as ToolUse<"search_and_replace">, {
askApproval,
handleError,
pushToolResult,
removeClosingTag,
toolProtocol,
})
break
case "search_replace":
await checkpointSaveAndMark(cline)
await searchReplaceTool.handle(cline, block as ToolUse<"search_replace">, {
askApproval,
handleError,
pushToolResult,
removeClosingTag,
toolProtocol,
})
break
case "edit_file": {
// Unified edit_file tool - route to correct handler based on editToolVariant
await checkpointSaveAndMark(cline)
@ -903,8 +904,8 @@ export async function presentAssistantMessage(cline: Task) {
switch (editToolVariant) {
case "roo":
// Route to apply_diff handler (Roo variant)
await applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, {
// Route to Roo variant (unified diff format)
await editFileRooTool.handle(cline, block as ToolUse<"edit_file_roo">, {
askApproval,
handleError,
pushToolResult,
@ -913,8 +914,8 @@ export async function presentAssistantMessage(cline: Task) {
})
break
case "anthropic":
// Route to search_and_replace handler (Anthropic variant)
await searchAndReplaceTool.handle(cline, block as ToolUse<"search_and_replace">, {
// Route to Anthropic variant (multi-edit search/replace)
await editFileAnthropicTool.handle(cline, block as ToolUse<"edit_file_anthropic">, {
askApproval,
handleError,
pushToolResult,
@ -923,8 +924,8 @@ export async function presentAssistantMessage(cline: Task) {
})
break
case "grok":
// Route to search_replace handler (Grok variant)
await searchReplaceTool.handle(cline, block as ToolUse<"search_replace">, {
// Route to Grok variant (single search/replace)
await editFileGrokTool.handle(cline, block as ToolUse<"edit_file_grok">, {
askApproval,
handleError,
pushToolResult,
@ -933,8 +934,8 @@ export async function presentAssistantMessage(cline: Task) {
})
break
case "gemini":
// Route to edit_file handler (Gemini variant)
await editFileTool.handle(cline, block as ToolUse<"edit_file">, {
// Route to Gemini variant (search/replace with expected_replacements)
await editFileGeminiTool.handle(cline, block as ToolUse<"edit_file">, {
askApproval,
handleError,
pushToolResult,
@ -943,8 +944,8 @@ export async function presentAssistantMessage(cline: Task) {
})
break
case "codex":
// Route to apply_patch handler (Codex variant)
await applyPatchTool.handle(cline, block as ToolUse<"apply_patch">, {
// Route to Codex variant (unified patch format)
await editFileCodexTool.handle(cline, block as ToolUse<"edit_file_codex">, {
askApproval,
handleError,
pushToolResult,
@ -955,7 +956,7 @@ export async function presentAssistantMessage(cline: Task) {
default: {
// Should never happen, but default to roo variant
const _exhaustiveCheck: never = editToolVariant
await applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, {
await editFileRooTool.handle(cline, block as ToolUse<"edit_file_roo">, {
askApproval,
handleError,
pushToolResult,
@ -966,16 +967,6 @@ export async function presentAssistantMessage(cline: Task) {
}
break
}
case "apply_patch":
await checkpointSaveAndMark(cline)
await applyPatchTool.handle(cline, block as ToolUse<"apply_patch">, {
askApproval,
handleError,
pushToolResult,
removeClosingTag,
toolProtocol,
})
break
case "read_file":
// Type assertion is safe here because we're in the "read_file" case
await readFileTool.handle(cline, block as ToolUse<"read_file">, {

View file

@ -25,12 +25,6 @@ const EDIT_TOOL_VARIANT_MAP: Record<EditToolVariant, string> = {
*/
const ALL_EDIT_TOOL_VARIANTS = new Set(Object.values(EDIT_TOOL_VARIANT_MAP))
/**
* Legacy edit tool names that are now aliases.
* These should be excluded from the tool list since they're replaced by the variants.
*/
const LEGACY_EDIT_TOOL_NAMES = new Set(["apply_diff", "search_and_replace", "search_replace", "apply_patch"])
/**
* Reverse lookup map - maps alias name to canonical tool name.
* Built once at module load from the central TOOL_ALIASES constant.
@ -326,12 +320,6 @@ export function filterNativeToolsForMode(
}
// Handle edit tool variant selection:
// 1. Remove legacy edit tool names (they're now aliases)
// 2. Remove non-selected edit tool variants
// 3. The selected variant will be renamed to "edit_file" below
for (const legacyTool of LEGACY_EDIT_TOOL_NAMES) {
allowedToolNames.delete(legacyTool)
}
for (const variantTool of ALL_EDIT_TOOL_VARIANTS) {
allowedToolNames.delete(variantTool)
}

View file

@ -24,8 +24,8 @@ interface SearchAndReplaceParams {
edits: EditOperation[]
}
export class SearchAndReplaceTool extends BaseTool<"search_and_replace"> {
readonly name = "search_and_replace" as const
export class SearchAndReplaceTool extends BaseTool<"edit_file_anthropic"> {
readonly name = "edit_file_anthropic" as const
parseLegacy(params: Partial<Record<string, string>>): SearchAndReplaceParams {
// Parse edits from JSON string if provided
@ -52,14 +52,14 @@ export class SearchAndReplaceTool extends BaseTool<"search_and_replace"> {
// Validate required parameters
if (!relPath) {
task.consecutiveMistakeCount++
task.recordToolError("search_and_replace")
pushToolResult(await task.sayAndCreateMissingParamError("search_and_replace", "path"))
task.recordToolError("edit_file_anthropic")
pushToolResult(await task.sayAndCreateMissingParamError("edit_file_anthropic", "path"))
return
}
if (!edits || !Array.isArray(edits) || edits.length === 0) {
task.consecutiveMistakeCount++
task.recordToolError("search_and_replace")
task.recordToolError("edit_file_anthropic")
pushToolResult(
formatResponse.toolError(
"Missing or empty 'edits' parameter. At least one edit operation is required.",
@ -73,13 +73,13 @@ export class SearchAndReplaceTool extends BaseTool<"search_and_replace"> {
const op = edits[i]
if (!op.old_text) {
task.consecutiveMistakeCount++
task.recordToolError("search_and_replace")
task.recordToolError("edit_file_anthropic")
pushToolResult(formatResponse.toolError(`Edit ${i + 1} is missing the 'old_text' field.`))
return
}
if (op.new_text === undefined) {
task.consecutiveMistakeCount++
task.recordToolError("search_and_replace")
task.recordToolError("edit_file_anthropic")
pushToolResult(formatResponse.toolError(`Edit ${i + 1} is missing the 'new_text' field.`))
return
}
@ -101,7 +101,7 @@ export class SearchAndReplaceTool extends BaseTool<"search_and_replace"> {
const fileExists = await fileExistsAtPath(absolutePath)
if (!fileExists) {
task.consecutiveMistakeCount++
task.recordToolError("search_and_replace")
task.recordToolError("edit_file_anthropic")
const errorMessage = `File not found: ${relPath}. Cannot perform search and replace on a non-existent file.`
await task.say("error", errorMessage)
pushToolResult(formatResponse.toolError(errorMessage))
@ -115,7 +115,7 @@ export class SearchAndReplaceTool extends BaseTool<"search_and_replace"> {
fileContent = fileContent.replace(/\r\n/g, "\n")
} catch (error) {
task.consecutiveMistakeCount++
task.recordToolError("search_and_replace")
task.recordToolError("edit_file_anthropic")
const errorMessage = `Failed to read file '${relPath}'. Please verify file permissions and try again.`
await task.say("error", errorMessage)
pushToolResult(formatResponse.toolError(errorMessage))
@ -152,7 +152,7 @@ export class SearchAndReplaceTool extends BaseTool<"search_and_replace"> {
// If all edits failed, return error
if (errors.length === edits.length) {
task.consecutiveMistakeCount++
task.recordToolError("search_and_replace", "no_match")
task.recordToolError("edit_file_anthropic", "no_match")
pushToolResult(formatResponse.toolError(`All edits failed:\n${errors.join("\n")}`))
return
}
@ -256,8 +256,6 @@ export class SearchAndReplaceTool extends BaseTool<"search_and_replace"> {
pushToolResult(message)
}
// Record successful tool usage and cleanup
task.recordToolUsage("search_and_replace")
await task.diffViewProvider.reset()
// Process any queued messages after file edit completes
@ -268,7 +266,7 @@ export class SearchAndReplaceTool extends BaseTool<"search_and_replace"> {
}
}
override async handlePartial(task: Task, block: ToolUse<"search_and_replace">): Promise<void> {
override async handlePartial(task: Task, block: ToolUse<"edit_file_anthropic">): Promise<void> {
const relPath: string | undefined = block.params.path
const editsStr: string | undefined = block.params.edits
@ -308,3 +306,5 @@ function escapeRegExp(input: string): string {
}
export const searchAndReplaceTool = new SearchAndReplaceTool()
// Alias for new naming convention
export const editFileAnthropicTool = searchAndReplaceTool

View file

@ -20,8 +20,8 @@ interface ApplyPatchParams {
patch: string
}
export class ApplyPatchTool extends BaseTool<"apply_patch"> {
readonly name = "apply_patch" as const
export class ApplyPatchTool extends BaseTool<"edit_file_codex"> {
readonly name = "edit_file_codex" as const
parseLegacy(params: Partial<Record<string, string>>): ApplyPatchParams {
return {
@ -37,8 +37,8 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> {
// Validate required parameters
if (!patch) {
task.consecutiveMistakeCount++
task.recordToolError("apply_patch")
pushToolResult(await task.sayAndCreateMissingParamError("apply_patch", "patch"))
task.recordToolError("edit_file_codex")
pushToolResult(await task.sayAndCreateMissingParamError("edit_file_codex", "patch"))
return
}
@ -48,7 +48,7 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> {
parsedPatch = parsePatch(patch)
} catch (error) {
task.consecutiveMistakeCount++
task.recordToolError("apply_patch")
task.recordToolError("edit_file_codex")
const errorMessage =
error instanceof ParseError
? `Invalid patch format: ${error.message}`
@ -73,7 +73,7 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> {
changes = await processAllHunks(parsedPatch.hunks, readFile)
} catch (error) {
task.consecutiveMistakeCount++
task.recordToolError("apply_patch")
task.recordToolError("edit_file_codex")
const errorMessage = `Failed to process patch: ${error instanceof Error ? error.message : String(error)}`
pushToolResult(formatResponse.toolError(errorMessage))
return
@ -108,7 +108,6 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> {
}
task.consecutiveMistakeCount = 0
task.recordToolUsage("apply_patch")
} catch (error) {
await handleError("apply patch", error as Error)
await task.diffViewProvider.reset()
@ -129,7 +128,7 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> {
const fileExists = await fileExistsAtPath(absolutePath)
if (fileExists) {
task.consecutiveMistakeCount++
task.recordToolError("apply_patch")
task.recordToolError("edit_file_codex")
const errorMessage = `File already exists: ${relPath}. Use Update File instead.`
await task.say("error", errorMessage)
pushToolResult(formatResponse.toolError(errorMessage))
@ -220,7 +219,7 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> {
const fileExists = await fileExistsAtPath(absolutePath)
if (!fileExists) {
task.consecutiveMistakeCount++
task.recordToolError("apply_patch")
task.recordToolError("edit_file_codex")
const errorMessage = `File not found: ${relPath}. Cannot delete a non-existent file.`
await task.say("error", errorMessage)
pushToolResult(formatResponse.toolError(errorMessage))
@ -278,7 +277,7 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> {
const fileExists = await fileExistsAtPath(absolutePath)
if (!fileExists) {
task.consecutiveMistakeCount++
task.recordToolError("apply_patch")
task.recordToolError("edit_file_codex")
const errorMessage = `File not found: ${relPath}. Cannot update a non-existent file.`
await task.say("error", errorMessage)
pushToolResult(formatResponse.toolError(errorMessage))
@ -363,7 +362,7 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> {
const isMovePathWriteProtected = task.rooProtectedController?.isWriteProtected(change.movePath) || false
if (isMovePathWriteProtected) {
task.consecutiveMistakeCount++
task.recordToolError("apply_patch")
task.recordToolError("edit_file_codex")
const errorMessage = `Cannot move file to write-protected path: ${change.movePath}`
await task.say("error", errorMessage)
pushToolResult(formatResponse.toolError(errorMessage))
@ -375,7 +374,7 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> {
const isMoveOutsideWorkspace = isPathOutsideWorkspace(moveAbsolutePath)
if (isMoveOutsideWorkspace) {
task.consecutiveMistakeCount++
task.recordToolError("apply_patch")
task.recordToolError("edit_file_codex")
const errorMessage = `Cannot move file to path outside workspace: ${change.movePath}`
await task.say("error", errorMessage)
pushToolResult(formatResponse.toolError(errorMessage))
@ -426,7 +425,7 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> {
task.processQueuedMessages()
}
override async handlePartial(task: Task, block: ToolUse<"apply_patch">): Promise<void> {
override async handlePartial(task: Task, block: ToolUse<"edit_file_codex">): Promise<void> {
const patch: string | undefined = block.params.patch
let patchPreview: string | undefined
@ -448,3 +447,5 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> {
}
export const applyPatchTool = new ApplyPatchTool()
// Alias for new naming convention
export const editFileCodexTool = applyPatchTool

View file

@ -324,8 +324,6 @@ export class EditFileTool extends BaseTool<"edit_file"> {
pushToolResult(message + replacementInfo)
// Record successful tool usage and cleanup
task.recordToolUsage("edit_file")
await task.diffViewProvider.reset()
// Process any queued messages after file edit completes
@ -371,3 +369,5 @@ export class EditFileTool extends BaseTool<"edit_file"> {
}
export const editFileTool = new EditFileTool()
// Alias for new naming convention
export const editFileGeminiTool = editFileTool

View file

@ -20,8 +20,8 @@ interface SearchReplaceParams {
new_string: string
}
export class SearchReplaceTool extends BaseTool<"search_replace"> {
readonly name = "search_replace" as const
export class SearchReplaceTool extends BaseTool<"edit_file_grok"> {
readonly name = "edit_file_grok" as const
parseLegacy(params: Partial<Record<string, string>>): SearchReplaceParams {
return {
@ -39,29 +39,29 @@ export class SearchReplaceTool extends BaseTool<"search_replace"> {
// Validate required parameters
if (!file_path) {
task.consecutiveMistakeCount++
task.recordToolError("search_replace")
pushToolResult(await task.sayAndCreateMissingParamError("search_replace", "file_path"))
task.recordToolError("edit_file_grok")
pushToolResult(await task.sayAndCreateMissingParamError("edit_file_grok", "file_path"))
return
}
if (!old_string) {
task.consecutiveMistakeCount++
task.recordToolError("search_replace")
pushToolResult(await task.sayAndCreateMissingParamError("search_replace", "old_string"))
task.recordToolError("edit_file_grok")
pushToolResult(await task.sayAndCreateMissingParamError("edit_file_grok", "old_string"))
return
}
if (new_string === undefined) {
task.consecutiveMistakeCount++
task.recordToolError("search_replace")
pushToolResult(await task.sayAndCreateMissingParamError("search_replace", "new_string"))
task.recordToolError("edit_file_grok")
pushToolResult(await task.sayAndCreateMissingParamError("edit_file_grok", "new_string"))
return
}
// Validate that old_string and new_string are different
if (old_string === new_string) {
task.consecutiveMistakeCount++
task.recordToolError("search_replace")
task.recordToolError("edit_file_grok")
pushToolResult(
formatResponse.toolError(
"The 'old_string' and 'new_string' parameters must be different.",
@ -95,7 +95,7 @@ export class SearchReplaceTool extends BaseTool<"search_replace"> {
const fileExists = await fileExistsAtPath(absolutePath)
if (!fileExists) {
task.consecutiveMistakeCount++
task.recordToolError("search_replace")
task.recordToolError("edit_file_grok")
const errorMessage = `File not found: ${relPath}. Cannot perform search and replace on a non-existent file.`
await task.say("error", errorMessage)
pushToolResult(formatResponse.toolError(errorMessage, toolProtocol))
@ -109,7 +109,7 @@ export class SearchReplaceTool extends BaseTool<"search_replace"> {
fileContent = fileContent.replace(/\r\n/g, "\n")
} catch (error) {
task.consecutiveMistakeCount++
task.recordToolError("search_replace")
task.recordToolError("edit_file_grok")
const errorMessage = `Failed to read file '${relPath}'. Please verify file permissions and try again.`
await task.say("error", errorMessage)
pushToolResult(formatResponse.toolError(errorMessage, toolProtocol))
@ -125,7 +125,7 @@ export class SearchReplaceTool extends BaseTool<"search_replace"> {
if (matchCount === 0) {
task.consecutiveMistakeCount++
task.recordToolError("search_replace", "no_match")
task.recordToolError("edit_file_grok", "no_match")
pushToolResult(
formatResponse.toolError(
`No match found for the specified 'old_string'. Please ensure it matches the file contents exactly, including whitespace and indentation.`,
@ -137,7 +137,7 @@ export class SearchReplaceTool extends BaseTool<"search_replace"> {
if (matchCount > 1) {
task.consecutiveMistakeCount++
task.recordToolError("search_replace", "multiple_matches")
task.recordToolError("edit_file_grok", "multiple_matches")
pushToolResult(
formatResponse.toolError(
`Found ${matchCount} matches for the specified 'old_string'. This tool can only replace ONE occurrence at a time. Please provide more context (3-5 lines before and after) to uniquely identify the specific instance you want to change.`,
@ -237,8 +237,6 @@ export class SearchReplaceTool extends BaseTool<"search_replace"> {
const message = await task.diffViewProvider.pushToolWriteResult(task, task.cwd, false)
pushToolResult(message)
// Record successful tool usage and cleanup
task.recordToolUsage("search_replace")
await task.diffViewProvider.reset()
// Process any queued messages after file edit completes
@ -249,7 +247,7 @@ export class SearchReplaceTool extends BaseTool<"search_replace"> {
}
}
override async handlePartial(task: Task, block: ToolUse<"search_replace">): Promise<void> {
override async handlePartial(task: Task, block: ToolUse<"edit_file_grok">): Promise<void> {
const filePath: string | undefined = block.params.file_path
const oldString: string | undefined = block.params.old_string
@ -281,3 +279,5 @@ export class SearchReplaceTool extends BaseTool<"search_replace"> {
}
export const searchReplaceTool = new SearchReplaceTool()
// Alias for new naming convention
export const editFileGrokTool = searchReplaceTool

View file

@ -16,22 +16,22 @@ import { computeDiffStats, sanitizeUnifiedDiff } from "../diff/stats"
import { BaseTool, ToolCallbacks } from "./BaseTool"
import type { ToolUse } from "../../shared/tools"
interface ApplyDiffParams {
interface EditFileRooParams {
path: string
diff: string
}
export class ApplyDiffTool extends BaseTool<"apply_diff"> {
readonly name = "apply_diff" as const
export class EditFileRooTool extends BaseTool<"edit_file_roo"> {
readonly name = "edit_file_roo" as const
parseLegacy(params: Partial<Record<string, string>>): ApplyDiffParams {
parseLegacy(params: Partial<Record<string, string>>): EditFileRooParams {
return {
path: params.path || "",
diff: params.diff || "",
}
}
async execute(params: ApplyDiffParams, task: Task, callbacks: ToolCallbacks): Promise<void> {
async execute(params: EditFileRooParams, task: Task, callbacks: ToolCallbacks): Promise<void> {
const { askApproval, handleError, pushToolResult, toolProtocol } = callbacks
let { path: relPath, diff: diffContent } = params
@ -42,15 +42,15 @@ export class ApplyDiffTool extends BaseTool<"apply_diff"> {
try {
if (!relPath) {
task.consecutiveMistakeCount++
task.recordToolError("apply_diff")
pushToolResult(await task.sayAndCreateMissingParamError("apply_diff", "path"))
task.recordToolError("edit_file_roo")
pushToolResult(await task.sayAndCreateMissingParamError("edit_file_roo", "path"))
return
}
if (!diffContent) {
task.consecutiveMistakeCount++
task.recordToolError("apply_diff")
pushToolResult(await task.sayAndCreateMissingParamError("apply_diff", "diff"))
task.recordToolError("edit_file_roo")
pushToolResult(await task.sayAndCreateMissingParamError("edit_file_roo", "diff"))
return
}
@ -67,7 +67,7 @@ export class ApplyDiffTool extends BaseTool<"apply_diff"> {
if (!fileExists) {
task.consecutiveMistakeCount++
task.recordToolError("apply_diff")
task.recordToolError("edit_file_roo")
const formattedError = `File does not exist at path: ${absolutePath}\n\n<error_details>\nThe specified file could not be found. Please verify the file path and try again.\n</error_details>`
await task.say("error", formattedError)
task.didToolFailInCurrentTurn = true
@ -118,7 +118,7 @@ export class ApplyDiffTool extends BaseTool<"apply_diff"> {
await task.say("diff_error", formattedError)
}
task.recordToolError("apply_diff", formattedError)
task.recordToolError("edit_file_roo", formattedError)
pushToolResult(formattedError)
return
@ -164,9 +164,9 @@ export class ApplyDiffTool extends BaseTool<"apply_diff"> {
let toolProgressStatus
if (task.diffStrategy && task.diffStrategy.getProgressStatus) {
const block: ToolUse<"apply_diff"> = {
const block: ToolUse<"edit_file_roo"> = {
type: "tool_use",
name: "apply_diff",
name: "edit_file_roo",
params: { path: relPath, diff: diffContent },
partial: false,
}
@ -208,9 +208,9 @@ export class ApplyDiffTool extends BaseTool<"apply_diff"> {
let toolProgressStatus
if (task.diffStrategy && task.diffStrategy.getProgressStatus) {
const block: ToolUse<"apply_diff"> = {
const block: ToolUse<"edit_file_roo"> = {
type: "tool_use",
name: "apply_diff",
name: "edit_file_roo",
params: { path: relPath, diff: diffContent },
partial: false,
}
@ -272,7 +272,7 @@ export class ApplyDiffTool extends BaseTool<"apply_diff"> {
}
}
override async handlePartial(task: Task, block: ToolUse<"apply_diff">): Promise<void> {
override async handlePartial(task: Task, block: ToolUse<"edit_file_roo">): Promise<void> {
const relPath: string | undefined = block.params.path
const diffContent: string | undefined = block.params.diff
@ -296,4 +296,6 @@ export class ApplyDiffTool extends BaseTool<"apply_diff"> {
}
}
export const applyDiffTool = new ApplyDiffTool()
export const editFileRooTool = new EditFileRooTool()
// Legacy alias for backward compatibility
export const applyDiffTool = editFileRooTool

View file

@ -14,7 +14,7 @@ import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
import { unescapeHtmlEntities } from "../../utils/text-normalization"
import { parseXmlForDiff } from "../../utils/xml"
import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
import { applyDiffTool as applyDiffToolClass } from "./ApplyDiffTool"
import { applyDiffTool as applyDiffToolClass } from "./EditFileRooTool"
import { computeDiffStats, sanitizeUnifiedDiff } from "../diff/stats"
import { isNativeProtocol } from "@roo-code/types"
import { resolveToolProtocol } from "../../utils/resolveToolProtocol"
@ -65,7 +65,7 @@ export async function applyDiffTool(
// Use the task's locked protocol for consistency throughout the task lifetime
const toolProtocol = resolveToolProtocol(cline.apiConfiguration, cline.api.getModel().info, cline.taskToolProtocol)
if (isNativeProtocol(toolProtocol)) {
return applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, {
return applyDiffToolClass.handle(cline, block as ToolUse<"edit_file_roo">, {
askApproval,
handleError,
pushToolResult,
@ -85,7 +85,7 @@ export async function applyDiffTool(
// If experiment is disabled, use single-file class-based tool
if (!isMultiFileApplyDiffEnabled) {
return applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, {
return applyDiffToolClass.handle(cline, block as ToolUse<"edit_file_roo">, {
askApproval,
handleError,
pushToolResult,

View file

@ -7,7 +7,7 @@ import { fileExistsAtPath } from "../../../utils/fs"
import { isPathOutsideWorkspace } from "../../../utils/pathUtils"
import { getReadablePath } from "../../../utils/path"
import { ToolUse, ToolResponse } from "../../../shared/tools"
import { searchReplaceTool } from "../SearchReplaceTool"
import { searchReplaceTool } from "../EditFileGrokTool"
vi.mock("fs/promises", () => ({
default: {
@ -179,7 +179,7 @@ describe("searchReplaceTool", () => {
const toolUse: ToolUse = {
type: "tool_use",
name: "search_replace",
name: "edit_file_grok",
params: {
file_path: testFilePath,
old_string: testOldString,
@ -193,7 +193,7 @@ describe("searchReplaceTool", () => {
toolResult = result
})
await searchReplaceTool.handle(mockCline, toolUse as ToolUse<"search_replace">, {
await searchReplaceTool.handle(mockCline, toolUse as ToolUse<"edit_file_grok">, {
askApproval: mockAskApproval,
handleError: mockHandleError,
pushToolResult: mockPushToolResult,
@ -210,7 +210,7 @@ describe("searchReplaceTool", () => {
expect(result).toBe("Missing param error")
expect(mockCline.consecutiveMistakeCount).toBe(1)
expect(mockCline.recordToolError).toHaveBeenCalledWith("search_replace")
expect(mockCline.recordToolError).toHaveBeenCalledWith("edit_file_grok")
})
it("returns error when old_string is missing", async () => {
@ -268,7 +268,7 @@ describe("searchReplaceTool", () => {
expect(result).toContain("Error:")
expect(result).toContain("No match found")
expect(mockCline.consecutiveMistakeCount).toBe(1)
expect(mockCline.recordToolError).toHaveBeenCalledWith("search_replace", "no_match")
expect(mockCline.recordToolError).toHaveBeenCalledWith("edit_file_grok", "no_match")
})
it("returns error when multiple matches are found", async () => {
@ -280,7 +280,7 @@ describe("searchReplaceTool", () => {
expect(result).toContain("Error:")
expect(result).toContain("3 matches")
expect(mockCline.consecutiveMistakeCount).toBe(1)
expect(mockCline.recordToolError).toHaveBeenCalledWith("search_replace", "multiple_matches")
expect(mockCline.recordToolError).toHaveBeenCalledWith("edit_file_grok", "multiple_matches")
})
it("successfully replaces single unique match", async () => {
@ -306,7 +306,7 @@ describe("searchReplaceTool", () => {
expect(mockCline.diffViewProvider.saveChanges).toHaveBeenCalled()
expect(mockCline.didEditFile).toBe(true)
expect(mockCline.recordToolUsage).toHaveBeenCalledWith("search_replace")
expect(mockCline.recordToolUsage).not.toHaveBeenCalled()
})
it("reverts changes when user rejects", async () => {
@ -335,7 +335,7 @@ describe("searchReplaceTool", () => {
const toolUse: ToolUse = {
type: "tool_use",
name: "search_replace",
name: "edit_file_grok",
params: {
file_path: testFilePath,
old_string: testOldString,
@ -349,7 +349,7 @@ describe("searchReplaceTool", () => {
capturedResult = result
})
await searchReplaceTool.handle(mockCline, toolUse as ToolUse<"search_replace">, {
await searchReplaceTool.handle(mockCline, toolUse as ToolUse<"edit_file_grok">, {
askApproval: mockAskApproval,
handleError: mockHandleError,
pushToolResult: localPushToolResult,

View file

@ -8,8 +8,8 @@ vi.mock("vscode", () => ({
},
}))
// Mock the ApplyDiffTool module
vi.mock("../ApplyDiffTool", () => ({
// Mock the EditFileRooTool module
vi.mock("../EditFileRooTool", () => ({
applyDiffTool: {
handle: vi.fn(),
},
@ -17,7 +17,7 @@ vi.mock("../ApplyDiffTool", () => ({
// Import after mocking to get the mocked version
import { applyDiffTool as multiApplyDiffTool } from "../MultiApplyDiffTool"
import { applyDiffTool as applyDiffToolClass } from "../ApplyDiffTool"
import { applyDiffTool as applyDiffToolClass } from "../EditFileRooTool"
describe("applyDiffTool experiment routing", () => {
let mockCline: any

View file

@ -7,7 +7,7 @@ import { fileExistsAtPath } from "../../../utils/fs"
import { isPathOutsideWorkspace } from "../../../utils/pathUtils"
import { getReadablePath } from "../../../utils/path"
import { ToolUse, ToolResponse } from "../../../shared/tools"
import { editFileTool } from "../EditFileTool"
import { editFileTool } from "../EditFileGeminiTool"
vi.mock("fs/promises", () => ({
default: {
@ -348,7 +348,7 @@ describe("editFileTool", () => {
expect(mockTask.diffViewProvider.saveChanges).toHaveBeenCalled()
expect(mockTask.didEditFile).toBe(true)
expect(mockTask.recordToolUsage).toHaveBeenCalledWith("edit_file")
expect(mockTask.recordToolUsage).not.toHaveBeenCalled()
})
it("reverts changes when user rejects", async () => {

View file

@ -326,7 +326,7 @@ describe("isToolAllowedForMode", () => {
it("disallows customTools by default (not in includedTools)", () => {
// search_and_replace is a customTool in the edit group, should be disallowed by default
expect(isToolAllowedForMode("search_and_replace", "test-custom-tools", customModesWithEditGroup)).toBe(
expect(isToolAllowedForMode("edit_file_anthropic", "test-custom-tools", customModesWithEditGroup)).toBe(
false,
)
})
@ -335,13 +335,13 @@ describe("isToolAllowedForMode", () => {
// search_and_replace should be allowed when explicitly included
expect(
isToolAllowedForMode(
"search_and_replace",
"edit_file_anthropic",
"test-custom-tools",
customModesWithEditGroup,
undefined,
undefined,
undefined,
["search_and_replace"],
["edit_file_anthropic"],
),
).toBe(true)
})
@ -359,13 +359,13 @@ describe("isToolAllowedForMode", () => {
// Even if included, should be disallowed because the mode doesn't have edit group
expect(
isToolAllowedForMode(
"search_and_replace",
"edit_file_anthropic",
"no-edit-mode",
customModesWithoutEdit,
undefined,
undefined,
undefined,
["search_and_replace"],
["edit_file_anthropic"],
),
).toBe(false)
})

View file

@ -70,14 +70,14 @@ export const toolParamNames = [
"prompt",
"image",
"files", // Native protocol parameter for read_file
"edits", // edit_file_anthropic parameter for multiple edit operations
"old_text", // edit_file_anthropic parameter for text to replace
"new_text", // edit_file_anthropic parameter for replacement text
"patch", // apply_patch parameter
"file_path", // search_replace and edit_file parameter
"old_string", // search_replace and edit_file parameter
"new_string", // search_replace and edit_file parameter
"expected_replacements", // edit_file parameter for multiple occurrences
"edits", // edit_file (anthropic variant) parameter for multiple edit operations
"old_text", // edit_file (anthropic variant) parameter for text to replace
"new_text", // edit_file (anthropic variant) parameter for replacement text
"patch", // edit_file (codex variant) parameter
"file_path", // edit_file (grok/gemini variant) parameter
"old_string", // edit_file (grok/gemini variant) parameter
"new_string", // edit_file (grok/gemini variant) parameter
"expected_replacements", // edit_file (gemini variant) parameter for multiple occurrences
] as const
export type ToolParamName = (typeof toolParamNames)[number]
@ -93,13 +93,22 @@ export type NativeToolArgs = {
read_file: { files: FileEntry[] }
attempt_completion: { result: string }
execute_command: { command: string; cwd?: string }
// Legacy edit tool names (deprecated, mapped to new names via aliases)
// apply_diff is kept for XML protocol backward compatibility
apply_diff: { path: string; diff: string }
search_and_replace: { path: string; edits: Array<{ old_text: string; new_text: string }> }
search_replace: { file_path: string; old_string: string; new_string: string }
edit_file: { file_path: string; old_string: string; new_string: string; expected_replacements?: number }
apply_patch: { patch: string }
// New edit tool variant names (all present "edit_file" to LLM)
// Unified edit_file for native protocol - variant-specific args based on modelInfo.editToolVariant
// The actual arg structure depends on which variant is selected:
// - roo: { path, diff }
// - anthropic: { path, edits }
// - grok: { file_path, old_string, new_string }
// - gemini: { file_path, old_string, new_string, expected_replacements? }
// - codex: { patch }
edit_file:
| { path: string; diff: string }
| { path: string; edits: Array<{ old_text: string; new_text: string }> }
| { file_path: string; old_string: string; new_string: string }
| { file_path: string; old_string: string; new_string: string; expected_replacements?: number }
| { patch: string }
// Internal edit tool variant names (used by native protocol, presented to LLM as "edit_file")
edit_file_roo: { path: string; diff: string }
edit_file_anthropic: { path: string; edits: Array<{ old_text: string; new_text: string }> }
edit_file_grok: { file_path: string; old_string: string; new_string: string }
@ -257,13 +266,11 @@ export const TOOL_DISPLAY_NAMES: Record<ToolName, string> = {
read_file: "read files",
fetch_instructions: "fetch instructions",
write_to_file: "write files",
// Legacy edit tool names (deprecated)
// apply_diff is kept for XML protocol backward compatibility
apply_diff: "apply changes",
search_and_replace: "apply changes using search and replace",
search_replace: "apply single search and replace",
edit_file: "edit files using search and replace",
apply_patch: "apply patches using codex format",
// New edit tool variant names
// Unified edit tool for native protocol
edit_file: "edit files",
// Internal edit tool variant names (used by native protocol)
edit_file_roo: "edit files (roo format)",
edit_file_anthropic: "edit files (anthropic format)",
edit_file_grok: "edit files (grok format)",
@ -292,25 +299,12 @@ export const TOOL_GROUPS: Record<ToolGroup, ToolGroupConfig> = {
tools: ["read_file", "fetch_instructions", "search_files", "list_files", "codebase_search"],
},
edit: {
// apply_diff is included for XML protocol backward compatibility
// For native protocol, filterNativeToolsForMode selects the appropriate edit_file_* variant
// For XML protocol: apply_diff is included for backward compatibility
// For native protocol: filterNativeToolsForMode selects the appropriate edit_file_* variant
// and presents it as "edit_file" to the LLM
tools: ["write_to_file", "apply_diff", "edit_file", "generate_image"],
// All edit tool variants and legacy names - one is selected based on modelInfo.editToolVariant
// "edit_file" is the unified name that LLMs see (for modelInfo.includedTools validation)
customTools: [
// Unified edit tool name (for includedTools validation)
"edit_file",
// Legacy names (for backward compatibility with existing includedTools configs)
"search_and_replace",
"search_replace",
"apply_patch",
// Variant names (for native protocol internal use)
"edit_file_roo",
"edit_file_anthropic",
"edit_file_grok",
"edit_file_gemini",
"edit_file_codex",
],
// Variant names for native protocol internal use - one is selected based on modelInfo.editToolVariant
customTools: ["edit_file_roo", "edit_file_anthropic", "edit_file_grok", "edit_file_gemini", "edit_file_codex"],
},
browser: {
tools: ["browser_action"],
@ -341,20 +335,16 @@ export const ALWAYS_AVAILABLE_TOOLS: ToolName[] = [
* Central registry of tool aliases.
* Maps alias name -> canonical tool name.
*
* This allows models to use alternative names for tools (e.g., "edit_file" instead of "apply_diff").
* This allows models to use alternative names for tools.
* When a model calls a tool by its alias, the system resolves it to the canonical name for execution,
* but preserves the alias in API conversation history for consistency.
*
* To add a new alias, simply add an entry here. No other files need to be modified.
* Note: Legacy edit tool aliases (apply_diff, search_and_replace, search_replace, apply_patch)
* have been removed. Native protocol now uses editToolVariant to select the edit tool schema,
* and all edit tools are presented to the LLM as "edit_file". XML protocol still uses apply_diff directly.
*/
export const TOOL_ALIASES: Record<string, ToolName> = {
write_file: "write_to_file",
// Backward compatibility: map old edit tool names to new variant names
apply_diff: "edit_file_roo",
search_and_replace: "edit_file_anthropic",
search_replace: "edit_file_grok",
// Note: edit_file is kept as a tool name (for gemini variant) but also serves as the unified LLM-facing name
apply_patch: "edit_file_codex",
} as const
export type DiffResult =