Skip to content

fix: Opus prefill rejection during tool-call loops with thinking enabled#22404

Open
chan1103 wants to merge 1 commit intoanomalyco:devfrom
chan1103:fix/strip-trailing-assistant-prefill
Open

fix: Opus prefill rejection during tool-call loops with thinking enabled#22404
chan1103 wants to merge 1 commit intoanomalyco:devfrom
chan1103:fix/strip-trailing-assistant-prefill

Conversation

@chan1103
Copy link
Copy Markdown

@chan1103 chan1103 commented Apr 14, 2026

Issue for this PR

Fixes #17982
Related: #13286, #22001, #13768, #13577

Type of change

  • Bug fix

What does this PR do?

I ran into another failure mode while chasing #13286:

This model does not support assistant message prefill. The conversation must end with a user message.

What was happening here is that a single assistant turn could contain both a tool-call step and a text-only step after it. When toModelMessages flattens that into model messages, the text-only step becomes a trailing assistant(text) message. The loop is still open (finish === "tool-calls"), but the next request now ends with assistant instead of user.

bvironn also hit a similar message shape in #22001 (tool-call followed by text in the same turn), though with a different symptom.

That turns out to be model-dependent when thinking is on. Sonnet accepts it. Opus 4.6 rejects it.

Model thinking + assistant last Result
claude-sonnet-4 Accepted as prefill Accepted
claude-opus-4-6 Rejected "does not support assistant message prefill"

So the bug here isn't that the loop state is wrong. It's that we end up carrying forward a trailing text-only assistant message from the previous assistant turn, and some models treat that as unsupported prefill in thinking mode.

The fix strips trailing text-only assistant messages at the end of ProviderTransform.message() when options.thinking is set. I used a while loop so it also handles consecutive trailing text messages, and it covers both string content and array content.

I kept the scope narrow:

  • only when thinking is active
  • only when the last message is assistant
  • only when that last assistant message is text-only

Messages with tool calls, reasoning blocks, or other non-text content are left alone. If thinking isn't active, nothing changes. During an open tool-call loop, these trailing text-only assistant messages are intermediate conversion results rather than the final assistant turn, so stripping them doesn't lose meaningful content — the model re-derives its plan from tool results in the next iteration.

How did you verify your code works?

I reproduced the behavior directly with API calls:

Test Setup Result
1 opus + thinking + assistant last Rejected — error reproduced
2 opus + thinking + user last Accepted
3 opus + thinking + tool chain + trailing text Rejected — same history shape as the broken sessions
4 test 3 with trailing assistant removed Accepted

Full suite locally: 1906 pass, 0 fail.

Screenshots / recordings

Not a UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

@github-actions
Copy link
Copy Markdown
Contributor

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@github-actions
Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

Related PR Found:

Related but Different Issue:

The current PR (#22404) is a focused fix for a specific edge case identified while working on #13286, and #22001 is the most directly related existing PR dealing with similar message shape issues.

@chan1103
Copy link
Copy Markdown
Author

Noting for reviewers: #17010 addresses the root cause of this (loop exit condition using ULID ordering instead of parentID). This PR is a safety net at the transport layer — it prevents the API rejection even if the loop doesn't exit when it should. Both fixes are complementary: #17010 stops the loop from continuing, this PR catches the case at the message level.

Some models (Opus 4.6) reject assistant-last messages as unsupported
prefill when thinking is enabled. During tool-call loops, multi-step
assistant messages can produce a trailing text-only model message after
toModelMessages conversion. The loop continues (finish=tool-calls) but
the next request ends with assistant.

Strip trailing text-only assistant messages at the end of
ProviderTransform.message() when options.thinking is set.
@chan1103 chan1103 force-pushed the fix/strip-trailing-assistant-prefill branch from ab62259 to 1385351 Compare April 14, 2026 04:22
@chan1103 chan1103 changed the title fix: strip trailing text-only assistant messages when thinking mode rejects prefill fix: Opus prefill rejection during tool-call loops with thinking enabled Apr 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: OpenCode prompt loop continues after finish=stop, triggering prefill error on claude-opus-4-6

1 participant