Skip to content
Draft
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
59 changes: 59 additions & 0 deletions src/api/providers/__tests__/openrouter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
33 changes: 24 additions & 9 deletions src/api/providers/openrouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>
}

// Zod schema for OpenRouter error response structure (for caught exceptions)
Expand Down Expand Up @@ -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<string, unknown> = {
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,
Expand All @@ -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,
Expand Down
Loading