Uh oh!
There was an error while loading. Please reload this page.
fix(opencode): ignore tool calls emitted inside reasoning blocks - #35059
fix(opencode): ignore tool calls emitted inside reasoning blocks#35059danmaxis wants to merge 1 commit into
Conversation
Some reasoning models (Qwen, Kimi K2, GLM, ...) occasionally emit tool-call markup inside their <think> reasoning block while still thinking. The inference server promotes that to a structured tool call, which streamText then executes prematurely — running a side effect and ending the turn before the model produces its real answer. Add a language-model middleware that runs over the provider stream before tool calls are interpreted. It drops any tool call that begins while a reasoning block is still open (and that call's input/result parts) and downgrades a resulting tool-calls finish reason to stop. Tool calls emitted after reasoning-end pass through untouched, and the transform is a no-op for streams that never emit reasoning parts. Opt out per model with options.suppressToolCallsInReasoning: false. Refs: anomalyco#8851, anomalyco#6708, anomalyco#10996
The following comment was made by an LLM, it may be inaccurate: Found one potentially related PR:
Note: The current PR (#35059) mentions it supersedes #30277, which was previously closed. The searches confirm that #35059 is the active PR addressing this issue. |
Thanks for updating your PR! It now meets our contributing guidelines. 👍 |
Automated PR Cleanup Thank you for contributing to opencode. Due to the high volume of PRs from users and AI agents, we periodically close older PRs using automated criteria so maintainers can focus review time on the most active and community-supported contributions. This PR was closed because it matched the following cleanup criteria:
PRs created within the last month are not affected by this cleanup. If you believe this PR was closed incorrectly, or if you are still actively working on it, please leave a comment explaining why it should be reopened. A maintainer can review and reopen it if appropriate. Thanks again for taking the time to contribute. |
First off — thanks for maintaining opencode. It's become my daily driver.
Issue for this PR
Closes#6708
(Same underlying behavior was also reported in #8851 and #10996.)
Type of change
What does this PR do?
I run a reasoning model locally (Qwen via llama.cpp on my homelab box), and I basically couldn't use it agentically in opencode. Partway through a turn the model is still thinking inside its
<think>block, emits some tool-call markup in there, and the turn just… stops — the tool fires and the assistant never gets to finish its real answer. Once it happens mid-reasoning there's no recovering the turn, so agentic use was a non-starter for me.Digging in: for OpenAI-compatible providers the inference server has already promoted that in-
<think>markup to a structuredtool_callsentry by the time opencode sees it, and since opencode's tools carry anexecute,streamTextruns it. By the assistant-message stage there's no trace of where the call came from — but the ordering is still visible at the provider stream level, which is where I hooked the fix in.The fix is a small
LanguageModelV3wrapStreammiddleware (reasoning-tool-guard.ts), added right next to the existingtransformParamsmiddleware insession/llm.ts, that runs over the stream beforestreamTextinterprets tool calls:tool-input-*/tool-resultparts), andtool-callsfinish reason tostop, so the session loop doesn't wait on a tool that never runs.Calls emitted after
reasoning-endpass through untouched, a mixed turn keeps itstool-callsfinish reason, and it's a no-op for any stream that never emits reasoning parts. If a model actually wants the old behavior you can opt out per model withoptions.suppressToolCallsInReasoning: false.How did you verify your code works?
I added
test/session/reasoning-tool-guard.test.tswith 5 cases: in-reasoning call suppressed + finish downgraded; post-reasoning call preserved; mixed turn keepstool-calls; plain text passes through; and suppressed even when the model stops mid-<think>.Beyond the tests I reproduced it the way it bit me — the same prompt that reliably ended the turn early, watching the TUI. Before the change the turn dies mid-
<think>; with the guard in place the turn survives and the model finishes its answer.bun test,oxlint,prettier --check, andtsgo --noEmitall pass, andllm.test.tsis unchanged.Screenshots / recordings
N/A — this isn't a UI change.
Checklist
This supersedes #30277, which the automated cleanup closed for age rather than on review. More than happy to adjust the gating, naming, or placement if you'd prefer it done differently.