Skip to content

Commit 146d867

Browse files
committed
fix: add extra_body support for DeepSeek V3.1 Terminus reasoning control
- Use chat_template_kwargs with thinking parameter instead of reasoning param - Default to reasoning OFF for DeepSeek V3.1 Terminus - Enable reasoning only when explicitly requested via reasoning settings - Add comprehensive tests for the new functionality Fixes #8270
1 parent 4405f5a commit 146d867

File tree

2 files changed

+197
-2
lines changed

2 files changed

+197
-2
lines changed

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

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,4 +320,157 @@ describe("OpenRouterHandler", () => {
320320
await expect(handler.completePrompt("test prompt")).rejects.toThrow("Unexpected error")
321321
})
322322
})
323+
324+
describe("DeepSeek V3.1 Terminus", () => {
325+
it("uses extra_body for reasoning control in createMessage", async () => {
326+
const handler = new OpenRouterHandler({
327+
...mockOptions,
328+
openRouterModelId: "deepseek/deepseek-v3.1-terminus",
329+
})
330+
331+
const mockStream = {
332+
async *[Symbol.asyncIterator]() {
333+
yield {
334+
id: "test-id",
335+
choices: [{ delta: { content: "test response" } }],
336+
}
337+
},
338+
}
339+
340+
const mockCreate = vitest.fn().mockResolvedValue(mockStream)
341+
;(OpenAI as any).prototype.chat = {
342+
completions: { create: mockCreate },
343+
} as any
344+
345+
// Test with reasoning disabled (default)
346+
await handler.createMessage("test", []).next()
347+
348+
expect(mockCreate).toHaveBeenCalledWith(
349+
expect.objectContaining({
350+
model: "deepseek/deepseek-v3.1-terminus",
351+
extra_body: {
352+
chat_template_kwargs: {
353+
thinking: false,
354+
},
355+
},
356+
}),
357+
)
358+
expect(mockCreate).not.toHaveBeenCalledWith(
359+
expect.objectContaining({
360+
reasoning: expect.anything(),
361+
}),
362+
)
363+
})
364+
365+
it("enables thinking when reasoning is requested", async () => {
366+
// Mock getModels to return a model with reasoning capability
367+
const { getModels } = await import("../fetchers/modelCache")
368+
vitest.mocked(getModels).mockResolvedValueOnce({
369+
"deepseek/deepseek-v3.1-terminus": {
370+
maxTokens: 8192,
371+
contextWindow: 128000,
372+
supportsImages: false,
373+
supportsPromptCache: false,
374+
inputPrice: 0.5,
375+
outputPrice: 1.5,
376+
description: "DeepSeek V3.1 Terminus",
377+
reasoningEffort: "high",
378+
},
379+
})
380+
381+
const handler = new OpenRouterHandler({
382+
...mockOptions,
383+
openRouterModelId: "deepseek/deepseek-v3.1-terminus",
384+
reasoningEffort: "high",
385+
})
386+
387+
const mockStream = {
388+
async *[Symbol.asyncIterator]() {
389+
yield {
390+
id: "test-id",
391+
choices: [{ delta: { content: "test response" } }],
392+
}
393+
},
394+
}
395+
396+
const mockCreate = vitest.fn().mockResolvedValue(mockStream)
397+
;(OpenAI as any).prototype.chat = {
398+
completions: { create: mockCreate },
399+
} as any
400+
401+
await handler.createMessage("test", []).next()
402+
403+
expect(mockCreate).toHaveBeenCalledWith(
404+
expect.objectContaining({
405+
model: "deepseek/deepseek-v3.1-terminus",
406+
extra_body: {
407+
chat_template_kwargs: {
408+
thinking: true,
409+
},
410+
},
411+
}),
412+
)
413+
})
414+
415+
it("uses extra_body for reasoning control in completePrompt", async () => {
416+
const handler = new OpenRouterHandler({
417+
...mockOptions,
418+
openRouterModelId: "deepseek/deepseek-v3.1-terminus",
419+
})
420+
421+
const mockResponse = { choices: [{ message: { content: "test completion" } }] }
422+
423+
const mockCreate = vitest.fn().mockResolvedValue(mockResponse)
424+
;(OpenAI as any).prototype.chat = {
425+
completions: { create: mockCreate },
426+
} as any
427+
428+
await handler.completePrompt("test prompt")
429+
430+
expect(mockCreate).toHaveBeenCalledWith(
431+
expect.objectContaining({
432+
model: "deepseek/deepseek-v3.1-terminus",
433+
extra_body: {
434+
chat_template_kwargs: {
435+
thinking: false,
436+
},
437+
},
438+
}),
439+
)
440+
expect(mockCreate).not.toHaveBeenCalledWith(
441+
expect.objectContaining({
442+
reasoning: expect.anything(),
443+
}),
444+
)
445+
})
446+
447+
it("does not use extra_body for other models", async () => {
448+
const handler = new OpenRouterHandler({
449+
...mockOptions,
450+
openRouterModelId: "anthropic/claude-sonnet-4",
451+
})
452+
453+
const mockStream = {
454+
async *[Symbol.asyncIterator]() {
455+
yield {
456+
id: "test-id",
457+
choices: [{ delta: { content: "test response" } }],
458+
}
459+
},
460+
}
461+
462+
const mockCreate = vitest.fn().mockResolvedValue(mockStream)
463+
;(OpenAI as any).prototype.chat = {
464+
completions: { create: mockCreate },
465+
} as any
466+
467+
await handler.createMessage("test", []).next()
468+
469+
expect(mockCreate).not.toHaveBeenCalledWith(
470+
expect.objectContaining({
471+
extra_body: expect.anything(),
472+
}),
473+
)
474+
})
475+
})
323476
})

src/api/providers/openrouter.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ type OpenRouterChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParams & {
6060
include_reasoning?: boolean
6161
// https://openrouter.ai/docs/use-cases/reasoning-tokens
6262
reasoning?: OpenRouterReasoningParams
63+
// For DeepSeek models that require extra_body
64+
extra_body?: {
65+
chat_template_kwargs?: {
66+
thinking?: boolean
67+
}
68+
}
6369
}
6470

6571
// See `OpenAI.Chat.Completions.ChatCompletionChunk["usage"]`
@@ -141,6 +147,23 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
141147

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

150+
// For DeepSeek V3.1 Terminus, use extra_body to control reasoning
151+
const isDeepSeekV3Terminus = modelId === "deepseek/deepseek-v3.1-terminus"
152+
let extraBody: OpenRouterChatCompletionParams["extra_body"] = undefined
153+
154+
if (isDeepSeekV3Terminus) {
155+
// Default to reasoning OFF for DeepSeek V3.1 Terminus
156+
// Enable only if reasoning is explicitly requested
157+
const enableThinking = Boolean(
158+
reasoning && !reasoning.exclude && (reasoning.max_tokens || reasoning.effort),
159+
)
160+
extraBody = {
161+
chat_template_kwargs: {
162+
thinking: enableThinking,
163+
},
164+
}
165+
}
166+
144167
// https://openrouter.ai/docs/transforms
145168
const completionParams: OpenRouterChatCompletionParams = {
146169
model: modelId,
@@ -160,7 +183,8 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
160183
},
161184
}),
162185
...(transforms && { transforms }),
163-
...(reasoning && { reasoning }),
186+
// For DeepSeek V3.1 Terminus, use extra_body instead of reasoning param
187+
...(isDeepSeekV3Terminus ? { extra_body: extraBody } : reasoning && { reasoning }),
164188
}
165189

166190
let stream
@@ -248,6 +272,23 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
248272
async completePrompt(prompt: string) {
249273
let { id: modelId, maxTokens, temperature, reasoning } = await this.fetchModel()
250274

275+
// For DeepSeek V3.1 Terminus, use extra_body to control reasoning
276+
const isDeepSeekV3Terminus = modelId === "deepseek/deepseek-v3.1-terminus"
277+
let extraBody: OpenRouterChatCompletionParams["extra_body"] = undefined
278+
279+
if (isDeepSeekV3Terminus) {
280+
// Default to reasoning OFF for DeepSeek V3.1 Terminus
281+
// Enable only if reasoning is explicitly requested
282+
const enableThinking = Boolean(
283+
reasoning && !reasoning.exclude && (reasoning.max_tokens || reasoning.effort),
284+
)
285+
extraBody = {
286+
chat_template_kwargs: {
287+
thinking: enableThinking,
288+
},
289+
}
290+
}
291+
251292
const completionParams: OpenRouterChatCompletionParams = {
252293
model: modelId,
253294
max_tokens: maxTokens,
@@ -263,7 +304,8 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
263304
allow_fallbacks: false,
264305
},
265306
}),
266-
...(reasoning && { reasoning }),
307+
// For DeepSeek V3.1 Terminus, use extra_body instead of reasoning param
308+
...(isDeepSeekV3Terminus ? { extra_body: extraBody } : reasoning && { reasoning }),
267309
}
268310

269311
let response

0 commit comments

Comments
 (0)