feat: deprecate XML tool protocol selection, force native for new tasks (#10281)

- Disable tool protocol selector UI in ApiOptions.tsx
- Force native protocol for all new tasks in resolveToolProtocol()
- Keep locked protocol support for resumed tasks that used XML
- Remove models without supportsNativeTools: true from providers:
  - baseten.ts: removed 6 models
  - bedrock.ts: removed 2 embedding models
  - featherless.ts: removed 3 models, updated default
  - groq.ts: removed 5 models
  - sambanova.ts: removed 2 models
  - vertex.ts: removed 7 models
  - vercel-ai-gateway.ts: added supportsNativeTools: true
- Update tests to expect native format output
This commit is contained in:
Daniel 2025-12-22 20:49:50 -05:00 committed by GitHub
parent 9b06a98b85
commit d00d9edec5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 330 additions and 751 deletions

View file

@ -33,6 +33,7 @@ export const basetenModels = {
contextWindow: 163_840,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 2.55,
outputPrice: 5.95,
cacheWritesPrice: 0,
@ -44,6 +45,7 @@ export const basetenModels = {
contextWindow: 163_840,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 2.55,
outputPrice: 5.95,
cacheWritesPrice: 0,
@ -55,6 +57,7 @@ export const basetenModels = {
contextWindow: 163_840,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.77,
outputPrice: 0.77,
cacheWritesPrice: 0,
@ -66,6 +69,7 @@ export const basetenModels = {
contextWindow: 163_840,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.5,
outputPrice: 1.5,
cacheWritesPrice: 0,
@ -86,28 +90,6 @@ export const basetenModels = {
description:
"DeepSeek's hybrid reasoning model with efficient long context scaling with GPT-5 level performance",
},
"Qwen/Qwen3-235B-A22B-Instruct-2507": {
maxTokens: 16_384,
contextWindow: 262_144,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.22,
outputPrice: 0.8,
cacheWritesPrice: 0,
cacheReadsPrice: 0,
description: "Mixture-of-experts LLM with math and reasoning capabilities",
},
"Qwen/Qwen3-Coder-480B-A35B-Instruct": {
maxTokens: 16_384,
contextWindow: 262_144,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.38,
outputPrice: 1.53,
cacheWritesPrice: 0,
cacheReadsPrice: 0,
description: "Mixture-of-experts LLM with advanced coding and reasoning capabilities",
},
"openai/gpt-oss-120b": {
maxTokens: 16_384,
contextWindow: 128_072,
@ -120,6 +102,30 @@ export const basetenModels = {
cacheReadsPrice: 0,
description: "Extremely capable general-purpose LLM with strong, controllable reasoning capabilities",
},
"Qwen/Qwen3-235B-A22B-Instruct-2507": {
maxTokens: 16_384,
contextWindow: 262_144,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.22,
outputPrice: 0.8,
cacheWritesPrice: 0,
cacheReadsPrice: 0,
description: "Mixture-of-experts LLM with math and reasoning capabilities",
},
"Qwen/Qwen3-Coder-480B-A35B-Instruct": {
maxTokens: 16_384,
contextWindow: 262_144,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.38,
outputPrice: 1.53,
cacheWritesPrice: 0,
cacheReadsPrice: 0,
description: "Mixture-of-experts LLM with advanced coding and reasoning capabilities",
},
"moonshotai/Kimi-K2-Instruct-0905": {
maxTokens: 16_384,
contextWindow: 262_000,

View file

@ -454,22 +454,6 @@ export const bedrockModels = {
outputPrice: 0.6,
description: "Amazon Titan Text Express",
},
"amazon.titan-text-embeddings-v1:0": {
maxTokens: 8192,
contextWindow: 8_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.1,
description: "Amazon Titan Text Embeddings",
},
"amazon.titan-text-embeddings-v2:0": {
maxTokens: 8192,
contextWindow: 8_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.02,
description: "Amazon Titan Text Embeddings V2",
},
"moonshot.kimi-k2-thinking": {
maxTokens: 32_000,
contextWindow: 262_144,

View file

@ -13,6 +13,7 @@ export const featherlessModels = {
contextWindow: 32678,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0,
outputPrice: 0,
description: "DeepSeek V3 0324 model.",
@ -22,6 +23,7 @@ export const featherlessModels = {
contextWindow: 32678,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0,
outputPrice: 0,
description: "DeepSeek R1 0528 model.",
@ -41,6 +43,7 @@ export const featherlessModels = {
contextWindow: 32678,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0,
outputPrice: 0,
description: "GPT-OSS 120B model.",
@ -57,4 +60,4 @@ export const featherlessModels = {
},
} as const satisfies Record<string, ModelInfo>
export const featherlessDefaultModelId: FeatherlessModelId = "deepseek-ai/DeepSeek-R1-0528"
export const featherlessDefaultModelId: FeatherlessModelId = "moonshotai/Kimi-K2-Instruct"

View file

@ -5,12 +5,7 @@ export type GroqModelId =
| "llama-3.1-8b-instant"
| "llama-3.3-70b-versatile"
| "meta-llama/llama-4-scout-17b-16e-instruct"
| "meta-llama/llama-4-maverick-17b-128e-instruct"
| "mistral-saba-24b"
| "qwen-qwq-32b"
| "qwen/qwen3-32b"
| "deepseek-r1-distill-llama-70b"
| "moonshotai/kimi-k2-instruct"
| "moonshotai/kimi-k2-instruct-0905"
| "openai/gpt-oss-120b"
| "openai/gpt-oss-20b"
@ -52,33 +47,6 @@ export const groqModels = {
outputPrice: 0.34,
description: "Meta Llama 4 Scout 17B Instruct model, 128K context.",
},
"meta-llama/llama-4-maverick-17b-128e-instruct": {
maxTokens: 8192,
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.2,
outputPrice: 0.6,
description: "Meta Llama 4 Maverick 17B Instruct model, 128K context.",
},
"mistral-saba-24b": {
maxTokens: 8192,
contextWindow: 32768,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.79,
outputPrice: 0.79,
description: "Mistral Saba 24B model, 32K context.",
},
"qwen-qwq-32b": {
maxTokens: 8192,
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.29,
outputPrice: 0.39,
description: "Alibaba Qwen QwQ 32B model, 128K context.",
},
"qwen/qwen3-32b": {
maxTokens: 8192,
contextWindow: 131072,
@ -90,25 +58,6 @@ export const groqModels = {
outputPrice: 0.59,
description: "Alibaba Qwen 3 32B model, 128K context.",
},
"deepseek-r1-distill-llama-70b": {
maxTokens: 8192,
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.75,
outputPrice: 0.99,
description: "DeepSeek R1 Distill Llama 70B model, 128K context.",
},
"moonshotai/kimi-k2-instruct": {
maxTokens: 16384,
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: true,
inputPrice: 1.0,
outputPrice: 3.0,
cacheReadsPrice: 0.5, // 50% discount for cached input tokens
description: "Moonshot AI Kimi K2 Instruct 1T model, 128K context.",
},
"moonshotai/kimi-k2-instruct-0905": {
maxTokens: 16384,
contextWindow: 262144,

View file

@ -7,9 +7,7 @@ export type SambaNovaModelId =
| "DeepSeek-R1"
| "DeepSeek-V3-0324"
| "DeepSeek-V3.1"
| "DeepSeek-R1-Distill-Llama-70B"
| "Llama-4-Maverick-17B-128E-Instruct"
| "Llama-3.3-Swallow-70B-Instruct-v0.4"
| "Qwen3-32B"
| "gpt-oss-120b"
@ -72,15 +70,6 @@ export const sambaNovaModels = {
outputPrice: 4.5,
description: "DeepSeek V3.1 model with 32K context window.",
},
"DeepSeek-R1-Distill-Llama-70B": {
maxTokens: 8192,
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.7,
outputPrice: 1.4,
description: "DeepSeek R1 distilled Llama 70B model with 128K context window.",
},
"Llama-4-Maverick-17B-128E-Instruct": {
maxTokens: 8192,
contextWindow: 131072,
@ -92,15 +81,6 @@ export const sambaNovaModels = {
outputPrice: 1.8,
description: "Meta Llama 4 Maverick 17B 128E Instruct model with 128K context window.",
},
"Llama-3.3-Swallow-70B-Instruct-v0.4": {
maxTokens: 8192,
contextWindow: 16384,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.6,
outputPrice: 1.2,
description: "Tokyotech Llama 3.3 Swallow 70B Instruct v0.4 model with 16K context window.",
},
"Qwen3-32B": {
maxTokens: 8192,
contextWindow: 8192,

View file

@ -90,6 +90,7 @@ export const vercelAiGatewayDefaultModelInfo: ModelInfo = {
contextWindow: 200000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
inputPrice: 3,
outputPrice: 15,
cacheWritesPrice: 3.75,

View file

@ -477,6 +477,7 @@ export const vertexModels = {
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.35,
outputPrice: 1.15,
description: "Meta Llama 4 Maverick 17B Instruct model, 128K context.",
@ -486,6 +487,7 @@ export const vertexModels = {
contextWindow: 163_840,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 1.35,
outputPrice: 5.4,
description: "DeepSeek R1 (0528). Available in us-central1",
@ -495,6 +497,7 @@ export const vertexModels = {
contextWindow: 163_840,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.6,
outputPrice: 1.7,
description: "DeepSeek V3.1. Available in us-west2",
@ -504,6 +507,7 @@ export const vertexModels = {
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.15,
outputPrice: 0.6,
description: "OpenAI gpt-oss 120B. Available in us-central1",
@ -513,6 +517,7 @@ export const vertexModels = {
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.075,
outputPrice: 0.3,
description: "OpenAI gpt-oss 20B. Available in us-central1",
@ -522,6 +527,7 @@ export const vertexModels = {
contextWindow: 262_144,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 1.0,
outputPrice: 4.0,
description: "Qwen3 Coder 480B A35B Instruct. Available in us-south1",
@ -531,6 +537,7 @@ export const vertexModels = {
contextWindow: 262_144,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.25,
outputPrice: 1.0,
description: "Qwen3 235B A22B Instruct. Available in us-south1",

View file

@ -1200,7 +1200,8 @@ describe("VertexHandler", () => {
)
})
it("should not include tools when toolProtocol is xml", async () => {
it("should include tools even when toolProtocol is set to xml (user preference now ignored)", async () => {
// XML protocol deprecation: user preference is now ignored when model supports native tools
handler = new AnthropicVertexHandler({
apiModelId: "claude-3-5-sonnet-v2@20241022",
vertexProjectId: "test-project",
@ -1241,9 +1242,14 @@ describe("VertexHandler", () => {
// Just consume
}
// Native is forced when supportsNativeTools===true, so tools should still be included
expect(mockCreate).toHaveBeenCalledWith(
expect.not.objectContaining({
tools: expect.anything(),
expect.objectContaining({
tools: expect.arrayContaining([
expect.objectContaining({
name: "get_weather",
}),
]),
}),
undefined,
)

View file

@ -451,8 +451,8 @@ describe("AnthropicHandler", () => {
)
})
it("should not include tools when toolProtocol is xml", async () => {
// Create handler with xml tool protocol in options
it("should include tools even when toolProtocol is set to xml (user preference now ignored)", async () => {
// XML protocol deprecation: user preference is now ignored when model supports native tools
const xmlHandler = new AnthropicHandler({
...mockOptions,
toolProtocol: "xml",
@ -468,9 +468,14 @@ describe("AnthropicHandler", () => {
// Just consume
}
// Native is forced when supportsNativeTools===true, so tools should still be included
expect(mockCreate).toHaveBeenCalledWith(
expect.not.objectContaining({
tools: expect.anything(),
expect.objectContaining({
tools: expect.arrayContaining([
expect.objectContaining({
name: "get_weather",
}),
]),
}),
expect.anything(),
)

View file

@ -3,12 +3,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import {
type FeatherlessModelId,
featherlessDefaultModelId,
featherlessModels,
DEEP_SEEK_DEFAULT_TEMPERATURE,
} from "@roo-code/types"
import { type FeatherlessModelId, featherlessDefaultModelId, featherlessModels } from "@roo-code/types"
import { FeatherlessHandler } from "../featherless"
@ -76,7 +71,7 @@ describe("FeatherlessHandler", () => {
expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: featherlessApiKey }))
})
it("should handle DeepSeek R1 reasoning format", async () => {
it("should handle reasoning format from models that use <think> tags", async () => {
// Override the mock for this specific test
mockCreate.mockImplementationOnce(async () => ({
[Symbol.asyncIterator]: async function* () {
@ -113,7 +108,7 @@ describe("FeatherlessHandler", () => {
const systemPrompt = "You are a helpful assistant."
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hi" }]
vi.spyOn(handler, "getModel").mockReturnValue({
id: "deepseek-ai/DeepSeek-R1-0528",
id: "some-reasoning-model",
info: { maxTokens: 1024, temperature: 0.7 },
} as any)
@ -154,7 +149,7 @@ describe("FeatherlessHandler", () => {
})
it("should return specified model when valid model is provided", () => {
const testModelId: FeatherlessModelId = "deepseek-ai/DeepSeek-R1-0528"
const testModelId: FeatherlessModelId = "moonshotai/Kimi-K2-Instruct"
const handlerWithModel = new FeatherlessHandler({
apiModelId: testModelId,
featherlessApiKey: "test-featherless-api-key",
@ -225,8 +220,8 @@ describe("FeatherlessHandler", () => {
expect(firstChunk.value).toMatchObject({ type: "usage", inputTokens: 10, outputTokens: 20 })
})
it("createMessage should pass correct parameters to Featherless client for DeepSeek R1", async () => {
const modelId: FeatherlessModelId = "deepseek-ai/DeepSeek-R1-0528"
it("createMessage should pass correct parameters to Featherless client", async () => {
const modelId: FeatherlessModelId = "moonshotai/Kimi-K2-Instruct"
// Clear previous mocks and set up new implementation
mockCreate.mockClear()
@ -247,27 +242,9 @@ describe("FeatherlessHandler", () => {
const messageGenerator = handlerWithModel.createMessage(systemPrompt, messages)
await messageGenerator.next()
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
model: modelId,
messages: [
{
role: "user",
content: `${systemPrompt}\n${messages[0].content}`,
},
],
}),
)
})
it("should apply DeepSeek default temperature for R1 models", () => {
const testModelId: FeatherlessModelId = "deepseek-ai/DeepSeek-R1-0528"
const handlerWithModel = new FeatherlessHandler({
apiModelId: testModelId,
featherlessApiKey: "test-featherless-api-key",
})
const model = handlerWithModel.getModel()
expect(model.info.temperature).toBe(DEEP_SEEK_DEFAULT_TEMPERATURE)
expect(mockCreate).toHaveBeenCalled()
const callArgs = mockCreate.mock.calls[0][0]
expect(callArgs.model).toBe(modelId)
})
it("should use default temperature for non-DeepSeek models", () => {

View file

@ -84,7 +84,7 @@ describe("applyDiffTool experiment routing", () => {
mockRemoveClosingTag = vi.fn((tag, value) => value)
})
it("should use legacy tool when MULTI_FILE_APPLY_DIFF experiment is disabled", async () => {
it("should always use class-based tool with native protocol (XML deprecated)", async () => {
mockProvider.getState.mockResolvedValue({
experiments: {
[EXPERIMENT_IDS.MULTI_FILE_APPLY_DIFF]: false,
@ -103,16 +103,17 @@ describe("applyDiffTool experiment routing", () => {
mockRemoveClosingTag,
)
// Always uses native protocol now (XML deprecated)
expect(applyDiffToolClass.handle).toHaveBeenCalledWith(mockCline, mockBlock, {
askApproval: mockAskApproval,
handleError: mockHandleError,
pushToolResult: mockPushToolResult,
removeClosingTag: mockRemoveClosingTag,
toolProtocol: "xml",
toolProtocol: "native",
})
})
it("should use legacy tool when experiments are not defined", async () => {
it("should use class-based tool when experiments are not defined", async () => {
mockProvider.getState.mockResolvedValue({})
// Mock the class-based tool to resolve successfully
@ -127,24 +128,26 @@ describe("applyDiffTool experiment routing", () => {
mockRemoveClosingTag,
)
// Always uses native protocol now (XML deprecated)
expect(applyDiffToolClass.handle).toHaveBeenCalledWith(mockCline, mockBlock, {
askApproval: mockAskApproval,
handleError: mockHandleError,
pushToolResult: mockPushToolResult,
removeClosingTag: mockRemoveClosingTag,
toolProtocol: "xml",
toolProtocol: "native",
})
})
it("should use multi-file tool when MULTI_FILE_APPLY_DIFF experiment is enabled and using XML protocol", async () => {
it("should use class-based tool when MULTI_FILE_APPLY_DIFF experiment is enabled (native protocol always used)", async () => {
mockProvider.getState.mockResolvedValue({
experiments: {
[EXPERIMENT_IDS.MULTI_FILE_APPLY_DIFF]: true,
},
})
// Mock the new tool behavior - it should continue with the multi-file implementation
// Since we're not mocking the entire function, we'll just verify it doesn't call the class-based tool
// Mock the class-based tool to resolve successfully
;(applyDiffToolClass.handle as any).mockResolvedValue(undefined)
await multiApplyDiffTool(
mockCline,
mockBlock,
@ -154,7 +157,14 @@ describe("applyDiffTool experiment routing", () => {
mockRemoveClosingTag,
)
expect(applyDiffToolClass.handle).not.toHaveBeenCalled()
// Native protocol is always used now, so class-based tool is always called
expect(applyDiffToolClass.handle).toHaveBeenCalledWith(mockCline, mockBlock, {
askApproval: mockAskApproval,
handleError: mockHandleError,
pushToolResult: mockPushToolResult,
removeClosingTag: mockRemoveClosingTag,
toolProtocol: "native",
})
})
it("should use class-based tool when model defaults to native protocol", async () => {

View file

@ -1,4 +1,5 @@
import { applyDiffTool } from "../MultiApplyDiffTool"
import { applyDiffTool as applyDiffToolClass } from "../ApplyDiffTool"
import { EXPERIMENT_IDS } from "../../../shared/experiments"
import * as fs from "fs/promises"
import * as fileUtils from "../../../utils/fs"
@ -9,8 +10,12 @@ vi.mock("fs/promises")
vi.mock("../../../utils/fs")
vi.mock("../../../utils/path")
vi.mock("../../../utils/xml")
vi.mock("../applyDiffTool", () => ({
applyDiffToolLegacy: vi.fn(),
// Mock the ApplyDiffTool class-based tool that MultiApplyDiffTool delegates to for native protocol
vi.mock("../ApplyDiffTool", () => ({
applyDiffTool: {
handle: vi.fn().mockResolvedValue(undefined),
},
}))
// Mock TelemetryService
@ -116,8 +121,8 @@ describe("multiApplyDiffTool", () => {
;(pathUtils.getReadablePath as any).mockImplementation((cwd: string, path: string) => path)
})
describe("Early content validation", () => {
it("should filter out non-string content at parse time", async () => {
describe("Native protocol delegation", () => {
it("should delegate to applyDiffToolClass.handle for XML args format", async () => {
mockBlock = {
params: {
args: `<file>
@ -130,22 +135,6 @@ describe("multiApplyDiffTool", () => {
partial: false,
}
// Mock parseXml to return mixed content types
const parseXml = await import("../../../utils/xml")
;(parseXml.parseXml as any).mockReturnValue({
file: {
path: "test.ts",
diff: [
{ content: "<<<<<<< SEARCH\ntest\n=======\nreplaced\n>>>>>>> REPLACE" },
{ content: null },
{ content: undefined },
{ content: 42 },
{ content: "" }, // Empty string should also be filtered
{ content: "<<<<<<< SEARCH\nmore\n=======\nchanges\n>>>>>>> REPLACE" },
],
},
})
await applyDiffTool(
mockCline,
mockBlock,
@ -155,23 +144,26 @@ describe("multiApplyDiffTool", () => {
mockRemoveClosingTag,
)
// Should complete without error and only process valid string content
expect(mockPushToolResult).toHaveBeenCalled()
expect(mockHandleError).not.toHaveBeenCalled()
// Verify that only valid diffs were processed
const resultCall = mockPushToolResult.mock.calls[0][0]
// Should not include the single block notice since we have 2 valid blocks
expect(resultCall).not.toContain("Making multiple related changes")
// Should delegate to the class-based tool
expect(applyDiffToolClass.handle).toHaveBeenCalled()
expect(applyDiffToolClass.handle).toHaveBeenCalledWith(
mockCline,
mockBlock,
expect.objectContaining({
askApproval: mockAskApproval,
handleError: mockHandleError,
pushToolResult: mockPushToolResult,
removeClosingTag: mockRemoveClosingTag,
toolProtocol: "native",
}),
)
})
})
describe("SEARCH block counting with non-string content", () => {
it("should handle diffItem.content being undefined", async () => {
it("should delegate to applyDiffToolClass.handle for legacy path/diff params", async () => {
mockBlock = {
params: {
path: "test.ts",
diff: undefined, // This will result in undefined content
diff: "<<<<<<< SEARCH\nold\n=======\nnew\n>>>>>>> REPLACE",
},
partial: false,
}
@ -185,12 +177,44 @@ describe("multiApplyDiffTool", () => {
mockRemoveClosingTag,
)
// Should complete without throwing an error
expect(mockPushToolResult).toHaveBeenCalled()
expect(mockHandleError).not.toHaveBeenCalled()
// Should delegate to the class-based tool
expect(applyDiffToolClass.handle).toHaveBeenCalled()
expect(applyDiffToolClass.handle).toHaveBeenCalledWith(
mockCline,
mockBlock,
expect.objectContaining({
askApproval: mockAskApproval,
handleError: mockHandleError,
pushToolResult: mockPushToolResult,
removeClosingTag: mockRemoveClosingTag,
toolProtocol: "native",
}),
)
})
it("should handle diffItem.content being null", async () => {
it("should handle undefined diff content by delegating to class-based tool", async () => {
mockBlock = {
params: {
path: "test.ts",
diff: undefined,
},
partial: false,
}
await applyDiffTool(
mockCline,
mockBlock,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
// Should delegate to the class-based tool (which will handle the error)
expect(applyDiffToolClass.handle).toHaveBeenCalled()
})
it("should handle null diff content by delegating to class-based tool", async () => {
mockBlock = {
params: {
args: `<file>
@ -203,17 +227,6 @@ describe("multiApplyDiffTool", () => {
partial: false,
}
// Mock parseXml to return null content
const parseXml = await import("../../../utils/xml")
;(parseXml.parseXml as any).mockReturnValue({
file: {
path: "test.ts",
diff: {
content: null,
},
},
})
await applyDiffTool(
mockCline,
mockBlock,
@ -223,50 +236,11 @@ describe("multiApplyDiffTool", () => {
mockRemoveClosingTag,
)
// Should complete without throwing an error
expect(mockPushToolResult).toHaveBeenCalled()
expect(mockHandleError).not.toHaveBeenCalled()
// Should delegate to the class-based tool
expect(applyDiffToolClass.handle).toHaveBeenCalled()
})
it("should handle diffItem.content being a number", async () => {
mockBlock = {
params: {
args: `<file>
<path>test.ts</path>
<diff>
<content>123</content>
</diff>
</file>`,
},
partial: false,
}
// Mock parseXml to return number content
const parseXml = await import("../../../utils/xml")
;(parseXml.parseXml as any).mockReturnValue({
file: {
path: "test.ts",
diff: {
content: 123, // Number instead of string
},
},
})
await applyDiffTool(
mockCline,
mockBlock,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
// Should complete without throwing an error
expect(mockPushToolResult).toHaveBeenCalled()
expect(mockHandleError).not.toHaveBeenCalled()
})
it("should correctly count SEARCH blocks when content is a valid string", async () => {
it("should delegate multiple SEARCH blocks to class-based tool", async () => {
const diffContent = `<<<<<<< SEARCH
old content
=======
@ -296,14 +270,11 @@ another new content
mockRemoveClosingTag,
)
// Should complete successfully
expect(mockPushToolResult).toHaveBeenCalled()
const resultCall = mockPushToolResult.mock.calls[0][0]
// Should not include the single block notice since we have 2 blocks
expect(resultCall).not.toContain("Making multiple related changes")
// Should delegate to the class-based tool
expect(applyDiffToolClass.handle).toHaveBeenCalled()
})
it("should show single block notice when only one SEARCH block exists", async () => {
it("should delegate single SEARCH block to class-based tool", async () => {
const diffContent = `<<<<<<< SEARCH
old content
=======
@ -327,16 +298,13 @@ new content
mockRemoveClosingTag,
)
// Should complete successfully
expect(mockPushToolResult).toHaveBeenCalled()
const resultCall = mockPushToolResult.mock.calls[0][0]
// Should include the single block notice
expect(resultCall).toContain("Making multiple related changes")
// Should delegate to the class-based tool
expect(applyDiffToolClass.handle).toHaveBeenCalled()
})
})
describe("Edge cases for diff content", () => {
it("should handle empty diff array", async () => {
it("should handle empty diff by delegating to class-based tool", async () => {
mockBlock = {
params: {
args: `<file>
@ -347,14 +315,6 @@ new content
partial: false,
}
const parseXml = await import("../../../utils/xml")
;(parseXml.parseXml as any).mockReturnValue({
file: {
path: "test.ts",
diff: [],
},
})
await applyDiffTool(
mockCline,
mockBlock,
@ -364,12 +324,12 @@ new content
mockRemoveClosingTag,
)
// Should complete without error
expect(mockPushToolResult).toHaveBeenCalled()
// Should delegate to the class-based tool
expect(applyDiffToolClass.handle).toHaveBeenCalled()
expect(mockHandleError).not.toHaveBeenCalled()
})
it("should handle mixed content types in diff array", async () => {
it("should handle mixed content types by delegating to class-based tool", async () => {
mockBlock = {
params: {
args: `<file>
@ -382,20 +342,6 @@ new content
partial: false,
}
const parseXml = await import("../../../utils/xml")
;(parseXml.parseXml as any).mockReturnValue({
file: {
path: "test.ts",
diff: [
{ content: "<<<<<<< SEARCH\ntest\n=======\nreplaced\n>>>>>>> REPLACE" },
{ content: null },
{ content: undefined },
{ content: 42 },
{ content: "<<<<<<< SEARCH\nmore\n=======\nchanges\n>>>>>>> REPLACE" },
],
},
})
await applyDiffTool(
mockCline,
mockBlock,
@ -405,8 +351,8 @@ new content
mockRemoveClosingTag,
)
// Should complete without error and count only valid string SEARCH blocks
expect(mockPushToolResult).toHaveBeenCalled()
// Should delegate to the class-based tool
expect(applyDiffToolClass.handle).toHaveBeenCalled()
expect(mockHandleError).not.toHaveBeenCalled()
})
})

View file

@ -380,9 +380,9 @@ describe("read_file tool with maxReadFileLine setting", () => {
// Execute
const result = await executeReadFileTool({}, { maxReadFileLine: -1 })
// Verify - just check that the result contains the expected elements
expect(result).toContain(`<file><path>${testFilePath}</path>`)
expect(result).toContain(`<content lines="1-5">`)
// Verify - check that the result contains the expected native format elements
expect(result).toContain(`File: ${testFilePath}`)
expect(result).toContain(`Lines 1-5:`)
})
it("should not show line snippet in approval message when maxReadFileLine is -1", async () => {
@ -418,17 +418,14 @@ describe("read_file tool with maxReadFileLine setting", () => {
},
)
// Verify
expect(result).toContain(`<file><path>${testFilePath}</path>`)
expect(result).toContain(`<list_code_definition_names>`)
// Verify - native format
expect(result).toContain(`File: ${testFilePath}`)
expect(result).toContain(`Code Definitions:`)
// Verify XML structure
expect(result).toContain("<notice>Showing only 0 of 5 total lines")
expect(result).toContain("</notice>")
expect(result).toContain("<list_code_definition_names>")
// Verify native structure
expect(result).toContain("Note: Showing only 0 of 5 total lines")
expect(result).toContain(sourceCodeDef.trim())
expect(result).toContain("</list_code_definition_names>")
expect(result).not.toContain("<content") // No content when maxReadFileLine is 0
expect(result).not.toContain("Lines 1-") // No content when maxReadFileLine is 0
})
})
@ -446,11 +443,11 @@ describe("read_file tool with maxReadFileLine setting", () => {
// Execute
const result = await executeReadFileTool({}, { maxReadFileLine: 3 })
// Verify - just check that the result contains the expected elements
expect(result).toContain(`<file><path>${testFilePath}</path>`)
expect(result).toContain(`<content lines="1-3">`)
expect(result).toContain(`<list_code_definition_names>`)
expect(result).toContain("<notice>Showing only 3 of 5 total lines")
// Verify - native format
expect(result).toContain(`File: ${testFilePath}`)
expect(result).toContain(`Lines 1-3:`)
expect(result).toContain(`Code Definitions:`)
expect(result).toContain("Note: Showing only 3 of 5 total lines")
})
it("should truncate code definitions when file exceeds maxReadFileLine", async () => {
@ -471,17 +468,17 @@ describe("read_file tool with maxReadFileLine setting", () => {
// Execute with maxReadFileLine = 30
const result = await executeReadFileTool({}, { maxReadFileLine: 30, totalLines: 100 })
// Verify that only definitions within the first 30 lines are included
expect(result).toContain(`<file><path>${testFilePath}</path>`)
expect(result).toContain(`<content lines="1-30">`)
expect(result).toContain(`<list_code_definition_names>`)
// Verify - native format
expect(result).toContain(`File: ${testFilePath}`)
expect(result).toContain(`Lines 1-30:`)
expect(result).toContain(`Code Definitions:`)
// Should include foo (starts at line 10) but not bar (starts at line 50) or baz (starts at line 80)
expect(result).toContain("10--20 | function foo()")
expect(result).not.toContain("50--60 | function bar()")
expect(result).not.toContain("80--90 | function baz()")
expect(result).toContain("<notice>Showing only 30 of 100 total lines")
expect(result).toContain("Note: Showing only 30 of 100 total lines")
})
it("should handle truncation when all definitions are beyond the line limit", async () => {
@ -499,10 +496,10 @@ describe("read_file tool with maxReadFileLine setting", () => {
// Execute with maxReadFileLine = 30
const result = await executeReadFileTool({}, { maxReadFileLine: 30, totalLines: 100 })
// Verify that only the header is included (all definitions filtered out)
expect(result).toContain(`<file><path>${testFilePath}</path>`)
expect(result).toContain(`<content lines="1-30">`)
expect(result).toContain(`<list_code_definition_names>`)
// Verify - native format
expect(result).toContain(`File: ${testFilePath}`)
expect(result).toContain(`Lines 1-30:`)
expect(result).toContain(`Code Definitions:`)
expect(result).toContain("# file.txt")
expect(result).not.toContain("50--60 | function foo()")
expect(result).not.toContain("80--90 | function bar()")
@ -518,9 +515,9 @@ describe("read_file tool with maxReadFileLine setting", () => {
// Execute
const result = await executeReadFileTool({}, { maxReadFileLine: 10, totalLines: 5 })
// Verify - just check that the result contains the expected elements
expect(result).toContain(`<file><path>${testFilePath}</path>`)
expect(result).toContain(`<content lines="1-5">`)
// Verify - native format
expect(result).toContain(`File: ${testFilePath}`)
expect(result).toContain(`Lines 1-5:`)
})
it("should read with extractTextFromFile when file has few lines", async () => {
@ -540,9 +537,9 @@ describe("read_file tool with maxReadFileLine setting", () => {
// Execute
const result = await executeReadFileTool({}, { maxReadFileLine: 5, totalLines: 3 })
// Verify - just check that the result contains the expected elements
expect(result).toContain(`<file><path>${testFilePath}</path>`)
expect(result).toContain(`<content lines="1-3">`)
// Verify - native format
expect(result).toContain(`File: ${testFilePath}`)
expect(result).toContain(`Lines 1-3:`)
})
})
@ -556,8 +553,8 @@ describe("read_file tool with maxReadFileLine setting", () => {
// Execute
const result = await executeReadFileTool({}, { maxReadFileLine: 3, totalLines: 3 })
// Verify - just check basic structure, the actual binary handling may vary
expect(result).toContain(`<file><path>${testFilePath}</path>`)
// Verify - native format for binary files
expect(result).toContain(`File: ${testFilePath}`)
expect(typeof result).toBe("string")
})
})
@ -576,14 +573,14 @@ describe("read_file tool with maxReadFileLine setting", () => {
},
)
// Verify - just check that the result contains the expected elements
expect(rangeResult).toContain(`<file><path>${testFilePath}</path>`)
expect(rangeResult).toContain(`<content lines="2-4">`)
// Verify - native format
expect(rangeResult).toContain(`File: ${testFilePath}`)
expect(rangeResult).toContain(`Lines 2-4:`)
})
})
})
describe("read_file tool XML output structure", () => {
describe("read_file tool output structure", () => {
// Test basic XML structure
const testFilePath = "test/file.txt"
const absoluteFilePath = "/test/file.txt"
@ -691,8 +688,8 @@ describe("read_file tool XML output structure", () => {
return toolResult
}
describe("Basic XML Structure Tests", () => {
it("should produce XML output with no unnecessary indentation", async () => {
describe("Basic Structure Tests", () => {
it("should produce native output with proper format", async () => {
// Setup
const numberedContent = "1 | Line 1\n2 | Line 2\n3 | Line 3\n4 | Line 4\n5 | Line 5"
@ -713,24 +710,19 @@ describe("read_file tool XML output structure", () => {
// Execute
const result = await executeReadFileTool()
// Verify
expect(result).toBe(
`<files>\n<file><path>${testFilePath}</path>\n<content lines="1-5">\n${numberedContent}</content>\n</file>\n</files>`,
)
// Verify native format
expect(result).toBe(`File: ${testFilePath}\nLines 1-5:\n${numberedContent}`)
})
it("should follow the correct XML structure format", async () => {
it("should follow the correct native structure format", async () => {
// Setup
mockInputContent = fileContent
// Execute
const result = await executeReadFileTool({}, { maxReadFileLine: -1 })
// Verify using regex to check structure
const xmlStructureRegex = new RegExp(
`^<files>\\n<file><path>${testFilePath}</path>\\n<content lines="1-5">\\n.*</content>\\n</file>\\n</files>$`,
"s",
)
expect(result).toMatch(xmlStructureRegex)
// Verify using regex to check native structure
const nativeStructureRegex = new RegExp(`^File: ${testFilePath}\\nLines 1-5:\\n.*$`, "s")
expect(result).toMatch(nativeStructureRegex)
})
it("should handle empty files correctly", async () => {
@ -754,10 +746,8 @@ describe("read_file tool XML output structure", () => {
// Execute
const result = await executeReadFileTool({}, { totalLines: 0 })
// Verify
expect(result).toBe(
`<files>\n<file><path>${testFilePath}</path>\n<content/><notice>File is empty</notice>\n</file>\n</files>`,
)
// Verify native format for empty file
expect(result).toBe(`File: ${testFilePath}\nNote: File is empty`)
})
describe("Total Image Memory Limit", () => {
@ -1406,7 +1396,7 @@ describe("read_file tool XML output structure", () => {
})
describe("Error Handling Tests", () => {
it("should include error tag for invalid path", async () => {
it("should include error in output for invalid path", async () => {
// Setup - missing path parameter
const toolUse: ReadFileToolUse = {
type: "tool_use",
@ -1426,17 +1416,17 @@ describe("read_file tool XML output structure", () => {
toolProtocol: "xml",
})
// Verify
expect(toolResult).toBe(`<files><error>Missing required parameter</error></files>`)
// Verify - native format for error
expect(toolResult).toBe(`Error: Missing required parameter`)
})
it("should include error tag for RooIgnore error", async () => {
it("should include error for RooIgnore error", async () => {
// Execute - skip addLineNumbers check as it returns early with an error
const result = await executeReadFileTool({}, { validateAccess: false })
// Verify
// Verify - native format for error
expect(result).toBe(
`<files>\n<file><path>${testFilePath}</path><error>Access to ${testFilePath} is blocked by the .rooignore file settings. You must try to continue in the task without using this file, or ask the user to update the .rooignore file.</error></file>\n</files>`,
`File: ${testFilePath}\nError: Access to ${testFilePath} is blocked by the .rooignore file settings. You must try to continue in the task without using this file, or ask the user to update the .rooignore file.`,
)
})
})
@ -1548,10 +1538,10 @@ describe("read_file tool with image support", () => {
const textPart = (result as any[]).find((p) => p.type === "text")?.text
const imagePart = (result as any[]).find((p) => p.type === "image")
// Verify text part
expect(textPart).toContain(`<file><path>${imagePath}</path>`)
// Verify text part - native format
expect(textPart).toContain(`File: ${imagePath}`)
expect(textPart).not.toContain("<image_data>")
expect(textPart).toContain(`<notice>Image file`)
expect(textPart).toContain(`Note: Image file`)
// Verify image part
expect(imagePart).toBeDefined()
@ -1570,10 +1560,10 @@ describe("read_file tool with image support", () => {
const textPart = (result as any[]).find((p) => p.type === "text")?.text
const imagePart = (result as any[]).find((p) => p.type === "image")
// Verify text part
expect(textPart).toContain(`<file><path>${testImagePath}</path>`)
// Verify text part - native format
expect(textPart).toContain(`File: ${testImagePath}`)
expect(textPart).not.toContain(`<image_data>`)
expect(textPart).toContain(`<notice>Image file`)
expect(textPart).toContain(`Note: Image file`)
// Verify image part
expect(imagePart).toBeDefined()
@ -1591,7 +1581,8 @@ describe("read_file tool with image support", () => {
const textArg = callArgs[0]
const imagesArg = callArgs[1]
expect(textArg).toContain(`<file><path>${testImagePath}</path>`)
// Native format
expect(textArg).toContain(`File: ${testImagePath}`)
expect(imagesArg).toBeDefined()
expect(imagesArg).toBeInstanceOf(Array)
expect(imagesArg!.length).toBe(1)
@ -1622,11 +1613,12 @@ describe("read_file tool with image support", () => {
// Execute
const result = await executeReadImageTool()
// When images are not supported, the tool should return just XML (not call formatResponse.toolResult)
// When images are not supported, the tool should return just text (not call formatResponse.toolResult)
expect(toolResultMock).not.toHaveBeenCalled()
expect(typeof result).toBe("string")
expect(result).toContain(`<file><path>${testImagePath}</path>`)
expect(result).toContain(`<notice>Image file`)
// Native format
expect(result).toContain(`File: ${testImagePath}`)
expect(result).toContain(`Note: Image file`)
})
it("should include images when model supports images", async () => {
@ -1642,7 +1634,8 @@ describe("read_file tool with image support", () => {
const textArg = callArgs[0]
const imagesArg = callArgs[1]
expect(textArg).toContain(`<file><path>${testImagePath}</path>`)
// Native format
expect(textArg).toContain(`File: ${testImagePath}`)
expect(imagesArg).toBeDefined() // Images should be included
expect(imagesArg).toBeInstanceOf(Array)
expect(imagesArg!.length).toBe(1)
@ -1656,11 +1649,12 @@ describe("read_file tool with image support", () => {
// Execute
const result = await executeReadImageTool()
// When supportsImages is undefined, should default to false and return just XML
// When supportsImages is undefined, should default to false and return just text
expect(toolResultMock).not.toHaveBeenCalled()
expect(typeof result).toBe("string")
expect(result).toContain(`<file><path>${testImagePath}</path>`)
expect(result).toContain(`<notice>Image file`)
// Native format
expect(result).toContain(`File: ${testImagePath}`)
expect(result).toContain(`Note: Image file`)
})
it("should handle errors when reading image files", async () => {
@ -1686,8 +1680,8 @@ describe("read_file tool with image support", () => {
toolProtocol: "xml",
})
// Verify error handling
expect(toolResult).toContain("<error>Error reading image file: Failed to read image</error>")
// Verify error handling - native format
expect(toolResult).toContain("Error: Error reading image file: Failed to read image")
// Verify that say was called to show error to user
expect(localMockCline.say).toHaveBeenCalledWith("error", expect.stringContaining("Failed to read image"))
})
@ -1722,9 +1716,9 @@ describe("read_file tool with image support", () => {
// Execute
const result = await executeReadImageTool(binaryPath)
// Verify
// Verify - native format for binary files
expect(result).not.toContain("<image_data>")
expect(result).toContain('<binary_file format="bin"')
expect(result).toContain("Binary file (bin)")
})
})

View file

@ -5,269 +5,73 @@ import type { ProviderSettings, ModelInfo } from "@roo-code/types"
import type { Anthropic } from "@anthropic-ai/sdk"
describe("resolveToolProtocol", () => {
describe("Precedence Level 1: User Profile Setting", () => {
it("should use profile toolProtocol when explicitly set to xml", () => {
/**
* XML Protocol Deprecation:
*
* XML tool protocol has been fully deprecated. All models now use Native
* tool calling. User preferences and model defaults are ignored.
*
* Precedence:
* 1. Locked Protocol (for resumed tasks that used XML)
* 2. Native (always, for all new tasks)
*/
describe("Locked Protocol (Precedence Level 0 - Highest Priority)", () => {
it("should return lockedProtocol when provided", () => {
const settings: ProviderSettings = {
toolProtocol: "xml", // Ignored
apiProvider: "openai-native",
}
// lockedProtocol overrides everything
const result = resolveToolProtocol(settings, undefined, "native")
expect(result).toBe(TOOL_PROTOCOL.NATIVE)
})
it("should return XML lockedProtocol for resumed tasks that used XML", () => {
const settings: ProviderSettings = {
toolProtocol: "native", // Ignored
apiProvider: "anthropic",
}
// lockedProtocol forces XML for backward compatibility
const result = resolveToolProtocol(settings, undefined, "xml")
expect(result).toBe(TOOL_PROTOCOL.XML)
})
it("should fall through to Native when lockedProtocol is undefined", () => {
const settings: ProviderSettings = {
toolProtocol: "xml", // Ignored
apiProvider: "anthropic",
}
// undefined lockedProtocol should return native
const result = resolveToolProtocol(settings, undefined, undefined)
expect(result).toBe(TOOL_PROTOCOL.NATIVE)
})
})
describe("Native Protocol Always Used For New Tasks", () => {
it("should always use native for new tasks", () => {
const settings: ProviderSettings = {
toolProtocol: "xml",
apiProvider: "anthropic",
}
const result = resolveToolProtocol(settings)
expect(result).toBe(TOOL_PROTOCOL.XML)
})
it("should use profile toolProtocol when explicitly set to native", () => {
const settings: ProviderSettings = {
toolProtocol: "native",
apiProvider: "anthropic",
}
const modelInfo: ModelInfo = {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
supportsNativeTools: true, // Model supports native tools
}
const result = resolveToolProtocol(settings, modelInfo)
expect(result).toBe(TOOL_PROTOCOL.NATIVE)
})
it("should override model default when profile setting is present", () => {
it("should use native even when user preference is XML (user prefs ignored)", () => {
const settings: ProviderSettings = {
toolProtocol: "xml",
toolProtocol: "xml", // User wants XML - ignored
apiProvider: "openai-native",
}
const modelInfo: ModelInfo = {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
defaultToolProtocol: "native",
supportsNativeTools: true,
}
const result = resolveToolProtocol(settings, modelInfo)
expect(result).toBe(TOOL_PROTOCOL.XML) // Profile setting wins
})
it("should override model capability when profile setting is present", () => {
const settings: ProviderSettings = {
toolProtocol: "xml",
apiProvider: "openai-native",
}
const modelInfo: ModelInfo = {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
supportsNativeTools: true,
}
const result = resolveToolProtocol(settings, modelInfo)
expect(result).toBe(TOOL_PROTOCOL.XML) // Profile setting wins
})
})
describe("Precedence Level 2: Model Default", () => {
it("should use model defaultToolProtocol when no profile setting", () => {
const settings: ProviderSettings = {
apiProvider: "roo",
}
const modelInfo: ModelInfo = {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
defaultToolProtocol: "native",
supportsNativeTools: true, // Model must support native tools
}
const result = resolveToolProtocol(settings, modelInfo)
expect(result).toBe(TOOL_PROTOCOL.NATIVE) // Model default wins when experiment is disabled
})
it("should override model capability when model default is present", () => {
const settings: ProviderSettings = {
apiProvider: "roo",
}
const modelInfo: ModelInfo = {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
defaultToolProtocol: "xml",
supportsNativeTools: true,
}
const result = resolveToolProtocol(settings, modelInfo)
expect(result).toBe(TOOL_PROTOCOL.XML) // Model default wins over capability
})
})
describe("Support Validation", () => {
it("should fall back to XML when model doesn't support native", () => {
const settings: ProviderSettings = {
apiProvider: "anthropic",
}
const modelInfo: ModelInfo = {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
supportsNativeTools: false,
}
const result = resolveToolProtocol(settings, modelInfo)
expect(result).toBe(TOOL_PROTOCOL.XML)
})
it("should fall back to XML when user prefers native but model doesn't support it", () => {
const settings: ProviderSettings = {
toolProtocol: "native", // User wants native
apiProvider: "anthropic",
}
const modelInfo: ModelInfo = {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
supportsNativeTools: false, // But model doesn't support it
}
const result = resolveToolProtocol(settings, modelInfo)
expect(result).toBe(TOOL_PROTOCOL.XML) // Falls back to XML due to lack of support
})
it("should fall back to XML when user prefers native but model support is undefined", () => {
const settings: ProviderSettings = {
toolProtocol: "native", // User wants native
apiProvider: "anthropic",
}
const modelInfo: ModelInfo = {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
// supportsNativeTools is undefined (not specified)
}
const result = resolveToolProtocol(settings, modelInfo)
expect(result).toBe(TOOL_PROTOCOL.XML) // Falls back to XML - undefined treated as unsupported
})
})
describe("Precedence Level 3: Native Fallback", () => {
it("should use Native fallback when no model default is specified and model supports native", () => {
const settings: ProviderSettings = {
apiProvider: "anthropic",
}
const modelInfo: ModelInfo = {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
supportsNativeTools: true,
}
const result = resolveToolProtocol(settings, modelInfo)
expect(result).toBe(TOOL_PROTOCOL.NATIVE) // Native fallback
})
})
describe("Complete Precedence Chain", () => {
it("should respect full precedence: Profile > Model Default > Native Fallback", () => {
// Set up a scenario with all levels defined
const settings: ProviderSettings = {
toolProtocol: "native", // Level 1: User profile setting
apiProvider: "roo",
}
const modelInfo: ModelInfo = {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
defaultToolProtocol: "xml", // Level 2: Model default
supportsNativeTools: true, // Support check
}
const result = resolveToolProtocol(settings, modelInfo)
expect(result).toBe(TOOL_PROTOCOL.NATIVE) // Profile setting wins
})
it("should skip to model default when profile setting is undefined", () => {
const settings: ProviderSettings = {
apiProvider: "openai-native",
}
const modelInfo: ModelInfo = {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
defaultToolProtocol: "xml", // Level 2
supportsNativeTools: true, // Support check
}
const result = resolveToolProtocol(settings, modelInfo)
expect(result).toBe(TOOL_PROTOCOL.XML) // Model default wins
})
it("should skip to Native fallback when profile and model default are undefined", () => {
const settings: ProviderSettings = {
apiProvider: "openai-native",
}
const modelInfo: ModelInfo = {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
supportsNativeTools: true,
}
const result = resolveToolProtocol(settings, modelInfo)
expect(result).toBe(TOOL_PROTOCOL.NATIVE) // Native fallback
})
it("should skip to XML fallback when model info is unavailable", () => {
const settings: ProviderSettings = {
apiProvider: "anthropic",
}
const result = resolveToolProtocol(settings, undefined)
expect(result).toBe(TOOL_PROTOCOL.XML) // XML fallback (no model info means no native support)
})
})
describe("Locked Protocol (Precedence Level 0)", () => {
it("should return lockedProtocol when provided, ignoring all other settings", () => {
const settings: ProviderSettings = {
toolProtocol: "xml", // User wants XML
apiProvider: "openai-native",
}
const modelInfo: ModelInfo = {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "xml",
}
// lockedProtocol overrides everything
const result = resolveToolProtocol(settings, modelInfo, "native")
const result = resolveToolProtocol(settings)
expect(result).toBe(TOOL_PROTOCOL.NATIVE)
})
it("should return XML lockedProtocol even when model supports native", () => {
it("should use native for OpenAI compatible provider", () => {
const settings: ProviderSettings = {
toolProtocol: "native", // User wants native
apiProvider: "anthropic",
apiProvider: "openai",
}
const modelInfo: ModelInfo = {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
supportsNativeTools: true, // Model supports native
defaultToolProtocol: "native",
}
// lockedProtocol forces XML
const result = resolveToolProtocol(settings, modelInfo, "xml")
expect(result).toBe(TOOL_PROTOCOL.XML)
})
it("should fall through to normal resolution when lockedProtocol is undefined", () => {
const settings: ProviderSettings = {
toolProtocol: "xml",
apiProvider: "anthropic",
}
const modelInfo: ModelInfo = {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
supportsNativeTools: true,
}
// undefined lockedProtocol should use normal precedence
const result = resolveToolProtocol(settings, modelInfo, undefined)
expect(result).toBe(TOOL_PROTOCOL.XML) // User setting wins
const result = resolveToolProtocol(settings, openAiModelInfoSaneDefaults)
expect(result).toBe(TOOL_PROTOCOL.NATIVE)
})
})
@ -275,7 +79,7 @@ describe("resolveToolProtocol", () => {
it("should handle missing provider name gracefully", () => {
const settings: ProviderSettings = {}
const result = resolveToolProtocol(settings)
expect(result).toBe(TOOL_PROTOCOL.XML) // Falls back to XML (no model info)
expect(result).toBe(TOOL_PROTOCOL.NATIVE) // Always native now
})
it("should handle undefined model info gracefully", () => {
@ -283,26 +87,18 @@ describe("resolveToolProtocol", () => {
apiProvider: "openai-native",
}
const result = resolveToolProtocol(settings, undefined)
expect(result).toBe(TOOL_PROTOCOL.XML) // XML fallback (no model info)
expect(result).toBe(TOOL_PROTOCOL.NATIVE) // Always native now
})
it("should fall back to XML when model doesn't support native", () => {
const settings: ProviderSettings = {
apiProvider: "roo",
}
const modelInfo: ModelInfo = {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
supportsNativeTools: false, // Model doesn't support native
}
const result = resolveToolProtocol(settings, modelInfo)
expect(result).toBe(TOOL_PROTOCOL.XML) // Falls back to XML due to lack of support
it("should handle empty settings", () => {
const settings: ProviderSettings = {}
const result = resolveToolProtocol(settings)
expect(result).toBe(TOOL_PROTOCOL.NATIVE) // Always native now
})
})
describe("Real-world Scenarios", () => {
it("should use Native fallback for models without defaultToolProtocol", () => {
it("should use Native for OpenAI models", () => {
const settings: ProviderSettings = {
apiProvider: "openai-native",
}
@ -313,10 +109,10 @@ describe("resolveToolProtocol", () => {
supportsNativeTools: true,
}
const result = resolveToolProtocol(settings, modelInfo)
expect(result).toBe(TOOL_PROTOCOL.NATIVE) // Native fallback
expect(result).toBe(TOOL_PROTOCOL.NATIVE)
})
it("should use XML for Claude models with Anthropic provider", () => {
it("should use Native for Claude models", () => {
const settings: ProviderSettings = {
apiProvider: "anthropic",
}
@ -324,65 +120,39 @@ describe("resolveToolProtocol", () => {
maxTokens: 8192,
contextWindow: 200000,
supportsPromptCache: true,
supportsNativeTools: false,
supportsNativeTools: true,
}
const result = resolveToolProtocol(settings, modelInfo)
expect(result).toBe(TOOL_PROTOCOL.NATIVE)
})
it("should honor locked protocol for resumed tasks that used XML", () => {
const settings: ProviderSettings = {
apiProvider: "anthropic",
}
// Task was started when XML was used, so it's locked to XML
const result = resolveToolProtocol(settings, undefined, "xml")
expect(result).toBe(TOOL_PROTOCOL.XML)
})
})
it("should allow user to force XML on native-supporting model", () => {
describe("Backward Compatibility - User Preferences Ignored", () => {
it("should ignore user preference for XML", () => {
const settings: ProviderSettings = {
toolProtocol: "xml", // User explicitly wants XML
toolProtocol: "xml", // User explicitly wants XML - ignored
apiProvider: "openai-native",
}
const modelInfo: ModelInfo = {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
supportsNativeTools: true, // Model supports native but user wants XML
defaultToolProtocol: "native",
}
const result = resolveToolProtocol(settings, modelInfo)
expect(result).toBe(TOOL_PROTOCOL.XML) // User preference wins
const result = resolveToolProtocol(settings)
expect(result).toBe(TOOL_PROTOCOL.NATIVE) // Native is always used
})
it("should not allow user to force native when model doesn't support it", () => {
it("should return native regardless of user preference", () => {
const settings: ProviderSettings = {
toolProtocol: "native", // User wants native
toolProtocol: "native", // User preference - ignored but happens to match
apiProvider: "anthropic",
}
const modelInfo: ModelInfo = {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
supportsNativeTools: false, // Model doesn't support native
}
const result = resolveToolProtocol(settings, modelInfo)
expect(result).toBe(TOOL_PROTOCOL.XML) // Falls back to XML due to lack of support
})
it("should use model default when available", () => {
const settings: ProviderSettings = {
apiProvider: "roo",
}
const modelInfo: ModelInfo = {
maxTokens: 8192,
contextWindow: 200000,
supportsPromptCache: true,
defaultToolProtocol: "xml",
supportsNativeTools: true,
}
const result = resolveToolProtocol(settings, modelInfo)
expect(result).toBe(TOOL_PROTOCOL.XML) // Model default wins
})
it("should use native tools for OpenAI compatible provider with default model info", () => {
const settings: ProviderSettings = {
apiProvider: "openai",
}
// Using the actual openAiModelInfoSaneDefaults to verify the fix
const result = resolveToolProtocol(settings, openAiModelInfoSaneDefaults)
expect(result).toBe(TOOL_PROTOCOL.NATIVE) // Should use native tools by default
const result = resolveToolProtocol(settings)
expect(result).toBe(TOOL_PROTOCOL.NATIVE)
})
})
})

View file

@ -1,5 +1,5 @@
import { ToolProtocol, TOOL_PROTOCOL } from "@roo-code/types"
import type { ProviderSettings, ModelInfo } from "@roo-code/types"
import type { ProviderSettings } from "@roo-code/types"
import type { Anthropic } from "@anthropic-ai/sdk"
import { findLast, findLastIndex } from "../shared/array"
@ -12,48 +12,35 @@ type ApiMessageForDetection = Anthropic.MessageParam & {
}
/**
* Resolve the effective tool protocol based on the precedence hierarchy:
* Resolve the effective tool protocol.
*
* 0. Locked Protocol (task-level lock, if provided - highest priority)
* 1. User Preference - Per-Profile (explicit profile setting)
* 2. Model Default (defaultToolProtocol in ModelInfo)
* 3. Native Fallback (final fallback)
* **Deprecation Note (XML Protocol):**
* XML tool protocol has been deprecated. All models now use Native tool calling.
* User/profile preferences (`providerSettings.toolProtocol`) and model defaults
* (`modelInfo.defaultToolProtocol`) are ignored.
*
* Then check support: if protocol is "native" but model doesn't support it, use XML.
* Precedence:
* 1. Locked Protocol (task-level lock for resumed tasks - highest priority)
* 2. Native (always, for all new tasks)
*
* @param providerSettings - The provider settings for the current profile
* @param modelInfo - Optional model information containing capabilities
* @param _providerSettings - The provider settings (toolProtocol field is ignored)
* @param _modelInfo - Unused, kept for API compatibility
* @param lockedProtocol - Optional task-locked protocol that takes absolute precedence
* @returns The resolved tool protocol (either "xml" or "native")
*/
export function resolveToolProtocol(
providerSettings: ProviderSettings,
modelInfo?: ModelInfo,
_providerSettings: ProviderSettings,
_modelInfo?: unknown,
lockedProtocol?: ToolProtocol,
): ToolProtocol {
// 0. Locked Protocol - task-level lock takes absolute precedence
// This ensures tasks continue using their original protocol even if settings change
// 1. Locked Protocol - task-level lock takes absolute precedence
// This ensures resumed tasks continue using their original protocol
if (lockedProtocol) {
return lockedProtocol
}
// If model doesn't support native tools, return XML immediately
// Treat undefined as unsupported (only allow native when explicitly true)
if (modelInfo?.supportsNativeTools !== true) {
return TOOL_PROTOCOL.XML
}
// 1. User Preference - Per-Profile (explicit profile setting, highest priority)
if (providerSettings.toolProtocol) {
return providerSettings.toolProtocol
}
// 2. Model Default - model's preferred protocol
if (modelInfo?.defaultToolProtocol) {
return modelInfo.defaultToolProtocol
}
// 3. Native Fallback
// 2. Always return Native protocol for new tasks
// All models now support native tools; XML is deprecated
return TOOL_PROTOCOL.NATIVE
}

View file

@ -38,8 +38,6 @@ import {
vercelAiGatewayDefaultModelId,
deepInfraDefaultModelId,
minimaxDefaultModelId,
type ToolProtocol,
TOOL_PROTOCOL,
} from "@roo-code/types"
import { vscode } from "@src/utils/vscode"
@ -413,17 +411,6 @@ const ApiOptions = ({
}
}, [selectedProvider])
// Calculate the default protocol that would be used if toolProtocol is not set
// Mirrors the simplified logic in resolveToolProtocol.ts:
// 1. User preference (toolProtocol) - handled by the select value binding
// 2. Model default - use if available
// 3. Native fallback
const defaultProtocol = selectedModelInfo?.defaultToolProtocol || TOOL_PROTOCOL.NATIVE
// Show the tool protocol selector when model supports native tools.
// For OpenAI Compatible providers we always show it so users can force XML/native explicitly.
const showToolProtocolSelector = selectedProvider === "openai" || selectedModelInfo?.supportsNativeTools === true
// Convert providers to SearchableSelect options
const providerOptions = useMemo(() => {
// First filter by organization allow list
@ -938,39 +925,6 @@ const ApiOptions = ({
</div>
</div>
)}
{showToolProtocolSelector && (
<div>
<label className="block font-medium mb-1">{t("settings:toolProtocol.label")}</label>
<Select
value={apiConfiguration.toolProtocol || "default"}
onValueChange={(value) => {
const newValue = value === "default" ? undefined : (value as ToolProtocol)
setApiConfigurationField("toolProtocol", newValue)
}}>
<SelectTrigger className="w-full">
<SelectValue placeholder={t("settings:common.select")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">
{t("settings:toolProtocol.default")} (
{defaultProtocol === TOOL_PROTOCOL.NATIVE
? t("settings:toolProtocol.native")
: t("settings:toolProtocol.xml")}
)
</SelectItem>
<SelectItem value={TOOL_PROTOCOL.XML}>
{t("settings:toolProtocol.xml")}
</SelectItem>
<SelectItem value={TOOL_PROTOCOL.NATIVE}>
{t("settings:toolProtocol.native")}
</SelectItem>
</SelectContent>
</Select>
<div className="text-sm text-vscode-descriptionForeground mt-1">
{t("settings:toolProtocol.description")}
</div>
</div>
)}
</CollapsibleContent>
</Collapsible>
)}