Skip to content

Commit 52be5e5

Browse files
committed
fix: use chat_template_kwargs for DeepSeek V3.1 Terminus reasoning control
- Add chat_template_kwargs support to OpenRouterChatCompletionParams type - Convert reasoning configuration to chat_template_kwargs for DeepSeek V3.1 Terminus models - Set thinking parameter based on reasoning enabled state (not excluded) - Add comprehensive tests for the new behavior - Ensures reasoning can be properly disabled (default OFF) for DeepSeek V3.1 Terminus Fixes #8270
1 parent 63b4a78 commit 52be5e5

File tree

2 files changed

+181
-2
lines changed

2 files changed

+181
-2
lines changed

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

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,17 @@ vitest.mock("../fetchers/modelCache", () => ({
5151
cacheReadsPrice: 0.3,
5252
description: "Claude 3.7 Sonnet with thinking",
5353
},
54+
"deepseek/deepseek-v3.1-terminus": {
55+
maxTokens: 8192,
56+
contextWindow: 128000,
57+
supportsImages: false,
58+
supportsPromptCache: false,
59+
inputPrice: 0.3,
60+
outputPrice: 1.2,
61+
description: "DeepSeek V3.1 Terminus",
62+
supportsReasoningEffort: true,
63+
supportedReasoningEfforts: ["low", "medium", "high"],
64+
},
5465
})
5566
}),
5667
}))
@@ -330,4 +341,144 @@ describe("OpenRouterHandler", () => {
330341
await expect(handler.completePrompt("test prompt")).rejects.toThrow("Unexpected error")
331342
})
332343
})
344+
345+
describe("DeepSeek V3.1 Terminus handling", () => {
346+
it("should use chat_template_kwargs with thinking:true when reasoning is enabled for V3.1 Terminus", async () => {
347+
const handler = new OpenRouterHandler({
348+
openRouterApiKey: "test-key",
349+
openRouterModelId: "deepseek/deepseek-v3.1-terminus",
350+
reasoningEffort: "medium",
351+
})
352+
353+
const mockStream = {
354+
async *[Symbol.asyncIterator]() {
355+
yield {
356+
id: "test-id",
357+
choices: [{ delta: { content: "test response" } }],
358+
}
359+
},
360+
}
361+
362+
const mockCreate = vitest.fn().mockResolvedValue(mockStream)
363+
;(OpenAI as any).prototype.chat = {
364+
completions: { create: mockCreate },
365+
} as any
366+
367+
await handler.createMessage("test", []).next()
368+
369+
// Should include chat_template_kwargs with thinking:true and NOT include reasoning parameter
370+
expect(mockCreate).toHaveBeenCalledWith(
371+
expect.objectContaining({
372+
model: "deepseek/deepseek-v3.1-terminus",
373+
chat_template_kwargs: { thinking: true },
374+
}),
375+
)
376+
// Ensure reasoning parameter is NOT included
377+
expect(mockCreate).not.toHaveBeenCalledWith(
378+
expect.objectContaining({
379+
reasoning: expect.anything(),
380+
}),
381+
)
382+
})
383+
384+
it("should use chat_template_kwargs with thinking:false when reasoning is disabled for V3.1 Terminus", async () => {
385+
const handler = new OpenRouterHandler({
386+
openRouterApiKey: "test-key",
387+
openRouterModelId: "deepseek/deepseek-v3.1-terminus",
388+
// No reasoning effort specified
389+
})
390+
391+
const mockStream = {
392+
async *[Symbol.asyncIterator]() {
393+
yield {
394+
id: "test-id",
395+
choices: [{ delta: { content: "test response" } }],
396+
}
397+
},
398+
}
399+
400+
const mockCreate = vitest.fn().mockResolvedValue(mockStream)
401+
;(OpenAI as any).prototype.chat = {
402+
completions: { create: mockCreate },
403+
} as any
404+
405+
await handler.createMessage("test", []).next()
406+
407+
// Should include chat_template_kwargs with thinking:false
408+
expect(mockCreate).toHaveBeenCalledWith(
409+
expect.objectContaining({
410+
model: "deepseek/deepseek-v3.1-terminus",
411+
chat_template_kwargs: { thinking: false },
412+
}),
413+
)
414+
// Ensure reasoning parameter is NOT included
415+
expect(mockCreate).not.toHaveBeenCalledWith(
416+
expect.objectContaining({
417+
reasoning: expect.anything(),
418+
}),
419+
)
420+
})
421+
422+
it("should not use chat_template_kwargs for non-Terminus models", async () => {
423+
const handler = new OpenRouterHandler({
424+
openRouterApiKey: "test-key",
425+
openRouterModelId: "anthropic/claude-sonnet-4",
426+
reasoningEffort: "medium",
427+
})
428+
429+
const mockStream = {
430+
async *[Symbol.asyncIterator]() {
431+
yield {
432+
id: "test-id",
433+
choices: [{ delta: { content: "test response" } }],
434+
}
435+
},
436+
}
437+
438+
const mockCreate = vitest.fn().mockResolvedValue(mockStream)
439+
;(OpenAI as any).prototype.chat = {
440+
completions: { create: mockCreate },
441+
} as any
442+
443+
await handler.createMessage("test", []).next()
444+
445+
// Should NOT include chat_template_kwargs for non-Terminus models
446+
expect(mockCreate).not.toHaveBeenCalledWith(
447+
expect.objectContaining({
448+
chat_template_kwargs: expect.anything(),
449+
}),
450+
)
451+
})
452+
453+
it("should handle chat_template_kwargs in completePrompt for V3.1 Terminus", async () => {
454+
const handler = new OpenRouterHandler({
455+
openRouterApiKey: "test-key",
456+
openRouterModelId: "deepseek/deepseek-v3.1-terminus",
457+
reasoningEffort: "high",
458+
})
459+
460+
const mockResponse = { choices: [{ message: { content: "test completion" } }] }
461+
const mockCreate = vitest.fn().mockResolvedValue(mockResponse)
462+
;(OpenAI as any).prototype.chat = {
463+
completions: { create: mockCreate },
464+
} as any
465+
466+
await handler.completePrompt("test prompt")
467+
468+
// Should include chat_template_kwargs with thinking:true for non-streaming as well
469+
expect(mockCreate).toHaveBeenCalledWith(
470+
expect.objectContaining({
471+
model: "deepseek/deepseek-v3.1-terminus",
472+
chat_template_kwargs: { thinking: true },
473+
stream: false,
474+
}),
475+
)
476+
// Ensure reasoning parameter is NOT included
477+
expect(mockCreate).not.toHaveBeenCalledWith(
478+
expect.objectContaining({
479+
reasoning: expect.anything(),
480+
}),
481+
)
482+
})
483+
})
333484
})

src/api/providers/openrouter.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ type OpenRouterChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParams & {
6060
include_reasoning?: boolean
6161
// https://openrouter.ai/docs/use-cases/reasoning-tokens
6262
reasoning?: OpenRouterReasoningParams
63+
// For DeepSeek V3.1 Terminus models that require chat_template_kwargs
64+
chat_template_kwargs?: { thinking?: boolean }
6365
}
6466

6567
// See `OpenAI.Chat.Completions.ChatCompletionChunk["usage"]`
@@ -141,6 +143,20 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
141143

142144
const transforms = (this.options.openRouterUseMiddleOutTransform ?? true) ? ["middle-out"] : undefined
143145

146+
// Special handling for DeepSeek V3.1 Terminus models
147+
// These models use chat_template_kwargs with thinking parameter instead of reasoning
148+
let chatTemplateKwargs: { thinking?: boolean } | undefined
149+
let finalReasoning = reasoning
150+
151+
if (modelId.startsWith("deepseek/deepseek-v3.1-terminus")) {
152+
// For DeepSeek V3.1 Terminus, convert reasoning to chat_template_kwargs
153+
// The reasoning object will be present if reasoning is enabled
154+
const hasReasoningEnabled = Boolean(reasoning && !reasoning.exclude)
155+
chatTemplateKwargs = { thinking: hasReasoningEnabled }
156+
// Don't pass reasoning parameter for this model
157+
finalReasoning = undefined
158+
}
159+
144160
// https://openrouter.ai/docs/transforms
145161
const completionParams: OpenRouterChatCompletionParams = {
146162
model: modelId,
@@ -160,7 +176,8 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
160176
},
161177
}),
162178
...(transforms && { transforms }),
163-
...(reasoning && { reasoning }),
179+
...(finalReasoning && { reasoning: finalReasoning }),
180+
...(chatTemplateKwargs && { chat_template_kwargs: chatTemplateKwargs }),
164181
}
165182

166183
let stream
@@ -248,6 +265,16 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
248265
async completePrompt(prompt: string) {
249266
let { id: modelId, maxTokens, temperature, reasoning } = await this.fetchModel()
250267

268+
// Handle DeepSeek V3.1 Terminus for non-streaming as well
269+
let chatTemplateKwargs: { thinking?: boolean } | undefined
270+
let finalReasoning = reasoning
271+
272+
if (modelId.startsWith("deepseek/deepseek-v3.1-terminus")) {
273+
const hasReasoningEnabled = Boolean(reasoning && !reasoning.exclude)
274+
chatTemplateKwargs = { thinking: hasReasoningEnabled }
275+
finalReasoning = undefined
276+
}
277+
251278
const completionParams: OpenRouterChatCompletionParams = {
252279
model: modelId,
253280
max_tokens: maxTokens,
@@ -263,7 +290,8 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
263290
allow_fallbacks: false,
264291
},
265292
}),
266-
...(reasoning && { reasoning }),
293+
...(finalReasoning && { reasoning: finalReasoning }),
294+
...(chatTemplateKwargs && { chat_template_kwargs: chatTemplateKwargs }),
267295
}
268296

269297
let response

0 commit comments

Comments
 (0)