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
3 changes: 3 additions & 0 deletions packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@ const baseProviderSettingsSchema = z.object({

// Model verbosity.
verbosity: verbosityLevelsSchema.optional(),

// Tool calling protocol.
useXmlToolCalling: z.boolean().optional(),
})

// Several of the providers share common model config properties.
Expand Down
7 changes: 7 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@ export interface ApiHandlerCreateMessageMetadata {
* Only applies to providers that support function calling restrictions (e.g., Gemini).
*/
allowedFunctionNames?: string[]
/**
* When true, native tool definitions are omitted from the API request body.
* The model relies solely on XML tool documentation in the system prompt
* and outputs tool calls as raw XML text, which the existing TagMatcher
* in presentAssistantMessage() parses into ToolUse objects.
Comment on lines +91 to +93
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc comment claims XML tool calls are parsed by TagMatcher in presentAssistantMessage(), but presentAssistantMessage currently treats missing tool_use.id as an invalid legacy/XML tool call and rejects it, and tools generally require nativeArgs. Please update this comment to reflect the actual execution/parsing flow, or add the missing XML parsing implementation and adjust this description accordingly.

Suggested change
* The model relies solely on XML tool documentation in the system prompt
* and outputs tool calls as raw XML text, which the existing TagMatcher
* in presentAssistantMessage() parses into ToolUse objects.
* The model is expected to rely solely on XML tool documentation in the system prompt
* and may output tool calls as raw XML (or XML-like) text.
*
* This flag only affects how the request is constructed; any parsing of XML tool
* calls into ToolUse objects must be handled by higher-level consumer code.

Copilot uses AI. Check for mistakes.
*/
useXmlToolCalling?: boolean
}

export interface ApiHandler {
Expand Down
58 changes: 58 additions & 0 deletions src/api/providers/__tests__/anthropic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -787,5 +787,63 @@ describe("AnthropicHandler", () => {
arguments: '"London"}',
})
})

it("should omit tools and tool_choice when useXmlToolCalling is true", async () => {
const stream = handler.createMessage(systemPrompt, messages, {
taskId: "test-task",
tools: mockTools,
tool_choice: "auto",
useXmlToolCalling: true,
})

// Consume the stream to trigger the API call
for await (const _chunk of stream) {
// Just consume
}

const callArgs = mockCreate.mock.calls[mockCreate.mock.calls.length - 1][0]
// When useXmlToolCalling is true, the tools and tool_choice should NOT be in the request
expect(callArgs.tools).toBeUndefined()
expect(callArgs.tool_choice).toBeUndefined()
})

it("should include tools when useXmlToolCalling is false", async () => {
const stream = handler.createMessage(systemPrompt, messages, {
taskId: "test-task",
tools: mockTools,
tool_choice: "auto",
useXmlToolCalling: false,
})

// Consume the stream to trigger the API call
for await (const _chunk of stream) {
// Just consume
}

const callArgs = mockCreate.mock.calls[mockCreate.mock.calls.length - 1][0]
// When useXmlToolCalling is false, tools should be included normally
expect(callArgs.tools).toBeDefined()
expect(callArgs.tools.length).toBeGreaterThan(0)
expect(callArgs.tool_choice).toBeDefined()
})

it("should include tools when useXmlToolCalling is undefined", async () => {
const stream = handler.createMessage(systemPrompt, messages, {
taskId: "test-task",
tools: mockTools,
tool_choice: "auto",
})

// Consume the stream to trigger the API call
for await (const _chunk of stream) {
// Just consume
}

const callArgs = mockCreate.mock.calls[mockCreate.mock.calls.length - 1][0]
// Default behavior: tools should be included
expect(callArgs.tools).toBeDefined()
expect(callArgs.tools.length).toBeGreaterThan(0)
expect(callArgs.tool_choice).toBeDefined()
})
})
})
127 changes: 127 additions & 0 deletions src/api/providers/__tests__/openai.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,133 @@ describe("OpenAiHandler", () => {
})
})

describe("useXmlToolCalling", () => {
const systemPrompt = "You are a helpful assistant."
const messages: Anthropic.Messages.MessageParam[] = [
{
role: "user",
content: [{ type: "text" as const, text: "Hello!" }],
},
]

const mockTools: OpenAI.Chat.ChatCompletionTool[] = [
{
type: "function",
function: {
name: "read_file",
description: "Read a file",
parameters: {
type: "object",
properties: { path: { type: "string" } },
required: ["path"],
},
},
},
]

it("should omit tools and tool_choice when useXmlToolCalling is true (streaming)", async () => {
const stream = handler.createMessage(systemPrompt, messages, {
taskId: "test",
tools: mockTools,
tool_choice: "auto",
useXmlToolCalling: true,
})

for await (const _chunk of stream) {
}

const callArgs = mockCreate.mock.calls[mockCreate.mock.calls.length - 1][0]
// When useXmlToolCalling is true, the tools and tool_choice should NOT be in the request
expect(callArgs.tools).toBeUndefined()
expect(callArgs.tool_choice).toBeUndefined()
expect(callArgs.parallel_tool_calls).toBeUndefined()
})

it("should omit tools and tool_choice when useXmlToolCalling is true (non-streaming)", async () => {
const nonStreamHandler = new OpenAiHandler({
...mockOptions,
openAiStreamingEnabled: false,
})

const stream = nonStreamHandler.createMessage(systemPrompt, messages, {
taskId: "test",
tools: mockTools,
tool_choice: "auto",
useXmlToolCalling: true,
})

for await (const _chunk of stream) {
}

const callArgs = mockCreate.mock.calls[mockCreate.mock.calls.length - 1][0]
expect(callArgs.tools).toBeUndefined()
expect(callArgs.tool_choice).toBeUndefined()
expect(callArgs.parallel_tool_calls).toBeUndefined()
})

it("should include tools when useXmlToolCalling is false", async () => {
const stream = handler.createMessage(systemPrompt, messages, {
taskId: "test",
tools: mockTools,
tool_choice: "auto",
useXmlToolCalling: false,
})

for await (const _chunk of stream) {
}

const callArgs = mockCreate.mock.calls[mockCreate.mock.calls.length - 1][0]
expect(callArgs.tools).toBeDefined()
expect(callArgs.tools.length).toBeGreaterThan(0)
expect(callArgs.tool_choice).toBe("auto")
expect(callArgs.parallel_tool_calls).toBe(true)
})

it("should include tools when useXmlToolCalling is undefined", async () => {
const stream = handler.createMessage(systemPrompt, messages, {
taskId: "test",
tools: mockTools,
tool_choice: "auto",
})

for await (const _chunk of stream) {
}

const callArgs = mockCreate.mock.calls[mockCreate.mock.calls.length - 1][0]
expect(callArgs.tools).toBeDefined()
expect(callArgs.tools.length).toBeGreaterThan(0)
expect(callArgs.tool_choice).toBe("auto")
})

it("should omit tools and tool_choice for O3 family when useXmlToolCalling is true", async () => {
const o3Handler = new OpenAiHandler({
...mockOptions,
openAiModelId: "o3-mini",
openAiCustomModelInfo: {
contextWindow: 128_000,
maxTokens: 65536,
supportsPromptCache: false,
reasoningEffort: "medium" as "low" | "medium" | "high",
},
})

const stream = o3Handler.createMessage(systemPrompt, messages, {
taskId: "test",
tools: mockTools,
tool_choice: "auto",
useXmlToolCalling: true,
})

for await (const _chunk of stream) {
}

const callArgs = mockCreate.mock.calls[mockCreate.mock.calls.length - 1][0]
expect(callArgs.tools).toBeUndefined()
expect(callArgs.tool_choice).toBeUndefined()
expect(callArgs.parallel_tool_calls).toBeUndefined()
})
})

describe("error handling", () => {
const testMessages: Anthropic.Messages.MessageParam[] = [
{
Expand Down
13 changes: 9 additions & 4 deletions src/api/providers/anthropic-vertex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,15 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple
// Filter out non-Anthropic blocks (reasoning, thoughtSignature, etc.) before sending to the API
const sanitizedMessages = filterNonAnthropicBlocks(messages)

const nativeToolParams = {
tools: convertOpenAIToolsToAnthropic(metadata?.tools ?? []),
tool_choice: convertOpenAIToolChoiceToAnthropic(metadata?.tool_choice, metadata?.parallelToolCalls),
}
// When useXmlToolCalling is enabled, omit native tool definitions from the API request.
// The model will rely on XML tool documentation in the system prompt instead,
// and output tool calls as raw XML text parsed by TagMatcher.
const nativeToolParams = metadata?.useXmlToolCalling
? {}
: {
tools: convertOpenAIToolsToAnthropic(metadata?.tools ?? []),
tool_choice: convertOpenAIToolChoiceToAnthropic(metadata?.tool_choice, metadata?.parallelToolCalls),
}
Comment on lines +78 to +86
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as Anthropic: omitting native tools/tool_choice when useXmlToolCalling is true will leave the system without a working tool-call execution path unless XML tool calls are parsed into ToolUse blocks with ids/nativeArgs. As-is, this will likely break tool use for Anthropic Vertex. Either implement the XML parsing + tool documentation path, or continue sending native tool params.

Suggested change
// When useXmlToolCalling is enabled, omit native tool definitions from the API request.
// The model will rely on XML tool documentation in the system prompt instead,
// and output tool calls as raw XML text parsed by TagMatcher.
const nativeToolParams = metadata?.useXmlToolCalling
? {}
: {
tools: convertOpenAIToolsToAnthropic(metadata?.tools ?? []),
tool_choice: convertOpenAIToolChoiceToAnthropic(metadata?.tool_choice, metadata?.parallelToolCalls),
}
// Always send native tool definitions to the API request so that tool calling
// continues to work even when XML-based tool documentation is used elsewhere.
const nativeToolParams = {
tools: convertOpenAIToolsToAnthropic(metadata?.tools ?? []),
tool_choice: convertOpenAIToolChoiceToAnthropic(metadata?.tool_choice, metadata?.parallelToolCalls),
}

Copilot uses AI. Check for mistakes.

/**
* Vertex API has specific limitations for prompt caching:
Expand Down
13 changes: 9 additions & 4 deletions src/api/providers/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,15 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
betas.push("context-1m-2025-08-07")
}

const nativeToolParams = {
tools: convertOpenAIToolsToAnthropic(metadata?.tools ?? []),
tool_choice: convertOpenAIToolChoiceToAnthropic(metadata?.tool_choice, metadata?.parallelToolCalls),
}
// When useXmlToolCalling is enabled, omit native tool definitions from the API request.
// The model will rely on XML tool documentation in the system prompt instead,
// and output tool calls as raw XML text parsed by TagMatcher.
const nativeToolParams = metadata?.useXmlToolCalling
? {}
: {
tools: convertOpenAIToolsToAnthropic(metadata?.tools ?? []),
tool_choice: convertOpenAIToolChoiceToAnthropic(metadata?.tool_choice, metadata?.parallelToolCalls),
}
Comment on lines +78 to +86
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When metadata.useXmlToolCalling is true, this omits tools/tool_choice from the Anthropic request, but the codebase currently executes tools via native tool_use blocks with ids/nativeArgs (XML/legacy tool calls are explicitly rejected in presentAssistantMessage/BaseTool). Without an XML-to-ToolUse parser (and tool schema documentation) this will prevent any tool execution. Either implement the XML parsing + tool catalog path end-to-end, or keep sending native tools for Anthropic.

Suggested change
// When useXmlToolCalling is enabled, omit native tool definitions from the API request.
// The model will rely on XML tool documentation in the system prompt instead,
// and output tool calls as raw XML text parsed by TagMatcher.
const nativeToolParams = metadata?.useXmlToolCalling
? {}
: {
tools: convertOpenAIToolsToAnthropic(metadata?.tools ?? []),
tool_choice: convertOpenAIToolChoiceToAnthropic(metadata?.tool_choice, metadata?.parallelToolCalls),
}
// Always send native tool definitions for Anthropic so that tool_use blocks are produced.
// The useXmlToolCalling flag is currently ignored here because the rest of the codebase
// expects native tool_use events and does not support XML-based tool calling.
const nativeToolParams = {
tools: convertOpenAIToolsToAnthropic(metadata?.tools ?? []),
tool_choice: convertOpenAIToolChoiceToAnthropic(metadata?.tool_choice, metadata?.parallelToolCalls),
}

Copilot uses AI. Check for mistakes.

switch (modelId) {
case "claude-sonnet-4-6":
Expand Down
11 changes: 8 additions & 3 deletions src/api/providers/base-openai-compatible-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,14 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
stream: true,
stream_options: { include_usage: true },
tools: this.convertToolsForOpenAI(metadata?.tools),
tool_choice: metadata?.tool_choice,
parallel_tool_calls: metadata?.parallelToolCalls ?? true,
// When useXmlToolCalling is enabled, omit native tool definitions from the API request.
...(metadata?.useXmlToolCalling
? {}
: {
tools: this.convertToolsForOpenAI(metadata?.tools),
tool_choice: metadata?.tool_choice,
parallel_tool_calls: metadata?.parallelToolCalls ?? true,
}),
}

// Add thinking parameter if reasoning is enabled and model supports it
Expand Down
13 changes: 8 additions & 5 deletions src/api/providers/bedrock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,10 +450,13 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
additionalModelRequestFields.anthropic_beta = anthropicBetas
}

const toolConfig: ToolConfiguration = {
tools: this.convertToolsForBedrock(metadata?.tools ?? []),
toolChoice: this.convertToolChoiceForBedrock(metadata?.tool_choice),
}
// When useXmlToolCalling is enabled, omit native tool definitions from the API request.
const toolConfig: ToolConfiguration | undefined = metadata?.useXmlToolCalling
? undefined
: {
tools: this.convertToolsForBedrock(metadata?.tools ?? []),
toolChoice: this.convertToolChoiceForBedrock(metadata?.tool_choice),
}

// Build payload with optional service_tier at top level
// Service tier is a top-level parameter per AWS documentation, NOT inside additionalModelRequestFields
Expand All @@ -466,7 +469,7 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
...(additionalModelRequestFields && { additionalModelRequestFields }),
// Add anthropic_version at top level when using thinking features
...(thinkingEnabled && { anthropic_version: "bedrock-2023-05-31" }),
toolConfig,
...(toolConfig ? { toolConfig } : {}),
// Add service_tier as a top-level parameter (not inside additionalModelRequestFields)
...(useServiceTier && { service_tier: this.options.awsBedrockServiceTier }),
}
Expand Down
11 changes: 8 additions & 3 deletions src/api/providers/deepseek.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,14 @@ export class DeepSeekHandler extends OpenAiHandler {
stream_options: { include_usage: true },
// Enable thinking mode for deepseek-reasoner or when tools are used with thinking model
...(isThinkingModel && { thinking: { type: "enabled" } }),
tools: this.convertToolsForOpenAI(metadata?.tools),
tool_choice: metadata?.tool_choice,
parallel_tool_calls: metadata?.parallelToolCalls ?? true,
// When useXmlToolCalling is enabled, omit native tool definitions from the API request.
...(metadata?.useXmlToolCalling
? {}
: {
tools: this.convertToolsForOpenAI(metadata?.tools),
tool_choice: metadata?.tool_choice,
parallel_tool_calls: metadata?.parallelToolCalls ?? true,
}),
}

// Add max_tokens if needed
Expand Down
25 changes: 15 additions & 10 deletions src/api/providers/gemini.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,19 +128,22 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
.map((message) => convertAnthropicMessageToGemini(message, { includeThoughtSignatures, toolIdToName }))
.flat()

// When useXmlToolCalling is enabled, omit native tool definitions from the API request.
// Tools are always present (minimum ALWAYS_AVAILABLE_TOOLS).
// Google built-in tools (Grounding, URL Context) are mutually exclusive
// with function declarations in the Gemini API, so we always use
// function declarations when tools are provided.
const tools: GenerateContentConfig["tools"] = [
{
functionDeclarations: (metadata?.tools ?? []).map((tool) => ({
name: (tool as any).function.name,
description: (tool as any).function.description,
parametersJsonSchema: (tool as any).function.parameters,
})),
},
]
const tools: GenerateContentConfig["tools"] = metadata?.useXmlToolCalling
? []
: [
{
functionDeclarations: (metadata?.tools ?? []).map((tool) => ({
name: (tool as any).function.name,
description: (tool as any).function.description,
parametersJsonSchema: (tool as any).function.parameters,
})),
},
]

// Determine temperature respecting model capabilities and defaults:
// - If supportsTemperature is explicitly false, ignore user overrides
Expand All @@ -165,7 +168,9 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
// When provided, all tool definitions are passed to the model (so it can reference
// historical tool calls in conversation), but only the specified tools can be invoked.
// This takes precedence over tool_choice to ensure mode restrictions are honored.
if (metadata?.allowedFunctionNames && metadata.allowedFunctionNames.length > 0) {
if (metadata?.useXmlToolCalling) {
// Skip toolConfig entirely when using XML tool calling
} else if (metadata?.allowedFunctionNames && metadata.allowedFunctionNames.length > 0) {
config.toolConfig = {
functionCallingConfig: {
// Use ANY mode to allow calling any of the allowed functions
Expand Down
9 changes: 7 additions & 2 deletions src/api/providers/lite-llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,13 @@ export class LiteLLMHandler extends RouterProvider implements SingleCompletionHa
stream_options: {
include_usage: true,
},
tools: this.convertToolsForOpenAI(metadata?.tools),
tool_choice: metadata?.tool_choice,
// When useXmlToolCalling is enabled, omit native tool definitions from the API request.
...(metadata?.useXmlToolCalling
? {}
: {
tools: this.convertToolsForOpenAI(metadata?.tools),
tool_choice: metadata?.tool_choice,
}),
}

// GPT-5 models require max_completion_tokens instead of the deprecated max_tokens parameter
Expand Down
Loading
Loading