Repository navigation
Azure AI Claude rejects requests after tool abort: assistant message prefill not supported #19517
Description
Bug Description
When using OpenCode with Azure AI Claude (via LiteLLM proxy), aborting a tool call (Ctrl+C or interrupt) causes the next LLM request to fail with:
litellm.BadRequestError: Azure_aiException - "This model does not support assistant message prefill. The conversation must end with a user message."
The raw XML of the attempted tool call leaks into the conversation as visible text:
<invoke name="task">
<parameter name="description">...</parameter>
<parameter name="prompt">...</parameter>
</invoke>
litellm.BadRequestError: Azure_aiException - ...
The user must send a new message to reset the conversation turn. The question tool also consistently triggers this error on Azure AI Claude.
Environment
- OpenCode 1.3.0 (Homebrew)
- Provider: Azure AI Claude (Opus 4.6) via LiteLLM proxy (Orion)
- Model group:
azure_ai/claude-opus-4-6
Root Cause
Azure AI's Claude inference layer does not support "assistant message prefill" (trailing role: "assistant" in the messages array). The standard Anthropic API does support this, so OpenCode works fine with direct Anthropic, but fails through Azure AI / LiteLLM.
There are two code paths that produce a trailing assistant message:
1. Aborted assistant message with text but no tool calls
When a user aborts mid-stream after text has started streaming but before any tool call is registered, the aborted assistant message (with partial text) gets included in history.
The error filtering logic in toModelMessages() intentionally keeps aborted messages that have "substantive" parts (text qualifies):
packages/opencode/src/session/message-v2.ts lines 690-698:
if (
msg.info.error &&
!(
MessageV2.AbortedError.isInstance(msg.info.error) &&
msg.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning")
)
) {
continue // skip this assistant message
}When an aborted message has tool calls, the tool results get appended as role: "user" (Anthropic convention), so the sequence is fine. But when it only has text, the sequence ends with role: "assistant".
2. isLastStep prefill injection
When the agentic loop hits the step limit, a hardcoded assistant message is appended unconditionally:
packages/opencode/src/session/prompt.ts lines 700-710:
messages: [
...(await MessageV2.toModelMessages(msgs, model)),
...(isLastStep
? [{ role: "assistant" as const, content: MAX_STEPS }]
: []),
],This is valid prefill for the direct Anthropic API but rejected by Azure AI Claude.
Suggested Fix
Option A (recommended): Strip trailing assistant messages in normalizeMessages()
packages/opencode/src/provider/transform.ts function normalizeMessages() (line 49) already performs provider-specific message normalization. Add logic to detect when the message array ends with role: "assistant" and either remove it or append a synthetic user message, for providers that don't support prefill.
The codebase already has precedents for this pattern:
- Synthetic user messages for Gemini reasoning models (
prompt.tslines 520-543) - Message sequence fixes for Mistral (
transform.tslines 138-151) - LiteLLM dummy tool injection (
llm.tslines 160-178)
Detection criteria for "no prefill support":
- Provider uses
@ai-sdk/openai-compatible(LiteLLM setups) - Provider ID contains
azure_ai - Or a provider-level capability flag
Option B: Guard isLastStep by provider capability
Only inject the MAX_STEPS assistant message for providers known to support prefill (direct Anthropic).
Option C: Proxy-side workaround
Configure LiteLLM to strip trailing assistant messages before forwarding to Azure AI. Less ideal since it papers over the issue.
Related Code Paths
| File | Lines | Role |
|---|---|---|
packages/opencode/src/session/message-v2.ts |
690-698 | Error filtering includes aborted messages |
packages/opencode/src/session/message-v2.ts |
756-766 | Pending/running tool call -> error result conversion |
packages/opencode/src/session/prompt.ts |
700-710 | isLastStep prefill injection |
packages/opencode/src/session/processor.ts |
354-424 | Abort handling |
packages/opencode/src/provider/transform.ts |
49-190 | normalizeMessages() - provider-specific cleanup |
packages/opencode/src/session/llm.ts |
160-178 | Existing LiteLLM workarounds |
Workaround
After the error occurs, send any new user message (even just "continue") to reset the conversation turn. This works because the new user message ensures the history no longer ends with an assistant message.
Activity
github-actions commentedon Mar 28, 2026
This issue might be a duplicate of existing issues. Please check:
finish=stop, triggering prefill error on claude-opus-4-6 #17982: Same prefill error triggered by the prompt loop continuing afterfinish=stopwithclaude-opus-4-6— overlapping root cause analysis and fix suggestionsstefanc-makersite commentedon Mar 28, 2026
Relationship to flagged duplicates
Checked the linked issues and the two open PRs that fix the loop exit condition:
lastUser.id < lastAssistant.idwithlastAssistant.parentID === lastUser.idinprompt.ts:322. This is the correct semantic relationship. All CI green, includes regression test.time.createdinstead ofmessage.id, renders replies byparentID. All CI green, review requested but pending.Both PRs are solid and would fix the infinite loop trigger (especially in
opencode webwith clock skew). However, neither PR addresses the two code paths described in this issue:Aborted text-only assistant messages — when abort happens after text streaming starts but before tool calls, the aborted message stays in history ending the sequence with
role:assistant. This is inmessage-v2.ts:690-698and is unrelated to the loop exit condition.isLastStepunconditional prefill injection —prompt.ts:700-710unconditionally appends{role:"assistant", content: MAX_STEPS}at the step limit. This is valid for direct Anthropic API (which supports prefill) but rejected by Azure AI Claude / Bedrock / any provider behind LiteLLM that doesn't support assistant message prefill.So even with #17010 and #17149 merged, providers that don't support prefill will still hit
"This model does not support assistant message prefill"through these two paths. The fix for both would be stripping/replacing trailing assistant messages innormalizeMessages()(provider/transform.ts:49) for non-prefill providers — the codebase already has similar provider-specific message fixups for Gemini and Mistral.bussnet commentedon Apr 1, 2026
PR #20414 partially addresses this. It fixes root cause #1 (empty/aborted assistant messages surviving) by unifying the Anthropic detection logic —
normalizeMessages()now strips empty content for all Claude models regardless of SDK package, not just@ai-sdk/anthropic.Root cause #2 (
isLastStepprefill injection inprompt.tsL700-710) is a separate code path that unconditionally appends a trailing assistant message and is not addressed by this PR.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.