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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user