Replay assistant reasoning as a content part for DeepSeek thinking mode

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.
This commit is contained in:
nhat.nguyenhong 2026-09-10 22:40:50 +07:00
parent 0b1f933ead
commit ab760fb876
3 changed files with 17 additions and 3 deletions

View File

@ -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.

View File

@ -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 |

View File

@ -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;