fix: disable reasoning by default for DeepSeek V3.1 Terminus on OpenRouter

- Added explicit reasoning exclusion for deepseek/deepseek-v3.1-terminus when reasoning is not enabled
- Ensures consistent behavior with user expectations where reasoning should be opt-in
- Added comprehensive tests to verify the fix works correctly
- Fixes #8270
This commit is contained in:
Roo Code 2025-09-23 23:03:55 +00:00
parent fc4ee93585
commit 389c96a63f
2 changed files with 233 additions and 0 deletions

View file

@ -0,0 +1,214 @@
import { describe, it, expect, vi, beforeEach } from "vitest"
import { OpenRouterHandler } from "../openrouter"
import type { ApiHandlerOptions } from "../../../shared/api"
// Mock the fetchers
vi.mock("../fetchers/modelCache", () => ({
getModels: vi.fn().mockResolvedValue({
"deepseek/deepseek-v3.1-terminus": {
maxTokens: 8192,
contextWindow: 128000,
supportsImages: false,
supportsPromptCache: false,
supportsReasoningEffort: true,
inputPrice: 0.5,
outputPrice: 2.0,
description: "DeepSeek V3.1 Terminus model",
},
}),
}))
vi.mock("../fetchers/modelEndpointCache", () => ({
getModelEndpoints: vi.fn().mockResolvedValue({}),
}))
// Mock OpenAI client
vi.mock("openai", () => {
const mockStream = {
[Symbol.asyncIterator]: async function* () {
yield {
choices: [{ delta: { content: "Test response" } }],
usage: {
prompt_tokens: 10,
completion_tokens: 5,
total_tokens: 15,
},
}
},
}
return {
default: vi.fn().mockImplementation(() => ({
chat: {
completions: {
create: vi.fn().mockResolvedValue(mockStream),
},
},
})),
}
})
describe("OpenRouterHandler - DeepSeek V3.1 Terminus", () => {
let handler: OpenRouterHandler
let mockCreate: any
beforeEach(() => {
vi.clearAllMocks()
})
it("should exclude reasoning for DeepSeek V3.1 Terminus when reasoning is not enabled", async () => {
const options: ApiHandlerOptions = {
openRouterApiKey: "test-key",
openRouterModelId: "deepseek/deepseek-v3.1-terminus",
enableReasoningEffort: false,
}
handler = new OpenRouterHandler(options)
// Spy on the OpenAI client's create method
mockCreate = vi.fn().mockResolvedValue({
[Symbol.asyncIterator]: async function* () {
yield {
choices: [{ delta: { content: "Test" } }],
usage: { prompt_tokens: 10, completion_tokens: 5 },
}
},
})
;(handler as any).client.chat.completions.create = mockCreate
// Create a message
const generator = handler.createMessage("System prompt", [{ role: "user", content: "Test message" }])
// Consume the generator
const results = []
for await (const chunk of generator) {
results.push(chunk)
}
// Check that the create method was called with reasoning excluded
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
model: "deepseek/deepseek-v3.1-terminus",
reasoning: { exclude: true },
}),
)
})
it("should not exclude reasoning for DeepSeek V3.1 Terminus when reasoning is enabled", async () => {
const options: ApiHandlerOptions = {
openRouterApiKey: "test-key",
openRouterModelId: "deepseek/deepseek-v3.1-terminus",
enableReasoningEffort: true,
reasoningEffort: "medium",
}
handler = new OpenRouterHandler(options)
// Spy on the OpenAI client's create method
mockCreate = vi.fn().mockResolvedValue({
[Symbol.asyncIterator]: async function* () {
yield {
choices: [{ delta: { content: "Test" } }],
usage: { prompt_tokens: 10, completion_tokens: 5 },
}
},
})
;(handler as any).client.chat.completions.create = mockCreate
// Create a message
const generator = handler.createMessage("System prompt", [{ role: "user", content: "Test message" }])
// Consume the generator
const results = []
for await (const chunk of generator) {
results.push(chunk)
}
// Check that the create method was called with reasoning effort
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
model: "deepseek/deepseek-v3.1-terminus",
reasoning: { effort: "medium" },
}),
)
})
it("should not affect other models", async () => {
const options: ApiHandlerOptions = {
openRouterApiKey: "test-key",
openRouterModelId: "anthropic/claude-3-sonnet",
enableReasoningEffort: false,
}
// Mock a different model
const { getModels } = await import("../fetchers/modelCache")
vi.mocked(getModels).mockResolvedValue({
"anthropic/claude-3-sonnet": {
maxTokens: 4096,
contextWindow: 200000,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 3.0,
outputPrice: 15.0,
description: "Claude 3 Sonnet",
},
})
handler = new OpenRouterHandler(options)
// Spy on the OpenAI client's create method
mockCreate = vi.fn().mockResolvedValue({
[Symbol.asyncIterator]: async function* () {
yield {
choices: [{ delta: { content: "Test" } }],
usage: { prompt_tokens: 10, completion_tokens: 5 },
}
},
})
;(handler as any).client.chat.completions.create = mockCreate
// Create a message
const generator = handler.createMessage("System prompt", [{ role: "user", content: "Test message" }])
// Consume the generator
const results = []
for await (const chunk of generator) {
results.push(chunk)
}
// Check that reasoning was not excluded for other models
expect(mockCreate).toHaveBeenCalledWith(
expect.not.objectContaining({
reasoning: { exclude: true },
}),
)
})
it("should exclude reasoning in completePrompt for DeepSeek V3.1 Terminus", async () => {
const options: ApiHandlerOptions = {
openRouterApiKey: "test-key",
openRouterModelId: "deepseek/deepseek-v3.1-terminus",
enableReasoningEffort: false,
}
handler = new OpenRouterHandler(options)
// Mock the non-streaming response
mockCreate = vi.fn().mockResolvedValue({
choices: [{ message: { content: "Test response" } }],
})
;(handler as any).client.chat.completions.create = mockCreate
// Call completePrompt
await handler.completePrompt("Test prompt")
// Check that the create method was called with reasoning excluded
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
model: "deepseek/deepseek-v3.1-terminus",
reasoning: { exclude: true },
stream: false,
}),
)
})
})

View file

@ -118,6 +118,16 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
reasoning = { exclude: true }
}
// DeepSeek V3.1 Terminus also has reasoning enabled by default on OpenRouter
// We need to explicitly disable it when the user hasn't enabled reasoning
if (
modelId === "deepseek/deepseek-v3.1-terminus" &&
typeof reasoning === "undefined" &&
!this.options.enableReasoningEffort
) {
reasoning = { exclude: true }
}
// Convert Anthropic messages to OpenAI format.
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
@ -248,6 +258,15 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
async completePrompt(prompt: string) {
let { id: modelId, maxTokens, temperature, reasoning } = await this.fetchModel()
// Apply the same reasoning exclusion logic for DeepSeek V3.1 Terminus
if (
modelId === "deepseek/deepseek-v3.1-terminus" &&
typeof reasoning === "undefined" &&
!this.options.enableReasoningEffort
) {
reasoning = { exclude: true }
}
const completionParams: OpenRouterChatCompletionParams = {
model: modelId,
max_tokens: maxTokens,