mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: implement dynamic tool protocol resolution with proper precedence hierarchy (#9286)
Co-authored-by: Roo Code <roomote@roocode.com>
This commit is contained in:
parent
4e316e0de8
commit
744f4bd4c8
29 changed files with 765 additions and 50 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
====
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
====
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
====
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
====
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
====
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
====
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
====
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
====
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
====
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
====
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
====
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
====
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
====
|
||||
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) => ({
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
|||
470
src/utils/__tests__/resolveToolProtocol.spec.ts
Normal file
470
src/utils/__tests__/resolveToolProtocol.spec.ts
Normal 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
|
||||
})
|
||||
})
|
||||
})
|
||||
79
src/utils/resolveToolProtocol.ts
Normal file
79
src/utils/resolveToolProtocol.ts
Normal 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
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue