Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 47 additions & 7 deletions src/utils/__tests__/resolveToolProtocol.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,29 +48,69 @@ describe("resolveToolProtocol", () => {
})
})

describe("Native Protocol Always Used For New Tasks", () => {
it("should always use native for new tasks", () => {
describe("OpenAI-Compatible Provider Tool Protocol", () => {
it("should respect XML preference for OpenAI-Compatible provider", () => {
const settings: ProviderSettings = {
apiProvider: "openai",
toolProtocol: "xml",
}
// OpenAI-Compatible provider should respect user preference
const result = resolveToolProtocol(settings)
expect(result).toBe(TOOL_PROTOCOL.XML)
})

it("should respect native preference for OpenAI-Compatible provider", () => {
const settings: ProviderSettings = {
apiProvider: "openai",
toolProtocol: "native",
}
const result = resolveToolProtocol(settings)
expect(result).toBe(TOOL_PROTOCOL.NATIVE)
})

it("should default to native when no preference set for OpenAI-Compatible provider", () => {
const settings: ProviderSettings = {
apiProvider: "openai",
}
const result = resolveToolProtocol(settings, openAiModelInfoSaneDefaults)
expect(result).toBe(TOOL_PROTOCOL.NATIVE)
})

it("should still honor locked protocol over user preference for OpenAI-Compatible", () => {
const settings: ProviderSettings = {
apiProvider: "openai",
toolProtocol: "xml", // User wants XML
}
// Locked protocol takes precedence
const result = resolveToolProtocol(settings, undefined, "native")
expect(result).toBe(TOOL_PROTOCOL.NATIVE)
})
})

describe("Native Protocol Always Used For Other Providers", () => {
it("should always use native for new tasks with other providers", () => {
const settings: ProviderSettings = {
apiProvider: "anthropic",
}
const result = resolveToolProtocol(settings)
expect(result).toBe(TOOL_PROTOCOL.NATIVE)
})

it("should use native even when user preference is XML (user prefs ignored)", () => {
it("should use native even when user preference is XML for non-OpenAI-Compatible providers", () => {
const settings: ProviderSettings = {
toolProtocol: "xml", // User wants XML - ignored
toolProtocol: "xml", // User wants XML - ignored for non-OpenAI-Compatible
apiProvider: "openai-native",
}
const result = resolveToolProtocol(settings)
expect(result).toBe(TOOL_PROTOCOL.NATIVE)
})

it("should use native for OpenAI compatible provider", () => {
it("should ignore XML preference for Anthropic provider", () => {
const settings: ProviderSettings = {
apiProvider: "openai",
toolProtocol: "xml", // User preference - ignored for Anthropic
apiProvider: "anthropic",
}
const result = resolveToolProtocol(settings, openAiModelInfoSaneDefaults)
const result = resolveToolProtocol(settings)
expect(result).toBe(TOOL_PROTOCOL.NATIVE)
})
})
Expand Down
25 changes: 17 additions & 8 deletions src/utils/resolveToolProtocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,23 @@ type ApiMessageForDetection = Anthropic.MessageParam & {
* Resolve the effective tool protocol.
*
* **Deprecation Note (XML Protocol):**
* XML tool protocol has been deprecated. All models now use Native tool calling.
* User/profile preferences (`providerSettings.toolProtocol`) and model defaults
* (`modelInfo.defaultToolProtocol`) are ignored.
* XML tool protocol has been deprecated for most providers. All models now use
* Native tool calling by default. However, for the OpenAI-Compatible provider,
* users can explicitly opt for XML protocol when their backend servers (like
* SGLang, vLLM, etc.) don't support native tool calling.
*
* Precedence:
* 1. Locked Protocol (task-level lock for resumed tasks - highest priority)
* 2. Native (always, for all new tasks)
* 2. User Preference for OpenAI-Compatible provider (respects toolProtocol setting)
* 3. Native (default for all new tasks)
*
* @param _providerSettings - The provider settings (toolProtocol field is ignored)
* @param providerSettings - The provider settings (toolProtocol respected for OpenAI-Compatible)
* @param _modelInfo - Unused, kept for API compatibility
* @param lockedProtocol - Optional task-locked protocol that takes absolute precedence
* @returns The resolved tool protocol (either "xml" or "native")
*/
export function resolveToolProtocol(
_providerSettings: ProviderSettings,
providerSettings: ProviderSettings,
_modelInfo?: unknown,
lockedProtocol?: ToolProtocol,
): ToolProtocol {
Expand All @@ -39,8 +41,15 @@ export function resolveToolProtocol(
return lockedProtocol
}

// 2. Always return Native protocol for new tasks
// All models now support native tools; XML is deprecated
// 2. For OpenAI-Compatible provider, respect user preference
// Some backends (SGLang, vLLM, etc.) don't support native tool calling
// and require XML protocol to work properly
if (providerSettings.apiProvider === "openai" && providerSettings.toolProtocol) {
return providerSettings.toolProtocol
}

// 3. Default to Native protocol for new tasks
// Most providers support native tools; XML is deprecated for other providers
return TOOL_PROTOCOL.NATIVE
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,23 @@ import {
type ModelInfo,
type ReasoningEffort,
type OrganizationAllowList,
type ToolProtocol,
azureOpenAiDefaultApiVersion,
openAiModelInfoSaneDefaults,
} from "@roo-code/types"

import { ExtensionMessage } from "@roo/ExtensionMessage"

import { useAppTranslation } from "@src/i18n/TranslationContext"
import { Button, StandardTooltip } from "@src/components/ui"
import {
Button,
StandardTooltip,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@src/components/ui"

import { convertHeadersToObject } from "../utils/headers"
import { inputEventTransform, noTransform } from "../transforms"
Expand Down Expand Up @@ -170,6 +179,24 @@ export const OpenAICompatible = ({
onChange={handleInputChange("openAiStreamingEnabled", noTransform)}>
{t("settings:modelInfo.enableStreaming")}
</Checkbox>
{/* Tool Protocol selector for backends that don't support native tool calling */}
<div>
<label className="block font-medium mb-1">{t("settings:toolProtocol.label")}</label>
<Select
value={apiConfiguration?.toolProtocol || "native"}
onValueChange={(value) => setApiConfigurationField("toolProtocol", value as ToolProtocol)}>
<SelectTrigger className="w-full">
<SelectValue placeholder={t("settings:common.select")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="native">{t("settings:toolProtocol.native")}</SelectItem>
<SelectItem value="xml">{t("settings:toolProtocol.xml")}</SelectItem>
</SelectContent>
</Select>
<div className="text-sm text-vscode-descriptionForeground mt-1">
{t("settings:toolProtocol.description")}
</div>
</div>
<div>
<Checkbox
checked={apiConfiguration?.includeMaxTokens ?? true}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ vi.mock("@src/i18n/TranslationContext", () => ({
vi.mock("@src/components/ui", () => ({
Button: ({ children, onClick }: any) => <button onClick={onClick}>{children}</button>,
StandardTooltip: ({ children, content }: any) => <div title={content}>{children}</div>,
Select: ({ children, value, onValueChange: _onValueChange }: any) => (
<div data-testid="select" data-value={value}>
{children}
</div>
),
SelectContent: ({ children }: any) => <div data-testid="select-content">{children}</div>,
SelectItem: ({ children, value }: any) => <div data-testid={`select-item-${value}`}>{children}</div>,
SelectTrigger: ({ children }: any) => <button data-testid="select-trigger">{children}</button>,
SelectValue: ({ placeholder }: any) => <span data-testid="select-value">{placeholder}</span>,
}))

// Mock other components
Expand Down
Loading