Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions apps/vscode-e2e/src/suite/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as vscode from "vscode"
import type { RooCodeAPI } from "@roo-code/types"

import { waitFor } from "./utils"
import * as fs from "fs"

export async function run() {
const extension = vscode.extensions.getExtension<RooCodeAPI>("RooVeterinaryInc.roo-cline")
Expand All @@ -16,6 +17,32 @@ export async function run() {

const api = extension.isActive ? extension.exports : await extension.activate()

// Ensure OPENROUTER_API_KEY is loaded from .env.local in CI (written by workflow)
// __dirname at runtime is apps/vscode-e2e/out/suite, so go up two levels
const envPath = path.resolve(__dirname, "..", "..", ".env.local")
if (!process.env.OPENROUTER_API_KEY && fs.existsSync(envPath)) {
try {
const content = fs.readFileSync(envPath, "utf8")
for (const rawLine of content.split("\n")) {
const line = rawLine.trim()
if (!line || line.startsWith("#")) continue
const m = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/.exec(line)
if (!m) continue
const key: string = m[1] ?? ""
let val: string = m[2] ?? ""
// Strip surrounding quotes if present
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
val = val.slice(1, -1)
}
if (key && !(key in process.env)) {
;(process.env as Record<string, string>)[key] = val
}
}
} catch {
// ignore env load errors; tests may still pass without API calls
}
}

await api.setConfiguration({
apiProvider: "openrouter" as const,
openRouterApiKey: process.env.OPENROUTER_API_KEY!,
Expand Down
153 changes: 153 additions & 0 deletions src/api/providers/__tests__/openrouter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}),
)
})
})
})
46 changes: 44 additions & 2 deletions src/api/providers/openrouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]`
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
Loading