Repository navigation
Bug: OpenCode prompt loop continues after finish=stop, triggering prefill error on claude-opus-4-6 #17982
Description
Description
Summary
When using claude-opus-4-6 via the Anthropic provider, OpenCode's session prompt loop advances to step=1 even after the model returns finish=stop (a normal text completion, not a tool call). On step 1, the conversation's last message is an assistant turn, which Opus rejects with:
"This model does not support assistant message prefill. The conversation must end with a user message."
This creates a completed assistant message with an error and zero parts, which the Web UI renders as a blank/stuck session.
Root cause
The prompt loop in session.prompt does not gate continuation on the finish reason. After step 0 completes with finish=stop, the loop enters step 1, re-resolves all tools, and fires a new LLM stream call. Since the last message in the conversation is the assistant's own response from step 0, the Anthropic API rejects the call for models that do not support assistant message prefill (like claude-opus-4-6).
Models that do support prefill (e.g. big-pickle via the opencode provider) silently accept this extra call, which is why the bug only manifests with Opus.
Why the Web UI goes blank
The failed step 1 persists a new assistant message row in the database with:
error.name = "APIError"error.data.message = "This model does not support assistant message prefill..."parts.length = 0
The Web UI has no content to render for this zero-part error message, so the session appears blank or stuck. The successful step 0 response (with actual content) is effectively hidden behind this empty error shell.
This is related to issue #17895 ("Completed aborted assistant message with zero parts renders as blank/stuck in web UI"), but the root cause is different: here the empty message is not from an abort, but from an invalid continuation call that should never have been made.
Evidence from server logs
Full reproduction captured on 2026-03-17T16:50:54 with OpenCode v1.2.26, session ses_3036b446affer1VC9iSCDETcYo.
Step 0 starts normally
INFO 16:50:54 service=session.prompt step=0 sessionID=ses_3036b446affer1VC9iSCDETcYo loop
INFO 16:50:55 service=llm providerID=anthropic modelID=claude-opus-4-6 ... stream
Model streams response successfully (message.part.delta flood)
INFO 16:51:04 service=bus type=message.part.delta publishing
INFO 16:51:04 service=bus type=message.part.delta publishing
... (dozens of delta events over ~4 seconds) ...
INFO 16:51:08 service=bus type=message.part.delta publishing
Step 0 finishes, step 1 starts immediately
INFO 16:51:08 service=bus type=session.status publishing
INFO 16:51:08 service=session.prompt step=1 sessionID=ses_3036b446affer1VC9iSCDETcYo loop
INFO 16:51:08 service=session.prompt status=started resolveTools
Step 1 re-resolves all tools and fires another LLM call
INFO 16:51:08 service=tool.registry status=started bash
INFO 16:51:08 service=tool.registry status=started read
INFO 16:51:08 service=tool.registry status=started glob
... (all tools re-registered) ...
INFO 16:51:08 service=session.prompt status=completed duration=173 resolveTools
INFO 16:51:08 service=llm providerID=anthropic modelID=claude-opus-4-6 ... stream
API rejects the call (last message is assistant, Opus doesn't allow prefill)
ERROR 16:51:09 service=llm providerID=anthropic modelID=claude-opus-4-6 ...
error={"error":{"name":"AI_APICallError",
"responseBody":"{\"type\":\"error\",\"error\":{\"type\":\"invalid_request_error\",
\"message\":\"This model does not support assistant message prefill.
The conversation must end with a user message.\"}}"}} stream error
Error propagates, session goes idle with a broken message
ERROR 16:51:09 service=session.processor
error=This model does not support assistant message prefill. ...
stack="AI_APICallError: This model does not support assistant message prefill..."
INFO 16:51:09 service=bus type=session.error publishing
INFO 16:51:09 service=bus type=session.status publishing
INFO 16:51:09 service=bus type=session.idle publishing
INFO 16:51:09 service=bus type=message.updated publishing
Evidence from database
Querying the session's messages confirms the pattern repeats for every user prompt sent to Opus:
Msg 0 [user ] ok | model= provider=
Msg 1 [assistant] finish=stop | model=claude-opus-4-6 provider=anthropic <-- step 0, success
Msg 2 [assistant] ERROR | model=claude-opus-4-6 provider=anthropic <-- step 1, prefill error
Msg 3 [user ] ok | model= provider=
Msg 4 [assistant] finish=stop | model=claude-opus-4-6 provider=anthropic <-- step 0, success
Msg 5 [assistant] ERROR | model=claude-opus-4-6 provider=anthropic <-- step 1, prefill error
...
Msg 9 [user ] ok | model= provider=
Msg 10 [assistant] finish=stop | model=big-pickle provider=opencode <-- no error! supports prefill
Msg 11 [assistant] finish=stop | model=big-pickle provider=opencode <-- step 1 succeeds silently
Every Opus turn produces a successful response followed by a prefill error. big-pickle turns do not produce errors because that model accepts assistant-last conversations.
Expected behavior
When step N completes with finish=stop (not tool-calls), the prompt loop should not continue to step N+1. The loop should only continue when the model signals it wants to call tools.
Affected versions
- OpenCode v1.2.26 (confirmed)
- Likely affects all versions that use the prompt loop with Anthropic models that don't support prefill
Workarounds
- Use the CLI instead of the Web UI (the CLI displays the successful step 0 response correctly despite the step 1 error)
- Use a model that supports assistant message prefill (e.g.
big-pickle)
Environment
- Server: Linux (opencode serve)
- Model:
claude-opus-4-6viaanthropicprovider - OpenCode version: 1.2.26
Note: I may not be available for follow-up, but the logs and DB evidence above should be self-contained for reproduction.
Plugins
No response
OpenCode version
1.2.26
Steps to reproduce
- Start OpenCode with
opencode serve --print-logs --log-level INFO - Open the Web UI in a browser
- Select
claude-opus-4-6(Anthropic provider) as the model - Send any message (e.g. "hi")
- Observe: the server logs show a successful
finish=stopon step 0, immediately followed by step 1 which fails with"This model does not support assistant message prefill" - The Web UI shows a blank/stuck session instead of the assistant's response
Note: The response is generated successfully in step 0 - you can verify this by switching to the CLI, which displays it correctly. The issue is that step 1 creates an error message that the Web UI can't render.
Screenshot and/or share link
https://opncd.ai/share/SCDETcYo
Operating System
Debian Linux (opencode serve)
Terminal
ssh
Activity
github-actions commentedon Mar 17, 2026
This issue might be a duplicate of existing issues. Please check:
finish=stop— same underlying loop continuation bugjialigit2 commentedon Mar 18, 2026
the latest version 1.2.27 is the same.
kamtang0823 commentedon Mar 18, 2026
I also encountered this problem. Even after completing the task with the claude model, it still keeps requesting the model, causing my bill to explode,version = 1.2.27
jameskyle commentedon Mar 20, 2026
Pretty sure I'm being hit with this using Bedrock Anthropic models.
chnlkw commentedon Mar 23, 2026
Still present in v1.3.0
Confirmed this bug persists in v1.3.0. The loop exit condition is unchanged:
PRs #17010 and #17149 (which fix this) are still open and unreviewed.
Concrete trigger: remote
opencode webwith cross-machine clock skewThis reliably reproduces when
opencode webruns on a remote server (e.g. Kubernetes) and the browser is on a different machine. The browser-generated usermessageIDencodesDate.now()from the client clock; the assistantmessageIDuses the server clock. Even a few ms of skew where the browser is ahead causeslastUser.id > lastAssistant.id, so the loop never breaks.Does NOT reproduce when browser and server share the same machine (local
opencode web).Workaround: HTTP delay proxy
For anyone stuck on this, here's a minimal Node.js proxy (~40 lines) that sits between the browser and
opencode web. It only delaysPOST /session/*/prompt_asyncrequests by exactly the clock skew — all other HTTP and WebSocket traffic passes through instantly.Usage:
TARGET_PORT=3000 LISTEN_PORT=8080 node proxy.js, point browser at port 8080.Leslie-ller commentedon Mar 26, 2026
Adding another minimal repro that appears to hit the same underlying loop/termination bug from a different angle.
Observed behavior in a control sample:
finish="tool-calls")finish="stop"with empty textFrom this repro angle, the continuation bug manifests as an unnecessary empty stop-turn after tool completion, rather than as a visible prompt/prefill error.
Evidence from the local investigation:
userassistantwithfinish="tool-calls"assistantwithfinish="stop"and empty textjerome-benoit commentedon Mar 29, 2026
Root cause analysis
After tracing the code paths in
prompt.ts,message-v2.ts,processor.ts, andtransform.ts, there are 2 confirmed root causes producing trailingrole: "assistant"messages.S1 — Loop exit condition (
prompt.ts) — PRIMARYIDs are ULIDs encoding
Date.now(). When the user messageID is generated in the browser and the assistant ID on the server, clock divergence makes this comparison unreliable. The loop fails to break even thoughlastAssistant.finishis set → next iteration sends the previous assistant response as the last message → API rejects.The correct comparison is
lastAssistant.parentID === lastUser.id—parentIDis always explicitly set at assistant message creation and is a semantic relationship, not a temporal one.S2 — MAX_STEPS injection (
prompt.ts) — NARROWAppends
role: "assistant"whenstep >= agent.steps. Only triggers when the agent has a finitestepssetting (agent.steps ?? Infinity), so most agents are unaffected.Not independent sources
toModelMessagespreservesAbortedErrorwith content), butprocessor.process()returns"stop"on abort → the loop breaks. Creates dirty state but needs S1 to fail for the trailing assistant to reach the API.toModelMessages()only skips assistant messages with non-abort errors. User messages are always included if they have parts. No code path where filtering leaves an orphaned assistant at the tail.message.idvstime.created) but only affects UI rendering, not API request construction.Transport layer — current state
normalizeMessages()intransform.tshandles empty content filtering (Anthropic), toolCallId scrubbing (Claude/Mistral), and reasoning extraction. NostripTrailingAssistantorsupportsAssistantPrefillexists.ProviderTransform.message()is the single choke point for all outgoing messages.PRs
Root-cause fixes:
parentIDcomparison) + S2 (MAX_STEPSrole →"user"). 2-line surgical fix.parentIDfix as fix: resolve Claude 4.6 assistant prefill compatibility issues #16900 with regression test covering the web/cross-machine scenario.Transport-layer safety net:
stripTrailingAssistant()innormalizeMessages()withsupportsAssistantPrefill()model detection. Defense-in-depth for unanticipated edge cases.Partial or incorrect:
"Continue."user message, changes conversation semantics. Correctly rejected.Complete fix
A single PR combining #16900 root-cause fixes + #17010 regression test + #14772 transport-layer safety net would close this and all related duplicates (#13768, #12337, #13577). Neither layer alone is sufficient: #16900 does not protect against future unknown sources, #14772 alone masks the loop bug without fixing it.
github-actions commentedon Jul 3, 2026
To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.