diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 29cefd181..c415a944a 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -4,7 +4,7 @@ ### Fixed -- Fixed OpenCode Zen Kimi `400 thinking is enabled but reasoning_content is missing in assistant tool call message` by reactivating `requiresReasoningContentForToolCalls` for `opencode-go`/`opencode-zen` Kimi requests whose runtime options enable thinking, while the static compat default still omits the field for thinking-disabled turns to preserve the `Extra inputs are not permitted` guard from #1071. ([#1484](https://github.com/can1357/oh-my-pi/issues/1484)) +- Fixed OpenCode Zen Kimi `400 thinking is enabled but reasoning_content is missing in assistant tool call message` by reactivating `requiresReasoningContentForToolCalls` for `opencode-go`/`opencode-zen` Kimi requests whose runtime options enable thinking, while the static compat default still omits the field for thinking-disabled turns to preserve the `Extra inputs are not permitted` guard from #1071. The same gateway invariant also affected `opencode-go/deepseek-v4-flash` and `deepseek-v4-pro`, which now coerce the streamed `reasoning` signature onto `reasoning_content` instead of writing both fields. ([#1484](https://github.com/can1357/oh-my-pi/issues/1484)) ## [15.5.8] - 2026-05-28 diff --git a/packages/ai/test/openai-completions-compat.test.ts b/packages/ai/test/openai-completions-compat.test.ts index 826999992..9bbfdea94 100644 --- a/packages/ai/test/openai-completions-compat.test.ts +++ b/packages/ai/test/openai-completions-compat.test.ts @@ -862,6 +862,89 @@ describe("kimi model detection via detectCompat", () => { expect(payload.reasoning_effort).toBeUndefined(); }); + // #1484 follow-up: DeepSeek V4 on opencode-go exhibits the same gateway + // invariant as Kimi (same Zen gateway). DeepSeek emits reasoning under the + // `reasoning` signature, so the pre-fix code wrote both `reasoning` and + // `reasoning_content` to the wire body. The line-1488 fix in convertMessages + // now coerces the replay onto `reasoningContentField` whenever + // `allowsSyntheticReasoningContentForToolCalls=false`, so DeepSeek V4 + // payloads carry only `reasoning_content`. + it("emits only reasoning_content on deepseek-v4-flash opencode-go tool-call replays", async () => { + const model: Model<"openai-completions"> = { + ...getBundledModel("openai", "gpt-4o-mini"), + api: "openai-completions", + provider: "opencode-go", + baseUrl: "https://opencode.ai/zen/go/v1", + id: "deepseek-v4-flash", + reasoning: true, + }; + const priorAssistant: AssistantMessage = { + role: "assistant", + content: [ + { + type: "thinking", + thinking: "Need to read the file before answering.", + thinkingSignature: "reasoning", + }, + { + type: "toolCall", + id: "call_abc123", + name: "read", + arguments: { path: "README.md" }, + }, + ], + 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 { promise, resolve } = Promise.withResolvers(); + global.fetch = createMockFetch(["[DONE]"]); + streamOpenAICompletions( + model, + { + messages: [ + { role: "user", content: "Summarize the README", timestamp: Date.now() }, + priorAssistant, + { + role: "toolResult", + toolCallId: "call_abc123", + toolName: "read", + content: [{ type: "text", text: "# Hello\n" }], + isError: false, + timestamp: Date.now(), + }, + ], + }, + { + apiKey: "test-key", + reasoning: "high", + signal: createAbortedSignal(), + onPayload: payload => resolve(payload), + }, + ); + + const payload = (await promise) as { messages: Array> }; + const assistant = payload.messages.find(m => m.role === "assistant"); + expect(assistant).toBeDefined(); + expect(Reflect.get(assistant as object, "reasoning_content")).toBe( + "Need to read the file before answering.", + ); + // DeepSeek's allowsSynthetic=false must keep the stale `reasoning` key + // off the wire body so opencode's schema validation does not flag it. + expect(Reflect.get(assistant as object, "reasoning")).toBeUndefined(); + }); + it("injects reasoning_content placeholder when kimi-on-moonshot has tool calls without reasoning field", () => { const model = kimiMoonshotModel("kimi-k2.5"); const compat = detectCompat(model);