feat: implement dynamic tool protocol resolution with proper precedence hierarchy (#9286)

Co-authored-by: Roo Code <roomote@roocode.com>
This commit is contained in:
Daniel 2025-11-15 14:35:04 -05:00 committed by GitHub
parent 4e316e0de8
commit 744f4bd4c8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 765 additions and 50 deletions

View file

@ -109,6 +109,8 @@ export const modelInfoSchema = z.object({
isFree: z.boolean().optional(),
// Flag to indicate if the model supports native tool calling (OpenAI-style function calling)
supportsNativeTools: z.boolean().optional(),
// Default tool protocol preferred by this model (if not specified, falls back to capability/provider defaults)
defaultToolProtocol: z.enum(["xml", "native"]).optional(),
/**
* Service tiers with pricing information.
* Each tier can have a name (for OpenAI service tiers) and pricing overrides.

View file

@ -182,6 +182,9 @@ const baseProviderSettingsSchema = z.object({
// Model verbosity.
verbosity: verbosityLevelsSchema.optional(),
// Tool protocol override for this profile.
toolProtocol: z.enum(["xml", "native"]).optional(),
})
// Several of the providers share common model config properties.

View file

@ -1,6 +1,7 @@
import { z } from "zod"
import type { ModelInfo } from "../model.js"
import { TOOL_PROTOCOL } from "../tool.js"
/**
* Roo Code Cloud is a dynamic provider - models are loaded from the /v1/models API endpoint.
@ -14,6 +15,38 @@ export const rooDefaultModelId = "xai/grok-code-fast-1"
*/
export const rooModels = {} as const satisfies Record<string, ModelInfo>
/**
* Model-specific defaults for Roo provider models.
* These defaults are merged with dynamically fetched model data.
*
* Use this to configure model-specific settings like defaultToolProtocol.
*
* Example usage:
* ```typescript
* export const rooModelDefaults: Record<string, Partial<ModelInfo>> = {
* "anthropic/claude-3-5-sonnet-20241022": {
* defaultToolProtocol: "xml",
* },
* "openai/gpt-4o": {
* defaultToolProtocol: "native",
* },
* "xai/grok-code-fast-1": {
* defaultToolProtocol: "native",
* },
* }
* ```
*/
export const rooModelDefaults: Record<string, Partial<ModelInfo>> = {
// Add model-specific defaults below.
// You can configure defaultToolProtocol and other ModelInfo fields for specific model IDs.
"anthropic/claude-haiku-4.5": {
defaultToolProtocol: TOOL_PROTOCOL.NATIVE,
},
"minimax/minimax-m2:free": {
defaultToolProtocol: TOOL_PROTOCOL.NATIVE,
},
}
/**
* Roo Code Cloud API response schemas
*/

View file

@ -1,4 +1,4 @@
import { RooModelsResponseSchema } from "@roo-code/types"
import { RooModelsResponseSchema, rooModelDefaults } from "@roo-code/types"
import type { ModelRecord } from "../../../shared/api"
import { parseApiPrice } from "../../../shared/cost"
@ -101,7 +101,8 @@ export async function getRooModels(baseUrl: string, apiKey?: string): Promise<Mo
const cacheReadPrice = pricing.input_cache_read ? parseApiPrice(pricing.input_cache_read) : undefined
const cacheWritePrice = pricing.input_cache_write ? parseApiPrice(pricing.input_cache_write) : undefined
models[modelId] = {
// Build the base model info from API response
const baseModelInfo = {
maxTokens,
contextWindow,
supportsImages,
@ -117,6 +118,11 @@ export async function getRooModels(baseUrl: string, apiKey?: string): Promise<Mo
deprecated: model.deprecated || false,
isFree: tags.includes("free"),
}
// Merge with model-specific defaults if they exist
// Defaults take precedence over dynamically fetched data for specified fields
const modelDefaults = rooModelDefaults[modelId]
models[modelId] = modelDefaults ? { ...baseModelInfo, ...modelDefaults } : baseModelInfo
}
return models

View file

@ -1,7 +1,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { rooDefaultModelId, getApiProtocol } from "@roo-code/types"
import { rooDefaultModelId, rooModelDefaults, getApiProtocol } from "@roo-code/types"
import { CloudService } from "@roo-code/cloud"
import type { ApiHandlerOptions, ModelRecord } from "../../shared/api"
@ -274,18 +274,26 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
}
// Return the requested model ID even if not found, with fallback info.
// Check if there are model-specific defaults configured
const baseModelInfo = {
maxTokens: 16_384,
contextWindow: 262_144,
supportsImages: false,
supportsReasoningEffort: false,
supportsPromptCache: true,
supportsNativeTools: false,
inputPrice: 0,
outputPrice: 0,
isFree: false,
}
// Merge with model-specific defaults if they exist
const modelDefaults = rooModelDefaults[modelId]
const fallbackInfo = modelDefaults ? { ...baseModelInfo, ...modelDefaults } : baseModelInfo
return {
id: modelId,
info: {
maxTokens: 16_384,
contextWindow: 262_144,
supportsImages: false,
supportsReasoningEffort: false,
supportsPromptCache: true,
supportsNativeTools: false,
inputPrice: 0,
outputPrice: 0,
},
info: fallbackInfo,
}
}
}

View file

@ -39,7 +39,7 @@ import { codebaseSearchTool } from "../tools/CodebaseSearchTool"
import { experiments, EXPERIMENT_IDS } from "../../shared/experiments"
import { applyDiffTool as applyDiffToolClass } from "../tools/ApplyDiffTool"
import { isNativeProtocol } from "@roo-code/types"
import { getToolProtocolFromSettings } from "../../utils/toolProtocol"
import { resolveToolProtocol } from "../../utils/resolveToolProtocol"
/**
* Processes and presents assistant message content to the user interface.
@ -282,7 +282,12 @@ export async function presentAssistantMessage(cline: Task) {
const pushToolResult = (content: ToolResponse) => {
// Check if we're using native tool protocol
const isNative = isNativeProtocol(getToolProtocolFromSettings())
const toolProtocol = resolveToolProtocol(
cline.apiConfiguration,
cline.api.getModel().info,
cline.apiConfiguration.apiProvider,
)
const isNative = isNativeProtocol(toolProtocol)
// Get the tool call ID if this is a native tool call
const toolCallId = (block as any).id
@ -513,7 +518,12 @@ export async function presentAssistantMessage(cline: Task) {
await checkpointSaveAndMark(cline)
// Check if native protocol is enabled - if so, always use single-file class-based tool
if (isNativeProtocol(getToolProtocolFromSettings())) {
const applyDiffToolProtocol = resolveToolProtocol(
cline.apiConfiguration,
cline.api.getModel().info,
cline.apiConfiguration.apiProvider,
)
if (isNativeProtocol(applyDiffToolProtocol)) {
await applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, {
askApproval,
handleError,

View file

@ -4,7 +4,7 @@ You are Roo, an experienced technical leader who is inquisitive and an excellent
MARKDOWN RULES
ALL responses MUST show ANY `language construct` OR filename reference as clickable, exactly as [`filename OR language.declaration()`](relative/file/path.ext:line); line is required for `syntax` and optional for filename links. This applies to ALL markdown responses and ALSO those in <attempt_completion>
ALL responses MUST show ANY `language construct` OR filename reference as clickable, exactly as [`filename OR language.declaration()`](relative/file/path.ext:line); line is required for `syntax` and optional for filename links. This applies to ALL markdown responses and ALSO those in attempt_completion
====

View file

@ -4,7 +4,7 @@ You are Roo, a knowledgeable technical assistant focused on answering questions
MARKDOWN RULES
ALL responses MUST show ANY `language construct` OR filename reference as clickable, exactly as [`filename OR language.declaration()`](relative/file/path.ext:line); line is required for `syntax` and optional for filename links. This applies to ALL markdown responses and ALSO those in <attempt_completion>
ALL responses MUST show ANY `language construct` OR filename reference as clickable, exactly as [`filename OR language.declaration()`](relative/file/path.ext:line); line is required for `syntax` and optional for filename links. This applies to ALL markdown responses and ALSO those in attempt_completion
====

View file

@ -4,7 +4,7 @@ You are Roo, an experienced technical leader who is inquisitive and an excellent
MARKDOWN RULES
ALL responses MUST show ANY `language construct` OR filename reference as clickable, exactly as [`filename OR language.declaration()`](relative/file/path.ext:line); line is required for `syntax` and optional for filename links. This applies to ALL markdown responses and ALSO those in <attempt_completion>
ALL responses MUST show ANY `language construct` OR filename reference as clickable, exactly as [`filename OR language.declaration()`](relative/file/path.ext:line); line is required for `syntax` and optional for filename links. This applies to ALL markdown responses and ALSO those in attempt_completion
====

View file

@ -4,7 +4,7 @@ You are Roo, an experienced technical leader who is inquisitive and an excellent
MARKDOWN RULES
ALL responses MUST show ANY `language construct` OR filename reference as clickable, exactly as [`filename OR language.declaration()`](relative/file/path.ext:line); line is required for `syntax` and optional for filename links. This applies to ALL markdown responses and ALSO those in <attempt_completion>
ALL responses MUST show ANY `language construct` OR filename reference as clickable, exactly as [`filename OR language.declaration()`](relative/file/path.ext:line); line is required for `syntax` and optional for filename links. This applies to ALL markdown responses and ALSO those in attempt_completion
====

View file

@ -4,7 +4,7 @@ You are Roo, an experienced technical leader who is inquisitive and an excellent
MARKDOWN RULES
ALL responses MUST show ANY `language construct` OR filename reference as clickable, exactly as [`filename OR language.declaration()`](relative/file/path.ext:line); line is required for `syntax` and optional for filename links. This applies to ALL markdown responses and ALSO those in <attempt_completion>
ALL responses MUST show ANY `language construct` OR filename reference as clickable, exactly as [`filename OR language.declaration()`](relative/file/path.ext:line); line is required for `syntax` and optional for filename links. This applies to ALL markdown responses and ALSO those in attempt_completion
====

View file

@ -4,7 +4,7 @@ You are Roo, an experienced technical leader who is inquisitive and an excellent
MARKDOWN RULES
ALL responses MUST show ANY `language construct` OR filename reference as clickable, exactly as [`filename OR language.declaration()`](relative/file/path.ext:line); line is required for `syntax` and optional for filename links. This applies to ALL markdown responses and ALSO those in <attempt_completion>
ALL responses MUST show ANY `language construct` OR filename reference as clickable, exactly as [`filename OR language.declaration()`](relative/file/path.ext:line); line is required for `syntax` and optional for filename links. This applies to ALL markdown responses and ALSO those in attempt_completion
====

View file

@ -4,7 +4,7 @@ You are Roo, an experienced technical leader who is inquisitive and an excellent
MARKDOWN RULES
ALL responses MUST show ANY `language construct` OR filename reference as clickable, exactly as [`filename OR language.declaration()`](relative/file/path.ext:line); line is required for `syntax` and optional for filename links. This applies to ALL markdown responses and ALSO those in <attempt_completion>
ALL responses MUST show ANY `language construct` OR filename reference as clickable, exactly as [`filename OR language.declaration()`](relative/file/path.ext:line); line is required for `syntax` and optional for filename links. This applies to ALL markdown responses and ALSO those in attempt_completion
====

View file

@ -4,7 +4,7 @@ You are Roo, an experienced technical leader who is inquisitive and an excellent
MARKDOWN RULES
ALL responses MUST show ANY `language construct` OR filename reference as clickable, exactly as [`filename OR language.declaration()`](relative/file/path.ext:line); line is required for `syntax` and optional for filename links. This applies to ALL markdown responses and ALSO those in <attempt_completion>
ALL responses MUST show ANY `language construct` OR filename reference as clickable, exactly as [`filename OR language.declaration()`](relative/file/path.ext:line); line is required for `syntax` and optional for filename links. This applies to ALL markdown responses and ALSO those in attempt_completion
====

View file

@ -4,7 +4,7 @@ You are Roo, an experienced technical leader who is inquisitive and an excellent
MARKDOWN RULES
ALL responses MUST show ANY `language construct` OR filename reference as clickable, exactly as [`filename OR language.declaration()`](relative/file/path.ext:line); line is required for `syntax` and optional for filename links. This applies to ALL markdown responses and ALSO those in <attempt_completion>
ALL responses MUST show ANY `language construct` OR filename reference as clickable, exactly as [`filename OR language.declaration()`](relative/file/path.ext:line); line is required for `syntax` and optional for filename links. This applies to ALL markdown responses and ALSO those in attempt_completion
====

View file

@ -4,7 +4,7 @@ You are Roo, an experienced technical leader who is inquisitive and an excellent
MARKDOWN RULES
ALL responses MUST show ANY `language construct` OR filename reference as clickable, exactly as [`filename OR language.declaration()`](relative/file/path.ext:line); line is required for `syntax` and optional for filename links. This applies to ALL markdown responses and ALSO those in <attempt_completion>
ALL responses MUST show ANY `language construct` OR filename reference as clickable, exactly as [`filename OR language.declaration()`](relative/file/path.ext:line); line is required for `syntax` and optional for filename links. This applies to ALL markdown responses and ALSO those in attempt_completion
====

View file

@ -4,7 +4,7 @@ You are Roo, an experienced technical leader who is inquisitive and an excellent
MARKDOWN RULES
ALL responses MUST show ANY `language construct` OR filename reference as clickable, exactly as [`filename OR language.declaration()`](relative/file/path.ext:line); line is required for `syntax` and optional for filename links. This applies to ALL markdown responses and ALSO those in <attempt_completion>
ALL responses MUST show ANY `language construct` OR filename reference as clickable, exactly as [`filename OR language.declaration()`](relative/file/path.ext:line); line is required for `syntax` and optional for filename links. This applies to ALL markdown responses and ALSO those in attempt_completion
====

View file

@ -4,7 +4,7 @@ You are Roo, an experienced technical leader who is inquisitive and an excellent
MARKDOWN RULES
ALL responses MUST show ANY `language construct` OR filename reference as clickable, exactly as [`filename OR language.declaration()`](relative/file/path.ext:line); line is required for `syntax` and optional for filename links. This applies to ALL markdown responses and ALSO those in <attempt_completion>
ALL responses MUST show ANY `language construct` OR filename reference as clickable, exactly as [`filename OR language.declaration()`](relative/file/path.ext:line); line is required for `syntax` and optional for filename links. This applies to ALL markdown responses and ALSO those in attempt_completion
====

View file

@ -4,7 +4,7 @@ You are Roo, an experienced technical leader who is inquisitive and an excellent
MARKDOWN RULES
ALL responses MUST show ANY `language construct` OR filename reference as clickable, exactly as [`filename OR language.declaration()`](relative/file/path.ext:line); line is required for `syntax` and optional for filename links. This applies to ALL markdown responses and ALSO those in <attempt_completion>
ALL responses MUST show ANY `language construct` OR filename reference as clickable, exactly as [`filename OR language.declaration()`](relative/file/path.ext:line); line is required for `syntax` and optional for filename links. This applies to ALL markdown responses and ALSO those in attempt_completion
====

View file

@ -3,5 +3,5 @@ export function markdownFormattingSection(): string {
MARKDOWN RULES
ALL responses MUST show ANY \`language construct\` OR filename reference as clickable, exactly as [\`filename OR language.declaration()\`](relative/file/path.ext:line); line is required for \`syntax\` and optional for filename links. This applies to ALL markdown responses and ALSO those in <attempt_completion>`
ALL responses MUST show ANY \`language construct\` OR filename reference as clickable, exactly as [\`filename OR language.declaration()\`](relative/file/path.ext:line); line is required for \`syntax\` and optional for filename links. This applies to ALL markdown responses and ALSO those in attempt_completion`
}

View file

@ -47,6 +47,7 @@ import {
import { TelemetryService } from "@roo-code/telemetry"
import { CloudService, BridgeOrchestrator } from "@roo-code/cloud"
import { getToolProtocolFromSettings } from "../../utils/toolProtocol"
import { resolveToolProtocol } from "../../utils/resolveToolProtocol"
// api
import { ApiHandler, ApiHandlerCreateMessageMetadata, buildApiHandler } from "../../api"
@ -409,7 +410,12 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// Initialize the assistant message parser only for XML protocol.
// For native protocol, tool calls come as tool_call chunks, not XML.
this.assistantMessageParser = getToolProtocolFromSettings() === "xml" ? new AssistantMessageParser() : undefined
const toolProtocol = resolveToolProtocol(
this.apiConfiguration,
this.api.getModel().info,
this.apiConfiguration.apiProvider,
)
this.assistantMessageParser = toolProtocol === "xml" ? new AssistantMessageParser() : undefined
this.messageQueueService = new MessageQueueService()
@ -1261,7 +1267,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
relPath ? ` for '${relPath.toPosix()}'` : ""
} without value for required parameter '${paramName}'. Retrying...`,
)
return formatResponse.toolError(formatResponse.missingToolParameterError(paramName))
const modelInfo = this.api.getModel().info
const toolProtocol = resolveToolProtocol(this.apiConfiguration, modelInfo, this.apiConfiguration.apiProvider)
return formatResponse.toolError(formatResponse.missingToolParameterError(paramName, toolProtocol))
}
// Lifecycle
@ -1401,7 +1409,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// v2.0 xml tags refactor caveat: since we don't use tools anymore, we need to replace all tool use blocks with a text block since the API disallows conversations with tool uses and no tool schema
// Now also protocol-aware: format according to current protocol setting
const protocol = getCurrentToolProtocol()
const protocol = resolveToolProtocol(
this.apiConfiguration,
this.api.getModel().info,
this.apiConfiguration.apiProvider,
)
const useNative = isNativeProtocol(protocol)
const conversationWithoutToolBlocks = existingApiConversationHistory.map((message) => {
@ -1773,7 +1785,13 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// the user hits max requests and denies resetting the count.
break
} else {
nextUserContent = [{ type: "text", text: formatResponse.noToolsUsed() }]
const modelInfo = this.api.getModel().info
const toolProtocol = resolveToolProtocol(
this.apiConfiguration,
modelInfo,
this.apiConfiguration.apiProvider,
)
nextUserContent = [{ type: "text", text: formatResponse.noToolsUsed(toolProtocol) }]
this.consecutiveMistakeCount++
}
}
@ -2418,7 +2436,13 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
const parsedBlocks = this.assistantMessageParser.getContentBlocks()
// Check if we're using native protocol
const isNative = isNativeProtocol(getToolProtocolFromSettings())
const isNative = isNativeProtocol(
resolveToolProtocol(
this.apiConfiguration,
this.api.getModel().info,
this.apiConfiguration.apiProvider,
),
)
if (isNative) {
// For native protocol: Preserve tool_use blocks that were added via tool_call chunks
@ -2556,7 +2580,13 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
const didToolUse = this.assistantMessageContent.some((block) => block.type === "tool_use")
if (!didToolUse) {
this.userMessageContent.push({ type: "text", text: formatResponse.noToolsUsed() })
const modelInfo = this.api.getModel().info
const toolProtocol = resolveToolProtocol(
this.apiConfiguration,
modelInfo,
this.apiConfiguration.apiProvider,
)
this.userMessageContent.push({ type: "text", text: formatResponse.noToolsUsed(toolProtocol) })
this.consecutiveMistakeCount++
}
@ -2580,7 +2610,16 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// apiConversationHistory at line 1876. Since the assistant failed to respond,
// we need to remove that message before retrying to avoid having two consecutive
// user messages (which would cause tool_result validation errors).
if (isNativeProtocol(getToolProtocolFromSettings()) && this.apiConversationHistory.length > 0) {
if (
isNativeProtocol(
resolveToolProtocol(
this.apiConfiguration,
this.api.getModel().info,
this.apiConfiguration.apiProvider,
),
) &&
this.apiConversationHistory.length > 0
) {
const lastMessage = this.apiConversationHistory[this.apiConversationHistory.length - 1]
if (lastMessage.role === "user") {
// Remove the last user message that we added earlier
@ -2642,7 +2681,15 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
} else {
// User declined to retry
// For native protocol, re-add the user message we removed
if (isNativeProtocol(getToolProtocolFromSettings())) {
if (
isNativeProtocol(
resolveToolProtocol(
this.apiConfiguration,
this.api.getModel().info,
this.apiConfiguration.apiProvider,
),
)
) {
await this.addToApiConversationHistory({
role: "user",
content: currentUserContent,
@ -2739,6 +2786,13 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
const canUseBrowserTool = modelSupportsBrowser && modeSupportsBrowser && (browserToolEnabled ?? true)
// Resolve the tool protocol based on profile, model, and provider settings
const toolProtocol = resolveToolProtocol(
apiConfiguration ?? this.apiConfiguration,
modelInfo,
(apiConfiguration ?? this.apiConfiguration)?.apiProvider,
)
return SYSTEM_PROMPT(
provider.context,
this.cwd,
@ -2765,7 +2819,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
newTaskRequireTodos: vscode.workspace
.getConfiguration(Package.name)
.get<boolean>("newTaskRequireTodos", false),
toolProtocol: getToolProtocolFromSettings(),
toolProtocol,
},
undefined, // todoList
this.api.getModel().id,
@ -2975,8 +3029,8 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// Determine if we should include native tools based on:
// 1. Tool protocol is set to NATIVE
// 2. Model supports native tools
const toolProtocol = getToolProtocolFromSettings()
const modelInfo = this.api.getModel().info
const toolProtocol = resolveToolProtocol(this.apiConfiguration, modelInfo, this.apiConfiguration.apiProvider)
const shouldIncludeTools = toolProtocol === TOOL_PROTOCOL.NATIVE && (modelInfo.supportsNativeTools ?? false)
// Build complete tools array: native tools + dynamic MCP tools, filtered by mode restrictions

View file

@ -17,7 +17,7 @@ import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
import { applyDiffTool as applyDiffToolClass } from "./ApplyDiffTool"
import { computeDiffStats, sanitizeUnifiedDiff } from "../diff/stats"
import { isNativeProtocol } from "@roo-code/types"
import { getToolProtocolFromSettings } from "../../utils/toolProtocol"
import { resolveToolProtocol } from "../../utils/resolveToolProtocol"
interface DiffOperation {
path: string
@ -62,7 +62,12 @@ export async function applyDiffTool(
removeClosingTag: RemoveClosingTag,
) {
// Check if native protocol is enabled - if so, always use single-file class-based tool
if (isNativeProtocol(getToolProtocolFromSettings())) {
const toolProtocol = resolveToolProtocol(
cline.apiConfiguration,
cline.api.getModel().info,
cline.apiConfiguration.apiProvider,
)
if (isNativeProtocol(toolProtocol)) {
return applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, {
askApproval,
handleError,

View file

@ -15,7 +15,7 @@ import { readLines } from "../../integrations/misc/read-lines"
import { extractTextFromFile, addLineNumbers, getSupportedBinaryFormats } from "../../integrations/misc/extract-text"
import { parseSourceCodeDefinitionsForFile } from "../../services/tree-sitter"
import { parseXml } from "../../utils/xml"
import { getCurrentToolProtocol } from "./helpers/toolResultFormatting"
import { resolveToolProtocol } from "../../utils/resolveToolProtocol"
import {
DEFAULT_MAX_IMAGE_FILE_SIZE_MB,
DEFAULT_MAX_TOTAL_IMAGE_SIZE_MB,
@ -108,7 +108,8 @@ export class ReadFileTool extends BaseTool<"read_file"> {
async execute(params: { files: FileEntry[] }, task: Task, callbacks: ToolCallbacks): Promise<void> {
const { handleError, pushToolResult } = callbacks
const fileEntries = params.files
const protocol = getCurrentToolProtocol()
const modelInfo = task.api.getModel().info
const protocol = resolveToolProtocol(task.apiConfiguration, modelInfo, task.apiConfiguration.apiProvider)
const useNative = isNativeProtocol(protocol)
if (!fileEntries || fileEntries.length === 0) {
@ -120,7 +121,6 @@ export class ReadFileTool extends BaseTool<"read_file"> {
return
}
const modelInfo = task.api.getModel().info
const supportsImages = modelInfo.supportsImages ?? false
const fileResults: FileResult[] = fileEntries.map((entry) => ({

View file

@ -53,8 +53,19 @@ describe("applyDiffTool experiment routing", () => {
diffViewProvider: {
reset: vi.fn(),
},
apiConfiguration: {
apiProvider: "anthropic",
},
api: {
getModel: vi.fn().mockReturnValue({ id: "test-model" }),
getModel: vi.fn().mockReturnValue({
id: "test-model",
info: {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
supportsNativeTools: false,
},
}),
},
processQueuedMessages: vi.fn(),
} as any
@ -151,6 +162,17 @@ describe("applyDiffTool experiment routing", () => {
get: vi.fn().mockReturnValue(TOOL_PROTOCOL.NATIVE),
} as any)
// Update model to support native tools
mockCline.api.getModel = vi.fn().mockReturnValue({
id: "test-model",
info: {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
supportsNativeTools: true, // Enable native tools support
},
})
mockProvider.getState.mockResolvedValue({
experiments: {
[EXPERIMENT_IDS.MULTI_FILE_APPLY_DIFF]: true,

View file

@ -78,8 +78,19 @@ describe("multiApplyDiffTool", () => {
saveChanges: vi.fn().mockResolvedValue(undefined),
pushToolWriteResult: vi.fn().mockResolvedValue("File modified successfully"),
},
apiConfiguration: {
apiProvider: "anthropic",
},
api: {
getModel: vi.fn().mockReturnValue({ id: "test-model" }),
getModel: vi.fn().mockReturnValue({
id: "test-model",
info: {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
supportsNativeTools: false,
},
}),
},
rooIgnoreController: {
validateAccess: vi.fn().mockReturnValue(true),

View file

@ -204,10 +204,19 @@ function createMockCline(): any {
getTokenUsage: vi.fn().mockReturnValue({
contextTokens: 10000,
}),
apiConfiguration: {
apiProvider: "anthropic",
},
// CRITICAL: Always ensure image support is enabled
api: {
getModel: vi.fn().mockReturnValue({
info: { supportsImages: true, contextWindow: 200000 },
info: {
supportsImages: true,
contextWindow: 200000,
maxTokens: 4096,
supportsPromptCache: false,
supportsNativeTools: false,
},
}),
},
}

View file

@ -8,7 +8,7 @@ import { SYSTEM_PROMPT } from "../prompts/system"
import { MultiSearchReplaceDiffStrategy } from "../diff/strategies/multi-search-replace"
import { MultiFileSearchReplaceDiffStrategy } from "../diff/strategies/multi-file-search-replace"
import { Package } from "../../shared/package"
import { getToolProtocolFromSettings } from "../../utils/toolProtocol"
import { resolveToolProtocol } from "../../utils/resolveToolProtocol"
import { ClineProvider } from "./ClineProvider"
@ -69,6 +69,9 @@ export const generateSystemPrompt = async (provider: ClineProvider, message: Web
// and browser tools are enabled in settings
const canUseBrowserTool = modelSupportsBrowser && modeSupportsBrowser && (browserToolEnabled ?? true)
// Resolve tool protocol for system prompt generation
const toolProtocol = resolveToolProtocol(apiConfiguration, modelInfo, apiConfiguration.apiProvider)
const systemPrompt = await SYSTEM_PROMPT(
provider.context,
cwd,
@ -93,7 +96,7 @@ export const generateSystemPrompt = async (provider: ClineProvider, message: Web
newTaskRequireTodos: vscode.workspace
.getConfiguration(Package.name)
.get<boolean>("newTaskRequireTodos", false),
toolProtocol: getToolProtocolFromSettings(),
toolProtocol,
},
)

View file

@ -0,0 +1,470 @@
import { describe, it, expect, vi, beforeEach } from "vitest"
import { resolveToolProtocol } from "../resolveToolProtocol"
import { TOOL_PROTOCOL } from "@roo-code/types"
import type { ProviderSettings, ModelInfo, ProviderName } from "@roo-code/types"
import * as toolProtocolModule from "../toolProtocol"
// Mock the getToolProtocolFromSettings function
vi.mock("../toolProtocol", () => ({
getToolProtocolFromSettings: vi.fn(() => "xml"),
}))
describe("resolveToolProtocol", () => {
beforeEach(() => {
// Reset mock before each test
vi.mocked(toolProtocolModule.getToolProtocolFromSettings).mockReturnValue("xml")
})
describe("Precedence Level 1: User Profile Setting", () => {
it("should use profile toolProtocol when explicitly set to xml", () => {
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, "anthropic")
expect(result).toBe(TOOL_PROTOCOL.NATIVE)
})
it("should override model default when profile setting is present", () => {
const settings: ProviderSettings = {
toolProtocol: "xml",
apiProvider: "openai-native",
}
const modelInfo: ModelInfo = {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
defaultToolProtocol: "native",
}
const result = resolveToolProtocol(settings, modelInfo, "openai-native")
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, "openai-native")
expect(result).toBe(TOOL_PROTOCOL.XML) // Profile setting wins
})
})
describe("Precedence Level 2: Global User Preference (VSCode Setting)", () => {
it("should use global setting when no profile setting", () => {
vi.mocked(toolProtocolModule.getToolProtocolFromSettings).mockReturnValue("native")
const settings: ProviderSettings = {
apiProvider: "roo",
}
const modelInfo: ModelInfo = {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
supportsNativeTools: true, // Model supports native tools
}
const result = resolveToolProtocol(settings, modelInfo, "roo")
expect(result).toBe(TOOL_PROTOCOL.NATIVE) // Global setting wins over provider default
})
it("should use global setting over model default", () => {
vi.mocked(toolProtocolModule.getToolProtocolFromSettings).mockReturnValue("native")
const settings: ProviderSettings = {
apiProvider: "roo",
}
const modelInfo: ModelInfo = {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
defaultToolProtocol: "xml", // Model prefers XML
supportsNativeTools: true, // But model supports native tools
}
const result = resolveToolProtocol(settings, modelInfo, "roo")
expect(result).toBe(TOOL_PROTOCOL.NATIVE) // Global setting wins
})
})
describe("Precedence Level 3: Model Default", () => {
it("should use model defaultToolProtocol when no profile or global setting", () => {
vi.mocked(toolProtocolModule.getToolProtocolFromSettings).mockReturnValue("xml")
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, "roo")
expect(result).toBe(TOOL_PROTOCOL.NATIVE) // Model default wins when global is XML (default)
})
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, "roo")
expect(result).toBe(TOOL_PROTOCOL.XML) // Model default wins over capability
})
})
describe("Support Validation", () => {
it("should use provider default (XML) even when model supports native tools", () => {
const settings: ProviderSettings = {
apiProvider: "openai-native",
}
const modelInfo: ModelInfo = {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
supportsNativeTools: true,
}
const result = resolveToolProtocol(settings, modelInfo, "openai-native")
expect(result).toBe(TOOL_PROTOCOL.XML) // Provider default is XML (list is empty)
})
it("should fall back to XML when provider default is native but model doesn't support it", () => {
const settings: ProviderSettings = {
apiProvider: "openai-native",
}
const modelInfo: ModelInfo = {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
supportsNativeTools: false, // Model doesn't support native
}
const result = resolveToolProtocol(settings, modelInfo, "openai-native")
expect(result).toBe(TOOL_PROTOCOL.XML) // Falls back to XML due to lack of support
})
it("should use provider default (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, "anthropic")
expect(result).toBe(TOOL_PROTOCOL.XML) // Provider default is 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, "anthropic")
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, "anthropic")
expect(result).toBe(TOOL_PROTOCOL.XML) // Falls back to XML - undefined treated as unsupported
})
})
describe("Precedence Level 4: Provider Default", () => {
it("should use XML for all providers by default (when nativePreferredProviders is empty)", () => {
const settings: ProviderSettings = {
apiProvider: "anthropic",
}
const result = resolveToolProtocol(settings, undefined, "anthropic")
expect(result).toBe(TOOL_PROTOCOL.XML)
})
it("should use XML for Bedrock provider", () => {
const settings: ProviderSettings = {
apiProvider: "bedrock",
}
const result = resolveToolProtocol(settings, undefined, "bedrock")
expect(result).toBe(TOOL_PROTOCOL.XML)
})
it("should use XML for Claude Code provider", () => {
const settings: ProviderSettings = {
apiProvider: "claude-code",
}
const result = resolveToolProtocol(settings, undefined, "claude-code")
expect(result).toBe(TOOL_PROTOCOL.XML)
})
it("should use XML for OpenAI Native provider (when not in native list)", () => {
const settings: ProviderSettings = {
apiProvider: "openai-native",
}
const result = resolveToolProtocol(settings, undefined, "openai-native")
expect(result).toBe(TOOL_PROTOCOL.XML)
})
it("should use XML for Roo provider (when not in native list)", () => {
const settings: ProviderSettings = {
apiProvider: "roo",
}
const result = resolveToolProtocol(settings, undefined, "roo")
expect(result).toBe(TOOL_PROTOCOL.XML)
})
it("should use XML for Gemini provider (when not in native list)", () => {
const settings: ProviderSettings = {
apiProvider: "gemini",
}
const result = resolveToolProtocol(settings, undefined, "gemini")
expect(result).toBe(TOOL_PROTOCOL.XML)
})
it("should use XML for Mistral provider (when not in native list)", () => {
const settings: ProviderSettings = {
apiProvider: "mistral",
}
const result = resolveToolProtocol(settings, undefined, "mistral")
expect(result).toBe(TOOL_PROTOCOL.XML)
})
})
describe("Precedence Level 5: XML Fallback", () => {
it("should use XML fallback when no provider is specified and no preferences", () => {
vi.mocked(toolProtocolModule.getToolProtocolFromSettings).mockReturnValue("xml")
const settings: ProviderSettings = {}
const result = resolveToolProtocol(settings, undefined, undefined)
expect(result).toBe(TOOL_PROTOCOL.XML) // XML fallback
})
})
describe("Complete Precedence Chain", () => {
it("should respect full precedence: Profile > Model Default > Model Capability > Provider > Global", () => {
// Set up a scenario with all levels defined
vi.mocked(toolProtocolModule.getToolProtocolFromSettings).mockReturnValue("xml")
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, // Level 3: Model capability
}
// Level 4: Provider default would be "native" for roo
// Level 5: Global setting is "xml"
const result = resolveToolProtocol(settings, modelInfo, "roo")
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 (doesn't affect precedence)
}
const result = resolveToolProtocol(settings, modelInfo, "openai-native")
expect(result).toBe(TOOL_PROTOCOL.XML) // Model default wins over provider default
})
it("should skip to provider default when profile and model default are undefined", () => {
const settings: ProviderSettings = {
apiProvider: "openai-native",
}
const modelInfo: ModelInfo = {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
supportsNativeTools: true, // Support check (doesn't affect precedence)
}
const result = resolveToolProtocol(settings, modelInfo, "openai-native")
expect(result).toBe(TOOL_PROTOCOL.XML) // Provider default (XML for all when list is empty)
})
it("should skip to provider default when model info is unavailable", () => {
const settings: ProviderSettings = {
apiProvider: "anthropic",
}
const result = resolveToolProtocol(settings, undefined, "anthropic")
expect(result).toBe(TOOL_PROTOCOL.XML) // Provider default wins
})
it("should use global setting over provider default", () => {
vi.mocked(toolProtocolModule.getToolProtocolFromSettings).mockReturnValue("native")
const settings: ProviderSettings = {
apiProvider: "ollama", // Provider not in native list, defaults to XML
}
const modelInfo: ModelInfo = {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
supportsNativeTools: true, // Model supports native tools
}
const result = resolveToolProtocol(settings, modelInfo, "ollama")
expect(result).toBe(TOOL_PROTOCOL.NATIVE) // Global setting wins over provider default
})
})
describe("Edge Cases", () => {
it("should handle missing provider name gracefully", () => {
const settings: ProviderSettings = {}
const result = resolveToolProtocol(settings)
expect(result).toBe(TOOL_PROTOCOL.XML) // Falls back to global
})
it("should handle undefined model info gracefully", () => {
const settings: ProviderSettings = {
apiProvider: "openai-native",
}
const result = resolveToolProtocol(settings, undefined, "openai-native")
expect(result).toBe(TOOL_PROTOCOL.XML) // Provider default (XML for all)
})
it("should fall back to XML when provider prefers native but model doesn't support it", () => {
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, "roo")
expect(result).toBe(TOOL_PROTOCOL.XML) // Falls back to XML due to lack of support
})
})
describe("Real-world Scenarios", () => {
it("should use XML for GPT-4 with OpenAI provider (when list is empty)", () => {
const settings: ProviderSettings = {
apiProvider: "openai-native",
}
const modelInfo: ModelInfo = {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
supportsNativeTools: true,
}
const result = resolveToolProtocol(settings, modelInfo, "openai-native")
expect(result).toBe(TOOL_PROTOCOL.XML) // Provider default is XML
})
it("should use XML for Claude models with Anthropic provider", () => {
const settings: ProviderSettings = {
apiProvider: "anthropic",
}
const modelInfo: ModelInfo = {
maxTokens: 8192,
contextWindow: 200000,
supportsPromptCache: true,
supportsNativeTools: false,
}
const result = resolveToolProtocol(settings, modelInfo, "anthropic")
expect(result).toBe(TOOL_PROTOCOL.XML)
})
it("should allow user to force XML on native-supporting model", () => {
const settings: ProviderSettings = {
toolProtocol: "xml", // User explicitly wants XML
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, "openai-native")
expect(result).toBe(TOOL_PROTOCOL.XML) // User preference wins
})
it("should not allow user to force native when 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, // Model doesn't support native
}
const result = resolveToolProtocol(settings, modelInfo, "anthropic")
expect(result).toBe(TOOL_PROTOCOL.XML) // Falls back to XML due to lack of support
})
it("should use model default for Roo provider with mixed-protocol model", () => {
const settings: ProviderSettings = {
apiProvider: "roo",
}
const modelInfo: ModelInfo = {
maxTokens: 8192,
contextWindow: 200000,
supportsPromptCache: true,
defaultToolProtocol: "xml", // Anthropic model via Roo
supportsNativeTools: false,
}
const result = resolveToolProtocol(settings, modelInfo, "roo")
expect(result).toBe(TOOL_PROTOCOL.XML) // Model default wins over provider default
})
})
})

View file

@ -0,0 +1,79 @@
import { ToolProtocol, TOOL_PROTOCOL } from "@roo-code/types"
import type { ProviderSettings, ProviderName, ModelInfo } from "@roo-code/types"
import { getToolProtocolFromSettings } from "./toolProtocol"
/**
* Resolve the effective tool protocol based on the precedence hierarchy:
* Support > Preference > Defaults
*
* 1. User Preference - Per-Profile (explicit profile setting)
* 2. User Preference - Global (VSCode setting)
* 3. Model Default (defaultToolProtocol in ModelInfo)
* 4. Provider Default (XML by default, native for specific providers)
* 5. XML Fallback (final fallback)
*
* Then check support: if protocol is "native" but model doesn't support it, use XML.
*
* @param providerSettings - The provider settings for the current profile
* @param modelInfo - Optional model information containing capabilities
* @param provider - Optional provider name for provider-specific defaults
* @returns The resolved tool protocol (either "xml" or "native")
*/
export function resolveToolProtocol(
providerSettings: ProviderSettings,
modelInfo?: ModelInfo,
provider?: ProviderName,
): ToolProtocol {
let protocol: ToolProtocol
// 1. User Preference - Per-Profile (explicit profile setting, highest priority)
if (providerSettings.toolProtocol) {
protocol = providerSettings.toolProtocol
}
// 2. User Preference - Global (VSCode global setting)
// Only treat as user preference if explicitly set to native (non-default value)
else if (getToolProtocolFromSettings() === TOOL_PROTOCOL.NATIVE) {
protocol = TOOL_PROTOCOL.NATIVE
}
// 3. Model Default - model's preferred protocol
else if (modelInfo?.defaultToolProtocol) {
protocol = modelInfo.defaultToolProtocol
}
// 4. Provider Default - XML by default, native for specific providers
else if (provider) {
protocol = getProviderDefaultProtocol(provider)
}
// 5. XML Fallback
else {
protocol = TOOL_PROTOCOL.XML
}
// Check support: if protocol is native but model doesn't support it, use XML
// Treat undefined as unsupported (only allow native when explicitly true)
if (protocol === TOOL_PROTOCOL.NATIVE && modelInfo?.supportsNativeTools !== true) {
return TOOL_PROTOCOL.XML
}
return protocol
}
/**
* Get the default tool protocol for a provider.
* All providers default to XML unless explicitly listed as native-preferred.
*
* @param provider - The provider name
* @returns The tool protocol for this provider (XML by default, or native if explicitly listed)
*/
function getProviderDefaultProtocol(provider: ProviderName): ToolProtocol {
// Native tool providers - these providers support OpenAI-style function calling
// and work better with the native protocol
// You can empty this list to make all providers default to XML
const nativePreferredProviders: ProviderName[] = []
if (nativePreferredProviders.includes(provider)) {
return TOOL_PROTOCOL.NATIVE
}
// All other providers default to XML
return TOOL_PROTOCOL.XML
}