feat: restore XML tool protocol option for OpenAI-Compatible provider

This restores the ability for users of the OpenAI-Compatible provider to
explicitly select XML tool protocol, fixing compatibility issues with
backends like SGLang/vLLM that do not support native tool calling.

Changes:
- Modified resolveToolProtocol.ts to respect user toolProtocol preference
  for the OpenAI-Compatible provider ("openai")
- Added Tool Protocol selector to OpenAICompatible.tsx component
- Added tests for the new behavior in resolveToolProtocol.spec.ts
- Updated OpenAICompatible.spec.tsx to mock Select components

Fixes RooCodeInc#10319
This commit is contained in:
Roo Code 2025-12-26 15:29:27 +00:00
parent 343d5e9dcb
commit 89fd5a695f
4 changed files with 101 additions and 16 deletions

View file

@ -48,8 +48,47 @@ 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",
}
@ -57,20 +96,21 @@ describe("resolveToolProtocol", () => {
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)
})
})

View file

@ -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 {
@ -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
}

View file

@ -8,6 +8,7 @@ import {
type ModelInfo,
type ReasoningEffort,
type OrganizationAllowList,
type ToolProtocol,
azureOpenAiDefaultApiVersion,
openAiModelInfoSaneDefaults,
} from "@roo-code/types"
@ -15,7 +16,15 @@ import {
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"
@ -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}

View file

@ -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