diff --git a/litellm/llms/anthropic/pass_through/adapters/transformation.py b/litellm/llms/anthropic/pass_through/adapters/transformation.py index 022bc6337b5..7ff83954f5c 100644 --- a/litellm/llms/anthropic/pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/pass_through/adapters/transformation.py @@ -636,6 +636,25 @@ class LiteLLMAnthropicMessagesAdapter: assistant_message["reasoning_content"] = reasoning_content new_messages.append(assistant_message) + has_conversational_input = any(m.get("role") != "system" for m in replayable_messages) + has_translated_conversation = any(m.get("role") != "system" for m in new_messages) + if has_conversational_input and not has_translated_conversation: + unrecognized_block_types = sorted( + { + str(content.get("type")) + for message in replayable_messages + if message.get("role") != "system" and isinstance(message.get("content"), list) + for content in message["content"] + if isinstance(content, dict) and content.get("type") is not None + } + ) + type_list = ", ".join(json.dumps(block_type) for block_type in unrecognized_block_types) or "unknown" + raise litellm.BadRequestError( + message=f"Anthropic pass-through: every message's content blocks were of unrecognized types ({type_list}), so translation produced an empty conversation. Refusing to dispatch a request with no messages.", + model=model or "", + llm_provider="anthropic", + ) + return new_messages @staticmethod diff --git a/tests/unit/llms/anthropic/pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py b/tests/unit/llms/anthropic/pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py index 06aaa4e61fb..2ded8e9aa90 100644 --- a/tests/unit/llms/anthropic/pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py +++ b/tests/unit/llms/anthropic/pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py @@ -5167,3 +5167,49 @@ def test_eager_input_streaming_tool_reaches_bedrock_converse_as_beta(): assert data["additionalModelRequestFields"]["anthropic_beta"] == ["fine-grained-tool-streaming-2025-05-14"] assert data["toolConfig"]["tools"][0]["toolSpec"]["inputSchema"]["json"] == EAGER_INPUT_SCHEMA + + +def test_translate_anthropic_to_openai_rejects_all_unrecognized_content_blocks(): + request = { + "model": "openai/gpt-4o", + "max_tokens": 1024, + "messages": [{"role": "user", "content": [{"type": "not_a_real_block", "text": "hello"}]}], + } + + with pytest.raises(litellm.BadRequestError, match="not_a_real_block"): + LiteLLMAnthropicMessagesAdapter().translate_anthropic_to_openai(request) + + control_chars = { + **request, + "messages": [{"role": "user", "content": [{"type": "forged\nERROR: injected"}]}], + } + with pytest.raises(litellm.BadRequestError) as exc_info: + LiteLLMAnthropicMessagesAdapter().translate_anthropic_to_openai(control_chars) + assert "forged\\nERROR: injected" in str(exc_info.value) + assert "forged\nERROR: injected" not in str(exc_info.value) + + with_system = { + **request, + "messages": [ + {"role": "system", "content": "keep this"}, + *request["messages"], + ], + } + with pytest.raises(litellm.BadRequestError): + LiteLLMAnthropicMessagesAdapter().translate_anthropic_to_openai(with_system) + + mixed = { + "model": "openai/gpt-4o", + "max_tokens": 1024, + "messages": [ + { + "role": "user", + "content": [ + {"type": "not_a_real_block", "text": "ignored"}, + {"type": "text", "text": "hello"}, + ], + } + ], + } + openai_request, _ = LiteLLMAnthropicMessagesAdapter().translate_anthropic_to_openai(mixed) + assert openai_request["messages"] == [{"role": "user", "content": [{"type": "text", "text": "hello"}]}]