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
This commit is contained in:
Roo Code 2025-10-17 13:37:20 +00:00
parent 4405f5a0b3
commit 146d867db1
2 changed files with 197 additions and 2 deletions

View file

@ -320,4 +320,157 @@ describe("OpenRouterHandler", () => {
await expect(handler.completePrompt("test prompt")).rejects.toThrow("Unexpected error")
})
})
describe("DeepSeek V3.1 Terminus", () => {
it("uses extra_body for reasoning control in createMessage", async () => {
const handler = new OpenRouterHandler({
...mockOptions,
openRouterModelId: "deepseek/deepseek-v3.1-terminus",
})
const mockStream = {
async *[Symbol.asyncIterator]() {
yield {
id: "test-id",
choices: [{ delta: { content: "test response" } }],
}
},
}
const mockCreate = vitest.fn().mockResolvedValue(mockStream)
;(OpenAI as any).prototype.chat = {
completions: { create: mockCreate },
} as any
// Test with reasoning disabled (default)
await handler.createMessage("test", []).next()
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
model: "deepseek/deepseek-v3.1-terminus",
extra_body: {
chat_template_kwargs: {
thinking: false,
},
},
}),
)
expect(mockCreate).not.toHaveBeenCalledWith(
expect.objectContaining({
reasoning: expect.anything(),
}),
)
})
it("enables thinking when reasoning is requested", async () => {
// Mock getModels to return a model with reasoning capability
const { getModels } = await import("../fetchers/modelCache")
vitest.mocked(getModels).mockResolvedValueOnce({
"deepseek/deepseek-v3.1-terminus": {
maxTokens: 8192,
contextWindow: 128000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.5,
outputPrice: 1.5,
description: "DeepSeek V3.1 Terminus",
reasoningEffort: "high",
},
})
const handler = new OpenRouterHandler({
...mockOptions,
openRouterModelId: "deepseek/deepseek-v3.1-terminus",
reasoningEffort: "high",
})
const mockStream = {
async *[Symbol.asyncIterator]() {
yield {
id: "test-id",
choices: [{ delta: { content: "test response" } }],
}
},
}
const mockCreate = vitest.fn().mockResolvedValue(mockStream)
;(OpenAI as any).prototype.chat = {
completions: { create: mockCreate },
} as any
await handler.createMessage("test", []).next()
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
model: "deepseek/deepseek-v3.1-terminus",
extra_body: {
chat_template_kwargs: {
thinking: true,
},
},
}),
)
})
it("uses extra_body for reasoning control in completePrompt", async () => {
const handler = new OpenRouterHandler({
...mockOptions,
openRouterModelId: "deepseek/deepseek-v3.1-terminus",
})
const mockResponse = { choices: [{ message: { content: "test completion" } }] }
const mockCreate = vitest.fn().mockResolvedValue(mockResponse)
;(OpenAI as any).prototype.chat = {
completions: { create: mockCreate },
} as any
await handler.completePrompt("test prompt")
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
model: "deepseek/deepseek-v3.1-terminus",
extra_body: {
chat_template_kwargs: {
thinking: false,
},
},
}),
)
expect(mockCreate).not.toHaveBeenCalledWith(
expect.objectContaining({
reasoning: expect.anything(),
}),
)
})
it("does not use extra_body for other models", async () => {
const handler = new OpenRouterHandler({
...mockOptions,
openRouterModelId: "anthropic/claude-sonnet-4",
})
const mockStream = {
async *[Symbol.asyncIterator]() {
yield {
id: "test-id",
choices: [{ delta: { content: "test response" } }],
}
},
}
const mockCreate = vitest.fn().mockResolvedValue(mockStream)
;(OpenAI as any).prototype.chat = {
completions: { create: mockCreate },
} as any
await handler.createMessage("test", []).next()
expect(mockCreate).not.toHaveBeenCalledWith(
expect.objectContaining({
extra_body: expect.anything(),
}),
)
})
})
})

View file

@ -60,6 +60,12 @@ type OpenRouterChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParams & {
include_reasoning?: boolean
// https://openrouter.ai/docs/use-cases/reasoning-tokens
reasoning?: OpenRouterReasoningParams
// For DeepSeek models that require extra_body
extra_body?: {
chat_template_kwargs?: {
thinking?: boolean
}
}
}
// See `OpenAI.Chat.Completions.ChatCompletionChunk["usage"]`
@ -141,6 +147,23 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
const transforms = (this.options.openRouterUseMiddleOutTransform ?? true) ? ["middle-out"] : undefined
// For DeepSeek V3.1 Terminus, use extra_body to control reasoning
const isDeepSeekV3Terminus = modelId === "deepseek/deepseek-v3.1-terminus"
let extraBody: OpenRouterChatCompletionParams["extra_body"] = undefined
if (isDeepSeekV3Terminus) {
// Default to reasoning OFF for DeepSeek V3.1 Terminus
// Enable only if reasoning is explicitly requested
const enableThinking = Boolean(
reasoning && !reasoning.exclude && (reasoning.max_tokens || reasoning.effort),
)
extraBody = {
chat_template_kwargs: {
thinking: enableThinking,
},
}
}
// https://openrouter.ai/docs/transforms
const completionParams: OpenRouterChatCompletionParams = {
model: modelId,
@ -160,7 +183,8 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
},
}),
...(transforms && { transforms }),
...(reasoning && { reasoning }),
// For DeepSeek V3.1 Terminus, use extra_body instead of reasoning param
...(isDeepSeekV3Terminus ? { extra_body: extraBody } : reasoning && { reasoning }),
}
let stream
@ -248,6 +272,23 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
async completePrompt(prompt: string) {
let { id: modelId, maxTokens, temperature, reasoning } = await this.fetchModel()
// For DeepSeek V3.1 Terminus, use extra_body to control reasoning
const isDeepSeekV3Terminus = modelId === "deepseek/deepseek-v3.1-terminus"
let extraBody: OpenRouterChatCompletionParams["extra_body"] = undefined
if (isDeepSeekV3Terminus) {
// Default to reasoning OFF for DeepSeek V3.1 Terminus
// Enable only if reasoning is explicitly requested
const enableThinking = Boolean(
reasoning && !reasoning.exclude && (reasoning.max_tokens || reasoning.effort),
)
extraBody = {
chat_template_kwargs: {
thinking: enableThinking,
},
}
}
const completionParams: OpenRouterChatCompletionParams = {
model: modelId,
max_tokens: maxTokens,
@ -263,7 +304,8 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
allow_fallbacks: false,
},
}),
...(reasoning && { reasoning }),
// For DeepSeek V3.1 Terminus, use extra_body instead of reasoning param
...(isDeepSeekV3Terminus ? { extra_body: extraBody } : reasoning && { reasoning }),
}
let response