anomalyco / anomalyco/opencode
deepseek.v3.2 on amazon-bedrock: long --auto sessions intermittently end with a complete DSML tool call left as plain text (finish reason 'stop')
@rekram1-node is already working on this.
Since Aug 27, 2026.
- Dominant language
- TypeScript
- Stars
- 209k
- Forks
- 27.5k
- PR merge metrics
- PR metrics pending
Description
Summary
On amazon-bedrock/deepseek.v3.2, a long opencode --auto session intermittently ends mid-task: the assistant's final message contains a complete, well-formed DeepSeek DSML tool-call block as plain text, the step finishes with reason stop, and no tool part is produced. The session then sits idle with the task unfinished.
Reproduced 1 time in 3 at ~40 sequential tool calls. Reporting observed behaviour only — I have not tried to apportion this between the Bedrock serving layer and opencode's adapter, and the data below is consistent with either owning part of it.
Environment
- opencode 1.18.23 (latest release at time of testing)
- provider
amazon-bedrock, modeldeepseek.v3.2, region eu-west-2, AWS SSO auth - Linux (WSL2)
Repro
Prompt asks for N sequential one-per-step bash calls, each writing a distinct nonce file, then a final count:
Step 1: run echo step-1 > <dir>/step-1.txt
Step 2: run echo step-2 > <dir>/step-2.txt
... (40 steps)
Run as opencode --auto --model amazon-bedrock/deepseek.v3.2 --prompt "<the above>" under a pty. Count the nonce files that appear.
| entry point | sessions | outcome |
|---|---|---|
opencode --auto (TUI) |
3 × 40 steps | 1 stopped at step 32/40; 2 completed |
opencode run |
3 (22, 40, 61 tool calls) | all completed |
What the stalled session contains
Final three parts of the failing session, from the session store:
{"reason": "tool-calls", "type": "step-finish", "tokens": {"total": 26118, "input": 25749, "output": 369}}
{"type": "step-start"}
{"type": "text", "text": "\n\n<|DSML|function_calls\n<|DSML|invoke name=\"bash\">\n<|DSML|parameter name=\"command\" string=\"true\">echo step-32 > <dir>/step-32.txt</|DSML|parameter>\n</|DSML|invoke>\n</|DSML|function_calls>"}
{"reason": "stop", "type": "step-finish", "tokens": {"total": 26224, "input": 26137, "output": 87}}
So the intended call is fully formed and unambiguous — it just arrives as text, and --auto reasonably treats a no-tool-call message as the final answer.
Related, and present in every tool-calling turn on this model: the text block always ends with a bare \n\n<|DSML|function_calls fragment alongside a correct tool call. In the failing session, 35 of 36 text parts carry one.
Counter-evidence: the raw Bedrock API looked clean
To check whether the serving layer alone explains it, I drove bedrock-runtime directly with boto3, same model and region, classifying every turn as native toolUse / DSML-in-text / plain text:
| shape | calls | native toolUse |
call landed as text |
|---|---|---|---|
converse, 20 × 1 turn |
20 | 20 | 0 |
converse-stream, 20 × 1 turn |
20 | 20 | 0 |
converse-stream, 5 × 6 turns |
30 | 30 | 0 |
| as above + 10 tools and a long system prompt | 30 | 30 | 0 |
converse-stream, 1 × 30 turns |
30 | 30 | 0 |
converse-stream, 1 × 45 turns, padded tool results (~26k-token context) |
45 | 45 | 0 |
175/175 returned a native toolUse block. All 175 also carried the same trailing <|DSML|function_calls fragment in the text, so the leak reproduces at the API level — but the whole-call-as-text failure did not, including at the turn depth and context size where the real session broke.
The OpenAI-compatible endpoint (/openai/v1/chat/completions) shows the identical leak: correct tool_calls array, and "content" ending in ...\n\n<|DSML|function_calls.
Why I am raising it here
I could not reproduce the fatal case outside opencode, and it appeared only on the --auto TUI path, so I cannot rule out something about how that path builds requests or handles the assistant history. My synthetic API loop feeds the leaked text fragment back verbatim and still never fails, but it obviously does not replicate everything opencode sends.
Two things maintainers may find worth considering, offered as options rather than a diagnosis:
- A fallback parse for in-band DSML markup when a message finishes with
stopand contains a complete tool-call block — the information needed to recover the call is fully present. - Failing that, treating "final message contains unparsed tool-call markup" as a loud error rather than a normal completion, so it does not read as a finished task.
Downstream context, if useful: dare-global/conductor#2024. We have blocked the model for agentic use on our side meanwhile.
I can supply the full session parts or the API soak harness if either helps.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.