litellm - ✅(Solved) Fix [Bug]: Anthropic multi-turn tool call history destroyed when messages use Anthropic content format (tool_use/tool_result in content blocks) [2 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

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#25669Fetched 2026-04-14 05:38:16
View on GitHub
Comments
0
Participants
1
Timeline
3
Reactions
0
Author
Participants
Timeline (top)
labeled ×3

Error Message

litellm.BadRequestError: AnthropicException - tool_use ids were found without tool_result blocks immediately after: toolu_01xxx litellm.APIConnectionError: Invalid user message at index 2 - invalid content type=tool_result

PR fix notes

PR #25765: fix: handle Anthropic-format tool_use/tool_result content blocks in message sanitization

Description (problem / solution / changelog)

Summary

When clients send multi-turn conversations using Anthropic content block format (tool_use in assistant content list, tool_result in user content list), _is_orphaned_tool_result() only checks the OpenAI-style tool_calls array for matching tool IDs. Every tool_result is incorrectly flagged as orphaned and stripped, destroying conversation history.

Root Cause

  1. _is_orphaned_tool_result() in factory.py only checks prev_msg.get("tool_calls") (OpenAI format). It never checks content blocks for type=tool_use, so Anthropic-format tool calls are invisible to the orphan detection logic.

  2. ValidUserMessageContentTypes in openai.py does not include "tool_result", causing validate_chat_completion_user_messages() to reject Anthropic-format user messages before they reach the provider.

Fix

  • _is_orphaned_tool_result(): After checking tool_calls, also iterate over assistant message content blocks looking for type=tool_use with a matching id.
  • ValidUserMessageContentTypes and ValidUserMessageContentTypesLiteral: Add "tool_result" so Anthropic-format user messages pass validation.

Reproduction

import openai
client = openai.OpenAI(base_url="http://localhost:4000", api_key="sk-xxx")
client.chat.completions.create(
    model="anthropic/claude-sonnet-4-6",
    messages=[
        {"role": "user", "content": "run ls"},
        {"role": "assistant", "content": [
            {"type": "text", "text": "Running ls"},
            {"type": "tool_use", "id": "toolu_01xxx", "name": "Shell", "input": {"command": "ls"}}
        ]},
        {"role": "user", "content": [
            {"type": "tool_result", "tool_use_id": "toolu_01xxx", "content": [{"type": "text", "text": "file.txt"}]}
        ]}
    ],
    tools=[{"type": "function", "function": {"name": "Shell", "description": "run shell", "parameters": {"type": "object", "properties": {"command": {"type": "string"}}}}}]
)

Fixes #25669

Made with Cursor

Changed files

  • litellm/litellm_core_utils/prompt_templates/factory.py (modified, +11/-0)
  • litellm/types/llms/openai.py (modified, +5/-2)
  • tests/test_litellm/llms/anthropic/test_anthropic_tool_content.py (added, +110/-0)

PR #25853: fix(bedrock): handle orphaned tool blocks caused by conversation compaction

Description (problem / solution / changelog)

Summary

Fixes Bedrock Converse API failures when clients like OpenAI Codex CLI compact long conversations. Compaction truncates history, leaving behind orphaned toolUse/toolResult blocks that Bedrock rejects.

Errors fixed:

  • "tool_use ids were found without tool_result blocks"
  • "The toolConfig field must be defined when using toolUse and toolResult content blocks."
  • "tool_choice.type: Field required" (follow-on from injected toolConfig missing toolChoice)

Related issues: #25669, #24361

Changes

All fixes are gated on modify_params=True and follow existing LiteLLM patterns.

1. litellm/utils.pyhas_tool_call_blocks()

Extended to detect Anthropic-format content blocks (type="tool_use" / type="tool_result") and role="tool" messages, in addition to the existing OpenAI tool_calls array check. Previously, when a Responses API request arrived with Anthropic-format tool content, the function returned False and _transform_request_helper skipped injecting toolConfig.

2. litellm/litellm_core_utils/prompt_templates/factory.py

Applied to both _bedrock_converse_messages_pt (sync) and _bedrock_converse_messages_pt_async (async):

a. Sort assistant content blocks (inside loop, after deduplication): Ensures order reasoningContent → text → toolUse within each assistant message. Bedrock rejects requests where text blocks appear after toolUse blocks, as it interprets them as breaking the toolUse→toolResult pairing.

b. Tool pairing sanitization (post-loop):

  • Pass 1: For each assistant message with orphaned toolUse blocks (no matching toolResult), collect all orphaned IDs in order, then inject dummy toolResult blocks as a single batch into the next user message (or a newly inserted user message). Batch injection is important — injecting one at a time via prepend reverses the order, and Bedrock requires toolResult order to match toolUse order.
  • Pass 2: Remove toolResult blocks whose toolUseId has no matching toolUse in any assistant message. If a user message becomes empty, replace its content with [BedrockContentBlock(text=" ")] (single space — Bedrock rejects empty strings).

3. litellm/llms/bedrock/chat/converse_transformation.py

Applied to both _transform_request (sync) and _async_transform_request (async):

After building bedrock_messages, if tool blocks are now present (injected by fix #2) but toolConfig is absent (because the original OpenAI messages had no tool blocks, so _transform_request_helper never added it), inject a dummy toolConfig with:

  • A single ToolBlock(toolSpec=...) with empty input schema
  • toolChoice=ToolChoiceValuesBlock(auto={}) — required by Bedrock/Claude; omitting it causes "tool_choice.type: Field required"

Test plan

  • Verify no regression on normal tool-calling requests (tools present in original messages)
  • Verify compacted conversations with orphaned toolUse no longer hit "tool_use ids were found without tool_result blocks"
  • Verify compacted conversations with orphaned toolResult have them silently removed
  • Verify toolConfig is injected when tool blocks exist after compaction sanitization
  • Verify assistant messages with multiple parallel tool calls inject toolResult blocks in correct order

🤖 Generated with Claude Code

Changed files

  • litellm/litellm_core_utils/prompt_templates/factory.py (modified, +457/-814)
  • litellm/llms/bedrock/chat/converse_transformation.py (modified, +60/-0)
  • litellm/utils.py (modified, +15/-0)
  • tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py (modified, +260/-0)

Code Example

import openai
client = openai.OpenAI(base_url="http://localhost:4000", api_key="sk-xxx")
client.chat.completions.create(
    model="anthropic/claude-sonnet-4-6",
    messages=[
        {"role": "user", "content": "run ls"},
        {"role": "assistant", "content": [
            {"type": "text", "text": "Running ls"},
            {"type": "tool_use", "id": "toolu_01xxx", "name": "Shell", "input": {"command": "ls"}}
        ]},
        {"role": "user", "content": [
            {"type": "tool_result", "tool_use_id": "toolu_01xxx", "content": [{"type": "text", "text": "file.txt"}]}
        ]}
    ],
    tools=[{"type": "function", "function": {"name": "Shell", "description": "run shell", "parameters": {"type": "object", "properties": {"command": {"type": "string"}}}}}]
)

---

litellm.BadRequestError: AnthropicException - tool_use ids were found without tool_result blocks immediately after: toolu_01xxx
litellm.APIConnectionError: Invalid user message at index 2 - invalid content type=tool_result
RAW_BUFFERClick to expand / collapse

Check for existing issues

  • I have searched the existing issues and checked that my issue is not a duplicate.

What happened?

When using LiteLLM's /v1/chat/completions endpoint with Anthropic models, multi-turn conversations involving tool calls break in several ways when the client sends messages in Anthropic content block format (i.e. tool_use in assistant content list, tool_result in user content list) rather than OpenAI tool_calls array format:

  1. _is_orphaned_tool_result() in sanitize_messages_for_tool_calling() only checks tool_calls array for matching tool IDs. It never checks content[type=tool_use] blocks. Result: every tool_result message is flagged as orphaned and stripped, destroying conversation history. The model never sees tool results and re-runs the same tools indefinitely in a loop.

  2. anthropic_messages_pt() processes assistant messages with a content list but has no handler for type=tool_use blocks. They are silently dropped. Same loop effect.

  3. anthropic_messages_pt() processes user messages with a content list but has no handler for type=tool_result blocks. They are silently dropped. Anthropic then errors: tool_use ids were found without tool_result blocks immediately after.

  4. ValidUserMessageContentTypes does not include tool_result, causing validate_chat_completion_user_messages() to reject these messages before they even reach the provider.

  5. anthropic_messages_pt() can produce a trailing assistant message after transforming tool call history, causing Anthropic to reject with conversation must end with a user message.

Expected: Multi-turn conversations with tool calls should work correctly regardless of whether the client sends tool calls in OpenAI format (tool_calls array) or Anthropic format (content blocks).

Files affected:

  • litellm/litellm_core_utils/prompt_templates/factory.py_is_orphaned_tool_result(), anthropic_messages_pt()
  • litellm/types/llms/openai.pyValidUserMessageContentTypes

Steps to Reproduce

  1. Configure LiteLLM proxy with an Anthropic model
  2. Send a multi-turn conversation where assistant messages use content: [{type: "tool_use", id: "toolu_xxx", ...}] and user messages use content: [{type: "tool_result", tool_use_id: "toolu_xxx", ...}]
  3. Observe that tool results are stripped, the model loops re-running the same tools, and eventually errors

This is reproducible using Cursor IDE with a custom LiteLLM base URL, which sends messages in Anthropic content block format.

Minimal reproduce:

import openai
client = openai.OpenAI(base_url="http://localhost:4000", api_key="sk-xxx")
client.chat.completions.create(
    model="anthropic/claude-sonnet-4-6",
    messages=[
        {"role": "user", "content": "run ls"},
        {"role": "assistant", "content": [
            {"type": "text", "text": "Running ls"},
            {"type": "tool_use", "id": "toolu_01xxx", "name": "Shell", "input": {"command": "ls"}}
        ]},
        {"role": "user", "content": [
            {"type": "tool_result", "tool_use_id": "toolu_01xxx", "content": [{"type": "text", "text": "file.txt"}]}
        ]}
    ],
    tools=[{"type": "function", "function": {"name": "Shell", "description": "run shell", "parameters": {"type": "object", "properties": {"command": {"type": "string"}}}}}]
)

Relevant log output

litellm.BadRequestError: AnthropicException - tool_use ids were found without tool_result blocks immediately after: toolu_01xxx
litellm.APIConnectionError: Invalid user message at index 2 - invalid content type=tool_result

What part of LiteLLM is this about?

Proxy

What LiteLLM version are you on ?

1.82.3

Twitter / LinkedIn details

No response

extent analysis

TL;DR

The issue can be fixed by modifying the LiteLLM proxy to correctly handle Anthropic content block format for tool calls, including updating _is_orphaned_tool_result() and anthropic_messages_pt() functions.

Guidance

  • Update the _is_orphaned_tool_result() function in litellm/litellm_core_utils/prompt_templates/factory.py to check for tool_use blocks in the content list, not just the tool_calls array.
  • Modify the anthropic_messages_pt() function to handle type=tool_use and type=tool_result blocks in assistant and user messages, respectively.
  • Add tool_result to the ValidUserMessageContentTypes in litellm/types/llms/openai.py to prevent rejection of user messages with tool_result content.
  • Verify that the conversation history is preserved and tool results are correctly processed after applying these changes.

Example

# Modified _is_orphaned_tool_result() function
def _is_orphaned_tool_result(message):
    # Check for tool_use blocks in content list
    tool_use_ids = [block['id'] for block in message['content'] if block['type'] == 'tool_use']
    # Check for matching tool_result blocks
    tool_result_ids = [block['tool_use_id'] for block in message['content'] if block['type'] == 'tool_result']
    return not any(id in tool_result_ids for id in tool_use_ids)

Notes

The provided code snippet and log output suggest that the issue is specific to the LiteLLM proxy and its handling of Anthropic content block format. The modifications suggested above should fix the issue, but further testing may be necessary to ensure that all edge cases are covered.

Recommendation

Apply the suggested modifications to the _is_orphaned_tool_result() and anthropic_messages_pt() functions, and add tool_result to the ValidUserMessageContentTypes. This should fix the issue and allow multi-turn conversations with tool calls to work correctly regardless of the format used.

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

litellm - ✅(Solved) Fix [Bug]: Anthropic multi-turn tool call history destroyed when messages use Anthropic content format (tool_use/tool_result in content blocks) [2 pull requests, 1 participants]