From 1d3627b8c378b5ddfa771e80d27351aec689960f Mon Sep 17 00:00:00 2001 From: autofixer Date: Sun, 28 Jun 2026 01:25:22 +0000 Subject: [PATCH] autofix: autofix/Ev0BEGQGU664-1782608388191 --- litellm/litellm_core_utils/token_counter.py | 67 +++++++++++++++---- .../litellm_core_utils/test_token_counter.py | 65 +++++++++++++----- 2 files changed, 103 insertions(+), 29 deletions(-) diff --git a/litellm/litellm_core_utils/token_counter.py b/litellm/litellm_core_utils/token_counter.py index c766c6edec1..bd4c4cad7c7 100644 --- a/litellm/litellm_core_utils/token_counter.py +++ b/litellm/litellm_core_utils/token_counter.py @@ -710,6 +710,47 @@ def _count_anthropic_content( return tokens +def _count_unknown_content_tokens( + content: Any, + count_function: TokenCounterFunction, + use_default_image_token_count: bool, + default_token_count: Optional[int], +) -> int: + """ + Best-effort token count for a content block whose `type` is unrecognized or + absent. Clients legitimately send custom or newer block types; rather than + crash, we recurse the block's values and count any recognized text, mirroring + how `_count_anthropic_content` traverses nested content lists. The `type` + discriminator itself never contributes prompt tokens, so it is skipped. + """ + if isinstance(content, str): + return count_function(content) + if not isinstance(content, Mapping): + return 0 + + num_tokens = 0 + for key, value in content.items(): + if key == "type": + continue + if isinstance(value, str): + num_tokens += count_function(value) + elif isinstance(value, list): + num_tokens += _count_content_list( + count_function, + value, # type: ignore + use_default_image_token_count, + default_token_count, + ) + elif isinstance(value, Mapping): + num_tokens += _count_unknown_content_tokens( + value, + count_function, + use_default_image_token_count, + default_token_count, + ) + return num_tokens + + def _count_content_list( count_function: TokenCounterFunction, content_list: OpenAIMessageContent, @@ -724,27 +765,30 @@ def _count_content_list( for c in content_list: if isinstance(c, str): num_tokens += count_function(c) - elif c["type"] == "text": + continue + + content_type = c.get("type") if isinstance(c, dict) else None + if content_type == "text": num_tokens += count_function(str(c.get("text", ""))) - elif c["type"] == "image_url": + elif content_type == "image_url": image_url = c.get("image_url") num_tokens += _count_image_tokens( image_url, use_default_image_token_count ) - elif c["type"] in ("tool_use", "tool_result"): + elif content_type in ("tool_use", "tool_result"): num_tokens += _count_anthropic_content( c, count_function, use_default_image_token_count, default_token_count, ) - elif c["type"] == "thinking": + elif content_type == "thinking": # Claude extended thinking content block # Count the thinking text and skip signature (opaque signature blob) thinking_text = str(c.get("thinking", "")) if thinking_text: num_tokens += count_function(thinking_text) - elif c["type"] == "tool_reference": + elif content_type == "tool_reference": # Anthropic tool-search reference block: a lightweight pointer to # a deferred tool, e.g. {"type": "tool_reference", "tool_name": ...}. # The full tool definition is counted via the `tools` param, so we @@ -756,14 +800,11 @@ def _count_content_list( if tool_name: num_tokens += count_function(tool_name) else: - content_type = ( - c.get("type", type(c).__name__) - if isinstance(c, dict) - else type(c).__name__ - ) - raise ValueError( - f"Invalid content item type: {content_type}. " - f"Expected str or dict with 'type' field (text, image_url, tool_use, tool_result, thinking, tool_reference)." + num_tokens += _count_unknown_content_tokens( + c, + count_function, + use_default_image_token_count, + default_token_count, ) return num_tokens except Exception as e: diff --git a/tests/test_litellm/litellm_core_utils/test_token_counter.py b/tests/test_litellm/litellm_core_utils/test_token_counter.py index 60e5a797627..1466945ad6f 100644 --- a/tests/test_litellm/litellm_core_utils/test_token_counter.py +++ b/tests/test_litellm/litellm_core_utils/test_token_counter.py @@ -1051,22 +1051,55 @@ def test_token_counter_with_tool_reference_block(): assert tokens_empty >= 0 -def test_count_content_list_rejects_unknown_type(): +def test_count_content_list_degrades_on_unknown_type(): """ - An unrecognized content block type must raise, and the error message must - enumerate the supported types (including `tool_reference`). This pins the - catch-all contract so a future block type isn't silently dropped. + Regression: an unrecognized content block type must NOT raise. Clients + legitimately send custom or newer block types; the counter must degrade + gracefully, still counting recognized text in the same message rather than + crashing the whole request. + + Covers the full failure class: + - a dict block with an unknown `type` value + - a dict block missing the `type` key entirely + - recognized text nested inside an unknown wrapper block """ - from litellm.litellm_core_utils.token_counter import _count_content_list - - with pytest.raises(ValueError) as exc_info: - _count_content_list( - count_function=len, - content_list=[{"type": "totally_unknown_block"}], - use_default_image_token_count=False, - default_token_count=None, - ) + model = "gpt-3.5-turbo" + text_block = {"type": "text", "text": "the quick brown fox jumps over the lazy dog"} + baseline = token_counter_new(model=model, messages=[{"role": "user", "content": [text_block]}]) + assert baseline > 0 + + # Unknown `type` value: must not raise and must not drop the recognized text. + unknown_block = {"type": "totally_made_up_block_type_v9", "data": "irrelevant payload"} + with_unknown = token_counter_new( + model=model, messages=[{"role": "user", "content": [text_block, unknown_block]}] + ) + assert isinstance(with_unknown, int) and with_unknown >= baseline + only_unknown = token_counter_new( + model=model, messages=[{"role": "user", "content": [unknown_block]}] + ) + assert isinstance(only_unknown, int) and only_unknown >= 0 + + # Dict block missing the `type` key entirely: must not raise at dispatch. + typeless_block = {"text": "extra payload that should not crash the counter"} + with_typeless = token_counter_new( + model=model, messages=[{"role": "user", "content": [text_block, typeless_block]}] + ) + assert isinstance(with_typeless, int) and with_typeless >= baseline - message = str(exc_info.value) - assert "Invalid content item type: totally_unknown_block" in message - assert "tool_reference" in message + # Recognized text nested inside an unknown wrapper must still be counted, + # mirroring how tool_use/tool_result recurse nested content lists. + def wrapper_with(nested_text): + return [ + { + "type": "custom_wrapper_block_v9", + "content": [{"type": "text", "text": nested_text}], + } + ] + + nested_full = token_counter_new( + model=model, messages=[{"role": "user", "content": wrapper_with("the quick brown fox " * 8)}] + ) + nested_empty = token_counter_new( + model=model, messages=[{"role": "user", "content": wrapper_with("")}] + ) + assert nested_full > nested_empty