Remove experimental setting for native tool calls (#9333)

This commit is contained in:
Matt Rubens 2025-11-17 23:37:36 -05:00 committed by GitHub
parent c5b18c1581
commit dbaaef756e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 91 additions and 520 deletions

View file

@ -12,7 +12,6 @@ export const experimentIds = [
"preventFocusDisruption",
"imageGeneration",
"runSlashCommand",
"nativeToolCalling",
] as const
export const experimentIdsSchema = z.enum(experimentIds)
@ -29,7 +28,6 @@ export const experimentsSchema = z.object({
preventFocusDisruption: z.boolean().optional(),
imageGeneration: z.boolean().optional(),
runSlashCommand: z.boolean().optional(),
nativeToolCalling: z.boolean().optional(),
})
export type Experiments = z.infer<typeof experimentsSchema>

View file

@ -281,13 +281,7 @@ export async function presentAssistantMessage(cline: Task) {
let hasToolResult = false
// Check if we're using native tool protocol (do this once before defining pushToolResult)
const state = await cline.providerRef.deref()?.getState()
const toolProtocol = resolveToolProtocol(
cline.apiConfiguration,
cline.api.getModel().info,
cline.apiConfiguration.apiProvider,
state?.experiments,
)
const toolProtocol = resolveToolProtocol(cline.apiConfiguration, cline.api.getModel().info)
const isNative = isNativeProtocol(toolProtocol)
const toolCallId = (block as any).id
@ -518,13 +512,7 @@ export async function presentAssistantMessage(cline: Task) {
await checkpointSaveAndMark(cline)
// Check if native protocol is enabled - if so, always use single-file class-based tool
const state = await cline.providerRef.deref()?.getState()
const applyDiffToolProtocol = resolveToolProtocol(
cline.apiConfiguration,
cline.api.getModel().info,
cline.apiConfiguration.apiProvider,
state?.experiments,
)
const applyDiffToolProtocol = resolveToolProtocol(cline.apiConfiguration, cline.api.getModel().info)
if (isNativeProtocol(applyDiffToolProtocol)) {
await applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, {
askApproval,

View file

@ -411,12 +411,7 @@ 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.
// experiments is always provided via TaskOptions (defaults to experimentDefault in provider)
const toolProtocol = resolveToolProtocol(
this.apiConfiguration,
this.api.getModel().info,
this.apiConfiguration.apiProvider,
experimentsConfig,
)
const toolProtocol = resolveToolProtocol(this.apiConfiguration, this.api.getModel().info)
this.assistantMessageParser = toolProtocol === "xml" ? new AssistantMessageParser() : undefined
this.messageQueueService = new MessageQueueService()
@ -1271,12 +1266,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
)
const modelInfo = this.api.getModel().info
const state = await this.providerRef.deref()?.getState()
const toolProtocol = resolveToolProtocol(
this.apiConfiguration,
modelInfo,
this.apiConfiguration.apiProvider,
state?.experiments,
)
const toolProtocol = resolveToolProtocol(this.apiConfiguration, modelInfo)
return formatResponse.toolError(formatResponse.missingToolParameterError(paramName, toolProtocol))
}
@ -1420,12 +1410,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// conversations with tool uses and no tool schema.
// For native protocol, we preserve tool_use and tool_result blocks as they're expected by the API.
const state = await this.providerRef.deref()?.getState()
const protocol = resolveToolProtocol(
this.apiConfiguration,
this.api.getModel().info,
this.apiConfiguration.apiProvider,
state?.experiments,
)
const protocol = resolveToolProtocol(this.apiConfiguration, this.api.getModel().info)
const useNative = isNativeProtocol(protocol)
// Only convert tool blocks to text for XML protocol
@ -1803,12 +1788,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
} else {
const modelInfo = this.api.getModel().info
const state = await this.providerRef.deref()?.getState()
const toolProtocol = resolveToolProtocol(
this.apiConfiguration,
modelInfo,
this.apiConfiguration.apiProvider,
state?.experiments,
)
const toolProtocol = resolveToolProtocol(this.apiConfiguration, modelInfo)
nextUserContent = [{ type: "text", text: formatResponse.noToolsUsed(toolProtocol) }]
this.consecutiveMistakeCount++
}
@ -2456,12 +2436,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// Check if we're using native protocol
const state = await this.providerRef.deref()?.getState()
const isNative = isNativeProtocol(
resolveToolProtocol(
this.apiConfiguration,
this.api.getModel().info,
this.apiConfiguration.apiProvider,
state?.experiments,
),
resolveToolProtocol(this.apiConfiguration, this.api.getModel().info),
)
if (isNative) {
@ -2602,12 +2577,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
if (!didToolUse) {
const modelInfo = this.api.getModel().info
const state = await this.providerRef.deref()?.getState()
const toolProtocol = resolveToolProtocol(
this.apiConfiguration,
modelInfo,
this.apiConfiguration.apiProvider,
state?.experiments,
)
const toolProtocol = resolveToolProtocol(this.apiConfiguration, modelInfo)
this.userMessageContent.push({ type: "text", text: formatResponse.noToolsUsed(toolProtocol) })
this.consecutiveMistakeCount++
}
@ -2634,14 +2604,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// user messages (which would cause tool_result validation errors).
let state = await this.providerRef.deref()?.getState()
if (
isNativeProtocol(
resolveToolProtocol(
this.apiConfiguration,
this.api.getModel().info,
this.apiConfiguration.apiProvider,
state?.experiments,
),
) &&
isNativeProtocol(resolveToolProtocol(this.apiConfiguration, this.api.getModel().info)) &&
this.apiConversationHistory.length > 0
) {
const lastMessage = this.apiConversationHistory[this.apiConversationHistory.length - 1]
@ -2707,14 +2670,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// For native protocol, re-add the user message we removed
// Reuse the state variable from above
if (
isNativeProtocol(
resolveToolProtocol(
this.apiConfiguration,
this.api.getModel().info,
this.apiConfiguration.apiProvider,
state?.experiments,
),
)
isNativeProtocol(resolveToolProtocol(this.apiConfiguration, this.api.getModel().info))
) {
await this.addToApiConversationHistory({
role: "user",
@ -2813,12 +2769,7 @@ 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,
experiments,
)
const toolProtocol = resolveToolProtocol(apiConfiguration ?? this.apiConfiguration, modelInfo)
return SYSTEM_PROMPT(
provider.context,
@ -3057,12 +3008,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// 1. Tool protocol is set to NATIVE
// 2. Model supports native tools
const modelInfo = this.api.getModel().info
const toolProtocol = resolveToolProtocol(
this.apiConfiguration,
modelInfo,
this.apiConfiguration.apiProvider,
state?.experiments,
)
const toolProtocol = resolveToolProtocol(this.apiConfiguration, modelInfo)
const shouldIncludeTools = toolProtocol === TOOL_PROTOCOL.NATIVE && (modelInfo.supportsNativeTools ?? false)
// Build complete tools array: native tools + dynamic MCP tools, filtered by mode restrictions

View file

@ -62,14 +62,7 @@ export async function applyDiffTool(
removeClosingTag: RemoveClosingTag,
) {
// Check if native protocol is enabled - if so, always use single-file class-based tool
const provider = cline.providerRef.deref()
const state = await provider?.getState()
const toolProtocol = resolveToolProtocol(
cline.apiConfiguration,
cline.api.getModel().info,
cline.apiConfiguration.apiProvider,
state?.experiments,
)
const toolProtocol = resolveToolProtocol(cline.apiConfiguration, cline.api.getModel().info)
if (isNativeProtocol(toolProtocol)) {
return applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, {
askApproval,
@ -80,6 +73,8 @@ export async function applyDiffTool(
}
// Check if MULTI_FILE_APPLY_DIFF experiment is enabled
const provider = cline.providerRef.deref()
const state = await provider?.getState()
if (provider && state) {
const isMultiFileApplyDiffEnabled = experiments.isEnabled(
state.experiments ?? {},

View file

@ -109,13 +109,7 @@ export class ReadFileTool extends BaseTool<"read_file"> {
const { handleError, pushToolResult } = callbacks
const fileEntries = params.files
const modelInfo = task.api.getModel().info
const state = await task.providerRef.deref()?.getState()
const protocol = resolveToolProtocol(
task.apiConfiguration,
modelInfo,
task.apiConfiguration.apiProvider,
state?.experiments,
)
const protocol = resolveToolProtocol(task.apiConfiguration, modelInfo)
const useNative = isNativeProtocol(protocol)
if (!fileEntries || fileEntries.length === 0) {

View file

@ -155,22 +155,22 @@ describe("applyDiffTool experiment routing", () => {
expect(applyDiffToolClass.handle).not.toHaveBeenCalled()
})
it("should use class-based tool when native protocol is enabled regardless of experiment", async () => {
// Update model to support native tools
it("should use class-based tool when model defaults to native protocol", async () => {
// Update model to support native tools and default to native protocol
mockCline.api.getModel = vi.fn().mockReturnValue({
id: "test-model",
info: {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
supportsNativeTools: true, // Enable native tools support
supportsNativeTools: true, // Model supports native tools
defaultToolProtocol: "native", // Model defaults to native protocol
},
})
mockProvider.getState.mockResolvedValue({
experiments: {
[EXPERIMENT_IDS.MULTI_FILE_APPLY_DIFF]: true,
[EXPERIMENT_IDS.NATIVE_TOOL_CALLING]: true, // Enable native tool calling experiment
},
})
;(applyDiffToolClass.handle as any).mockResolvedValue(undefined)
@ -184,7 +184,7 @@ describe("applyDiffTool experiment routing", () => {
mockRemoveClosingTag,
)
// When native protocol is enabled, should always use class-based tool
// When native protocol is used, should always use class-based tool
expect(applyDiffToolClass.handle).toHaveBeenCalledWith(mockCline, mockBlock, {
askApproval: mockAskApproval,
handleError: mockHandleError,

View file

@ -70,7 +70,7 @@ export const generateSystemPrompt = async (provider: ClineProvider, message: Web
const canUseBrowserTool = modelSupportsBrowser && modeSupportsBrowser && (browserToolEnabled ?? true)
// Resolve tool protocol for system prompt generation
const toolProtocol = resolveToolProtocol(apiConfiguration, modelInfo, apiConfiguration.apiProvider, experiments)
const toolProtocol = resolveToolProtocol(apiConfiguration, modelInfo)
const systemPrompt = await SYSTEM_PROMPT(
provider.context,

View file

@ -23,15 +23,6 @@ describe("experiments", () => {
})
})
describe("NATIVE_TOOL_CALLING", () => {
it("is configured correctly", () => {
expect(EXPERIMENT_IDS.NATIVE_TOOL_CALLING).toBe("nativeToolCalling")
expect(experimentConfigsMap.NATIVE_TOOL_CALLING).toMatchObject({
enabled: false,
})
})
})
describe("isEnabled", () => {
it("returns false when POWER_STEERING experiment is not enabled", () => {
const experiments: Record<ExperimentId, boolean> = {
@ -40,7 +31,6 @@ describe("experiments", () => {
preventFocusDisruption: false,
imageGeneration: false,
runSlashCommand: false,
nativeToolCalling: false,
}
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false)
})
@ -52,7 +42,6 @@ describe("experiments", () => {
preventFocusDisruption: false,
imageGeneration: false,
runSlashCommand: false,
nativeToolCalling: false,
}
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(true)
})
@ -64,7 +53,6 @@ describe("experiments", () => {
preventFocusDisruption: false,
imageGeneration: false,
runSlashCommand: false,
nativeToolCalling: false,
}
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false)
})

View file

@ -6,7 +6,6 @@ export const EXPERIMENT_IDS = {
PREVENT_FOCUS_DISRUPTION: "preventFocusDisruption",
IMAGE_GENERATION: "imageGeneration",
RUN_SLASH_COMMAND: "runSlashCommand",
NATIVE_TOOL_CALLING: "nativeToolCalling",
} as const satisfies Record<string, ExperimentId>
type _AssertExperimentIds = AssertEqual<Equals<ExperimentId, Values<typeof EXPERIMENT_IDS>>>
@ -23,7 +22,6 @@ export const experimentConfigsMap: Record<ExperimentKey, ExperimentConfig> = {
PREVENT_FOCUS_DISRUPTION: { enabled: false },
IMAGE_GENERATION: { enabled: false },
RUN_SLASH_COMMAND: { enabled: false },
NATIVE_TOOL_CALLING: { enabled: false },
}
export const experimentDefault = Object.fromEntries(

View file

@ -1,7 +1,7 @@
import { describe, it, expect } from "vitest"
import { resolveToolProtocol } from "../resolveToolProtocol"
import { TOOL_PROTOCOL } from "@roo-code/types"
import type { ProviderSettings, ModelInfo, Experiments } from "@roo-code/types"
import type { ProviderSettings, ModelInfo } from "@roo-code/types"
describe("resolveToolProtocol", () => {
describe("Precedence Level 1: User Profile Setting", () => {
@ -25,7 +25,7 @@ describe("resolveToolProtocol", () => {
supportsPromptCache: false,
supportsNativeTools: true, // Model supports native tools
}
const result = resolveToolProtocol(settings, modelInfo, "anthropic")
const result = resolveToolProtocol(settings, modelInfo)
expect(result).toBe(TOOL_PROTOCOL.NATIVE)
})
@ -39,8 +39,9 @@ describe("resolveToolProtocol", () => {
contextWindow: 128000,
supportsPromptCache: false,
defaultToolProtocol: "native",
supportsNativeTools: true,
}
const result = resolveToolProtocol(settings, modelInfo, "openai-native")
const result = resolveToolProtocol(settings, modelInfo)
expect(result).toBe(TOOL_PROTOCOL.XML) // Profile setting wins
})
@ -55,82 +56,13 @@ describe("resolveToolProtocol", () => {
supportsPromptCache: false,
supportsNativeTools: true,
}
const result = resolveToolProtocol(settings, modelInfo, "openai-native")
const result = resolveToolProtocol(settings, modelInfo)
expect(result).toBe(TOOL_PROTOCOL.XML) // Profile setting wins
})
})
describe("Precedence Level 2: Experimental Setting (Native Tool Calling)", () => {
it("should use experimental setting when enabled and no profile setting", () => {
const settings: ProviderSettings = {
apiProvider: "roo",
}
const modelInfo: ModelInfo = {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
supportsNativeTools: true, // Model supports native tools
}
const experiments: Experiments = {
nativeToolCalling: true, // Experimental setting enabled
}
const result = resolveToolProtocol(settings, modelInfo, "roo", experiments)
expect(result).toBe(TOOL_PROTOCOL.NATIVE) // Experimental setting wins over provider default
})
it("should use experimental setting over model default", () => {
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 experiments: Experiments = {
nativeToolCalling: true, // Experimental setting enabled
}
const result = resolveToolProtocol(settings, modelInfo, "roo", experiments)
expect(result).toBe(TOOL_PROTOCOL.NATIVE) // Experimental setting wins
})
it("should not use experimental setting when disabled", () => {
const settings: ProviderSettings = {
apiProvider: "roo",
}
const modelInfo: ModelInfo = {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
supportsNativeTools: true,
}
const experiments: Experiments = {
nativeToolCalling: false, // Experimental setting disabled
}
const result = resolveToolProtocol(settings, modelInfo, "roo", experiments)
expect(result).toBe(TOOL_PROTOCOL.XML) // Falls back to provider default
})
it("should not use experimental setting when undefined", () => {
const settings: ProviderSettings = {
apiProvider: "roo",
}
const modelInfo: ModelInfo = {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
supportsNativeTools: true,
}
const experiments: Experiments = {} // nativeToolCalling not defined
const result = resolveToolProtocol(settings, modelInfo, "roo", experiments)
expect(result).toBe(TOOL_PROTOCOL.XML) // Falls back to provider default
})
})
describe("Precedence Level 3: Model Default", () => {
it("should use model defaultToolProtocol when no profile or experimental setting", () => {
describe("Precedence Level 2: Model Default", () => {
it("should use model defaultToolProtocol when no profile setting", () => {
const settings: ProviderSettings = {
apiProvider: "roo",
}
@ -141,7 +73,7 @@ describe("resolveToolProtocol", () => {
defaultToolProtocol: "native",
supportsNativeTools: true, // Model must support native tools
}
const result = resolveToolProtocol(settings, modelInfo, "roo")
const result = resolveToolProtocol(settings, modelInfo)
expect(result).toBe(TOOL_PROTOCOL.NATIVE) // Model default wins when experiment is disabled
})
@ -156,41 +88,13 @@ describe("resolveToolProtocol", () => {
defaultToolProtocol: "xml",
supportsNativeTools: true,
}
const result = resolveToolProtocol(settings, modelInfo, "roo")
const result = resolveToolProtocol(settings, modelInfo)
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", () => {
it("should fall back to XML when model doesn't support native", () => {
const settings: ProviderSettings = {
apiProvider: "anthropic",
}
@ -200,8 +104,8 @@ describe("resolveToolProtocol", () => {
supportsPromptCache: false,
supportsNativeTools: false,
}
const result = resolveToolProtocol(settings, modelInfo, "anthropic")
expect(result).toBe(TOOL_PROTOCOL.XML) // Provider default is XML
const result = resolveToolProtocol(settings, modelInfo)
expect(result).toBe(TOOL_PROTOCOL.XML)
})
it("should fall back to XML when user prefers native but model doesn't support it", () => {
@ -215,7 +119,7 @@ describe("resolveToolProtocol", () => {
supportsPromptCache: false,
supportsNativeTools: false, // But model doesn't support it
}
const result = resolveToolProtocol(settings, modelInfo, "anthropic")
const result = resolveToolProtocol(settings, modelInfo)
expect(result).toBe(TOOL_PROTOCOL.XML) // Falls back to XML due to lack of support
})
@ -230,79 +134,23 @@ describe("resolveToolProtocol", () => {
supportsPromptCache: false,
// supportsNativeTools is undefined (not specified)
}
const result = resolveToolProtocol(settings, modelInfo, "anthropic")
const result = resolveToolProtocol(settings, modelInfo)
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)", () => {
describe("Precedence Level 3: XML Fallback", () => {
it("should use XML fallback when no model default is specified", () => {
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", () => {
const settings: ProviderSettings = {}
const result = resolveToolProtocol(settings, undefined, undefined)
const result = resolveToolProtocol(settings, undefined)
expect(result).toBe(TOOL_PROTOCOL.XML) // XML fallback
})
})
describe("Complete Precedence Chain", () => {
it("should respect full precedence: Profile > Experimental > Model Default > Provider", () => {
it("should respect full precedence: Profile > Model Default > XML Fallback", () => {
// Set up a scenario with all levels defined
const settings: ProviderSettings = {
toolProtocol: "native", // Level 1: User profile setting
@ -313,18 +161,11 @@ describe("resolveToolProtocol", () => {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
defaultToolProtocol: "xml", // Level 3: Model default
defaultToolProtocol: "xml", // Level 2: Model default
supportsNativeTools: true, // Support check
}
const experiments: Experiments = {
nativeToolCalling: false, // Level 2: Experimental setting (disabled)
}
// Level 4: Provider default would be XML
// Level 5: XML fallback
const result = resolveToolProtocol(settings, modelInfo, "roo", experiments)
const result = resolveToolProtocol(settings, modelInfo)
expect(result).toBe(TOOL_PROTOCOL.NATIVE) // Profile setting wins
})
@ -338,14 +179,14 @@ describe("resolveToolProtocol", () => {
contextWindow: 128000,
supportsPromptCache: false,
defaultToolProtocol: "xml", // Level 2
supportsNativeTools: true, // Support check (doesn't affect precedence)
supportsNativeTools: true, // Support check
}
const result = resolveToolProtocol(settings, modelInfo, "openai-native")
expect(result).toBe(TOOL_PROTOCOL.XML) // Model default wins over provider default
const result = resolveToolProtocol(settings, modelInfo)
expect(result).toBe(TOOL_PROTOCOL.XML) // Model default wins
})
it("should skip to provider default when profile and model default are undefined", () => {
it("should skip to XML fallback when profile and model default are undefined", () => {
const settings: ProviderSettings = {
apiProvider: "openai-native",
}
@ -354,38 +195,20 @@ describe("resolveToolProtocol", () => {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
supportsNativeTools: true, // Support check (doesn't affect precedence)
supportsNativeTools: true,
}
const result = resolveToolProtocol(settings, modelInfo, "openai-native")
expect(result).toBe(TOOL_PROTOCOL.XML) // Provider default (XML for all when list is empty)
const result = resolveToolProtocol(settings, modelInfo)
expect(result).toBe(TOOL_PROTOCOL.XML) // XML fallback
})
it("should skip to provider default when model info is unavailable", () => {
it("should skip to XML fallback 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 experimental setting over provider default", () => {
const settings: ProviderSettings = {
apiProvider: "ollama", // Provider defaults to XML
}
const modelInfo: ModelInfo = {
maxTokens: 4096,
contextWindow: 128000,
supportsPromptCache: false,
supportsNativeTools: true, // Model supports native tools
}
const experiments: Experiments = {
nativeToolCalling: true, // Experimental setting enabled
}
const result = resolveToolProtocol(settings, modelInfo, "ollama", experiments)
expect(result).toBe(TOOL_PROTOCOL.NATIVE) // Experimental setting wins over provider default
const result = resolveToolProtocol(settings, undefined)
expect(result).toBe(TOOL_PROTOCOL.XML) // XML fallback
})
})
@ -400,11 +223,11 @@ describe("resolveToolProtocol", () => {
const settings: ProviderSettings = {
apiProvider: "openai-native",
}
const result = resolveToolProtocol(settings, undefined, "openai-native")
expect(result).toBe(TOOL_PROTOCOL.XML) // Provider default (XML for all)
const result = resolveToolProtocol(settings, undefined)
expect(result).toBe(TOOL_PROTOCOL.XML) // XML fallback
})
it("should fall back to XML when provider prefers native but model doesn't support it", () => {
it("should fall back to XML when model doesn't support native", () => {
const settings: ProviderSettings = {
apiProvider: "roo",
}
@ -414,13 +237,13 @@ describe("resolveToolProtocol", () => {
supportsPromptCache: false,
supportsNativeTools: false, // Model doesn't support native
}
const result = resolveToolProtocol(settings, modelInfo, "roo")
const result = resolveToolProtocol(settings, modelInfo)
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)", () => {
it("should use XML fallback for models without defaultToolProtocol", () => {
const settings: ProviderSettings = {
apiProvider: "openai-native",
}
@ -430,8 +253,8 @@ describe("resolveToolProtocol", () => {
supportsPromptCache: false,
supportsNativeTools: true,
}
const result = resolveToolProtocol(settings, modelInfo, "openai-native")
expect(result).toBe(TOOL_PROTOCOL.XML) // Provider default is XML
const result = resolveToolProtocol(settings, modelInfo)
expect(result).toBe(TOOL_PROTOCOL.XML) // XML fallback
})
it("should use XML for Claude models with Anthropic provider", () => {
@ -444,7 +267,7 @@ describe("resolveToolProtocol", () => {
supportsPromptCache: true,
supportsNativeTools: false,
}
const result = resolveToolProtocol(settings, modelInfo, "anthropic")
const result = resolveToolProtocol(settings, modelInfo)
expect(result).toBe(TOOL_PROTOCOL.XML)
})
@ -460,7 +283,7 @@ describe("resolveToolProtocol", () => {
supportsNativeTools: true, // Model supports native but user wants XML
defaultToolProtocol: "native",
}
const result = resolveToolProtocol(settings, modelInfo, "openai-native")
const result = resolveToolProtocol(settings, modelInfo)
expect(result).toBe(TOOL_PROTOCOL.XML) // User preference wins
})
@ -475,11 +298,11 @@ describe("resolveToolProtocol", () => {
supportsPromptCache: false,
supportsNativeTools: false, // Model doesn't support native
}
const result = resolveToolProtocol(settings, modelInfo, "anthropic")
const result = resolveToolProtocol(settings, modelInfo)
expect(result).toBe(TOOL_PROTOCOL.XML) // Falls back to XML due to lack of support
})
it("should use model default for Roo provider with mixed-protocol model", () => {
it("should use model default when available", () => {
const settings: ProviderSettings = {
apiProvider: "roo",
}
@ -487,11 +310,11 @@ describe("resolveToolProtocol", () => {
maxTokens: 8192,
contextWindow: 200000,
supportsPromptCache: true,
defaultToolProtocol: "xml", // Anthropic model via Roo
supportsNativeTools: false,
defaultToolProtocol: "xml",
supportsNativeTools: true,
}
const result = resolveToolProtocol(settings, modelInfo, "roo")
expect(result).toBe(TOOL_PROTOCOL.XML) // Model default wins over provider default
const result = resolveToolProtocol(settings, modelInfo)
expect(result).toBe(TOOL_PROTOCOL.XML) // Model default wins
})
})
})

View file

@ -1,81 +1,36 @@
import { ToolProtocol, TOOL_PROTOCOL, type Experiments } from "@roo-code/types"
import type { ProviderSettings, ProviderName, ModelInfo } from "@roo-code/types"
import { EXPERIMENT_IDS, experiments } from "../shared/experiments"
import { ToolProtocol, TOOL_PROTOCOL } from "@roo-code/types"
import type { ProviderSettings, ModelInfo } from "@roo-code/types"
/**
* Resolve the effective tool protocol based on the precedence hierarchy:
* Support > Preference > Defaults
*
* 1. User Preference - Per-Profile (explicit profile setting)
* 2. User Preference - Experimental Setting (nativeToolCalling experiment)
* 3. Model Default (defaultToolProtocol in ModelInfo)
* 4. Provider Default (XML by default, native for specific providers)
* 5. XML Fallback (final fallback)
* 2. Model Default (defaultToolProtocol in ModelInfo)
* 3. 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
* @param experimentsConfig - Optional experiments configuration
* @returns The resolved tool protocol (either "xml" or "native")
*/
export function resolveToolProtocol(
providerSettings: ProviderSettings,
modelInfo?: ModelInfo,
provider?: ProviderName,
experimentsConfig?: Experiments,
): ToolProtocol {
let protocol: ToolProtocol
// 1. User Preference - Per-Profile (explicit profile setting, highest priority)
if (providerSettings.toolProtocol) {
protocol = providerSettings.toolProtocol
}
// 2. User Preference - Experimental Setting (nativeToolCalling experiment)
// Only treat as user preference if explicitly enabled
else if (experiments.isEnabled(experimentsConfig ?? {}, EXPERIMENT_IDS.NATIVE_TOOL_CALLING)) {
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
export function resolveToolProtocol(providerSettings: ProviderSettings, modelInfo?: ModelInfo): ToolProtocol {
// If model doesn't support native tools, return XML immediately
// Treat undefined as unsupported (only allow native when explicitly true)
if (protocol === TOOL_PROTOCOL.NATIVE && modelInfo?.supportsNativeTools !== true) {
if (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
// 1. User Preference - Per-Profile (explicit profile setting, highest priority)
if (providerSettings.toolProtocol) {
return providerSettings.toolProtocol
}
// All other providers default to XML
// 2. Model Default - model's preferred protocol
if (modelInfo?.defaultToolProtocol) {
return modelInfo.defaultToolProtocol
}
// 3. XML Fallback
return TOOL_PROTOCOL.XML
}

View file

@ -48,7 +48,6 @@ import { useAppTranslation } from "@src/i18n/TranslationContext"
import { useRouterModels } from "@src/components/ui/hooks/useRouterModels"
import { useSelectedModel } from "@src/components/ui/hooks/useSelectedModel"
import { useExtensionState } from "@src/context/ExtensionStateContext"
import { EXPERIMENT_IDS, experimentDefault } from "@roo/experiments"
import {
useOpenRouterModelProviders,
OPENROUTER_DEFAULT_PROVIDER_NAME,
@ -140,7 +139,7 @@ const ApiOptions = ({
setErrorMessage,
}: ApiOptionsProps) => {
const { t } = useAppTranslation()
const { organizationAllowList, cloudIsAuthenticated, experiments: experimentsConfig } = useExtensionState()
const { organizationAllowList, cloudIsAuthenticated } = useExtensionState()
const [customHeaders, setCustomHeaders] = useState<[string, string][]>(() => {
const headers = apiConfiguration?.openAiHeaders || {}
@ -413,55 +412,14 @@ const ApiOptions = ({
}, [selectedProvider])
// Calculate the default protocol that would be used if toolProtocol is not set
// This mirrors the logic in resolveToolProtocol.ts (steps 2-5, skipping step 1 which is the user preference)
const defaultProtocol = useMemo((): ToolProtocol => {
// 2. Experimental Setting (nativeToolCalling experiment)
const nativeToolCallingEnabled =
experimentsConfig?.[EXPERIMENT_IDS.NATIVE_TOOL_CALLING] ??
experimentDefault[EXPERIMENT_IDS.NATIVE_TOOL_CALLING]
// Mirrors the simplified logic in resolveToolProtocol.ts:
// 1. User preference (toolProtocol) - handled by the select value binding
// 2. Model default - use if available
// 3. XML fallback
const defaultProtocol = selectedModelInfo?.defaultToolProtocol || TOOL_PROTOCOL.XML
if (nativeToolCallingEnabled) {
// Check if model supports native tools
if (selectedModelInfo?.supportsNativeTools === true) {
return TOOL_PROTOCOL.NATIVE
}
// If experiment is enabled but model doesn't support it, return XML immediately
// This matches resolveToolProtocol.ts behavior (lines 53-57)
return TOOL_PROTOCOL.XML
}
// 3. Model Default
if (selectedModelInfo?.defaultToolProtocol) {
// Still need to check support even for model defaults
if (
selectedModelInfo.defaultToolProtocol === TOOL_PROTOCOL.NATIVE &&
selectedModelInfo.supportsNativeTools !== true
) {
return TOOL_PROTOCOL.XML
}
return selectedModelInfo.defaultToolProtocol
}
// 4. Provider Default (currently all providers default to XML)
// The nativePreferredProviders list in resolveToolProtocol.ts is currently empty
// If you update it there, update this logic as well
// 5. XML Fallback
return TOOL_PROTOCOL.XML
}, [experimentsConfig, selectedModelInfo])
// Determine whether to show the tool protocol selector
// Separate from defaultProtocol calculation to keep concerns separate
const showToolProtocolSelector = useMemo(() => {
const nativeToolCallingEnabled =
experimentsConfig?.[EXPERIMENT_IDS.NATIVE_TOOL_CALLING] ??
experimentDefault[EXPERIMENT_IDS.NATIVE_TOOL_CALLING]
// Only show if experiment is enabled AND model supports native tools
// If model doesn't support native tools, showing the selector is confusing
// since native protocol won't actually be available
return nativeToolCallingEnabled && selectedModelInfo?.supportsNativeTools === true
}, [experimentsConfig, selectedModelInfo])
// Show the tool protocol selector when model supports native tools
const showToolProtocolSelector = selectedModelInfo?.supportsNativeTools === true
// Convert providers to SearchableSelect options
const providerOptions = useMemo(() => {

View file

@ -783,10 +783,6 @@
"RUN_SLASH_COMMAND": {
"name": "Habilitar comandes de barra diagonal iniciades pel model",
"description": "Quan està habilitat, Roo pot executar les vostres comandes de barra diagonal per executar fluxos de treball."
},
"NATIVE_TOOL_CALLING": {
"name": "Habilitar crida d'eines nativa",
"description": "Quan està activat, Roo utilitzarà l'API de crida de funció nativa del proveïdor en lloc de definicions d'eines basades en XML. Això és experimental i encara no funciona amb tots els proveïdors."
}
},
"promptCaching": {

View file

@ -783,10 +783,6 @@
"RUN_SLASH_COMMAND": {
"name": "Modellinitierte Slash-Befehle aktivieren",
"description": "Wenn aktiviert, kann Roo deine Slash-Befehle ausführen, um Workflows zu starten."
},
"NATIVE_TOOL_CALLING": {
"name": "Native Tool Calling aktivieren",
"description": "Wenn aktiviert, verwendet Roo die native Function-Calling-API des Providers anstelle von XML-basierten Tool-Definitionen. Dies ist experimentell und funktioniert noch nicht mit allen Providern."
}
},
"promptCaching": {

View file

@ -788,10 +788,6 @@
"RUN_SLASH_COMMAND": {
"name": "Enable model-initiated slash commands",
"description": "When enabled, Roo can run your slash commands to execute workflows."
},
"NATIVE_TOOL_CALLING": {
"name": "Enable native tool calling",
"description": "When enabled, Roo will use the provider's native function calling API instead of XML-based tool definitions. This is experimental and does not yet work with all providers."
}
},
"promptCaching": {

View file

@ -783,10 +783,6 @@
"RUN_SLASH_COMMAND": {
"name": "Habilitar comandos slash iniciados por el modelo",
"description": "Cuando está habilitado, Roo puede ejecutar tus comandos slash para ejecutar flujos de trabajo."
},
"NATIVE_TOOL_CALLING": {
"name": "Habilitar llamada de herramientas nativas",
"description": "Cuando está habilitado, Roo utilizará la API de llamada de funciones nativa del proveedor en lugar de definiciones de herramientas basadas en XML. Esto es experimental y aún no funciona con todos los proveedores."
}
},
"promptCaching": {

View file

@ -783,10 +783,6 @@
"RUN_SLASH_COMMAND": {
"name": "Activer les commandes slash initiées par le modèle",
"description": "Lorsque activé, Roo peut exécuter tes commandes slash pour lancer des workflows."
},
"NATIVE_TOOL_CALLING": {
"name": "Activer l'appel natif d'outils",
"description": "Lorsqu'activé, Roo utilisera l'API native d'appel de fonction du fournisseur au lieu des définitions d'outils basées sur XML. Ceci est expérimental et ne fonctionne pas encore avec tous les fournisseurs."
}
},
"promptCaching": {

View file

@ -784,10 +784,6 @@
"RUN_SLASH_COMMAND": {
"name": "मॉडल द्वारा शुरू किए गए स्लैश कमांड सक्षम करें",
"description": "जब सक्षम होता है, Roo वर्कफ़्लो चलाने के लिए आपके स्लैश कमांड चला सकता है।"
},
"NATIVE_TOOL_CALLING": {
"name": "नेटिव टूल कॉलिंग सक्षम करें",
"description": "सक्षम होने पर, Roo XML-आधारित टूल परिभाषाओं के बजाय प्रदाता के नेटिव फ़ंक्शन कॉलिंग API का उपयोग करेगा। यह प्रयोगात्मक है और अभी सभी प्रदाताओं के साथ काम नहीं करता है।"
}
},
"promptCaching": {

View file

@ -813,10 +813,6 @@
"RUN_SLASH_COMMAND": {
"name": "Aktifkan perintah slash yang dimulai model",
"description": "Ketika diaktifkan, Roo dapat menjalankan perintah slash Anda untuk mengeksekusi alur kerja."
},
"NATIVE_TOOL_CALLING": {
"name": "Aktifkan pemanggilan alat asli",
"description": "Ketika diaktifkan, Roo akan menggunakan API pemanggilan fungsi asli dari penyedia alih-alih definisi alat berbasis XML. Ini bersifat eksperimental dan belum berfungsi dengan semua penyedia."
}
},
"promptCaching": {

View file

@ -784,10 +784,6 @@
"RUN_SLASH_COMMAND": {
"name": "Abilita comandi slash avviati dal modello",
"description": "Quando abilitato, Roo può eseguire i tuoi comandi slash per eseguire flussi di lavoro."
},
"NATIVE_TOOL_CALLING": {
"name": "Abilita chiamata nativa degli strumenti",
"description": "Quando abilitato, Roo utilizzerà l'API di chiamata di funzioni nativa del provider invece delle definizioni di strumenti basate su XML. Questo è sperimentale e non funziona ancora con tutti i provider."
}
},
"promptCaching": {

View file

@ -784,10 +784,6 @@
"RUN_SLASH_COMMAND": {
"name": "モデル開始スラッシュコマンドを有効にする",
"description": "有効にすると、Rooがワークフローを実行するためにあなたのスラッシュコマンドを実行できます。"
},
"NATIVE_TOOL_CALLING": {
"name": "ネイティブツール呼び出しを有効にする",
"description": "有効にすると、RooはXMLベースのツール定義の代わりにプロバイダーのネイティブ関数呼び出しAPIを使用します。これは実験的なもので、まだすべてのプロバイダーで機能するわけではありません。"
}
},
"promptCaching": {

View file

@ -784,10 +784,6 @@
"RUN_SLASH_COMMAND": {
"name": "모델 시작 슬래시 명령 활성화",
"description": "활성화되면 Roo가 워크플로를 실행하기 위해 슬래시 명령을 실행할 수 있습니다."
},
"NATIVE_TOOL_CALLING": {
"name": "네이티브 도구 호출 활성화",
"description": "활성화하면 Roo는 XML 기반 도구 정의 대신 제공자의 네이티브 함수 호출 API를 사용합니다. 이는 실험적이며 아직 모든 제공자에서 작동하지 않습니다."
}
},
"promptCaching": {

View file

@ -784,10 +784,6 @@
"RUN_SLASH_COMMAND": {
"name": "Model-geïnitieerde slash-commando's inschakelen",
"description": "Wanneer ingeschakeld, kan Roo je slash-commando's uitvoeren om workflows uit te voeren."
},
"NATIVE_TOOL_CALLING": {
"name": "Schakel native tool calling in",
"description": "Wanneer ingeschakeld, gebruikt Roo de native function calling API van de provider in plaats van XML-gebaseerde tooldefinities. Dit is experimenteel en werkt nog niet met alle providers."
}
},
"promptCaching": {

View file

@ -784,10 +784,6 @@
"RUN_SLASH_COMMAND": {
"name": "Włącz polecenia slash inicjowane przez model",
"description": "Gdy włączone, Roo może uruchamiać twoje polecenia slash w celu wykonywania przepływów pracy."
},
"NATIVE_TOOL_CALLING": {
"name": "Włącz natywne wywoływanie narzędzi",
"description": "Gdy włączone, Roo będzie używać natywnego API wywoływania funkcji dostawcy zamiast definicji narzędzi opartych na XML. Jest to funkcja eksperymentalna i nie działa jeszcze ze wszystkimi dostawcami."
}
},
"promptCaching": {

View file

@ -784,10 +784,6 @@
"RUN_SLASH_COMMAND": {
"name": "Ativar comandos slash iniciados pelo modelo",
"description": "Quando ativado, Roo pode executar seus comandos slash para executar fluxos de trabalho."
},
"NATIVE_TOOL_CALLING": {
"name": "Ativar chamada nativa de ferramentas",
"description": "Quando ativado, Roo usará a API de chamada de função nativa do provedor em vez de definições de ferramentas baseadas em XML. Isso é experimental e ainda não funciona com todos os provedores."
}
},
"promptCaching": {

View file

@ -784,10 +784,6 @@
"RUN_SLASH_COMMAND": {
"name": "Включить слэш-команды, инициированные моделью",
"description": "Когда включено, Roo может выполнять ваши слэш-команды для выполнения рабочих процессов."
},
"NATIVE_TOOL_CALLING": {
"name": "Включить нативный вызов инструментов",
"description": "Когда включено, Roo будет использовать нативный API вызова функций провайдера вместо XML-определений инструментов. Это экспериментальная функция и пока не работает со всеми провайдерами."
}
},
"promptCaching": {

View file

@ -784,10 +784,6 @@
"RUN_SLASH_COMMAND": {
"name": "Model tarafından başlatılan slash komutlarını etkinleştir",
"description": "Etkinleştirildiğinde, Roo iş akışlarını yürütmek için slash komutlarınızı çalıştırabilir."
},
"NATIVE_TOOL_CALLING": {
"name": "Native tool calling'i etkinleştir",
"description": "Etkinleştirildiğinde, Roo XML tabanlı araç tanımları yerine sağlayıcının yerel fonksiyon çağırma API'sını kullanacaktır. Bu deneyseldir ve henüz tüm sağlayıcılarla çalışmamaktadır."
}
},
"promptCaching": {

View file

@ -784,10 +784,6 @@
"RUN_SLASH_COMMAND": {
"name": "Bật lệnh slash do mô hình khởi tạo",
"description": "Khi được bật, Roo có thể chạy các lệnh slash của bạn để thực hiện các quy trình làm việc."
},
"NATIVE_TOOL_CALLING": {
"name": "Bật gọi công cụ gốc",
"description": "Khi được bật, Roo sẽ sử dụng API gọi hàm gốc của nhà cung cấp thay vì định nghĩa công cụ dựa trên XML. Đây là tính năng thử nghiệm và chưa hoạt động với tất cả các nhà cung cấp."
}
},
"promptCaching": {

View file

@ -784,10 +784,6 @@
"RUN_SLASH_COMMAND": {
"name": "启用模型发起的斜杠命令",
"description": "启用后 Roo 可运行斜杠命令执行工作流程。"
},
"NATIVE_TOOL_CALLING": {
"name": "启用原生工具调用",
"description": "启用后Roo 将使用提供商的原生函数调用 API而不是基于 XML 的工具定义。这是实验性功能,目前尚未支持所有提供商。"
}
},
"promptCaching": {

View file

@ -784,10 +784,6 @@
"RUN_SLASH_COMMAND": {
"name": "啟用模型啟動的斜線命令",
"description": "啟用時Roo 可以執行您的斜線命令來執行工作流程。"
},
"NATIVE_TOOL_CALLING": {
"name": "啟用原生工具呼叫",
"description": "啟用後Roo 將使用提供商的原生函式呼叫 API而非基於 XML 的工具定義。這是實驗性功能,目前尚未支援所有提供商。"
}
},
"promptCaching": {