{"record":{"id":"043ed5109f00c465","repo":"BerriAI/litellm","slug":"error-getting-number-of-tokens-from-content-list","errorCode":null,"errorMessage":"Error getting number of tokens from content list: {e}, default_token_count={default_token_count}","messagePattern":"Error getting number of tokens from content list: (.+?), default_token_count=(.+?)","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"litellm/litellm_core_utils/token_counter.py","lineNumber":738,"sourceCode":"                # The full tool definition is counted via the `tools` param, so we\n                # only count the referenced name here. Without this branch,\n                # token_counter raises on tool-search traffic; on the streaming\n                # anthropic_messages path that nulls response_cost and causes the\n                # proxy to drop the SpendLogs row entirely (silent cost undercount).\n                tool_name = str(c.get(\"tool_name\") or \"\")\n                if tool_name:\n                    num_tokens += count_function(tool_name)\n            else:\n                content_type = c.get(\"type\", type(c).__name__) if isinstance(c, dict) else type(c).__name__\n                raise ValueError(\n                    f\"Invalid content item type: {content_type}. \"\n                    f\"Expected str or dict with 'type' field (text, image_url, tool_use, tool_result, thinking, tool_reference).\"\n                )\n        return num_tokens\n    except Exception as e:\n        if default_token_count is not None:\n            return default_token_count\n        raise ValueError(\n            f\"Error getting number of tokens from content list: {e}, default_token_count={default_token_count}\"\n        )\n\n\ndef _format_function_definitions(tools):\n    \"\"\"Formats tool definitions in the format that OpenAI appears to use.\n    Based on https://github.com/forestwanglin/openai-java/blob/main/jtokkit/src/main/java/xyz/felh/openai/jtokkit/utils/TikTokenUtils.java\n    \"\"\"\n    lines: Final = []\n    lines.append(\"namespace functions {\")\n    lines.append(\"\")\n    for tool in tools:\n        if not isinstance(tool, dict):\n            continue\n        function = tool.get(\"function\")\n        if not isinstance(function, dict):\n            # Anthropic tool shape → OpenAI function dict for token counting.\n            params = tool.get(\"input_schema\") or tool.get(\"parameters\") or {}","sourceCodeStart":720,"sourceCodeEnd":756,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/litellm_core_utils/token_counter.py#L720-L756","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before\nn = litellm.token_counter(messages=history)  # history contains {\"type\": \"redacted_thinking\"}\n# ValueError: Error getting number of tokens from content list ...\n\n# after\nSUPPORTED = {\"text\", \"image_url\", \"tool_use\", \"tool_result\", \"thinking\", \"tool_reference\"}\nclean_history = [\n    {**m, \"content\": [b for b in m[\"content\"] if isinstance(b, str) or b.get(\"type\") in SUPPORTED]}\n    if isinstance(m.get(\"content\"), list) else m\n    for m in history\n]\nn = litellm.token_counter(messages=clean_history)","handlingStrategy":"validation","validationCode":"SUPPORTED = {\"text\", \"image_url\", \"tool_use\", \"tool_result\", \"thinking\", \"tool_reference\"}\n\ndef countable_content(content):\n    if isinstance(content, str):\n        return content\n    return [b for b in content if isinstance(b, str) or (isinstance(b, dict) and b.get(\"type\") in SUPPORTED)]\n\nmessages = [\n    {**m, \"content\": countable_content(m[\"content\"])} if isinstance(m.get(\"content\"), list) else m\n    for m in messages\n]\ntokens = litellm.token_counter(messages=messages)","typeGuard":"SUPPORTED = {\"text\", \"image_url\", \"tool_use\", \"tool_result\", \"thinking\", \"tool_reference\"}\n\ndef is_supported_content_block(block) -> bool:\n    if isinstance(block, str):\n        return True\n    return isinstance(block, dict) and block.get(\"type\") in SUPPORTED","tryCatchPattern":"try:\n    tokens = litellm.token_counter(messages=messages)\nexcept ValueError as e:\n    if \"Error getting number of tokens from content list\" in str(e):\n        tokens = 0  # or re-run with sanitized messages / default_token_count\n    else:\n        raise","preventionTips":["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."],"tags":["token-counting","content-validation","messages","valueerror"],"backgroundTag":"message-content-validation","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}