BerriAI/litellm · error · ValueError
Error getting number of tokens from content list
Error message
Error getting number of tokens from content list: {e}, default_token_count={default_token_count} What it means
Raised by _count_content_list in litellm's token counter when a message content list contains an item it cannot count: anything that is not a str or a dict whose 'type' is one of text, image_url, tool_use, tool_result, thinking, or tool_reference. A dict missing the 'type' key also lands here (the c["type"] lookup raises KeyError first) and the inner error is wrapped by the outer handler. When default_token_count is not None the handler returns that fallback instead of raising, so this message always shows default_token_count=None when seen.
Solutions
- Normalize the content list before counting: keep only str items and dicts with 'type' in (text, image_url, tool_use, tool_result, thinking, tool_reference).
- Pass default_token_count (e.g. 0 or an estimate) so unrecognized blocks fall back to it instead of raising.
- Upgrade litellm to a release whose token counter supports your block type.
- If you control the producer, add the missing 'type' key to untyped dicts.
Example fix
# before
n = litellm.token_counter(messages=history) # history contains {"type": "redacted_thinking"}
# ValueError: Error getting number of tokens from content list ...
# after
SUPPORTED = {"text", "image_url", "tool_use", "tool_result", "thinking", "tool_reference"}
clean_history = [
{**m, "content": [b for b in m["content"] if isinstance(b, str) or b.get("type") in SUPPORTED]}
if isinstance(m.get("content"), list) else m
for m in history
]
n = litellm.token_counter(messages=clean_history) Defensive patterns
Strategy: validation
Validate before calling
SUPPORTED = {"text", "image_url", "tool_use", "tool_result", "thinking", "tool_reference"}
def countable_content(content):
if isinstance(content, str):
return content
return [b for b in content if isinstance(b, str) or (isinstance(b, dict) and b.get("type") in SUPPORTED)]
messages = [
{**m, "content": countable_content(m["content"])} if isinstance(m.get("content"), list) else m
for m in messages
]
tokens = litellm.token_counter(messages=messages) Type guard
SUPPORTED = {"text", "image_url", "tool_use", "tool_result", "thinking", "tool_reference"}
def is_supported_content_block(block) -> bool:
if isinstance(block, str):
return True
return isinstance(block, dict) and block.get("type") in SUPPORTED Try / catch
try:
tokens = litellm.token_counter(messages=messages)
except ValueError as e:
if "Error getting number of tokens from content list" in str(e):
tokens = 0 # or re-run with sanitized messages / default_token_count
else:
raise Prevention
- Keep content blocks to the documented set (text, image_url, tool_use, tool_result, thinking, tool_reference) for any message that will be cost-tracked.
- Pass default_token_count on cost-critical proxy paths so one unrecognized block cannot kill spend logging.
- Regression-test token_counter against every block type your traffic includes before rolling out new providers or versions.
When it happens
Trigger: Calling litellm.token_counter (or proxy cost/spend tracking) on messages whose content is a list containing blocks like {'type': 'input_audio', ...}, {'type': 'redacted_thinking'}, a dict with no 'type' key, or non-str/non-dict entries such as None or an int.
Common situations: Provider content blocks added after your litellm release (audio parts, redacted thinking, file parts); guardrails or middleware injecting custom block types; message histories converted from another SDK that uses a different block shape.
Related errors
- api_base is required for A2A provider
- `content` can only contain strings or openai content dicts
- custom_ui_sso_sign_in_handler is not configured. Please set…
- Each content item must be a dictionary
- Failed to extract video data
AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18).
Data as JSON: /api/errors/043ed5109f00c465.
Report an issue: GitHub.
Appendix: source
Thrown at litellm/litellm_core_utils/token_counter.py:738
# The full tool definition is counted via the `tools` param, so we
# only count the referenced name here. Without this branch,
# token_counter raises on tool-search traffic; on the streaming
# anthropic_messages path that nulls response_cost and causes the
# proxy to drop the SpendLogs row entirely (silent cost undercount).
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:
return default_token_count
raise ValueError(
f"Error getting number of tokens from content list: {e}, default_token_count={default_token_count}"
)
def _format_function_definitions(tools):
"""Formats tool definitions in the format that OpenAI appears to use.
Based on https://github.com/forestwanglin/openai-java/blob/main/jtokkit/src/main/java/xyz/felh/openai/jtokkit/utils/TikTokenUtils.java
"""
lines: Final = []
lines.append("namespace functions {")
lines.append("")
for tool in tools:
if not isinstance(tool, dict):
continue
function = tool.get("function")
if not isinstance(function, dict):
# Anthropic tool shape → OpenAI function dict for token counting.
params = tool.get("input_schema") or tool.get("parameters") or {}View on GitHub (pinned to 77b7c6c40c)