From bb640b4b538ed3a7a98a942a3e8266a8691c9452 Mon Sep 17 00:00:00 2001 From: Simon Gardling Date: Fri, 1 May 2026 14:42:59 -0400 Subject: [PATCH] omp: remove patch --- home/progs/pi.nix | 9 +- patches/omp/0001-fix-reasoning_content.patch | 804 ------------------- 2 files changed, 1 insertion(+), 812 deletions(-) delete mode 100644 patches/omp/0001-fix-reasoning_content.patch diff --git a/home/progs/pi.nix b/home/progs/pi.nix index 37b6b48..eb61745 100644 --- a/home/progs/pi.nix +++ b/home/progs/pi.nix @@ -78,14 +78,7 @@ let in { home.packages = [ - # `bun2nix.hook` sets `patchPhase = bunPatchPhase`, which only runs `patchShebangs` and - # silently ignores the standard `patches` attribute. Apply patches via `prePatch` instead - # so they actually take effect. Tracking: nothing upstream yet. - (inputs.llm-agents.packages.${pkgs.stdenv.hostPlatform.system}.omp.overrideAttrs (old: { - prePatch = (old.prePatch or "") + '' - patch -p1 < ${../../patches/omp/0001-fix-reasoning_content.patch} - ''; - })) + inputs.llm-agents.packages.${pkgs.stdenv.hostPlatform.system}.omp ]; home.file = androidSkillFiles // { diff --git a/patches/omp/0001-fix-reasoning_content.patch b/patches/omp/0001-fix-reasoning_content.patch deleted file mode 100644 index 12d1548..0000000 --- a/patches/omp/0001-fix-reasoning_content.patch +++ /dev/null @@ -1,804 +0,0 @@ -From e145b627cffb6907e6bde348f1318f48acba3801 Mon Sep 17 00:00:00 2001 -From: sonhyrd -Date: Mon, 27 Apr 2026 00:00:18 +0700 -Subject: [PATCH 1/5] fix(ai/providers): cover opencode-go reasoning tool-call - history - ---- - .../providers/openai-completions-compat.ts | 12 +++-- - .../ai/src/providers/openai-completions.ts | 4 +- - .../ai/test/openai-completions-compat.test.ts | 51 +++++++++++++++---- - 3 files changed, 49 insertions(+), 18 deletions(-) - -diff --git a/packages/ai/src/providers/openai-completions-compat.ts b/packages/ai/src/providers/openai-completions-compat.ts -index 69f4811c8..c777f312b 100644 ---- a/packages/ai/src/providers/openai-completions-compat.ts -+++ b/packages/ai/src/providers/openai-completions-compat.ts -@@ -107,12 +107,14 @@ export function detectOpenAICompat(model: Model<"openai-completions">, resolvedB - reasoningContentField: "reasoning_content", - // Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`: - // - Kimi: documented invariant on its native API and via OpenCode-Go. -- // - Any reasoning-capable model reached through OpenRouter: DeepSeek V4 Pro and similar enforce -- // this server-side whenever the request is in thinking mode. We can't translate Anthropic's -- // redacted/encrypted reasoning into DeepSeek's plaintext form, so cross-provider continuations -- // rely on a placeholder — see `convertMessages` for the placeholder injection. -+ // - Reasoning-capable models reached through OpenRouter or OpenCode-Go: DeepSeek V4 Pro and -+ // similar enforce this server-side whenever the request is in thinking mode. -+ // We can't translate Anthropic's redacted/encrypted reasoning into DeepSeek's plaintext form, so -+ // cross-provider continuations rely on a placeholder — see `convertMessages` for injection rules. - requiresReasoningContentForToolCalls: -- isKimiModel || ((provider === "openrouter" || baseUrl.includes("openrouter.ai")) && Boolean(model.reasoning)), -+ isKimiModel || -+ ((provider === "openrouter" || baseUrl.includes("openrouter.ai") || provider === "opencode-go" || -+ baseUrl.includes("opencode.ai/zen/go")) && Boolean(model.reasoning)), - requiresAssistantContentForToolCalls: isKimiModel, - openRouterRouting: undefined, - vercelGatewayRouting: undefined, -diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts -index 3785af106..70f2e3b63 100644 ---- a/packages/ai/src/providers/openai-completions.ts -+++ b/packages/ai/src/providers/openai-completions.ts -@@ -1213,8 +1213,8 @@ export function convertMessages( - // Inject a `reasoning_content` placeholder on assistant tool-call turns when the backend - // rejects history without it. The compat flag captures the rule: - // - Kimi (native or via OpenCode-Go): chat completion endpoint demands the field. -- // - Reasoning models reached through OpenRouter (e.g. DeepSeek V4 Pro): the underlying -- // provider's thinking-mode validator demands it on every prior assistant turn. omp -+ // - Reasoning models reached through OpenRouter or OpenCode-Go (e.g. DeepSeek V4 Pro): -+ // the upstream thinking-mode validator demands it on every prior assistant turn. omp - // cannot synthesize real reasoning when the conversation was warmed up by another - // provider whose reasoning is redacted/encrypted (Anthropic) or simply absent, so we - // emit a placeholder. Real captured reasoning, when present, is preserved earlier via -diff --git a/packages/ai/test/openai-completions-compat.test.ts b/packages/ai/test/openai-completions-compat.test.ts -index 6fc3ca9af..6d60ba5e4 100644 ---- a/packages/ai/test/openai-completions-compat.test.ts -+++ b/packages/ai/test/openai-completions-compat.test.ts -@@ -283,23 +283,59 @@ describe("openai-completions compatibility", () => { - }); - - describe("kimi model detection via detectCompat", () => { -- function kimiOpenCodeModel(id: string): Model<"openai-completions"> { -+ function openCodeGoModel(id: string, reasoning = true): Model<"openai-completions"> { - return { - ...getBundledModel("openai", "gpt-4o-mini"), - api: "openai-completions", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", - id, -- reasoning: true, -+ reasoning, - }; - } - -+ function kimiOpenCodeModel(id: string): Model<"openai-completions"> { -+ return openCodeGoModel(id, true); -+ } -+ - it("requires reasoning_content for tool calls on kimi-k2.5 (opencode-go)", () => { - const compat = detectCompat(kimiOpenCodeModel("kimi-k2.5")); - expect(compat.requiresReasoningContentForToolCalls).toBe(true); - expect(compat.requiresAssistantContentForToolCalls).toBe(true); - }); - -+ it("requires reasoning_content for tool calls on reasoning DeepSeek models via opencode-go", () => { -+ const compat = detectCompat(openCodeGoModel("deepseek-v4-pro", true)); -+ expect(compat.requiresReasoningContentForToolCalls).toBe(true); -+ expect(compat.requiresAssistantContentForToolCalls).toBe(false); -+ }); -+ -+ it("injects reasoning_content placeholder for reasoning DeepSeek tool-call turns via opencode-go", () => { -+ const model = openCodeGoModel("deepseek-v4-pro", true); -+ const compat = detectCompat(model); -+ const toolCallMessage: AssistantMessage = { -+ role: "assistant", -+ content: [{ type: "toolCall", id: "call_ds_go", name: "web_search", arguments: { query: "hi" } }], -+ api: model.api, -+ provider: model.provider, -+ model: model.id, -+ usage: { -+ input: 0, -+ output: 0, -+ cacheRead: 0, -+ cacheWrite: 0, -+ totalTokens: 0, -+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, -+ }, -+ stopReason: "toolUse", -+ timestamp: Date.now(), -+ }; -+ const messages = convertMessages(model, { messages: [toolCallMessage] }, compat); -+ const assistant = messages.find(m => m.role === "assistant"); -+ expect(assistant).toBeDefined(); -+ expect(Reflect.get(assistant as object, "reasoning_content")).toBe("."); -+ }); -+ - it("injects reasoning_content placeholder when assistant with tool calls has no reasoning field", () => { - const model = kimiOpenCodeModel("kimi-k2.5"); - const compat = detectCompat(model); -@@ -338,15 +374,8 @@ describe("kimi model detection via detectCompat", () => { - expect((reasoningContent as string).length).toBeGreaterThan(0); - }); - -- it("does not inject reasoning_content when model is not kimi", () => { -- const model: Model<"openai-completions"> = { -- ...getBundledModel("openai", "gpt-4o-mini"), -- api: "openai-completions", -- provider: "opencode-go", -- baseUrl: "https://opencode.ai/zen/go/v1", -- id: "some-other-model", -- }; -- const compat = detectCompat(model); -+ it("does not require reasoning_content when opencode-go model is not reasoning-capable", () => { -+ const compat = detectCompat(openCodeGoModel("some-other-model", false)); - expect(compat.requiresReasoningContentForToolCalls).toBe(false); - }); - - -From 70eda0132d7ff48314cbf2dc9560339f0a765d9e Mon Sep 17 00:00:00 2001 -From: sonhyrd -Date: Mon, 27 Apr 2026 00:08:04 +0700 -Subject: [PATCH 2/5] fix(ai/providers): generalize opencode reasoning_content - gating - ---- - .../providers/openai-completions-compat.ts | 14 +- - .../ai/src/providers/openai-completions.ts | 4 +- - .../ai/test/openai-completions-compat.test.ts | 160 ++++++++---------- - 3 files changed, 82 insertions(+), 96 deletions(-) - -diff --git a/packages/ai/src/providers/openai-completions-compat.ts b/packages/ai/src/providers/openai-completions-compat.ts -index c777f312b..b4825a31c 100644 ---- a/packages/ai/src/providers/openai-completions-compat.ts -+++ b/packages/ai/src/providers/openai-completions-compat.ts -@@ -54,6 +54,8 @@ export function detectOpenAICompat(model: Model<"openai-completions">, resolvedB - const isKimiModel = model.id.includes("moonshotai/kimi") || /^kimi[-.]/i.test(model.id); - const isAlibaba = provider === "alibaba-coding-plan" || baseUrl.includes("dashscope"); - const isQwen = model.id.toLowerCase().includes("qwen"); -+ const isOpenRouter = provider === "openrouter" || baseUrl.includes("openrouter.ai"); -+ const isOpenCode = provider === "opencode-zen" || provider === "opencode-go" || baseUrl.includes("opencode.ai/zen"); - - const isNonStandard = - isCerebras || -@@ -99,22 +101,20 @@ export function detectOpenAICompat(model: Model<"openai-completions">, resolvedB - requiresMistralToolIds: isMistral, - thinkingFormat: isZai - ? "zai" -- : provider === "openrouter" || baseUrl.includes("openrouter.ai") -+ : isOpenRouter - ? "openrouter" - : isAlibaba || isQwen - ? "qwen" - : "openai", - reasoningContentField: "reasoning_content", - // Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`: -- // - Kimi: documented invariant on its native API and via OpenCode-Go. -- // - Reasoning-capable models reached through OpenRouter or OpenCode-Go: DeepSeek V4 Pro and -- // similar enforce this server-side whenever the request is in thinking mode. -+ // - Kimi: documented invariant on its native API and via OpenCode. -+ // - Reasoning-capable models reached through OpenRouter or OpenCode (Zen/Go): DeepSeek V4 Pro, -+ // Kimi, and similar models can enforce this server-side whenever the request is in thinking mode. - // We can't translate Anthropic's redacted/encrypted reasoning into DeepSeek's plaintext form, so - // cross-provider continuations rely on a placeholder — see `convertMessages` for injection rules. - requiresReasoningContentForToolCalls: -- isKimiModel || -- ((provider === "openrouter" || baseUrl.includes("openrouter.ai") || provider === "opencode-go" || -- baseUrl.includes("opencode.ai/zen/go")) && Boolean(model.reasoning)), -+ isKimiModel || ((isOpenRouter || isOpenCode) && Boolean(model.reasoning)), - requiresAssistantContentForToolCalls: isKimiModel, - openRouterRouting: undefined, - vercelGatewayRouting: undefined, -diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts -index 70f2e3b63..e25aeffb3 100644 ---- a/packages/ai/src/providers/openai-completions.ts -+++ b/packages/ai/src/providers/openai-completions.ts -@@ -1212,8 +1212,8 @@ export function convertMessages( - (assistantMsg as any).reasoning_text !== undefined; - // Inject a `reasoning_content` placeholder on assistant tool-call turns when the backend - // rejects history without it. The compat flag captures the rule: -- // - Kimi (native or via OpenCode-Go): chat completion endpoint demands the field. -- // - Reasoning models reached through OpenRouter or OpenCode-Go (e.g. DeepSeek V4 Pro): -+ // - Kimi (native or via OpenCode Zen/Go): chat completion endpoint demands the field. -+ // - Reasoning models reached through OpenRouter or OpenCode Zen/Go (e.g. DeepSeek V4 Pro): - // the upstream thinking-mode validator demands it on every prior assistant turn. omp - // cannot synthesize real reasoning when the conversation was warmed up by another - // provider whose reasoning is redacted/encrypted (Anthropic) or simply absent, so we -diff --git a/packages/ai/test/openai-completions-compat.test.ts b/packages/ai/test/openai-completions-compat.test.ts -index 6d60ba5e4..c743dd246 100644 ---- a/packages/ai/test/openai-completions-compat.test.ts -+++ b/packages/ai/test/openai-completions-compat.test.ts -@@ -282,105 +282,91 @@ describe("openai-completions compatibility", () => { - }); - }); - --describe("kimi model detection via detectCompat", () => { -- function openCodeGoModel(id: string, reasoning = true): Model<"openai-completions"> { -+describe("opencode reasoning-content compatibility via detectCompat", () => { -+ type OpenCodeProvider = "opencode-go" | "opencode-zen"; -+ -+ function openCodeModel(provider: OpenCodeProvider, id: string, reasoning = true): Model<"openai-completions"> { -+ const baseUrl = provider === "opencode-go" ? "https://opencode.ai/zen/go/v1" : "https://opencode.ai/zen/v1"; - return { - ...getBundledModel("openai", "gpt-4o-mini"), - api: "openai-completions", -- provider: "opencode-go", -- baseUrl: "https://opencode.ai/zen/go/v1", -+ provider, -+ baseUrl, - id, - reasoning, - }; - } - -- function kimiOpenCodeModel(id: string): Model<"openai-completions"> { -- return openCodeGoModel(id, true); -- } -- -- it("requires reasoning_content for tool calls on kimi-k2.5 (opencode-go)", () => { -- const compat = detectCompat(kimiOpenCodeModel("kimi-k2.5")); -- expect(compat.requiresReasoningContentForToolCalls).toBe(true); -- expect(compat.requiresAssistantContentForToolCalls).toBe(true); -- }); -- -- it("requires reasoning_content for tool calls on reasoning DeepSeek models via opencode-go", () => { -- const compat = detectCompat(openCodeGoModel("deepseek-v4-pro", true)); -- expect(compat.requiresReasoningContentForToolCalls).toBe(true); -- expect(compat.requiresAssistantContentForToolCalls).toBe(false); -- }); -- -- it("injects reasoning_content placeholder for reasoning DeepSeek tool-call turns via opencode-go", () => { -- const model = openCodeGoModel("deepseek-v4-pro", true); -- const compat = detectCompat(model); -- const toolCallMessage: AssistantMessage = { -- role: "assistant", -- content: [{ type: "toolCall", id: "call_ds_go", name: "web_search", arguments: { query: "hi" } }], -- api: model.api, -- provider: model.provider, -- model: model.id, -- usage: { -- input: 0, -- output: 0, -- cacheRead: 0, -- cacheWrite: 0, -- totalTokens: 0, -- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, -- }, -- stopReason: "toolUse", -- timestamp: Date.now(), -+ it.each(["opencode-go", "opencode-zen"] as const)( -+ "requires reasoning_content for tool calls on kimi-k2.5 via %s", -+ provider => { -+ const compat = detectCompat(openCodeModel(provider, "kimi-k2.5", true)); -+ expect(compat.requiresReasoningContentForToolCalls).toBe(true); -+ expect(compat.requiresAssistantContentForToolCalls).toBe(true); -+ }, -+ ); -+ -+ it.each(["opencode-go", "opencode-zen"] as const)( -+ "requires reasoning_content for tool calls on reasoning DeepSeek models via %s", -+ provider => { -+ const compat = detectCompat(openCodeModel(provider, "deepseek-v4-pro", true)); -+ expect(compat.requiresReasoningContentForToolCalls).toBe(true); -+ expect(compat.requiresAssistantContentForToolCalls).toBe(false); -+ }, -+ ); -+ -+ it("requires reasoning_content when custom openai provider targets opencode zen baseUrl", () => { -+ const model: Model<"openai-completions"> = { -+ ...getBundledModel("openai", "gpt-4o-mini"), -+ api: "openai-completions", -+ provider: "openai", -+ baseUrl: "https://opencode.ai/zen/v1", -+ id: "deepseek-v4-pro", -+ reasoning: true, - }; -- const messages = convertMessages(model, { messages: [toolCallMessage] }, compat); -- const assistant = messages.find(m => m.role === "assistant"); -- expect(assistant).toBeDefined(); -- expect(Reflect.get(assistant as object, "reasoning_content")).toBe("."); -- }); -- -- it("injects reasoning_content placeholder when assistant with tool calls has no reasoning field", () => { -- const model = kimiOpenCodeModel("kimi-k2.5"); - const compat = detectCompat(model); -- const toolCallMessage: AssistantMessage = { -- role: "assistant", -- content: [ -- // Thinking returned as plain text (as kimi-k2.5 on opencode-go does) -- { type: "text", text: "Let me research this." }, -- { -- type: "toolCall", -- id: "call_abc123", -- name: "web_search", -- arguments: { query: "beads gastownhall" }, -- }, -- ], -- api: model.api, -- provider: model.provider, -- model: model.id, -- usage: { -- input: 0, -- output: 0, -- cacheRead: 0, -- cacheWrite: 0, -- totalTokens: 0, -- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, -- }, -- stopReason: "toolUse", -- timestamp: Date.now(), -- }; -- const messages = convertMessages(model, { messages: [toolCallMessage] }, compat); -- const assistant = messages.find(m => m.role === "assistant"); -- expect(assistant).toBeDefined(); -- const reasoningContent = Reflect.get(assistant as object, "reasoning_content"); -- expect(reasoningContent).toBeDefined(); -- expect(typeof reasoningContent).toBe("string"); -- expect((reasoningContent as string).length).toBeGreaterThan(0); -- }); -- -- it("does not require reasoning_content when opencode-go model is not reasoning-capable", () => { -- const compat = detectCompat(openCodeGoModel("some-other-model", false)); -- expect(compat.requiresReasoningContentForToolCalls).toBe(false); -+ expect(compat.requiresReasoningContentForToolCalls).toBe(true); - }); - -- it.each(["kimi-k2.5", "kimi-k1.5", "kimi-k2-5"])("matches kimi model id: %s", id => { -- const compat = detectCompat(kimiOpenCodeModel(id)); -+ it.each(["opencode-go", "opencode-zen"] as const)( -+ "injects reasoning_content placeholder for reasoning DeepSeek tool-call turns via %s", -+ provider => { -+ const model = openCodeModel(provider, "deepseek-v4-pro", true); -+ const compat = detectCompat(model); -+ const toolCallMessage: AssistantMessage = { -+ role: "assistant", -+ content: [{ type: "toolCall", id: `call_ds_${provider}`, name: "web_search", arguments: { query: "hi" } }], -+ api: model.api, -+ provider: model.provider, -+ model: model.id, -+ usage: { -+ input: 0, -+ output: 0, -+ cacheRead: 0, -+ cacheWrite: 0, -+ totalTokens: 0, -+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, -+ }, -+ stopReason: "toolUse", -+ timestamp: Date.now(), -+ }; -+ const messages = convertMessages(model, { messages: [toolCallMessage] }, compat); -+ const assistant = messages.find(m => m.role === "assistant"); -+ expect(assistant).toBeDefined(); -+ expect(Reflect.get(assistant as object, "reasoning_content")).toBe("."); -+ }, -+ ); -+ -+ it.each(["opencode-go", "opencode-zen"] as const)( -+ "does not require reasoning_content when %s model is not reasoning-capable", -+ provider => { -+ const compat = detectCompat(openCodeModel(provider, "some-other-model", false)); -+ expect(compat.requiresReasoningContentForToolCalls).toBe(false); -+ }, -+ ); -+ -+ it.each(["kimi-k2.5", "kimi-k1.5", "kimi-k2-5"])("matches kimi model id pattern via opencode-zen: %s", id => { -+ const compat = detectCompat(openCodeModel("opencode-zen", id, true)); - expect(compat.requiresReasoningContentForToolCalls).toBe(true); - }); - - -From 76c1fe9ee083836ecca43900fefc458c8cf4c4fb Mon Sep 17 00:00:00 2001 -From: sonhyrd -Date: Mon, 27 Apr 2026 00:14:27 +0700 -Subject: [PATCH 3/5] test(ai): restore non-kimi coverage while adding - opencode-zen cases - ---- - .../ai/test/openai-completions-compat.test.ts | 215 +++++++++++++----- - 1 file changed, 154 insertions(+), 61 deletions(-) - -diff --git a/packages/ai/test/openai-completions-compat.test.ts b/packages/ai/test/openai-completions-compat.test.ts -index c743dd246..8b8cef393 100644 ---- a/packages/ai/test/openai-completions-compat.test.ts -+++ b/packages/ai/test/openai-completions-compat.test.ts -@@ -282,38 +282,56 @@ describe("openai-completions compatibility", () => { - }); - }); - --describe("opencode reasoning-content compatibility via detectCompat", () => { -- type OpenCodeProvider = "opencode-go" | "opencode-zen"; -+describe("kimi model detection via detectCompat", () => { -+ function openCodeGoModel(id: string, reasoning = true): Model<"openai-completions"> { -+ return { -+ ...getBundledModel("openai", "gpt-4o-mini"), -+ api: "openai-completions", -+ provider: "opencode-go", -+ baseUrl: "https://opencode.ai/zen/go/v1", -+ id, -+ reasoning, -+ }; -+ } - -- function openCodeModel(provider: OpenCodeProvider, id: string, reasoning = true): Model<"openai-completions"> { -- const baseUrl = provider === "opencode-go" ? "https://opencode.ai/zen/go/v1" : "https://opencode.ai/zen/v1"; -+ function openCodeZenModel(id: string, reasoning = true): Model<"openai-completions"> { - return { - ...getBundledModel("openai", "gpt-4o-mini"), - api: "openai-completions", -- provider, -- baseUrl, -+ provider: "opencode-zen", -+ baseUrl: "https://opencode.ai/zen/v1", - id, - reasoning, - }; - } - -- it.each(["opencode-go", "opencode-zen"] as const)( -- "requires reasoning_content for tool calls on kimi-k2.5 via %s", -- provider => { -- const compat = detectCompat(openCodeModel(provider, "kimi-k2.5", true)); -- expect(compat.requiresReasoningContentForToolCalls).toBe(true); -- expect(compat.requiresAssistantContentForToolCalls).toBe(true); -- }, -- ); -- -- it.each(["opencode-go", "opencode-zen"] as const)( -- "requires reasoning_content for tool calls on reasoning DeepSeek models via %s", -- provider => { -- const compat = detectCompat(openCodeModel(provider, "deepseek-v4-pro", true)); -- expect(compat.requiresReasoningContentForToolCalls).toBe(true); -- expect(compat.requiresAssistantContentForToolCalls).toBe(false); -- }, -- ); -+ function kimiOpenCodeModel(id: string): Model<"openai-completions"> { -+ return openCodeGoModel(id, true); -+ } -+ -+ it("requires reasoning_content for tool calls on kimi-k2.5 (opencode-go)", () => { -+ const compat = detectCompat(kimiOpenCodeModel("kimi-k2.5")); -+ expect(compat.requiresReasoningContentForToolCalls).toBe(true); -+ expect(compat.requiresAssistantContentForToolCalls).toBe(true); -+ }); -+ -+ it("requires reasoning_content for tool calls on kimi-k2.5 (opencode-zen)", () => { -+ const compat = detectCompat(openCodeZenModel("kimi-k2.5", true)); -+ expect(compat.requiresReasoningContentForToolCalls).toBe(true); -+ expect(compat.requiresAssistantContentForToolCalls).toBe(true); -+ }); -+ -+ it("requires reasoning_content for tool calls on reasoning DeepSeek models via opencode-go", () => { -+ const compat = detectCompat(openCodeGoModel("deepseek-v4-pro", true)); -+ expect(compat.requiresReasoningContentForToolCalls).toBe(true); -+ expect(compat.requiresAssistantContentForToolCalls).toBe(false); -+ }); -+ -+ it("requires reasoning_content for tool calls on reasoning DeepSeek models via opencode-zen", () => { -+ const compat = detectCompat(openCodeZenModel("deepseek-v4-pro", true)); -+ expect(compat.requiresReasoningContentForToolCalls).toBe(true); -+ expect(compat.requiresAssistantContentForToolCalls).toBe(false); -+ }); - - it("requires reasoning_content when custom openai provider targets opencode zen baseUrl", () => { - const model: Model<"openai-completions"> = { -@@ -328,45 +346,120 @@ describe("opencode reasoning-content compatibility via detectCompat", () => { - expect(compat.requiresReasoningContentForToolCalls).toBe(true); - }); - -- it.each(["opencode-go", "opencode-zen"] as const)( -- "injects reasoning_content placeholder for reasoning DeepSeek tool-call turns via %s", -- provider => { -- const model = openCodeModel(provider, "deepseek-v4-pro", true); -- const compat = detectCompat(model); -- const toolCallMessage: AssistantMessage = { -- role: "assistant", -- content: [{ type: "toolCall", id: `call_ds_${provider}`, name: "web_search", arguments: { query: "hi" } }], -- api: model.api, -- provider: model.provider, -- model: model.id, -- usage: { -- input: 0, -- output: 0, -- cacheRead: 0, -- cacheWrite: 0, -- totalTokens: 0, -- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, -+ it("injects reasoning_content placeholder for reasoning DeepSeek tool-call turns via opencode-go", () => { -+ const model = openCodeGoModel("deepseek-v4-pro", true); -+ const compat = detectCompat(model); -+ const toolCallMessage: AssistantMessage = { -+ role: "assistant", -+ content: [{ type: "toolCall", id: "call_ds_go", name: "web_search", arguments: { query: "hi" } }], -+ api: model.api, -+ provider: model.provider, -+ model: model.id, -+ usage: { -+ input: 0, -+ output: 0, -+ cacheRead: 0, -+ cacheWrite: 0, -+ totalTokens: 0, -+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, -+ }, -+ stopReason: "toolUse", -+ timestamp: Date.now(), -+ }; -+ const messages = convertMessages(model, { messages: [toolCallMessage] }, compat); -+ const assistant = messages.find(m => m.role === "assistant"); -+ expect(assistant).toBeDefined(); -+ expect(Reflect.get(assistant as object, "reasoning_content")).toBe("."); -+ }); -+ -+ it("injects reasoning_content placeholder for reasoning DeepSeek tool-call turns via opencode-zen", () => { -+ const model = openCodeZenModel("deepseek-v4-pro", true); -+ const compat = detectCompat(model); -+ const toolCallMessage: AssistantMessage = { -+ role: "assistant", -+ content: [{ type: "toolCall", id: "call_ds_zen", name: "web_search", arguments: { query: "hi" } }], -+ api: model.api, -+ provider: model.provider, -+ model: model.id, -+ usage: { -+ input: 0, -+ output: 0, -+ cacheRead: 0, -+ cacheWrite: 0, -+ totalTokens: 0, -+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, -+ }, -+ stopReason: "toolUse", -+ timestamp: Date.now(), -+ }; -+ const messages = convertMessages(model, { messages: [toolCallMessage] }, compat); -+ const assistant = messages.find(m => m.role === "assistant"); -+ expect(assistant).toBeDefined(); -+ expect(Reflect.get(assistant as object, "reasoning_content")).toBe("."); -+ }); -+ -+ it("injects reasoning_content placeholder when assistant with tool calls has no reasoning field", () => { -+ const model = kimiOpenCodeModel("kimi-k2.5"); -+ const compat = detectCompat(model); -+ const toolCallMessage: AssistantMessage = { -+ role: "assistant", -+ content: [ -+ // Thinking returned as plain text (as kimi-k2.5 on opencode-go does) -+ { type: "text", text: "Let me research this." }, -+ { -+ type: "toolCall", -+ id: "call_abc123", -+ name: "web_search", -+ arguments: { query: "beads gastownhall" }, - }, -- stopReason: "toolUse", -- timestamp: Date.now(), -- }; -- const messages = convertMessages(model, { messages: [toolCallMessage] }, compat); -- const assistant = messages.find(m => m.role === "assistant"); -- expect(assistant).toBeDefined(); -- expect(Reflect.get(assistant as object, "reasoning_content")).toBe("."); -- }, -- ); -- -- it.each(["opencode-go", "opencode-zen"] as const)( -- "does not require reasoning_content when %s model is not reasoning-capable", -- provider => { -- const compat = detectCompat(openCodeModel(provider, "some-other-model", false)); -- expect(compat.requiresReasoningContentForToolCalls).toBe(false); -- }, -- ); -- -- it.each(["kimi-k2.5", "kimi-k1.5", "kimi-k2-5"])("matches kimi model id pattern via opencode-zen: %s", id => { -- const compat = detectCompat(openCodeModel("opencode-zen", id, true)); -+ ], -+ api: model.api, -+ provider: model.provider, -+ model: model.id, -+ usage: { -+ input: 0, -+ output: 0, -+ cacheRead: 0, -+ cacheWrite: 0, -+ totalTokens: 0, -+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, -+ }, -+ stopReason: "toolUse", -+ timestamp: Date.now(), -+ }; -+ const messages = convertMessages(model, { messages: [toolCallMessage] }, compat); -+ const assistant = messages.find(m => m.role === "assistant"); -+ expect(assistant).toBeDefined(); -+ const reasoningContent = Reflect.get(assistant as object, "reasoning_content"); -+ expect(reasoningContent).toBeDefined(); -+ expect(typeof reasoningContent).toBe("string"); -+ expect((reasoningContent as string).length).toBeGreaterThan(0); -+ }); -+ -+ it("does not inject reasoning_content when model is not kimi", () => { -+ const model: Model<"openai-completions"> = { -+ ...getBundledModel("openai", "gpt-4o-mini"), -+ api: "openai-completions", -+ provider: "opencode-go", -+ baseUrl: "https://opencode.ai/zen/go/v1", -+ id: "some-other-model", -+ }; -+ const compat = detectCompat(model); -+ expect(compat.requiresReasoningContentForToolCalls).toBe(false); -+ }); -+ -+ it("does not require reasoning_content when opencode-go model is not reasoning-capable", () => { -+ const compat = detectCompat(openCodeGoModel("some-other-model", false)); -+ expect(compat.requiresReasoningContentForToolCalls).toBe(false); -+ }); -+ -+ it("does not require reasoning_content when opencode-zen model is not reasoning-capable", () => { -+ const compat = detectCompat(openCodeZenModel("some-other-model", false)); -+ expect(compat.requiresReasoningContentForToolCalls).toBe(false); -+ }); -+ -+ it.each(["kimi-k2.5", "kimi-k1.5", "kimi-k2-5"])("matches kimi model id: %s", id => { -+ const compat = detectCompat(kimiOpenCodeModel(id)); - expect(compat.requiresReasoningContentForToolCalls).toBe(true); - }); - - -From 9c7a8958c682b16990504500551827320508087d Mon Sep 17 00:00:00 2001 -From: sonhyrd -Date: Mon, 27 Apr 2026 00:29:48 +0700 -Subject: [PATCH 4/5] fix(ai/providers): gate reasoning_content stubs on - deepseek models - ---- - .../providers/openai-completions-compat.ts | 7 ++-- - .../ai/src/providers/openai-completions.ts | 4 +-- - .../ai/test/openai-completions-compat.test.ts | 36 +++++++++++++++++++ - 3 files changed, 42 insertions(+), 5 deletions(-) - -diff --git a/packages/ai/src/providers/openai-completions-compat.ts b/packages/ai/src/providers/openai-completions-compat.ts -index b4825a31c..bba1cef70 100644 ---- a/packages/ai/src/providers/openai-completions-compat.ts -+++ b/packages/ai/src/providers/openai-completions-compat.ts -@@ -54,6 +54,7 @@ export function detectOpenAICompat(model: Model<"openai-completions">, resolvedB - const isKimiModel = model.id.includes("moonshotai/kimi") || /^kimi[-.]/i.test(model.id); - const isAlibaba = provider === "alibaba-coding-plan" || baseUrl.includes("dashscope"); - const isQwen = model.id.toLowerCase().includes("qwen"); -+ const isDeepSeekModel = model.id.toLowerCase().includes("deepseek"); - const isOpenRouter = provider === "openrouter" || baseUrl.includes("openrouter.ai"); - const isOpenCode = provider === "opencode-zen" || provider === "opencode-go" || baseUrl.includes("opencode.ai/zen"); - -@@ -109,12 +110,12 @@ export function detectOpenAICompat(model: Model<"openai-completions">, resolvedB - reasoningContentField: "reasoning_content", - // Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`: - // - Kimi: documented invariant on its native API and via OpenCode. -- // - Reasoning-capable models reached through OpenRouter or OpenCode (Zen/Go): DeepSeek V4 Pro, -- // Kimi, and similar models can enforce this server-side whenever the request is in thinking mode. -+ // - DeepSeek reasoning models reached through OpenRouter or OpenCode (Zen/Go): enforced when -+ // thinking mode is enabled on those model families. - // We can't translate Anthropic's redacted/encrypted reasoning into DeepSeek's plaintext form, so - // cross-provider continuations rely on a placeholder — see `convertMessages` for injection rules. - requiresReasoningContentForToolCalls: -- isKimiModel || ((isOpenRouter || isOpenCode) && Boolean(model.reasoning)), -+ isKimiModel || (isDeepSeekModel && (isOpenRouter || isOpenCode) && Boolean(model.reasoning)), - requiresAssistantContentForToolCalls: isKimiModel, - openRouterRouting: undefined, - vercelGatewayRouting: undefined, -diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts -index e25aeffb3..89a997a0f 100644 ---- a/packages/ai/src/providers/openai-completions.ts -+++ b/packages/ai/src/providers/openai-completions.ts -@@ -1213,8 +1213,8 @@ export function convertMessages( - // Inject a `reasoning_content` placeholder on assistant tool-call turns when the backend - // rejects history without it. The compat flag captures the rule: - // - Kimi (native or via OpenCode Zen/Go): chat completion endpoint demands the field. -- // - Reasoning models reached through OpenRouter or OpenCode Zen/Go (e.g. DeepSeek V4 Pro): -- // the upstream thinking-mode validator demands it on every prior assistant turn. omp -+ // - DeepSeek reasoning models reached through OpenRouter or OpenCode Zen/Go: the upstream -+ // thinking-mode validator demands it on every prior assistant turn. omp - // cannot synthesize real reasoning when the conversation was warmed up by another - // provider whose reasoning is redacted/encrypted (Anthropic) or simply absent, so we - // emit a placeholder. Real captured reasoning, when present, is preserved earlier via -diff --git a/packages/ai/test/openai-completions-compat.test.ts b/packages/ai/test/openai-completions-compat.test.ts -index 8b8cef393..c083c2151 100644 ---- a/packages/ai/test/openai-completions-compat.test.ts -+++ b/packages/ai/test/openai-completions-compat.test.ts -@@ -333,6 +333,29 @@ describe("kimi model detection via detectCompat", () => { - expect(compat.requiresAssistantContentForToolCalls).toBe(false); - }); - -+ it("does not require reasoning_content for non-DeepSeek reasoning models via opencode-go", () => { -+ const compat = detectCompat(openCodeGoModel("glm-5", true)); -+ expect(compat.requiresReasoningContentForToolCalls).toBe(false); -+ }); -+ -+ it("does not require reasoning_content for non-DeepSeek reasoning models via opencode-zen", () => { -+ const compat = detectCompat(openCodeZenModel("glm-5", true)); -+ expect(compat.requiresReasoningContentForToolCalls).toBe(false); -+ }); -+ -+ it("does not require reasoning_content when custom openai provider targets opencode zen baseUrl with non-DeepSeek model", () => { -+ const model: Model<"openai-completions"> = { -+ ...getBundledModel("openai", "gpt-4o-mini"), -+ api: "openai-completions", -+ provider: "openai", -+ baseUrl: "https://opencode.ai/zen/v1", -+ id: "glm-5", -+ reasoning: true, -+ }; -+ const compat = detectCompat(model); -+ expect(compat.requiresReasoningContentForToolCalls).toBe(false); -+ }); -+ - it("requires reasoning_content when custom openai provider targets opencode zen baseUrl", () => { - const model: Model<"openai-completions"> = { - ...getBundledModel("openai", "gpt-4o-mini"), -@@ -453,6 +476,19 @@ describe("kimi model detection via detectCompat", () => { - expect(compat.requiresReasoningContentForToolCalls).toBe(false); - }); - -+ it("does not require reasoning_content for non-DeepSeek reasoning models via openrouter", () => { -+ const model: Model<"openai-completions"> = { -+ ...getBundledModel("openai", "gpt-4o-mini"), -+ api: "openai-completions", -+ provider: "openrouter", -+ baseUrl: "https://openrouter.ai/api/v1", -+ id: "openai/gpt-4.1-mini", -+ reasoning: true, -+ }; -+ const compat = detectCompat(model); -+ expect(compat.requiresReasoningContentForToolCalls).toBe(false); -+ }); -+ - it("does not require reasoning_content when opencode-zen model is not reasoning-capable", () => { - const compat = detectCompat(openCodeZenModel("some-other-model", false)); - expect(compat.requiresReasoningContentForToolCalls).toBe(false); - -From 53a03286cf658bb4aeab67dad3246b7ba80cf244 Mon Sep 17 00:00:00 2001 -From: sonhyrd -Date: Mon, 27 Apr 2026 00:52:22 +0700 -Subject: [PATCH 5/5] fix(ai/providers): set content when reasoning placeholder - is injected - ---- - packages/ai/src/providers/openai-completions.ts | 3 ++- - packages/ai/test/openai-completions-compat.test.ts | 2 ++ - 2 files changed, 4 insertions(+), 1 deletion(-) - -diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts -index 89a997a0f..b490e254e 100644 ---- a/packages/ai/src/providers/openai-completions.ts -+++ b/packages/ai/src/providers/openai-completions.ts -@@ -1206,7 +1206,7 @@ export function convertMessages( - } - - const toolCalls = msg.content.filter(b => b.type === "toolCall") as ToolCall[]; -- const hasReasoningField = -+ let hasReasoningField = - (assistantMsg as any).reasoning_content !== undefined || - (assistantMsg as any).reasoning !== undefined || - (assistantMsg as any).reasoning_text !== undefined; -@@ -1227,6 +1227,7 @@ export function convertMessages( - if (toolCalls.length > 0 && stubsReasoningContent && !hasReasoningField) { - const reasoningField = compat.reasoningContentField ?? "reasoning_content"; - (assistantMsg as any)[reasoningField] = "."; -+ hasReasoningField = true; - } - if (toolCalls.length > 0) { - assistantMsg.tool_calls = toolCalls.map((tc, toolCallIndex) => { -diff --git a/packages/ai/test/openai-completions-compat.test.ts b/packages/ai/test/openai-completions-compat.test.ts -index c083c2151..8efae899a 100644 ---- a/packages/ai/test/openai-completions-compat.test.ts -+++ b/packages/ai/test/openai-completions-compat.test.ts -@@ -393,6 +393,7 @@ describe("kimi model detection via detectCompat", () => { - const assistant = messages.find(m => m.role === "assistant"); - expect(assistant).toBeDefined(); - expect(Reflect.get(assistant as object, "reasoning_content")).toBe("."); -+ expect(Reflect.get(assistant as object, "content")).toBe(""); - }); - - it("injects reasoning_content placeholder for reasoning DeepSeek tool-call turns via opencode-zen", () => { -@@ -419,6 +420,7 @@ describe("kimi model detection via detectCompat", () => { - const assistant = messages.find(m => m.role === "assistant"); - expect(assistant).toBeDefined(); - expect(Reflect.get(assistant as object, "reasoning_content")).toBe("."); -+ expect(Reflect.get(assistant as object, "content")).toBe(""); - }); - - it("injects reasoning_content placeholder when assistant with tool calls has no reasoning field", () => {