mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
Fix: add OpenAI-compatible API for Anthropic with modify_params=True (#17106)
* docs: add OpenAI-compatible API limitations for Anthropic thinking Document the fundamental incompatibility between Anthropic extended thinking and OpenAI-compatible API clients. Explains: - Why thinking_blocks must be resent (stateless vs stateful APIs) - OpenAI vs Anthropic architecture differences - Solutions for client developers * Update docs * fix: auto-drop thinking param when thinking_blocks missing When modify_params=True, LiteLLM now automatically drops the 'thinking' param if the last assistant message with tool_calls is missing thinking_blocks. This prevents the Anthropic error: "Expected thinking or redacted_thinking, but found tool_use" This workaround addresses the OpenAI-Anthropic API incompatibility where OpenAI-compatible clients don't preserve thinking_blocks.
This commit is contained in:
parent
bba229f922
commit
6ddc7875ea
3 changed files with 144 additions and 0 deletions
|
|
@ -114,6 +114,107 @@ curl http://0.0.0.0:4000/v1/chat/completions \
|
|||
|
||||
Here's how to use `thinking` blocks by Anthropic with tool calling.
|
||||
|
||||
### Important: OpenAI-Compatible API Limitations
|
||||
|
||||
:::warning Compatibility Notice
|
||||
|
||||
Anthropic extended thinking with tool calling is **not fully compatible** with OpenAI-compatible API clients. This is due to fundamental architectural differences between how OpenAI and Anthropic handle reasoning in multi-turn conversations.
|
||||
|
||||
:::
|
||||
|
||||
When using Anthropic models with `thinking` enabled and tool calling, you **must include `thinking_blocks`** from the previous assistant response when sending tool results back. Failure to do so will result in a `400 Bad Request` error.
|
||||
|
||||
**OpenAI vs Anthropic Architecture:**
|
||||
|
||||
| Provider | API Architecture | Reasoning Storage | Multi-turn Handling |
|
||||
|----------|------------------|-------------------|---------------------|
|
||||
| **OpenAI** (o1, o3) | Responses API (Stateful) | Server-side | Server stores reasoning internally; client sends `previous_response_id` |
|
||||
| **Anthropic** (Claude) | Messages API (Stateless) | Client-side | Client must store and resend `thinking_blocks` with every request |
|
||||
|
||||
|
||||
1. OpenAI's Chat Completions spec has **no field** for `thinking_blocks`
|
||||
2. OpenAI-compatible clients (LibreChat, Open WebUI, Vercel AI SDK, etc.) **ignore** the `thinking_blocks` field in responses
|
||||
3. When these clients reconstruct the assistant message for the next turn, the thinking blocks are lost
|
||||
4. Anthropic rejects the request because the assistant message doesn't start with a thinking block
|
||||
|
||||
:::tip LiteLLM supports thinking_blocks
|
||||
LiteLLM's `completion()` API **does support** sending `thinking_blocks` in assistant messages. If you're using LiteLLM directly (not through an OpenAI-compatible client), you can preserve and resend `thinking_blocks` and everything will work correctly.
|
||||
:::
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Use LiteLLM's built-in workaround** (recommended): Set `litellm.modify_params = True` and LiteLLM will automatically handle this incompatibility by dropping the `thinking` param when `thinking_blocks` are missing (see below)
|
||||
2. **For client developers**: Explicitly handle and resend the `thinking_blocks` field (see example below)
|
||||
3. **Disable extended thinking** when using tools with OpenAI-compatible clients that don't support `thinking_blocks`
|
||||
4. **Use Anthropic's native API** directly instead of OpenAI-compatible endpoints
|
||||
|
||||
### LiteLLM Built-in Workaround
|
||||
|
||||
LiteLLM can automatically handle this incompatibility when `modify_params=True` is set. If the client sends a request with `thinking` enabled but the assistant message with `tool_calls` is missing `thinking_blocks`, LiteLLM will automatically drop the `thinking` param for that turn to avoid the error.
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="sdk" label="SDK">
|
||||
|
||||
```python showLineNumbers
|
||||
import litellm
|
||||
|
||||
# Enable automatic parameter modification
|
||||
litellm.modify_params = True
|
||||
|
||||
# Now this will work even if thinking_blocks are missing from the assistant message
|
||||
response = litellm.completion(
|
||||
model="anthropic/claude-sonnet-4-20250514",
|
||||
thinking={"type": "enabled", "budget_tokens": 1024},
|
||||
tools=[...],
|
||||
messages=[
|
||||
{"role": "user", "content": "What's the weather in Madrid?"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"tool_calls": [{"id": "call_123", "type": "function", "function": {"name": "get_weather", "arguments": '{"city": "Madrid"}'}}]
|
||||
# Note: thinking_blocks is missing here - LiteLLM will handle it
|
||||
},
|
||||
{"role": "tool", "tool_call_id": "call_123", "content": "22°C sunny"}
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="proxy" label="PROXY">
|
||||
|
||||
```yaml showLineNumbers title="config.yaml"
|
||||
litellm_settings:
|
||||
modify_params: true # Enable automatic parameter modification
|
||||
|
||||
model_list:
|
||||
- model_name: claude-thinking
|
||||
litellm_params:
|
||||
model: anthropic/claude-sonnet-4-20250514
|
||||
thinking:
|
||||
type: enabled
|
||||
budget_tokens: 1024
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::info
|
||||
When `modify_params=True` and LiteLLM drops the `thinking` param, the model will **not** use extended thinking for that specific turn. The conversation will continue normally, but without reasoning for that response.
|
||||
:::
|
||||
|
||||
**Correct way to include `thinking_blocks`:**
|
||||
|
||||
```python
|
||||
# After receiving a response with tool_calls, include thinking_blocks when sending back:
|
||||
assistant_message = {
|
||||
"role": "assistant",
|
||||
"content": response.choices[0].message.content,
|
||||
"tool_calls": [...],
|
||||
"thinking_blocks": response.choices[0].message.thinking_blocks # ← Required!
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="sdk" label="SDK">
|
||||
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ from litellm.utils import (
|
|||
add_dummy_tool,
|
||||
get_max_tokens,
|
||||
has_tool_call_blocks,
|
||||
last_assistant_with_tool_calls_has_no_thinking_blocks,
|
||||
supports_reasoning,
|
||||
token_counter,
|
||||
)
|
||||
|
|
@ -999,6 +1000,20 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
llm_provider="anthropic",
|
||||
)
|
||||
|
||||
# Drop thinking param if thinking is enabled but thinking_blocks are missing
|
||||
# This prevents the error: "Expected thinking or redacted_thinking, but found tool_use"
|
||||
if (
|
||||
optional_params.get("thinking") is not None
|
||||
and messages is not None
|
||||
and last_assistant_with_tool_calls_has_no_thinking_blocks(messages)
|
||||
):
|
||||
if litellm.modify_params:
|
||||
optional_params.pop("thinking", None)
|
||||
litellm.verbose_logger.warning(
|
||||
"Dropping 'thinking' param because the last assistant message with tool_calls "
|
||||
"has no thinking_blocks. The model won't use extended thinking for this turn."
|
||||
)
|
||||
|
||||
headers = self.update_headers_with_optional_anthropic_beta(
|
||||
headers=headers, optional_params=optional_params
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6875,6 +6875,34 @@ def has_tool_call_blocks(messages: List[AllMessageValues]) -> bool:
|
|||
return False
|
||||
|
||||
|
||||
def last_assistant_with_tool_calls_has_no_thinking_blocks(
|
||||
messages: List[AllMessageValues],
|
||||
) -> bool:
|
||||
"""
|
||||
Returns true if the last assistant message with tool_calls has no thinking_blocks.
|
||||
|
||||
This is used to detect when thinking param should be dropped to avoid
|
||||
Anthropic error: "Expected thinking or redacted_thinking, but found tool_use"
|
||||
|
||||
When thinking is enabled, assistant messages with tool_calls must include thinking_blocks.
|
||||
If the client didn't preserve thinking_blocks, we need to drop the thinking param.
|
||||
|
||||
Related issues: https://github.com/BerriAI/litellm/issues/14194, https://github.com/BerriAI/litellm/issues/9020
|
||||
"""
|
||||
# Find the last assistant message with tool_calls
|
||||
last_assistant_with_tools = None
|
||||
for message in messages:
|
||||
if message.get("role") == "assistant" and message.get("tool_calls") is not None:
|
||||
last_assistant_with_tools = message
|
||||
|
||||
if last_assistant_with_tools is None:
|
||||
return False
|
||||
|
||||
# Check if it has thinking_blocks
|
||||
thinking_blocks = last_assistant_with_tools.get("thinking_blocks")
|
||||
return thinking_blocks is None or len(thinking_blocks) == 0
|
||||
|
||||
|
||||
def add_dummy_tool(custom_llm_provider: str) -> List[ChatCompletionToolParam]:
|
||||
"""
|
||||
Prevent Anthropic from raising error when tool_use block exists but no tools are provided.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue