test(ai): cover opencode-go deepseek-v4 reasoning_content replay

@yofriadi reports the same OpenCode Zen 'reasoning_content is missing'
400 on `opencode-go/deepseek-v4-flash` and `deepseek-v4-pro`. The
gateway is the same Zen instance and the wire-body invariant matches:
the streamed-signature path in convertMessages previously emitted both
`reasoning` and `reasoning_content` on DeepSeek turns, which the
strict schema flags exactly like the Kimi case.

The line-1488 fix from 4215228b8 already coerces the replay onto the
configured `reasoningContentField` whenever
`allowsSyntheticReasoningContentForToolCalls=false` — DeepSeek family
sets that flag — so deepseek-v4 payloads now carry only
`reasoning_content`. Add a streamOpenAICompletions + onPayload
regression test that pins the shape.
This commit is contained in:
roboomp
2026-05-28 18:48:50 +00:00
parent 707a8fbdc5
commit 0b1d4b1e28
2 changed files with 84 additions and 1 deletions
+1 -1
View File
@@ -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
@@ -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<unknown>();
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<Record<string, unknown>> };
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);