fix(opencode): strip trailing assistant messages to prevent prefill 400

Merged
IC
5mo ago
+122-2

Problem

rewriteRequestBody() never removed trailing assistant-role messages. A resumed or compacted session that ends on an assistant turn (e.g. after a failed tool round, or a session restored mid-response) reached Anthropic with assistant-message prefill. Claude Code OAuth models reject this:

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

This recurs in practice on session resume/compaction and forces the user to manually send another message to recover.

Fix

Add stripTrailingAssistantMessages() and call it right after JSON.parse(body), so trailing assistant messages are popped before billing-header computation and signing. This mirrors the behavior of the built-in @ai-sdk/anthropic provider and the packages/pi conversion path, which already strip trailing assistant messages.

Only messages after the last user message are removed — the active user turn and any assistant message followed by a user message are always preserved. The alternative (no strip) is a hard 400 that returns nothing.

Tests

Adds 5 tests:

  • strips single trailing assistant message
  • strips multiple trailing assistant messages
  • preserves assistant message followed by user message
  • preserves assistant tool_use followed by user tool_result
  • strips trailing assistant after tool_result + assistant

Updates existing fixtures that previously ended on assistant turns to end on user messages. All opencode tests pass (264), typecheck clean, biome clean.


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag @codesmith with what you need. Autofix is disabled.


Summary by cubic

Strips trailing assistant messages during request rewrite so conversations end with a user message, preventing Anthropic Claude Code OAuth 400 “assistant message prefill” errors. Aligns behavior with @ai-sdk/anthropic and packages/pi.

  • Bug Fixes
    • Added stripTrailingAssistantMessages() and called it in rewriteRequestBody() right after JSON.parse.
    • Only removes assistant messages after the last user message; preserves assistant→user and tool_use→tool_result flows.
    • Added 5 tests for strip/preserve scenarios and updated fixtures to end on user messages.