litellm - 💡(How to fix) Fix [Bug]: Anthropic /v1/messages → Bedrock Converse 400 "text field is blank" when assistant message contains empty leading text block (Claude Code compatibility) [1 participants]

Official PRs (…)
ON THIS PAGE

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
BerriAI/litellm#26554Fetched 2026-04-27 05:29:35
View on GitHub
Comments
0
Participants
1
Timeline
3
Reactions
0
Author
Participants
Timeline (top)
labeled ×3

Error Message

BedrockException - {"message":"The text field in the ContentBlock object at messages.1.content.0 is blank. Add text to the text field, and try again."}

Root Cause

Root cause: the streamed response from LiteLLM contains a leading empty text block:

Fix Action

Workaround

Pin Claude Code to 2.1.92 — older versions happen not to replay empty text blocks in the same way, so the broken assistant turn never reaches Bedrock. Not a real fix; users need access to newer Claude Code features.

Code Example

BedrockException - {"message":"The text field in the ContentBlock object at
messages.1.content.0 is blank. Add text to the text field, and try again."}

---

event: content_block_start
data: {"type": "content_block_start", "index": 0,
       "content_block": {"type": "text", "text": ""}}

---

drop_params: true
  modify_params: true
  use_chat_completions_url_for_anthropic_messages: true

---

curl -sS -X POST "$LITELLM_URL/v1/messages" \
  -H "x-api-key: $LITELLM_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "global.anthropic.claude-opus-4-7",
    "max_tokens": 1024,
    "messages": [
      {"role": "user",  "content": [{"type":"text","text":"Write hello.txt with the word hi"}]},
      {"role": "assistant", "content": [
        {"type":"text","text":""},
        {"type":"thinking","thinking":"","signature":"<valid-signature-from-previous-response>"},
        {"type":"tool_use","id":"tooluse_abc","name":"Write",
         "input":{"file_path":"/tmp/hello.txt","content":"hi"}}
      ]},
      {"role": "user", "content": [
        {"type":"tool_result","tool_use_id":"tooluse_abc",
         "content":"permission denied","is_error":true}
      ]}
    ]
  }'

---

{"error":{"message":"litellm.BadRequestError: BedrockException - {\"message\":\"The text field in the ContentBlock object at messages.1.content.0 is blank. Add text to the text field, and try again.\"}. Received Model Group=global.anthropic.claude-opus-4-7\nAvailable Model Group Fallbacks=None","type":null,"param":null,"code":"400"}}

---

unset CLAUDE_CODE_EXECPATH
export ANTHROPIC_BASE_URL="$LITELLM_URL"
export ANTHROPIC_AUTH_TOKEN="$LITELLM_KEY"
export ANTHROPIC_MODEL="global.anthropic.claude-opus-4-7"

claude --print "Create a tiny file called hello.txt with the word hi in it, then read it back to me, then think about whether file was created successfully."

---

API Error: 400 {"error":{"message":"litellm.BadRequestError: BedrockException - {\"message\":\"The text field in the ContentBlock object at messages.1.content.0 is blank. ...

---

event: content_block_start
data: {"type": "content_block_start", "index": 0,
       "content_block": {"type": "text", "text": ""}}

event: content_block_start
data: {"type": "content_block_start", "index": 1,
       "content_block": {"type": "thinking", "thinking": "",
                         "signature": "Eu8B...<valid-base64-signature>"}}

event: content_block_start
data: {"type": "content_block_start", "index": 2,
       "content_block": {"type": "tool_use",
                         "id": "tooluse_REDACTED", "name": "Write", "input": {}}}

---

{
  "model": "global.anthropic.claude-opus-4-7",
  "messages": [
    {"role": "user", "content": [
      {"type": "text", "text": "<system-reminder>... (trimmed — Claude Code boilerplate)</system-reminder>"},
      {"type": "text", "text": "<system-reminder>... (trimmed — Claude Code boilerplate)</system-reminder>"},
      {"type": "text", "text": "Create a tiny file called hello.txt with the word hi in it, then read it back to me, then think about whether file was created successfully."}
    ]},
    {"role": "assistant", "content": [
      {"type": "text", "text": ""},
      {"type": "thinking", "thinking": "", "signature": "Eu8B...<valid-signature>"},
      {"type": "tool_use", "id": "tooluse_REDACTED", "name": "Write",
       "input": {"file_path": "/tmp/hello.txt", "content": "hi\n"}}
    ]},
    {"role": "user", "content": [
      {"type": "tool_result",
       "content": "Claude requested permissions to write to /tmp/hello.txt, but you haven't granted it yet.",
       "is_error": true,
       "tool_use_id": "tooluse_REDACTED",
       "cache_control": {"type": "ephemeral"}}
    ]}
  ],
  "max_tokens": 64000
}

---

{"error":{"message":"litellm.BadRequestError: BedrockException - {\"message\":\"The text field in the ContentBlock object at messages.1.content.0 is blank. Add text to the text field, and try again.\"}. Received Model Group=global.anthropic.claude-opus-4-7\nAvailable Model Group Fallbacks=None","type":null,"param":null,"code":"400"}}
RAW_BUFFERClick to expand / collapse

[Bug]: Anthropic /v1/messages → Bedrock Converse 400 "text field is blank" when assistant message contains empty leading text block (Claude Code compatibility)

What happened

When Claude Code (Anthropic SDK / /v1/messages endpoint) sends a multi-turn request through LiteLLM to Bedrock Converse, Bedrock rejects it with:

BedrockException - {"message":"The text field in the ContentBlock object at
messages.1.content.0 is blank. Add text to the text field, and try again."}

Root cause: the streamed response from LiteLLM contains a leading empty text block:

event: content_block_start
data: {"type": "content_block_start", "index": 0,
       "content_block": {"type": "text", "text": ""}}

…with no subsequent content_block_delta text for index 0. Claude Code (and the Anthropic SDK generally) stores that empty text block as part of the assistant turn. On the next request, Claude Code replays the assistant message verbatim as history. The replayed message now has messages[1].content[0] = {"type":"text","text":""} — which Bedrock's Converse API validates and rejects.

The fix from #20390 / #15850 (litellm_core_utils/prompt_templates/factory.py) addresses this for the OpenAI chat/completions → Bedrock Converse path, but the Anthropic /v1/messages → Bedrock path does not apply the same empty-text filter. I verified on ghcr.io/berriai/litellm:main-v1.83.10-stable that litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py has zero empty-text / whitespace guards.

Users experience this as breakage of newer Claude Code versions against a LiteLLM proxy in front of Bedrock Claude. Pinning Claude Code to 2.1.92 is a known workaround; later versions fail.

Relates to #20364, #20390, #15850, #22797, #22847.

Relevant issues / PRs

  • Closed by #20390 (Feb 4, 2026) — but fix applies only to the OpenAI-shim path, not /v1/messages.
  • Predecessor PR #15850 (Nov 1, 2025) — same scope (chat/completions → Converse only).
  • Issue #20364 — same error text, same class of bug, on the Converse path.
  • The /v1/messages → Bedrock translator already has Claude-Code-specific patches (#22797 output_config stripping, #22847 custom field stripping) but not this empty-text filter.

Steps to reproduce

Environment

  • LiteLLM image: ghcr.io/berriai/litellm:main-v1.83.10-stable (also reproduces on v1.83.3).
  • Model: global.anthropic.claude-opus-4-7 declared as bedrock/converse/global.anthropic.claude-opus-4-7 in config.yaml.
  • Relevant litellm_settings:
    drop_params: true
    modify_params: true
    use_chat_completions_url_for_anthropic_messages: true
  • Client: Claude Code 2.1.119.265 (also reproduces on 2.1.119+; does not reproduce on 2.1.92).

Reproducer (curl) — directly reproduce the failing request shape

The failing request replays the assistant turn that LiteLLM returned on a previous streamed call. You can reproduce end-to-end with Claude Code, or bypass the client by sending the exact shape directly:

curl -sS -X POST "$LITELLM_URL/v1/messages" \
  -H "x-api-key: $LITELLM_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "global.anthropic.claude-opus-4-7",
    "max_tokens": 1024,
    "messages": [
      {"role": "user",  "content": [{"type":"text","text":"Write hello.txt with the word hi"}]},
      {"role": "assistant", "content": [
        {"type":"text","text":""},
        {"type":"thinking","thinking":"","signature":"<valid-signature-from-previous-response>"},
        {"type":"tool_use","id":"tooluse_abc","name":"Write",
         "input":{"file_path":"/tmp/hello.txt","content":"hi"}}
      ]},
      {"role": "user", "content": [
        {"type":"tool_result","tool_use_id":"tooluse_abc",
         "content":"permission denied","is_error":true}
      ]}
    ]
  }'

Expected response (bug):

{"error":{"message":"litellm.BadRequestError: BedrockException - {\"message\":\"The text field in the ContentBlock object at messages.1.content.0 is blank. Add text to the text field, and try again.\"}. Received Model Group=global.anthropic.claude-opus-4-7\nAvailable Model Group Fallbacks=None","type":null,"param":null,"code":"400"}}

End-to-end reproducer with Claude Code

unset CLAUDE_CODE_EXECPATH
export ANTHROPIC_BASE_URL="$LITELLM_URL"
export ANTHROPIC_AUTH_TOKEN="$LITELLM_KEY"
export ANTHROPIC_MODEL="global.anthropic.claude-opus-4-7"

claude --print "Create a tiny file called hello.txt with the word hi in it, then read it back to me, then think about whether file was created successfully."

Expected terminal output:

API Error: 400 {"error":{"message":"litellm.BadRequestError: BedrockException - {\"message\":\"The text field in the ContentBlock object at messages.1.content.0 is blank. ...

The multi-turn path is required to trigger the bug: Claude Code sends an initial request, receives a streamed assistant turn that starts with an empty content_block_start text block, then sends a follow-up request that replays that turn as history — and the follow-up is what Bedrock rejects.

Evidence — captured request/response bodies

A minimal logging proxy in front of LiteLLM captured all three calls. The sequence on LiteLLM 1.83.10-stable:

#EndpointStatusNotes
1POST /v1/messages?beta=true200Claude Code's session-title request to Haiku. Unrelated.
2POST /v1/messages?beta=true200First real turn → Opus 4.7. Assistant streamed [text:"", thinking:"", tool_use(Write)].
3POST /v1/messages?beta=true400Replay of #2's assistant turn as history → Bedrock rejects empty messages.1.content.0.

Response from call #2 — content_block_start events from LiteLLM

event: content_block_start
data: {"type": "content_block_start", "index": 0,
       "content_block": {"type": "text", "text": ""}}

event: content_block_start
data: {"type": "content_block_start", "index": 1,
       "content_block": {"type": "thinking", "thinking": "",
                         "signature": "Eu8B...<valid-base64-signature>"}}

event: content_block_start
data: {"type": "content_block_start", "index": 2,
       "content_block": {"type": "tool_use",
                         "id": "tooluse_REDACTED", "name": "Write", "input": {}}}

No content_block_delta of type text_delta for index 0 follows — the text block remains empty. The thinking block has a valid signature but empty text.

Request body of call #3 (the failing request)

{
  "model": "global.anthropic.claude-opus-4-7",
  "messages": [
    {"role": "user", "content": [
      {"type": "text", "text": "<system-reminder>... (trimmed — Claude Code boilerplate)</system-reminder>"},
      {"type": "text", "text": "<system-reminder>... (trimmed — Claude Code boilerplate)</system-reminder>"},
      {"type": "text", "text": "Create a tiny file called hello.txt with the word hi in it, then read it back to me, then think about whether file was created successfully."}
    ]},
    {"role": "assistant", "content": [
      {"type": "text", "text": ""},
      {"type": "thinking", "thinking": "", "signature": "Eu8B...<valid-signature>"},
      {"type": "tool_use", "id": "tooluse_REDACTED", "name": "Write",
       "input": {"file_path": "/tmp/hello.txt", "content": "hi\n"}}
    ]},
    {"role": "user", "content": [
      {"type": "tool_result",
       "content": "Claude requested permissions to write to /tmp/hello.txt, but you haven't granted it yet.",
       "is_error": true,
       "tool_use_id": "tooluse_REDACTED",
       "cache_control": {"type": "ephemeral"}}
    ]}
  ],
  "max_tokens": 64000
}

messages[1].content[0] = {"type":"text","text":""} is what Bedrock flags as blank.

Response body of call #3

{"error":{"message":"litellm.BadRequestError: BedrockException - {\"message\":\"The text field in the ContentBlock object at messages.1.content.0 is blank. Add text to the text field, and try again.\"}. Received Model Group=global.anthropic.claude-opus-4-7\nAvailable Model Group Fallbacks=None","type":null,"param":null,"code":"400"}}

Why the existing fix does not apply

I scanned ghcr.io/berriai/litellm:main-v1.83.10-stable:

  • litellm/litellm_core_utils/prompt_templates/factory.py — has four occurrences of the empty-string filter introduced by #20390:
    • L4589, L4619, L4958, L4986: # Skip completely empty strings to avoid blank content blocks
    • L4590, L4959: if element.get("text", "").strip():
    • L4620, L4987: if _assistant_content.strip():
  • litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py (the translator used for /v1/messages → Bedrock): zero occurrences of any empty-text / whitespace / strip guard across 755 lines. transform_anthropic_messages_request() forwards blocks unchanged; AnthropicMessagesConfig.transform_anthropic_messages_request(...) is invoked on L457 without downstream empty-text filtering.

So the empty leading text block passes through into the Bedrock payload on the /v1/messages path.

Expected behaviour

Option A — output-side fix (preferred): LiteLLM should not emit a content_block_start with {"type":"text","text":""} when no actual text will follow. This makes the response round-trippable through Anthropic-format history on any provider.

Option B — input-side fix: Apply the same empty-text filter from #20390 to the /v1/messages → Bedrock path, i.e. inside or before AnthropicMessagesConfig.transform_anthropic_messages_request(...), for assistant messages whose content list contains {"type":"text","text":""} or whitespace-only text blocks.

Either fix, applied independently, would resolve the 400. Option A also benefits any downstream consumer of LiteLLM's Anthropic-format responses, not just Bedrock.

Workaround

Pin Claude Code to 2.1.92 — older versions happen not to replay empty text blocks in the same way, so the broken assistant turn never reaches Bedrock. Not a real fix; users need access to newer Claude Code features.

What part of LiteLLM is this about?

Proxy

What LiteLLM version are you on?

Reproduced on:

  • ghcr.io/berriai/litellm:main-v1.83.10-stable
  • ghcr.io/berriai/litellm:v1.83.3-stable

extent analysis

TL;DR

Apply an empty-text filter to the /v1/messages → Bedrock path, similar to the fix in #20390, to prevent empty text blocks from being sent to Bedrock.

Guidance

  • Identify the transform_anthropic_messages_request function in litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py and add a check to remove or filter out empty text blocks ({"type":"text","text":""}) from the assistant message content.
  • Verify that the filter is applied correctly by checking the request body of the failing request (call #3) and ensuring that the empty text block is removed.
  • Consider implementing the output-side fix (Option A) to prevent LiteLLM from emitting empty content_block_start events in the first place.
  • Test the fix by running the end-to-end reproducer with Claude Code and verifying that the 400 error is no longer returned.

Example

def transform_anthropic_messages_request(...):
    # ...
    assistant_content = [...]
    # Filter out empty text blocks
    assistant_content = [block for block in assistant_content if block.get("type") != "text" or block.get("text").strip()]
    # ...

Notes

The existing fix in #20390 only applies to the OpenAI-shim path, and the /v1/messages → Bedrock path needs a similar fix. The workaround of pinning Claude Code to 2.1.92 is not a real fix and may not be feasible for all users.

Recommendation

Apply the workaround of pinning Claude Code to 2.1.92 temporarily, and then apply the empty-text filter to the /v1/messages → Bedrock path to prevent empty text blocks from being sent to Bedrock. This will ensure that the 400 error is resolved and users can access newer Claude

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING