This commit is contained in:
roomote-v0[bot] 2026-04-09 00:52:56 +00:00 committed by GitHub
commit 353065e56a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 83 additions and 9 deletions

View file

@ -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", () => {

View file

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