diff --git a/.changeset/openrouter-deepseek-reasoning.md b/.changeset/openrouter-deepseek-reasoning.md new file mode 100644 index 00000000000..ea67392d3e8 --- /dev/null +++ b/.changeset/openrouter-deepseek-reasoning.md @@ -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. diff --git a/bun.lock b/bun.lock index c8226ba5e24..e8a96176641 100644 --- a/bun.lock +++ b/bun.lock @@ -210,7 +210,7 @@ "@clack/prompts": "1.0.0-alpha.1", "@kilocode/plugin": "workspace:*", "@kilocode/sdk": "workspace:*", - "@openrouter/ai-sdk-provider": "2.5.1", + "@openrouter/ai-sdk-provider": "2.8.1", "ai": "catalog:", "open": "10.1.2", "zod": "catalog:", @@ -416,7 +416,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", @@ -1576,7 +1576,7 @@ "@opencode-ai/ui": ["@opencode-ai/ui@workspace:packages/ui"], - "@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.5.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-r1fJL1Cb3gQDa2MpWH/sfx1BsEW0uzlRriJM6eihaKqbtKDmZoBisF32VcVaQYassighX7NGCkF68EsrZA43uQ=="], + "@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.8.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Y6j3yivgoEUf/kutD/k5GX/mzZfioRFoSx0gbQ+mIOzMaH/vJv1rCkztiuvlLw5xRYQil7oxHUZvmSfXqOx1NQ=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], @@ -5286,6 +5286,8 @@ "ai-gateway-provider/@ai-sdk/xai": ["@ai-sdk/xai@3.0.75", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-V8UKK4fNpI9cnrtsZBvUp9O9J6Y9fTKBRoSLyEaNGPirACewixmLDbXsSgAeownPVWiWpK34bFysd+XouI5Ywg=="], + "ai-gateway-provider/@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.5.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-r1fJL1Cb3gQDa2MpWH/sfx1BsEW0uzlRriJM6eihaKqbtKDmZoBisF32VcVaQYassighX7NGCkF68EsrZA43uQ=="], + "ajv-keywords/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], "app-builder-lib/@electron/get": ["@electron/get@3.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ=="], diff --git a/packages/kilo-gateway/package.json b/packages/kilo-gateway/package.json index cedd11ba49a..1a35b3cb9d8 100644 --- a/packages/kilo-gateway/package.json +++ b/packages/kilo-gateway/package.json @@ -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", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 24e0a3c1fad..5ff8e85338d 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -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", diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index f3dee7e6ec4..51725600af3 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -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)) { @@ -425,7 +452,12 @@ export function variants(model: Provider.Model): Record [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 diff --git a/packages/opencode/test/session/message-v2.test.ts b/packages/opencode/test/session/message-v2.test.ts index 55ae65c5602..a3137ff01d0 100644 --- a/packages/opencode/test/session/message-v2.test.ts +++ b/packages/opencode/test/session/message-v2.test.ts @@ -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"