Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .changeset/openrouter-deepseek-reasoning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@kilocode/cli": patch
"@kilocode/kilo-gateway": patch
---

Fix multi-turn DeepSeek reasoning round-tripping on OpenRouter by bumping `@openrouter/ai-sdk-provider` to 2.8.1 in both the CLI and Kilo Gateway packages and letting the SDK handle reasoning details, plus pulling in upstream DeepSeek variant, reasoning-effort, and assistant-reasoning fixes. New DeepSeek conversations are fixed; existing sessions that already stored empty reasoning metadata may still need to be restarted.
8 changes: 5 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/kilo-gateway/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"@ai-sdk/anthropic": "3.0.71",
"@ai-sdk/openai": "3.0.48",
"@ai-sdk/openai-compatible": "2.0.37",
"@openrouter/ai-sdk-provider": "2.5.1",
"@openrouter/ai-sdk-provider": "2.8.1",
"@clack/prompts": "1.0.0-alpha.1",
"ai": "catalog:",
"open": "10.1.2",
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@
"@octokit/rest": "catalog:",
"@openauthjs/openauth": "catalog:",
"@opencode-ai/script": "workspace:*",
"@openrouter/ai-sdk-provider": "2.5.1",
"@openrouter/ai-sdk-provider": "2.8.1",
"@opentelemetry/api": "1.9.0",
"@opentelemetry/context-async-hooks": "2.6.1",
"@opentelemetry/exporter-trace-otlp-http": "0.214.0",
Expand Down
44 changes: 41 additions & 3 deletions packages/opencode/src/provider/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,34 @@ function normalizeMessages(
return result
}

if (typeof model.capabilities.interleaved === "object" && model.capabilities.interleaved.field) {
// kilocode_change start - cherry-picked from anomalyco/opencode#24180;
// will be reverted on the next wholesale upstream merge.
// Deepseek requires all assistant messages to have reasoning on them
if (model.api.id.includes("deepseek")) {
msgs = msgs.map((msg) => {
if (msg.role !== "assistant") return msg
if (Array.isArray(msg.content)) {
if (msg.content.some((part) => part.type === "reasoning")) return msg
return { ...msg, content: [...msg.content, { type: "reasoning", text: "" }] }
}
return {
...msg,
content: [
...(msg.content ? [{ type: "text" as const, text: msg.content }] : []),
{ type: "reasoning" as const, text: "" },
],
}
})
}
// kilocode_change end

// kilocode_change start - cherry-picked from anomalyco/opencode#24435
if (
typeof model.capabilities.interleaved === "object" &&
model.capabilities.interleaved.field &&
model.api.npm !== "@openrouter/ai-sdk-provider"
) {
// kilocode_change end
const field = model.capabilities.interleaved.field
return msgs.map((msg) => {
if (msg.role === "assistant" && Array.isArray(msg.content)) {
Expand Down Expand Up @@ -425,7 +452,12 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
const adaptiveEfforts = anthropicAdaptiveEfforts(model.api.id)

if (
id.includes("deepseek") ||
// kilocode_change start - cherry-picked from anomalyco/opencode#24157
id.includes("deepseek-chat") ||
id.includes("deepseek-reasoner") ||
id.includes("deepseek-r1") ||
id.includes("deepseek-v3") ||
// kilocode_change end
id.includes("minimax") ||
// id.includes("glm") || // kilocode_change
id.includes("mistral") ||
Expand Down Expand Up @@ -569,7 +601,13 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
case "venice-ai-sdk-provider":
// https://docs.venice.ai/overview/guides/reasoning-models#reasoning-effort
case "@ai-sdk/openai-compatible":
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
// kilocode_change start - cherry-picked from anomalyco/opencode#24163
const efforts = [...WIDELY_SUPPORTED_EFFORTS]
if (model.api.id.includes("deepseek-v4")) {
efforts.push("max")
}
return Object.fromEntries(efforts.map((effort) => [effort, { reasoningEffort: effort }]))
// kilocode_change end

case "@ai-sdk/azure":
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/azure
Expand Down
75 changes: 75 additions & 0 deletions packages/opencode/test/session/message-v2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,81 @@ describe("session.message-v2.toModelMessage", () => {
])
})

// kilocode_change start - cherry-picked from anomalyco/opencode#24435
test("preserves OpenRouter reasoning details through provider transform", async () => {
const assistantID = "m-assistant"
const openrouterModel: Provider.Model = {
...model,
id: ModelID.make("deepseek/deepseek-v4-pro"),
providerID: ProviderID.make("openrouter"),
api: {
id: "deepseek/deepseek-v4-pro",
url: "https://openrouter.ai/api/v1",
npm: "@openrouter/ai-sdk-provider",
},
capabilities: {
...model.capabilities,
reasoning: true,
interleaved: { field: "reasoning_details" },
},
}
const reasoningDetails = [
{
type: "reasoning.text",
text: "thinking",
format: "unknown",
index: 0,
},
]
const input: MessageV2.WithParts[] = [
{
info: assistantInfo(assistantID, "m-parent", undefined, {
providerID: openrouterModel.providerID,
modelID: openrouterModel.id,
}),
parts: [
{
...basePart(assistantID, "a1"),
type: "reasoning",
text: "thinking",
time: { start: 0 },
metadata: {
openrouter: {
reasoning_details: reasoningDetails,
},
},
},
{
...basePart(assistantID, "a2"),
type: "text",
text: "answer",
},
] as MessageV2.Part[],
},
]

expect(
ProviderTransform.message(await MessageV2.toModelMessages(input, openrouterModel), openrouterModel, {}),
).toStrictEqual([
{
role: "assistant",
content: [
{
type: "reasoning",
text: "thinking",
providerOptions: {
openrouter: {
reasoning_details: reasoningDetails,
},
},
},
{ type: "text", text: "answer" },
],
},
])
})
// kilocode_change end

test("splits assistant messages on step-start boundaries", async () => {
const assistantID = "m-assistant"

Expand Down
Loading