From dd2e33c039a59a17b61dd26ebb356c0f72c8ad7f Mon Sep 17 00:00:00 2001 From: Roo Code Date: Thu, 22 Jan 2026 03:36:02 +0000 Subject: [PATCH] feat: add openAiNativeToolsEnabled setting for OpenAI-Compatible provider This adds a new setting that allows users to disable native tool calling for the OpenAI-Compatible provider. When disabled, tools are not passed to the API, allowing users with local providers that do not support native tool calling to use XML-style tool invocation via the system prompt instead. The setting defaults to true (enabled) for backward compatibility. Fixes #10875 --- packages/types/src/provider-settings.ts | 4 ++ src/api/providers/openai.ts | 54 +++++++++++++++++-------- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index ba652269b1..562f8176b1 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -245,6 +245,10 @@ const openAiSchema = baseProviderSettingsSchema.extend({ openAiStreamingEnabled: z.boolean().optional(), openAiHostHeader: z.string().optional(), // Keep temporarily for backward compatibility during migration. openAiHeaders: z.record(z.string(), z.string()).optional(), + // When true (default), native function calling is used. When false, tools are not passed + // to the API, allowing users with local providers that don't support native tool calling + // to use XML-style tool invocation via the system prompt instead. + openAiNativeToolsEnabled: z.boolean().optional(), }) const ollamaSchema = baseProviderSettingsSchema.extend({ diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index 74cbb51113..c0eecd4c73 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -90,9 +90,11 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl const enabledR1Format = this.options.openAiR1FormatEnabled ?? false const isAzureAiInference = this._isAzureAiInference(modelUrl) const deepseekReasoner = modelId.includes("deepseek-reasoner") || enabledR1Format + // Default to true for backward compatibility - when false, tools are not passed to the API + const nativeToolsEnabled = this.options.openAiNativeToolsEnabled !== false if (modelId.includes("o1") || modelId.includes("o3") || modelId.includes("o4")) { - yield* this.handleO3FamilyMessage(modelId, systemPrompt, messages, metadata) + yield* this.handleO3FamilyMessage(modelId, systemPrompt, messages, metadata, nativeToolsEnabled) return } @@ -159,9 +161,14 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl stream: true as const, ...(isGrokXAI ? {} : { stream_options: { include_usage: true } }), ...(reasoning && reasoning), - tools: this.convertToolsForOpenAI(metadata?.tools), - tool_choice: metadata?.tool_choice, - parallel_tool_calls: metadata?.parallelToolCalls ?? false, + // Only include tools if native tool calling is enabled + ...(nativeToolsEnabled + ? { + tools: this.convertToolsForOpenAI(metadata?.tools), + tool_choice: metadata?.tool_choice, + parallel_tool_calls: metadata?.parallelToolCalls ?? false, + } + : {}), } // Add max_tokens if needed @@ -226,10 +233,14 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl messages: deepseekReasoner ? convertToR1Format([{ role: "user", content: systemPrompt }, ...messages]) : [systemMessage, ...convertToOpenAiMessages(messages)], - // Tools are always present (minimum ALWAYS_AVAILABLE_TOOLS) - tools: this.convertToolsForOpenAI(metadata?.tools), - tool_choice: metadata?.tool_choice, - parallel_tool_calls: metadata?.parallelToolCalls ?? false, + // Only include tools if native tool calling is enabled + ...(nativeToolsEnabled + ? { + tools: this.convertToolsForOpenAI(metadata?.tools), + tool_choice: metadata?.tool_choice, + parallel_tool_calls: metadata?.parallelToolCalls ?? false, + } + : {}), } // Add max_tokens if needed @@ -324,7 +335,8 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl modelId: string, systemPrompt: string, messages: Anthropic.Messages.MessageParam[], - metadata?: ApiHandlerCreateMessageMetadata, + metadata: ApiHandlerCreateMessageMetadata | undefined, + nativeToolsEnabled: boolean, ): ApiStream { const modelInfo = this.getModel().info const methodIsAzureAiInference = this._isAzureAiInference(this.options.openAiBaseUrl) @@ -345,10 +357,14 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl ...(isGrokXAI ? {} : { stream_options: { include_usage: true } }), reasoning_effort: modelInfo.reasoningEffort as "low" | "medium" | "high" | undefined, temperature: undefined, - // Tools are always present (minimum ALWAYS_AVAILABLE_TOOLS) - tools: this.convertToolsForOpenAI(metadata?.tools), - tool_choice: metadata?.tool_choice, - parallel_tool_calls: metadata?.parallelToolCalls ?? false, + // Only include tools if native tool calling is enabled + ...(nativeToolsEnabled + ? { + tools: this.convertToolsForOpenAI(metadata?.tools), + tool_choice: metadata?.tool_choice, + parallel_tool_calls: metadata?.parallelToolCalls ?? false, + } + : {}), } // O3 family models do not support the deprecated max_tokens parameter @@ -379,10 +395,14 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl ], reasoning_effort: modelInfo.reasoningEffort as "low" | "medium" | "high" | undefined, temperature: undefined, - // Tools are always present (minimum ALWAYS_AVAILABLE_TOOLS) - tools: this.convertToolsForOpenAI(metadata?.tools), - tool_choice: metadata?.tool_choice, - parallel_tool_calls: metadata?.parallelToolCalls ?? false, + // Only include tools if native tool calling is enabled + ...(nativeToolsEnabled + ? { + tools: this.convertToolsForOpenAI(metadata?.tools), + tool_choice: metadata?.tool_choice, + parallel_tool_calls: metadata?.parallelToolCalls ?? false, + } + : {}), } // O3 family models do not support the deprecated max_tokens parameter