Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit d8f0c64

Browse files
committed
fix(openrouter): enable reasoning support via AI SDK extraBody
- Add support for 'reasoning' and 'text' event types in AI SDK stream processing - Pass reasoning parameters via createOpenRouter extraBody instead of providerOptions - Support both effort-based (effort: 'high') and budget-based (max_tokens: N) reasoning - Add comprehensive tests for reasoning parameter passing and stream event handling - Fixes reasoning tokens not being displayed for models like DeepSeek R1 and Gemini Thinking Changes: - src/api/transform/ai-sdk.ts: Add 'text' and 'reasoning' event type handlers - src/api/providers/openrouter.ts: Pass reasoning via extraBody in provider creation - Add tests for new event types and reasoning parameter flow - All 53 tests passing
1 parent 54de1ab commit d8f0c64

6 files changed

Lines changed: 301 additions & 62 deletions

File tree

pnpm-lock.yaml

Lines changed: 27 additions & 27 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/api/providers/__tests__/openrouter.spec.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,16 @@ vitest.mock("../fetchers/modelCache", () => ({
7878
cacheReadsPrice: 0.3,
7979
description: "Claude 3.7 Sonnet with thinking",
8080
},
81+
"deepseek/deepseek-r1": {
82+
maxTokens: 8192,
83+
contextWindow: 64000,
84+
supportsImages: false,
85+
supportsPromptCache: false,
86+
inputPrice: 0.55,
87+
outputPrice: 2.19,
88+
description: "DeepSeek R1",
89+
supportsReasoningEffort: true,
90+
},
8191
"openai/gpt-4o": {
8292
maxTokens: 16384,
8393
contextWindow: 128000,
@@ -638,6 +648,86 @@ describe("OpenRouterHandler", () => {
638648
}),
639649
)
640650
})
651+
652+
it("passes reasoning parameters via extraBody when reasoning effort is enabled", async () => {
653+
const handler = new OpenRouterHandler({
654+
openRouterApiKey: "test-key",
655+
openRouterModelId: "deepseek/deepseek-r1",
656+
reasoningEffort: "high",
657+
enableReasoningEffort: true,
658+
})
659+
660+
const mockFullStream = (async function* () {
661+
yield { type: "reasoning-delta", text: "thinking...", id: "1" }
662+
yield { type: "text-delta", text: "result", id: "2" }
663+
})()
664+
665+
mockStreamText.mockReturnValue({
666+
fullStream: mockFullStream,
667+
usage: Promise.resolve({ inputTokens: 10, outputTokens: 20, totalTokens: 30 }),
668+
totalUsage: Promise.resolve({ inputTokens: 10, outputTokens: 20, totalTokens: 30 }),
669+
})
670+
671+
const generator = handler.createMessage("test", [{ role: "user", content: "test" }])
672+
673+
for await (const _ of generator) {
674+
// consume
675+
}
676+
677+
// Verify that reasoning was passed via extraBody when creating the provider
678+
expect(mockCreateOpenRouter).toHaveBeenCalledWith(
679+
expect.objectContaining({
680+
extraBody: expect.objectContaining({
681+
reasoning: expect.objectContaining({
682+
effort: "high",
683+
}),
684+
}),
685+
}),
686+
)
687+
688+
// Verify that providerOptions does NOT contain extended_thinking
689+
expect(mockStreamText).toHaveBeenCalledWith(
690+
expect.objectContaining({
691+
providerOptions: undefined,
692+
}),
693+
)
694+
})
695+
696+
it("does not pass reasoning via extraBody when reasoning is disabled", async () => {
697+
const handler = new OpenRouterHandler({
698+
openRouterApiKey: "test-key",
699+
openRouterModelId: "anthropic/claude-sonnet-4",
700+
})
701+
702+
const mockFullStream = (async function* () {
703+
yield { type: "text-delta", text: "test", id: "1" }
704+
})()
705+
706+
mockStreamText.mockReturnValue({
707+
fullStream: mockFullStream,
708+
usage: Promise.resolve({ inputTokens: 10, outputTokens: 20, totalTokens: 30 }),
709+
totalUsage: Promise.resolve({ inputTokens: 10, outputTokens: 20, totalTokens: 30 }),
710+
})
711+
712+
const generator = handler.createMessage("test", [{ role: "user", content: "test" }])
713+
714+
for await (const _ of generator) {
715+
// consume
716+
}
717+
718+
// Verify that createOpenRouter was NOT called with extraBody
719+
expect(mockCreateOpenRouter).toHaveBeenCalledWith({
720+
apiKey: "test-key",
721+
baseURL: "https://openrouter.ai/api/v1",
722+
})
723+
724+
// Verify that providerOptions is undefined when no provider routing
725+
expect(mockStreamText).toHaveBeenCalledWith(
726+
expect.objectContaining({
727+
providerOptions: undefined,
728+
}),
729+
)
730+
})
641731
})
642732

643733
describe("completePrompt", () => {
@@ -660,6 +750,41 @@ describe("OpenRouterHandler", () => {
660750
)
661751
})
662752

753+
it("passes reasoning parameters via extraBody when reasoning effort is enabled", async () => {
754+
const handler = new OpenRouterHandler({
755+
openRouterApiKey: "test-key",
756+
openRouterModelId: "deepseek/deepseek-r1",
757+
reasoningEffort: "medium",
758+
enableReasoningEffort: true,
759+
})
760+
761+
mockGenerateText.mockResolvedValue({
762+
text: "test completion with reasoning",
763+
})
764+
765+
const result = await handler.completePrompt("test prompt")
766+
767+
expect(result).toBe("test completion with reasoning")
768+
769+
// Verify that reasoning was passed via extraBody when creating the provider
770+
expect(mockCreateOpenRouter).toHaveBeenCalledWith(
771+
expect.objectContaining({
772+
extraBody: expect.objectContaining({
773+
reasoning: expect.objectContaining({
774+
effort: "medium",
775+
}),
776+
}),
777+
}),
778+
)
779+
780+
// Verify that providerOptions does NOT contain extended_thinking
781+
expect(mockGenerateText).toHaveBeenCalledWith(
782+
expect.objectContaining({
783+
providerOptions: undefined,
784+
}),
785+
)
786+
})
787+
663788
it("handles API errors", async () => {
664789
const handler = new OpenRouterHandler(mockOptions)
665790

src/api/providers/openrouter.ts

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,16 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
8686

8787
/**
8888
* Create the OpenRouter provider instance using the AI SDK
89+
* @param reasoning - Optional reasoning parameters to pass via extraBody
8990
*/
90-
private createOpenRouterProvider() {
91+
private createOpenRouterProvider(reasoning?: { effort?: string; max_tokens?: number; exclude?: boolean }) {
9192
const apiKey = this.options.openRouterApiKey ?? "not-provided"
9293
const baseURL = this.options.openRouterBaseUrl || "https://openrouter.ai/api/v1"
9394

9495
return createOpenRouter({
9596
apiKey,
9697
baseURL,
98+
...(reasoning && { extraBody: { reasoning } }),
9799
})
98100
}
99101

@@ -182,14 +184,25 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
182184
this.currentReasoningDetails = []
183185

184186
const model = await this.fetchModel()
185-
const { id: modelId, maxTokens, temperature } = model
187+
const { id: modelId, maxTokens, temperature, reasoning } = model
186188

187-
const openrouter = this.createOpenRouterProvider()
189+
// Pass reasoning parameters to extraBody when creating the provider
190+
const openrouter = this.createOpenRouterProvider(reasoning)
188191
const coreMessages = convertToAiSdkMessages(messages)
189192
const tools = convertToolsForAiSdk(metadata?.tools)
190193

191194
// Build provider options for specific provider routing
192-
const providerOptions =
195+
const providerOptions:
196+
| {
197+
openrouter?: {
198+
provider?: {
199+
order: string[]
200+
only: string[]
201+
allow_fallbacks: boolean
202+
}
203+
}
204+
}
205+
| undefined =
193206
this.options.openRouterSpecificProvider &&
194207
this.options.openRouterSpecificProvider !== OPENROUTER_DEFAULT_PROVIDER_NAME
195208
? {
@@ -255,7 +268,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
255268
model.info,
256269
)
257270
yield usageChunk
258-
} catch (error) {
271+
} catch (error: any) {
259272
const errorMessage = error instanceof Error ? error.message : String(error)
260273
const apiError = new ApiProviderError(errorMessage, this.providerName, modelId, "createMessage")
261274
TelemetryService.instance.captureException(apiError)
@@ -325,12 +338,23 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
325338
}
326339

327340
async completePrompt(prompt: string): Promise<string> {
328-
const { id: modelId, maxTokens, temperature } = await this.fetchModel()
341+
const { id: modelId, maxTokens, temperature, reasoning } = await this.fetchModel()
329342

330-
const openrouter = this.createOpenRouterProvider()
343+
// Pass reasoning parameters to extraBody when creating the provider
344+
const openrouter = this.createOpenRouterProvider(reasoning)
331345

332346
// Build provider options for specific provider routing
333-
const providerOptions =
347+
const providerOptions:
348+
| {
349+
openrouter?: {
350+
provider?: {
351+
order: string[]
352+
only: string[]
353+
allow_fallbacks: boolean
354+
}
355+
}
356+
}
357+
| undefined =
334358
this.options.openRouterSpecificProvider &&
335359
this.options.openRouterSpecificProvider !== OPENROUTER_DEFAULT_PROVIDER_NAME
336360
? {

0 commit comments

Comments
 (0)