diff --git a/packages/types/src/model.ts b/packages/types/src/model.ts index df43de936b0..49e8e73edff 100644 --- a/packages/types/src/model.ts +++ b/packages/types/src/model.ts @@ -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. diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index e2bb861256a..7a84e6d2dea 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -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. diff --git a/packages/types/src/providers/roo.ts b/packages/types/src/providers/roo.ts index 0b7ed89bd92..2ab59ef52da 100644 --- a/packages/types/src/providers/roo.ts +++ b/packages/types/src/providers/roo.ts @@ -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 +/** + * 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> = { + * "anthropic/claude-3-5-sonnet-20241022": { + * defaultToolProtocol: "xml", + * }, + * "openai/gpt-4o": { + * defaultToolProtocol: "native", + * }, + * "xai/grok-code-fast-1": { + * defaultToolProtocol: "native", + * }, + * } + * ``` + */ +export const rooModelDefaults: Record> = { + // 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 */ diff --git a/src/api/providers/fetchers/roo.ts b/src/api/providers/fetchers/roo.ts index a1671e3b8d2..880148f3c7b 100644 --- a/src/api/providers/fetchers/roo.ts +++ b/src/api/providers/fetchers/roo.ts @@ -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 { } // 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, } } } diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index 4d3fba3104a..3ab7fddcb37 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -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, diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap index affe2863e89..dbab96eca1d 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap @@ -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 +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 ==== diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/ask-mode-prompt.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/ask-mode-prompt.snap index d080e363eca..e49d2bc0a73 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/ask-mode-prompt.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/ask-mode-prompt.snap @@ -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 +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 ==== diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap index 56e68336835..3581a924c92 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap @@ -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 +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 ==== diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap index f854c956fa0..0e0e9200ecf 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap @@ -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 +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 ==== diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap index 34d12c5b0e7..44a34544dc1 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap @@ -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 +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 ==== diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap index affe2863e89..dbab96eca1d 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap @@ -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 +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 ==== diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap index 4b195d35d8c..03e66365c7c 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap @@ -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 +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 ==== diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap index affe2863e89..dbab96eca1d 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap @@ -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 +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 ==== diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap index 791e3e91275..9bb8c3e4624 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap @@ -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 +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 ==== diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap index affe2863e89..dbab96eca1d 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap @@ -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 +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 ==== diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap index affe2863e89..dbab96eca1d 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap @@ -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 +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 ==== diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap index f854c956fa0..0e0e9200ecf 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap @@ -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 +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 ==== diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap index affe2863e89..dbab96eca1d 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap @@ -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 +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 ==== diff --git a/src/core/prompts/sections/markdown-formatting.ts b/src/core/prompts/sections/markdown-formatting.ts index 87f922e94aa..0e47385632e 100644 --- a/src/core/prompts/sections/markdown-formatting.ts +++ b/src/core/prompts/sections/markdown-formatting.ts @@ -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 ` +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` } diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index b78a7080d16..cc4558a74b4 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -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 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 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 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 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 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 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 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 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 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 implements TaskLike { newTaskRequireTodos: vscode.workspace .getConfiguration(Package.name) .get("newTaskRequireTodos", false), - toolProtocol: getToolProtocolFromSettings(), + toolProtocol, }, undefined, // todoList this.api.getModel().id, @@ -2975,8 +3029,8 @@ export class Task extends EventEmitter 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 diff --git a/src/core/tools/MultiApplyDiffTool.ts b/src/core/tools/MultiApplyDiffTool.ts index dbe88304bc6..9f154591546 100644 --- a/src/core/tools/MultiApplyDiffTool.ts +++ b/src/core/tools/MultiApplyDiffTool.ts @@ -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, diff --git a/src/core/tools/ReadFileTool.ts b/src/core/tools/ReadFileTool.ts index dc08086a815..a87399b763d 100644 --- a/src/core/tools/ReadFileTool.ts +++ b/src/core/tools/ReadFileTool.ts @@ -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 { 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) => ({ diff --git a/src/core/tools/__tests__/applyDiffTool.experiment.spec.ts b/src/core/tools/__tests__/applyDiffTool.experiment.spec.ts index da45031ff85..1d0f4cc01d7 100644 --- a/src/core/tools/__tests__/applyDiffTool.experiment.spec.ts +++ b/src/core/tools/__tests__/applyDiffTool.experiment.spec.ts @@ -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, diff --git a/src/core/tools/__tests__/multiApplyDiffTool.spec.ts b/src/core/tools/__tests__/multiApplyDiffTool.spec.ts index 423660c0daf..5a9a1af1da3 100644 --- a/src/core/tools/__tests__/multiApplyDiffTool.spec.ts +++ b/src/core/tools/__tests__/multiApplyDiffTool.spec.ts @@ -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), diff --git a/src/core/tools/__tests__/readFileTool.spec.ts b/src/core/tools/__tests__/readFileTool.spec.ts index a81906718b9..2401df2bbaf 100644 --- a/src/core/tools/__tests__/readFileTool.spec.ts +++ b/src/core/tools/__tests__/readFileTool.spec.ts @@ -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, + }, }), }, } diff --git a/src/core/webview/generateSystemPrompt.ts b/src/core/webview/generateSystemPrompt.ts index 1de422089d0..f8ac6d250cf 100644 --- a/src/core/webview/generateSystemPrompt.ts +++ b/src/core/webview/generateSystemPrompt.ts @@ -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("newTaskRequireTodos", false), - toolProtocol: getToolProtocolFromSettings(), + toolProtocol, }, ) diff --git a/src/utils/__tests__/resolveToolProtocol.spec.ts b/src/utils/__tests__/resolveToolProtocol.spec.ts new file mode 100644 index 00000000000..93b27315876 --- /dev/null +++ b/src/utils/__tests__/resolveToolProtocol.spec.ts @@ -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 + }) + }) +}) diff --git a/src/utils/resolveToolProtocol.ts b/src/utils/resolveToolProtocol.ts new file mode 100644 index 00000000000..81d85d79882 --- /dev/null +++ b/src/utils/resolveToolProtocol.ts @@ -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 +}