diff --git a/src/api/providers/__tests__/openrouter.spec.ts b/src/api/providers/__tests__/openrouter.spec.ts index e03abea635..1e124dfae1 100644 --- a/src/api/providers/__tests__/openrouter.spec.ts +++ b/src/api/providers/__tests__/openrouter.spec.ts @@ -525,6 +525,65 @@ describe("OpenRouterHandler", () => { expect(endChunks).toHaveLength(1) expect(endChunks[0].id).toBe("call_openrouter_test") }) + + it("always includes require_parameters for tools in provider config", async () => { + const handler = new OpenRouterHandler(mockOptions) + + const mockStream = { + async *[Symbol.asyncIterator]() { + yield { + id: "test-id", + choices: [{ delta: { content: "test" } }], + } + }, + } + + const mockCreate = vitest.fn().mockResolvedValue(mockStream) + ;(OpenAI as any).prototype.chat = { + completions: { create: mockCreate }, + } as any + + const generator = handler.createMessage("test system", [{ role: "user", content: "test" }]) + await generator.next() + + const callArgs = mockCreate.mock.calls[0][0] + expect(callArgs.provider).toBeDefined() + expect(callArgs.provider.require_parameters).toEqual(["tools", "tool_choice"]) + // Should NOT have routing fields when no specific provider + expect(callArgs.provider.order).toBeUndefined() + expect(callArgs.provider.only).toBeUndefined() + }) + + it("merges require_parameters with specific provider routing config", async () => { + const handler = new OpenRouterHandler({ + ...mockOptions, + openRouterSpecificProvider: "anthropic", + }) + + const mockStream = { + async *[Symbol.asyncIterator]() { + yield { + id: "test-id", + choices: [{ delta: { content: "test" } }], + } + }, + } + + const mockCreate = vitest.fn().mockResolvedValue(mockStream) + ;(OpenAI as any).prototype.chat = { + completions: { create: mockCreate }, + } as any + + const generator = handler.createMessage("test system", [{ role: "user", content: "test" }]) + await generator.next() + + const callArgs = mockCreate.mock.calls[0][0] + expect(callArgs.provider).toBeDefined() + expect(callArgs.provider.require_parameters).toEqual(["tools", "tool_choice"]) + expect(callArgs.provider.order).toEqual(["anthropic"]) + expect(callArgs.provider.only).toEqual(["anthropic"]) + expect(callArgs.provider.allow_fallbacks).toBe(false) + }) }) describe("completePrompt", () => { diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index 7fcc24b15f..d9705b9756 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -46,6 +46,8 @@ type OpenRouterChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParams & { include_reasoning?: boolean // https://openrouter.ai/docs/use-cases/reasoning-tokens reasoning?: OpenRouterReasoningParams + // https://openrouter.ai/docs/features/provider-routing + provider?: Record } // Zod schema for OpenRouter error response structure (for caught exceptions) @@ -308,6 +310,27 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH } } + // Build the provider configuration. + // Always include require_parameters to ensure OpenRouter passes tool-calling + // parameters through to the model, even when the model's metadata doesn't + // list "tools" in supported_parameters. Without this, OpenRouter may strip + // tool parameters or apply prompt-based transforms that produce poor results + // for models that actually support native tool calling (e.g. Nemotron 3 Super). + // See: https://github.com/RooCodeInc/Roo-Code/issues/11968 + const providerConfig: Record = { + require_parameters: ["tools", "tool_choice"], + } + + // Add specific provider routing if configured. + if ( + this.options.openRouterSpecificProvider && + this.options.openRouterSpecificProvider !== OPENROUTER_DEFAULT_PROVIDER_NAME + ) { + providerConfig.order = [this.options.openRouterSpecificProvider] + providerConfig.only = [this.options.openRouterSpecificProvider] + providerConfig.allow_fallbacks = false + } + // https://openrouter.ai/docs/transforms const completionParams: OpenRouterChatCompletionParams = { model: modelId, @@ -317,15 +340,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH messages: openAiMessages, stream: true, stream_options: { include_usage: true }, - // Only include provider if openRouterSpecificProvider is not "[default]". - ...(this.options.openRouterSpecificProvider && - this.options.openRouterSpecificProvider !== OPENROUTER_DEFAULT_PROVIDER_NAME && { - provider: { - order: [this.options.openRouterSpecificProvider], - only: [this.options.openRouterSpecificProvider], - allow_fallbacks: false, - }, - }), + provider: providerConfig, ...(reasoning && { reasoning }), tools: this.convertToolsForOpenAI(metadata?.tools), tool_choice: metadata?.tool_choice,