# Claude "Does Not Support Assistant Message Prefill" 400 Fix

> Claude 4.6 and later models reject any request that ends with an assistant turn; Sonnet 4.5 and Haiku 4.5 still accept one. Replace the prefill by its job.

- URL: https://blog.laozhang.ai/en/posts/claude-opus-prefill-error-fix
- Published: 2026-02-08
- Updated: 2026-10-05
- Author: LaoZhang AI Team (https://blog.laozhang.ai/en/about)
- Topic: API Guides
- Tags: Claude API, Assistant Prefill, 400 Error, Structured Outputs, Opus 5.5, Sonnet 4.6

---
The full error is `This model does not support assistant message prefill. The conversation must end with a user message.` It comes back as a 400 `invalid_request_error` whenever the last item in `messages` has `role: "assistant"`, and the model you called is Claude 4.6 or later: Opus 4.6 through Opus 5.5, Sonnet 4.6 through Sonnet 5.5, Fable 5 and 5.1, and Claude Mythos Preview. Claude Sonnet 4.5, Claude Haiku 4.5, and older models still accept a prefill.

The fix has two parts. Make the request end with a user message, then move whatever the prefill was doing somewhere the new models accept: a JSON schema in `output_config.format`, an instruction in the system prompt, or text in the user turn. If the error appears inside Copilot, opencode, an agent framework, or a router, the tool built that trailing assistant turn, not you. The sections on frameworks and closed tools cover what you can still do in that case.

## What "assistant message prefill" means in this 400

A prefill is an assistant message placed at the end of `messages`. On older models, Claude treated that text as the start of its own reply and continued from it. That was the usual way to force JSON (prefill `{`), skip a preamble, or pick up a reply that was cut off.

On the models listed above, the API rejects the request before generating anything. According to [Anthropic's error reference](https://platform.claude.com/docs/en/api/errors), the response body looks like this:

```json
{
  "type": "error",
  "error": {
    "type": "invalid_request_error",
    "message": "This model does not support assistant message prefill. The conversation must end with a user message."
  }
}
```

Three details decide whether this is your problem:

- **Only the last message matters.** Earlier assistant turns in the history are normal conversation and stay allowed. The rule is that the final message must be `user`.
- **Content doesn't matter.** An empty assistant message, an aborted turn, or a full paragraph all count. Several agent bugs below come from an assistant turn with no text at all.
- **The model ID matters.** The same code works on `claude-haiku-4-5` and fails on `claude-sonnet-4-6`. That is why the error often appears right after a model upgrade or when a tool silently switches to a newer default.

To confirm, log the outgoing request and check the role of its last message. If it says `assistant`, you have found the cause.

## Which Claude models reject prefill and which still accept it

Anthropic's error reference states the rule as "Claude 4.6 and later models and Claude Mythos Preview do not support prefilling assistant messages." The per-model migration guides for Opus 5.5, Sonnet 5.5, and Fable 5.1 confirm it model by model. The table also shows forced `tool_choice`, because the classic workaround of "force a tool call instead of prefilling" fails on the newest models too.

| Model | Final assistant turn (prefill) | Forced `tool_choice` (`any` / `tool`) | Structured outputs |
| --- | --- | --- | --- |
| Opus 5.5 | 400 | 400 | Yes |
| Opus 5, Opus 4.8, Opus 4.7, Opus 4.6 | 400 | Accepted | Yes |
| Sonnet 5.5 | 400 | 400 | Yes (not on Amazon Bedrock) |
| Sonnet 5, Sonnet 4.6 | 400 | Accepted | Yes |
| Fable 5.1 | 400 | 400 | Yes |
| Fable 5 | 400 | Accepted | Yes |
| Mythos Preview, Mythos 5, Mythos 5.1 | 400 | 400 on Mythos 5.1 | Yes |
| Sonnet 4.5, Haiku 4.5, Opus 4.5 | Accepted | Accepted | Yes |
| Older models | Accepted | Accepted | Not in the supported list |

Sources: the [error reference](https://platform.claude.com/docs/en/api/errors), the [Opus 5.5](https://platform.claude.com/docs/en/models/opus-5-5/migration-guide), [Sonnet 5.5](https://platform.claude.com/docs/en/models/sonnet-5-5/migration-guide), and [Fable 5.1](https://platform.claude.com/docs/en/models/fable-5-1/migration-guide) migration guides, and the supported-model list on the [structured outputs page](https://platform.claude.com/docs/en/build-with-claude/structured-outputs), all as of October 5, 2026. Mythos 5 and 5.1 are covered by the "Claude 4.6 and later" rule rather than by a dedicated prefill line. Model availability changes, so check the current model list before you pin an older ID as a fallback.

Two practical readings of this table:

- **Sonnet 4.5, Haiku 4.5, and Opus 4.5 are the newest models that still accept prefill.** Pointing a tool at one of them is a valid short-term workaround, but it is not a migration.
- **Every replacement below also works on Sonnet 4.5, Haiku 4.5, and Opus 4.5**, since they support structured outputs. You can remove prefill from all code paths at once instead of keeping a model-specific branch.

## Replace the prefill by what it was doing

There is no single drop-in replacement, because prefill did different jobs in different code. The [Sonnet 5.5 migration guide](https://platform.claude.com/docs/en/models/sonnet-5-5/migration-guide) maps each job to its replacement:

| What the prefill did | Example prefill | Replacement |
| --- | --- | --- |
| Force an output format | `{` or `[` | JSON outputs via `output_config.format`, or a strict tool |
| Force a label or category | `Category:` | `enum` field in a JSON schema, or a tool with `enum` fields |
| Skip the preamble | `Here is the summary:` | System prompt: "Answer directly, without a preamble" |
| Head off unwanted refusals | `Sure, here are the steps:` | Clear instructions in the user message |
| Continue an interrupted answer | the partial reply so far | User message: "Your previous response was interrupted and ended with … Continue from where you left off." |
| Remind the model of context | `(Remember: answer in French)` | Put the reminder in the user turn |

![Three routes for replacing prefill: a JSON schema in output_config.format, a system prompt for direct prose, or a strict classification tool with tool_choice auto](https://blog.laozhang.ai/posts/en/claude-opus-prefill-error-fix/img/prefill-replacement-routes.webp)

The code below follows the request shapes in Anthropic's documentation as of October 5, 2026. Run one call against your own model ID before you ship it.

### Forcing JSON: `output_config.format` instead of a `{` prefill

This is the most common prefill. The old request ended with `{"role": "assistant", "content": "{"}`. The new one ends with the user message and carries a JSON schema:

```python
import json
import anthropic

client = anthropic.Anthropic()

response = client.messages.create(
    model="claude-opus-5-5",
    max_tokens=1024,
    messages=[
        {"role": "user", "content": "List the top 3 programming languages by popularity"}
    ],
    output_config={
        "format": {
            "type": "json_schema",
            "schema": {
                "type": "object",
                "properties": {
                    "languages": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "name": {"type": "string"},
                                "rank": {"type": "integer"}
                            },
                            "required": ["name", "rank"],
                            "additionalProperties": False
                        }
                    }
                },
                "required": ["languages"],
                "additionalProperties": False
            }
        }
    },
)

# Thinking blocks can come first on newer models, so pick the text block by type
text = next(block.text for block in response.content if block.type == "text")
data = json.loads(text)
```

Two changes from older examples matter here. First, on models where thinking is always on, such as Opus 5.5, `response.content[0]` may be a `thinking` block, so `response.content[0].text` breaks. Select the text block by type. Second, every object in the schema needs `"additionalProperties": false`. The API rejects other values with a 400 that names the problem.

With the SDK helpers you pass a type instead of a raw schema. In Python, `client.messages.parse()` takes a Pydantic model:

```python
from pydantic import BaseModel

class Language(BaseModel):
    name: str
    rank: int

class LanguageList(BaseModel):
    languages: list[Language]

response = client.messages.parse(
    model="claude-opus-5-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "List the top 3 programming languages by popularity"}],
    output_format=LanguageList,
)
print(response.parsed_output)
```

In TypeScript, wrap a Zod schema in `zodOutputFormat()`. Passing the bare Zod object as `schema` doesn't work:

```typescript
import Anthropic from "@anthropic-ai/sdk";
import { z } from "zod";
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";

const client = new Anthropic();

const LanguageList = z.object({
  languages: z.array(z.object({ name: z.string(), rank: z.number() })),
});

const response = await client.messages.parse({
  model: "claude-opus-5-5",
  max_tokens: 1024,
  messages: [{ role: "user", content: "List the top 3 programming languages by popularity" }],
  output_config: { format: zodOutputFormat(LanguageList) },
});
console.log(response.parsed_output);
```

The Python helper's `output_format=` argument is part of the SDK, not the API. Inside a raw request, the old top-level `output_format` parameter is deprecated. Per the [structured outputs page](https://platform.claude.com/docs/en/build-with-claude/structured-outputs), it now returns a 400 unless you add the `structured-outputs-2025-11-13` beta header, so send `output_config.format` instead.

Limits worth knowing before you switch:

- JSON outputs can't be combined with citations. A request with both returns 400.
- The first request with a new schema has extra latency while the grammar compiles. Compiled grammars are cached for 24 hours from last use.
- A safety refusal (`stop_reason: "refusal"`) or a cut-off reply (`stop_reason: "max_tokens"`) can return output that doesn't match the schema. Check `stop_reason` before you parse.

### Classification labels: an enum schema, or a strict tool with `tool_choice` auto

If the prefill was `Category:` and you parsed one word, put the allowed values in an `enum` field of a JSON schema. That is the shortest path, and it works on every model in the structured outputs list.

If you prefer a tool, don't replace the prefill with a forced tool call. On Opus 5.5, Sonnet 5.5, and Fable 5.1, `tool_choice` of type `any` or `tool` returns its own 400: `tool_choice: type "tool" and "any" are not supported for this model.` The migration guides recommend leaving `tool_choice` at `auto`, setting `strict: true`, and saying in the prompt when to use the tool:

```python
response = client.messages.create(
    model="claude-sonnet-5-5",
    max_tokens=1024,
    tools=[{
        "name": "record_category",
        "description": "Record the category of a support ticket",
        "strict": True,
        "input_schema": {
            "type": "object",
            "properties": {
                "category": {"type": "string", "enum": ["billing", "bug", "feature_request", "other"]}
            },
            "required": ["category"],
            "additionalProperties": False
        }
    }],
    tool_choice={"type": "auto"},
    messages=[{
        "role": "user",
        "content": "Classify this ticket with the record_category tool: 'I was charged twice this month.'"
    }],
)
```

With `auto`, the model can still answer in plain text, so handle a response that has no `tool_use` block. Structured outputs don't guarantee the capitalization of `enum` values, so compare labels case-insensitively. JSON outputs and strict tools can be used in the same request when an agent needs both.

### Skipping the preamble: instructions in the system prompt

A prefill such as `## Bug Analysis\n\n` or `Here is the summary:` forced the opening of the reply. Put that requirement in the system prompt and end the request with the user message:

```python
response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    system=(
        "You are a code review assistant. Answer directly, with no introduction. "
        "Start with the heading '## Bug Analysis', then list each bug with a severity "
        "and a suggested fix."
    ),
    messages=[{"role": "user", "content": "Analyze this function for bugs: ..."}],
)
```

The system prompt gives no schema guarantee. If downstream code parses the reply, use JSON outputs instead. If the reply is read by a person, the system prompt is the simplest replacement and works alongside citations, which JSON outputs don't. On Fable 5.1, the migration guide recommends system prompt instructions as the replacement for prefill.

### Continuing an interrupted answer: move the partial text into the user turn

Prefill used to be the way to resume a reply that stopped on `max_tokens`, a timeout, or a dropped stream: send the partial text back as an assistant message and let Claude keep writing. On the new models, keep the history, then end with a user message that quotes where the reply stopped. Anthropic's suggested wording is "Your previous response was interrupted and ended with `[previous_response]`. Continue from where you left off."

```python
CONTINUE = (
    "Your previous response was interrupted and ended with:\n\n{tail}\n\n"
    "Continue from where you left off."
)

def continuation_request(history, partial_text):
    """history ends with the user turn that produced partial_text."""
    return history + [
        {"role": "assistant", "content": partial_text},
        {"role": "user", "content": CONTINUE.format(tail=partial_text[-400:])},
    ]
```

The partial reply stays in the history as a normal assistant turn, and the request now ends with a user message, so it is valid. The API no longer joins the two pieces for you. Stitch the continuation to the saved partial text in your own code, and check the seam for a repeated word or sentence.

![Continuation diagram that keeps a partial assistant reply in history, appends a user instruction, and joins the saved text with the new continuation in the application](https://blog.laozhang.ai/posts/en/claude-opus-prefill-error-fix/img/continue-with-user-turn.webp)

### Refusal steering and context reminders: say it in the user message

Prefills such as `Sure, here's how:` were meant to stop the model from declining a legitimate request. The Sonnet 5.5 guide says that clear instructions in the user message are usually enough, so spell out the task and the output you expect there instead of putting words in the model's mouth. Reminders that used to ride in a prefill, such as language, persona, or constraints from earlier in the conversation, also go in the user turn.

## Amazon Bedrock: no structured outputs on Claude in Amazon Bedrock

The replacements above assume structured outputs are available, and on Bedrock that depends on the integration. According to the [structured outputs page](https://platform.claude.com/docs/en/build-with-claude/structured-outputs), the legacy "Amazon Bedrock (Opus 4.6 and earlier)" integration supports them for Opus 4.6, Sonnet 4.6, Sonnet 4.5, Opus 4.5, and Haiku 4.5. The newer "Claude in Amazon Bedrock" integration does not. The Sonnet 5.5 guide repeats this for Sonnet 5.5 on Bedrock.

On that integration, replace a prefill with system or user instructions, or with a tool definition without `strict: true`, and validate the output in your code. If the model ignores the format now and then, a validation-and-retry loop is your guarantee. Bedrock reports request errors as `ValidationException`, so the prefill message may arrive inside that wrapper. For other Bedrock 400s, see [AWS Bedrock Claude 400 Error: Match the Exact Message to the Fix](https://blog.laozhang.ai/en/posts/aws-claude-400-error).

## Migrating a codebase: find every trailing assistant turn

Prefill is often hidden in a helper that builds `messages`, so searching for model IDs won't find it. Search for code that appends an assistant message right before the API call:

```bash
grep -rnE "role[\"']?[[:space:]]*[:=][[:space:]]*[\"']assistant" src/
```

Review each hit and ask whether that message can end up last in the request. Then add a guard where requests are built, so a regression fails in tests instead of production:

```python
def assert_ends_with_user(messages):
    if messages and messages[-1]["role"] == "assistant":
        raise ValueError(
            "Request ends with an assistant turn; Claude 4.6 and later models "
            "reject this with 'does not support assistant message prefill'."
        )
```

Don't gate the guard on a hardcoded list of model IDs. The list has grown with every release since Opus 4.6, and the replacements work on the older models too.

If you use Claude Code, the migration guides describe a bundled Claude API skill that does this work. Run `/claude-api migrate this project to claude-sonnet-5-5` (or your target model). The skill swaps model IDs and applies breaking parameter changes, prefill replacement, and effort calibration. It asks you to confirm the scope before editing files and ends with a checklist of items to verify by hand.

Moving to Opus 4.7 or later also brings other 400s, such as non-default sampling parameters and manual thinking budgets. [Claude Opus 4.7 vs 4.6: API Migration, Token Costs, and When to Upgrade](https://blog.laozhang.ai/en/posts/claude-opus-4-7-vs-claude-opus-4-6) covers the full list for that step.

## Why agent frameworks and routers hit the prefill 400

When the error comes from a library rather than your own request, it usually traces to one of two patterns. Both are public in issue trackers.

**Pattern A: resuming or falling back by prefill.** A router or agent that retries after an interrupted stream sends the partial reply back as an assistant message. That worked on older models and now fails on every hop. LiteLLM's mid-stream fallback did exactly this. [PR #30242](https://github.com/BerriAI/litellm/pull/30242) (opened June 11, 2026, still open as of October 5, 2026) describes every fallback hop on Sonnet and Opus 4.6+ returning `litellm.BadRequestError: AnthropicException - {"type":"invalid_request_error","message":"This model does not support assistant message prefill. ..."}`. It also lists the same breakage in livekit/agents#4907, crewAIInc/crewAI#4798, and agno-agi/agno#7015. The proposed fix marks those models `supports_assistant_prefill: false` and sends the partial reply in a trailing user message, which is Anthropic's continuation pattern. Dyad shipped the same change in [PR #3027](https://github.com/dyad-sh/dyad/pull/3027) (merged March 23, 2026). Its continuation retry now sends a user message containing the partial response.

**Pattern B: the transcript ends on an empty or stray assistant turn.** The client stores an assistant message with no text, for example after an aborted step, a heartbeat, or a UI event. It then sends the transcript with no fresh user input behind it. An [OpenClaw pull request (#69268)](https://github.com/openclaw/openclaw/pull/69268) describes this cause: a session whose tail was an assistant turn with no new user input. It proposed skipping the call in that state, on top of a patch that drops trailing empty or aborted assistant turns before sending. In [opencode issue #13768](https://github.com/anomalyco/opencode/issues/13768), contributors traced several reports to empty assistant messages left at the end of the prompt.

If you maintain the framework, the fix list is short:

1. Before every provider call, drop trailing assistant turns that have no text or tool content.
2. If a non-empty assistant turn would be last, keep it in history and append a user message with the continuation instruction.
3. If there is no new user input at all, don't send the request.
4. Track prefill support as a per-model capability that defaults to "not supported" for unknown Claude models. A default of "supported" breaks again with each new release.

Dyad's PR also notes that some non-Anthropic providers reject an assistant-final request too, so the user-message continuation is the safer default for every provider.

## Copilot, opencode, Databricks: what to do when you can't edit the request

Public reports of this error come from GitHub Copilot (Visual Studio Developer Community), opencode, Antigravity (Google AI Developers Forum, March 29, 2026), Databricks Foundation Model APIs (May 11, 2026), LibreChat agents (May 19, 2026), and OpenHands with `claude-opus-5-5` (September 2026). In all of them the tool builds the conversation, so the fix has to come from the tool. The opencode issue was auto-closed on September 2, 2026, and a comment on October 2, 2026 reports the error still happening on Opus 5.5.

The message often arrives wrapped, which makes it harder to search for:

| Where you see it | What it looks like |
| --- | --- |
| Claude API and SDKs | 400 `invalid_request_error` with the full message |
| LiteLLM | `litellm.BadRequestError: AnthropicException - {...}` with the message inside |
| GitHub Copilot endpoint (via opencode) | the message with `"code":"invalid_request_body"` |
| OpenRouter | `Provider returned error`; the upstream message appears in logs |
| Amazon Bedrock | `ValidationException` |
| Databricks FMAPI | HTTP 400 `BAD_REQUEST` with the message |

One opencode contributor also reported that a streaming request through a custom gateway came back as a 400 with an empty body. Replaying the same request without streaming showed the prefill message. If you see a bare 400 while streaming, that replay is a quick way to read the actual message.

Without access to the code, these are your options, roughly in order of effort:

1. **Update the tool.** Fixes land in tool releases, not in your settings. Check the changelog for "prefill" or "trailing assistant".
2. **Send a new message.** The conversation then ends with a user turn again. opencode users report that typing "continue" or "proceed" sometimes clears the error but not reliably, because the tool may append another assistant turn.
3. **Start a new conversation or session.** This clears a transcript stuck on a stray assistant turn.
4. **Switch the model the tool uses.** Sonnet 4.5, Haiku 4.5, and Opus 4.5 still accept prefill, and a non-Claude model avoids this Claude rule, although some other providers reject assistant-final requests too.
5. **Report it to the vendor with evidence.** Attach a session export or request log that shows the last outgoing message with `role: "assistant"`. That points to the cause faster than the error text alone.

## Other Claude 400 errors that look like the prefill error

All of these are `invalid_request_error` with status 400. The message text tells them apart:

| Message contains | Cause | Fix |
| --- | --- | --- |
| `does not support assistant message prefill` | Last message has `role: "assistant"` | End with a user message; replace the prefill by its job |
| `tool_choice: type "tool" and "any" are not supported for this model.` | Forced tool call on Opus 5.5, Sonnet 5.5, Fable 5.1, or Mythos 5.1 | `tool_choice` auto, a strict tool, and an instruction in the prompt |
| `` `thinking` or `redacted_thinking` blocks in the latest assistant message cannot be modified `` | Thinking blocks edited, reordered, or filtered before being sent back | Pass thinking blocks back exactly as received |
| `"thinking.type.enabled" is not supported for this model` | Manual thinking budget on Claude 4.7 or later | Adaptive thinking plus `output_config.effort` |
| `temperature` or `top_p` named in the message | Non-default sampling parameters on Opus 4.7 or later | [Claude Code Opus 4.7 top_p Deprecated? Fix the 400 Without Guesswork](https://blog.laozhang.ai/en/posts/claude-code-top-p-deprecated) and [Claude Opus 4.7 Temperature Parameter: Remove It, Do Not Retune It](https://blog.laozhang.ai/en/posts/claude-opus-4-7-temperature-parameter) |
| A `tool_use` ID without a matching `tool_result` | The user turn after a tool call lacks its result | Send a `tool_result` for every `tool_use` before adding new text |

The last row matters if you fix Pattern A by appending a user message. If the trailing assistant turn contains a `tool_use` block, a plain-text user message trades the prefill 400 for the missing-result 400. Return the tool result first.

A 500 is different: it is a server-side failure, not a problem with the request, and changing `messages` won't fix it. See [Claude API Error 500: Handle Internal Server Errors and Persistent Failures](https://blog.laozhang.ai/en/posts/claude-api-internal-server-error).

## Sources

External pages this guide links to, in the order they appear. Last updated 2026-10-05.

- [Anthropic's error reference](https://platform.claude.com/docs/en/api/errors) (platform.claude.com)
- [Opus 5.5](https://platform.claude.com/docs/en/models/opus-5-5/migration-guide) (platform.claude.com)
- [Sonnet 5.5](https://platform.claude.com/docs/en/models/sonnet-5-5/migration-guide) (platform.claude.com)
- [Fable 5.1](https://platform.claude.com/docs/en/models/fable-5-1/migration-guide) (platform.claude.com)
- [structured outputs page](https://platform.claude.com/docs/en/build-with-claude/structured-outputs) (platform.claude.com)
- [PR #30242](https://github.com/BerriAI/litellm/pull/30242) (github.com)
- [PR #3027](https://github.com/dyad-sh/dyad/pull/3027) (github.com)
- [OpenClaw pull request (#69268)](https://github.com/openclaw/openclaw/pull/69268) (github.com)
- [opencode issue #13768](https://github.com/anomalyco/opencode/issues/13768) (github.com)
