diff --git a/src/api/providers/__tests__/openrouter.spec.ts b/src/api/providers/__tests__/openrouter.spec.ts index 304811a48c..f9f3ed4748 100644 --- a/src/api/providers/__tests__/openrouter.spec.ts +++ b/src/api/providers/__tests__/openrouter.spec.ts @@ -339,8 +339,8 @@ describe("OpenRouterHandler", () => { }) }) - describe("DeepSeek V3 Model Handling", () => { - // Note: OpenRouter only has deepseek/deepseek-v3.2, not deepseek/deepseek-v3 + describe("DeepSeek Model Handling", () => { + // Test DeepSeek V3.2 model (uses standard format) it("should NOT use R1 format for DeepSeek V3.2 models", async () => { const deepseek32Handler = new OpenRouterHandler({ ...mockOptions, @@ -422,5 +422,131 @@ describe("OpenRouterHandler", () => { undefined, ) }) + + // Test DeepSeek Chat V3.1 model (uses standard format, NOT R1) + it("should NOT use R1 format for DeepSeek Chat V3.1 models", async () => { + const deepseekChatHandler = new OpenRouterHandler({ + ...mockOptions, + openRouterModelId: "deepseek/deepseek-chat-v3.1", + }) + + const mockStream = { + [Symbol.asyncIterator]: async function* () { + yield { + choices: [{ delta: { content: "test" }, finish_reason: null }], + usage: null, + } + yield { + choices: [{ delta: {}, finish_reason: "stop" }], + usage: { prompt_tokens: 10, completion_tokens: 5 }, + } + }, + } + + const mockCreate = vitest.fn().mockResolvedValue(mockStream) + ;(OpenAI as any).prototype.chat = { + completions: { create: mockCreate }, + } as any + + const generator = deepseekChatHandler.createMessage("system prompt", []) + const chunks = [] + for await (const chunk of generator) { + chunks.push(chunk) + } + + // Verify that the messages were NOT converted to R1 format (system role preserved) + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + messages: expect.arrayContaining([ + expect.objectContaining({ role: "system", content: expect.anything() }), + ]), + }), + undefined, + ) + }) + + // Test DeepSeek R1-0528 variant (uses R1 format) + it("should use R1 format for DeepSeek R1-0528 models", async () => { + const deepseekR1Handler = new OpenRouterHandler({ + ...mockOptions, + openRouterModelId: "deepseek/deepseek-r1-0528", + }) + + const mockStream = { + [Symbol.asyncIterator]: async function* () { + yield { + choices: [{ delta: { content: "test" }, finish_reason: null }], + usage: null, + } + yield { + choices: [{ delta: {}, finish_reason: "stop" }], + usage: { prompt_tokens: 10, completion_tokens: 5 }, + } + }, + } + + const mockCreate = vitest.fn().mockResolvedValue(mockStream) + ;(OpenAI as any).prototype.chat = { + completions: { create: mockCreate }, + } as any + + const generator = deepseekR1Handler.createMessage("system prompt", []) + const chunks = [] + for await (const chunk of generator) { + chunks.push(chunk) + } + + // Verify that the messages WERE converted to R1 format (user role instead of system) + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + messages: expect.arrayContaining([ + expect.objectContaining({ role: "user", content: expect.anything() }), + ]), + }), + undefined, + ) + }) + + // Test DeepSeek Prover V2 model (uses R1 format as it's a reasoning model) + it("should use R1 format for DeepSeek Prover V2 models", async () => { + const deepseekProverHandler = new OpenRouterHandler({ + ...mockOptions, + openRouterModelId: "deepseek/deepseek-prover-v2", + }) + + const mockStream = { + [Symbol.asyncIterator]: async function* () { + yield { + choices: [{ delta: { content: "test" }, finish_reason: null }], + usage: null, + } + yield { + choices: [{ delta: {}, finish_reason: "stop" }], + usage: { prompt_tokens: 10, completion_tokens: 5 }, + } + }, + } + + const mockCreate = vitest.fn().mockResolvedValue(mockStream) + ;(OpenAI as any).prototype.chat = { + completions: { create: mockCreate }, + } as any + + const generator = deepseekProverHandler.createMessage("system prompt", []) + const chunks = [] + for await (const chunk of generator) { + chunks.push(chunk) + } + + // Verify that the messages WERE converted to R1 format (user role instead of system) + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + messages: expect.arrayContaining([ + expect.objectContaining({ role: "user", content: expect.anything() }), + ]), + }), + undefined, + ) + }) }) }) diff --git a/src/api/providers/fetchers/openrouter.ts b/src/api/providers/fetchers/openrouter.ts index 4deb0d206c..66b82f0419 100644 --- a/src/api/providers/fetchers/openrouter.ts +++ b/src/api/providers/fetchers/openrouter.ts @@ -266,12 +266,12 @@ export const parseOpenRouterModel = ({ modelInfo.maxTokens = 32768 } - // Configure DeepSeek V3 models properly + // Configure DeepSeek V3/Chat models properly // These models support standard tool calling but NOT R1 format - // OpenRouter uses deepseek/deepseek-v3 and deepseek/deepseek-v3.2 naming + // OpenRouter uses deepseek/deepseek-v3, deepseek/deepseek-v3.2, deepseek/deepseek-chat-v3.1, etc. if ( - id.startsWith("deepseek/deepseek-v3") || // Matches deepseek/deepseek-v3, deepseek/deepseek-v3.2, etc. - id === "deepseek/deepseek-chat" + id.startsWith("deepseek/deepseek-v3") || // Matches deepseek/deepseek-v3, deepseek/deepseek-v3.2, deepseek/deepseek-v3.1-terminus, etc. + id.startsWith("deepseek/deepseek-chat") // Matches deepseek/deepseek-chat, deepseek/deepseek-chat-v3.1, deepseek/deepseek-chat-v3-0324, etc. ) { // Ensure these models are marked as supporting native tools // but NOT reasoning format (they're not R1 models) diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index c56f3858c9..d6adbe8ac2 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -135,9 +135,13 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH ...convertToOpenAiMessages(messages), ] - // DeepSeek R1 models require user instead of system role. - // Note: DeepSeek V3 models (deepseek-v3, deepseek-v3.2, etc.) do NOT use R1 format - if (modelId.startsWith("deepseek/deepseek-r1") || modelId === "perplexity/sonar-reasoning") { + // DeepSeek R1 and Prover models require user instead of system role. + // Note: DeepSeek V3/Chat models (deepseek-v3, deepseek-v3.2, deepseek-chat-v3.1, etc.) do NOT use R1 format + if ( + modelId.startsWith("deepseek/deepseek-r1") || + modelId.startsWith("deepseek/deepseek-prover") || + modelId === "perplexity/sonar-reasoning" + ) { openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages]) } @@ -389,12 +393,15 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH info = this.endpoints[this.options.openRouterSpecificProvider] } - // Only DeepSeek R1 models use special temperature and topP settings - // DeepSeek V3 models (v3, v3.2, etc.) use standard settings - const isDeepSeekR1 = id.startsWith("deepseek/deepseek-r1") || id === "perplexity/sonar-reasoning" + // Only DeepSeek R1 and Prover models use special temperature and topP settings + // DeepSeek V3/Chat models (v3, v3.2, chat-v3.1, etc.) use standard settings + const isDeepSeekR1 = + id.startsWith("deepseek/deepseek-r1") || + id.startsWith("deepseek/deepseek-prover") || + id === "perplexity/sonar-reasoning" const isDeepSeekV3 = id.startsWith("deepseek/deepseek-v3") || // Matches deepseek/deepseek-v3, deepseek/deepseek-v3.2, etc. - id === "deepseek/deepseek-chat" + id.startsWith("deepseek/deepseek-chat") // Matches deepseek/deepseek-chat, deepseek/deepseek-chat-v3.1, etc. const params = getModelParams({ format: "openrouter",