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

  1. 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).
  2. Pass default_token_count (e.g. 0 or an estimate) so unrecognized blocks fall back to it instead of raising.
  3. Upgrade litellm to a release whose token counter supports your block type.
  4. 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

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


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)