From ab760fb87639af38e8df2aba82afaba0d1180df1 Mon Sep 17 00:00:00 2001 From: "nhat.nguyenhong" Date: Thu, 10 Sep 2026 22:40:50 +0700 Subject: [PATCH] =?UTF-8?q?=EF=BB=BFReplay=20assistant=20reasoning=20as=20?= =?UTF-8?q?a=20content=20part=20for=20DeepSeek=20thinking=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Forward each assistant reasoning part to /alpha/generate as {type:"reasoning", text, signature?} instead of dropping it. DeepSeek thinking mode rejects any request carrying tools whose prior assistant turns omit their reasoning, so multi-turn tool-call conversations 400'd. Only real (non-empty) reasoning is forwarded; no fabricated injection. Update README and AGENTS.md accordingly. --- AGENTS.md | 9 +++++++-- README.md | 1 + src/transform.ts | 10 +++++++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 659614c..0796215 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -102,9 +102,14 @@ These are load-bearing. Breaking one causes silent failures in opencode. 7. **`supportedUrls` is `{}` and stays empty.** CommandsCode URLs are not fetched by the SDK. 8. **Redact before surfacing errors.** Any upstream error text passed to the client must go through `redact()`. -9. **`file://` npm specs bypass install.** opencode imports `dist/index.js` directly, so the repo +9. **Replay assistant reasoning as a `{type:"reasoning"}` content part.** DeepSeek thinking mode + rejects any request carrying `tools` whose prior assistant turns omit their reasoning. In + `transform.ts`, forward each assistant `reasoning` part as `{type:"reasoning", text, signature?}`; + do not drop it and do not fabricate empty reasoning. `server.py` predates this requirement and is + not the guide here. +10. **`file://` npm specs bypass install.** opencode imports `dist/index.js` directly, so the repo must be rebuilt for opencode to see source changes. -10. **`/models` is config-driven, not provider-driven.** opencode builds the model list from +11. **`/models` is config-driven, not provider-driven.** opencode builds the model list from `provider.commandcode.models` in `opencode.json` and never asks a custom `file://` provider to discover models (only internal providers can register `discoverModels`). Refresh the map with `npm run sync-models`; do not expect a discovery hook in `src/` to populate it. diff --git a/README.md b/README.md index 8d3d70c..76d3b28 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,7 @@ useful for diagnosing model selection, retry, tool-call, and finish-reason issue | Non-streaming (`doGenerate`) | Yes — buffers the stream internally | | Text deltas | Yes | | Reasoning deltas | Yes — emitted as `reasoning-start` / `reasoning-delta` / `reasoning-end` | +| Reasoning replay | Yes — assistant `reasoning` parts are sent back as `{type:"reasoning"}` content parts (required by DeepSeek thinking mode when tools are present); `signature` is forwarded when present | | Tool calls | Yes — `tool-input-start` / `tool-input-delta` / `tool-input-end` / `tool-call` | | Tool results | Yes — paired results are replayed; unpaired ids are dropped | | Multiple images (vision) | Yes — `data:` URIs, raw base64, `Uint8Array`, and remote URLs | diff --git a/src/transform.ts b/src/transform.ts index cc3a5a5..1747b16 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -193,6 +193,15 @@ export function transform(options: LanguageModelV3CallOptions, modelId: string): for (const part of message.content) { if (part.type === "text" && part.text) { parts.push({ type: "text", text: part.text }); + } else if (part.type === "reasoning") { + // DeepSeek thinking mode requires prior assistant reasoning to be passed + // back when `tools` are present; CommandCode reads it as a content part. + const reasoning = part as { text?: string; signature?: string }; + if (reasoning.text) { + const item: ContentPart = { type: "reasoning", text: reasoning.text }; + if (reasoning.signature) item["signature"] = reasoning.signature; + parts.push(item); + } } else if (part.type === "tool-call") { if (!paired.has(part.toolCallId)) continue; parts.push({ @@ -202,7 +211,6 @@ export function transform(options: LanguageModelV3CallOptions, modelId: string): input: (part as { input?: unknown }).input ?? {}, }); } - // reasoning parts are not replayed upstream } if (parts.length) messages.push({ role: "assistant", content: parts }); continue;