From fac1192aa559f803a5eaf490163ded79eb999512 Mon Sep 17 00:00:00 2001 From: Armen Petrosyan Date: Mon, 26 Oct 2026 11:53:15 +0200 Subject: [PATCH] fix(anthropic): accept a tool_calls turn without content in count validation The chat translation merged in #47363 counts an assistant tool_calls turn, but validate_request still rejects that turn when it carries no content key. OpenAI leaves content optional there, and an SDK message dumped with exclude_none drops the key entirely, so a caller going through the config or the handler directly gets a ValueError on a turn every other layer now accepts Treat tool_calls as the content for that check. A turn with neither is still refused Relates to #26324 --- .../anthropic/count_tokens/transformation.py | 2 +- ...t_anthropic_count_tokens_transformation.py | 34 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/litellm/llms/anthropic/count_tokens/transformation.py b/litellm/llms/anthropic/count_tokens/transformation.py index 1c8f0c206cd..01d0c47bde6 100644 --- a/litellm/llms/anthropic/count_tokens/transformation.py +++ b/litellm/llms/anthropic/count_tokens/transformation.py @@ -488,5 +488,5 @@ class AnthropicCountTokensConfig: if "role" not in message: raise ValueError(f"Message {i} must have a 'role' field") - if "content" not in message: + if "content" not in message and "tool_calls" not in message: raise ValueError(f"Message {i} must have a 'content' field") diff --git a/tests/unit/llms/anthropic/test_anthropic_count_tokens_transformation.py b/tests/unit/llms/anthropic/test_anthropic_count_tokens_transformation.py index aa7fa2727b4..93b07f6e595 100644 --- a/tests/unit/llms/anthropic/test_anthropic_count_tokens_transformation.py +++ b/tests/unit/llms/anthropic/test_anthropic_count_tokens_transformation.py @@ -1019,3 +1019,37 @@ async def test_counter_leaves_out_context_management_without_edits(context_manag ANTHROPIC_BETA_HEADER_VALUES.COMPACT_2026_01_12.value, ANTHROPIC_BETA_HEADER_VALUES.CONTEXT_MANAGEMENT_2025_06_27.value, } + + +@pytest.mark.parametrize("config_type", (AnthropicCountTokensConfig, AzureAIAnthropicCountTokensConfig)) +def test_validate_request_accepts_a_tool_call_turn_without_content( + config_type: type[AnthropicCountTokensConfig], +) -> None: + """OpenAI leaves ``content`` optional on an assistant ``tool_calls`` turn, and an SDK dump that drops + None values leaves the key out, so the shape the chat translation already counts is not rejected here.""" + validated: Final = config_type().validate_request( + model="claude-opus-5-5", + messages=[ + {"role": "user", "content": "Weather in SF?"}, + { + "role": "assistant", + "tool_calls": [ + {"id": "call-1", "type": "function", "function": {"name": "get_weather", "arguments": "{}"}} + ], + }, + ], + ) + + assert validated is None + + +@pytest.mark.parametrize("config_type", (AnthropicCountTokensConfig, AzureAIAnthropicCountTokensConfig)) +def test_validate_request_still_requires_content_on_a_plain_turn( + config_type: type[AnthropicCountTokensConfig], +) -> None: + """A turn that carries neither content nor tool calls has nothing to count and is still refused.""" + with pytest.raises(ValueError, match="Message 1 must have a 'content' field"): + config_type().validate_request( + model="claude-opus-5-5", + messages=[{"role": "user", "content": "Weather in SF?"}, {"role": "assistant"}], + )