Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 7 additions & 15 deletions litellm/litellm_core_utils/token_counter.py
Original file line number Diff line number Diff line change
Expand Up @@ -724,27 +724,29 @@ 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 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
Expand All @@ -755,16 +757,6 @@ def _count_content_list(
tool_name = str(c.get("tool_name") or "")
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)."
)
return num_tokens
except Exception as e:
if default_token_count is not None:
Expand Down
58 changes: 44 additions & 14 deletions tests/test_litellm/litellm_core_utils/test_token_counter.py
Original file line number Diff line number Diff line change
Expand Up @@ -1051,22 +1051,52 @@ 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_skips_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 test: a non-standard content block (e.g. a custom block emitted
by a tool orchestrator) must NOT crash the token counter.

Before the fix, _count_content_list raised
`Invalid content item type: ...` on any unrecognized block, which bubbled
up as `Error getting number of tokens from content list` and broke token
counting for the whole message. The counter must instead skip the unknown
block while still counting recognized siblings.
"""
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,
)
long_text = "the quick brown fox jumps over the lazy dog " * 20
text_only_tokens = _count_content_list(
count_function=len,
content_list=[{"type": "text", "text": long_text}],
use_default_image_token_count=False,
default_token_count=None,
)
assert text_only_tokens > 0

message = str(exc_info.value)
assert "Invalid content item type: totally_unknown_block" in message
assert "tool_reference" in message
with_unknown_tokens = _count_content_list(
count_function=len,
content_list=[
{"type": "text", "text": long_text},
{"type": "totally_unknown_block", "payload": {"k": "v"}},
],
use_default_image_token_count=False,
default_token_count=None,
)
assert with_unknown_tokens == text_only_tokens

only_unknown_tokens = _count_content_list(
count_function=len,
content_list=[{"type": "weird_custom_block", "blob": "x"}],
use_default_image_token_count=False,
default_token_count=None,
)
assert only_unknown_tokens == 0

# A dict block with no `type` key at all must also be skipped, not crash.
no_type_tokens = _count_content_list(
count_function=len,
content_list=[{"text": "ignored because there is no type field"}],
use_default_image_token_count=False,
default_token_count=None,
)
assert no_type_tokens == 0
Loading