From 389c96a63fe477618ea3f778d9d71ed4321cbd74 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Tue, 23 Sep 2025 23:03:55 +0000 Subject: [PATCH] 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 --- .../openrouter-deepseek-terminus.spec.ts | 214 ++++++++++++++++++ src/api/providers/openrouter.ts | 19 ++ 2 files changed, 233 insertions(+) create mode 100644 src/api/providers/__tests__/openrouter-deepseek-terminus.spec.ts diff --git a/src/api/providers/__tests__/openrouter-deepseek-terminus.spec.ts b/src/api/providers/__tests__/openrouter-deepseek-terminus.spec.ts new file mode 100644 index 00000000000..7a7da1a94a0 --- /dev/null +++ b/src/api/providers/__tests__/openrouter-deepseek-terminus.spec.ts @@ -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, + }), + ) + }) +}) diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index 580b1733119..6639dab40ad 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -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,