{"record":{"id":"243cabdba456baa8","repo":"BerriAI/litellm","slug":"invalid-content-item-type-content-type-expecte","errorCode":null,"errorMessage":"Invalid content item type: {content_type}. Expected str or dict with 'type' field (text, image_url, tool_use, tool_result, thinking, tool_reference).","messagePattern":"Invalid content item type: (.+?)\\. Expected str or dict with 'type' field \\(text, image_url, tool_use, tool_result, thinking, tool_reference\\)\\.","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"warning","filePath":"litellm/litellm_core_utils/token_counter.py","lineNumber":730,"sourceCode":"                # Claude extended thinking content block\n                # Count the thinking text and skip signature (opaque signature blob)\n                thinking_text = str(c.get(\"thinking\", \"\"))\n                if thinking_text:\n                    num_tokens += count_function(thinking_text)\n            elif c[\"type\"] == \"tool_reference\":\n                # Anthropic tool-search reference block: a lightweight pointer to\n                # a deferred tool, e.g. {\"type\": \"tool_reference\", \"tool_name\": ...}.\n                # 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 {\")","sourceCodeStart":712,"sourceCodeEnd":748,"githubUrl":"https://github.com/BerriAI/litellm/blob/6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d/litellm/litellm_core_utils/token_counter.py#L712-L748","documentation":"A message content list contained an item that is neither a str nor a dict with one of the recognized 'type' discriminators (text, image_url, tool_use, tool_result, thinking, tool_reference). The actual type (or the dict's 'type' value) is reported. When default_token_count is set this is suppressed and the fallback value used instead.","triggerScenarios":"Content lists containing None (an optional block that was never built), integers/floats, or dicts with novel 'type' values from newer provider features (e.g. 'document', 'server_tool_use') not yet handled by this litellm version.","commonSituations":"New Anthropic/OpenAI content block types shipped before litellm support lands; list comprehensions yielding None for skipped items; content assembled from mixed sources.","solutions":["Update litellm to a version that supports the block type (support for new block types lands quickly).","Filter content lists to known types (or stringify unknown blocks) before counting.","Pass default_token_count so unknown blocks degrade to an estimate instead of raising."],"exampleFix":"# before\ncontent = [maybe_block for maybe_block in raw if cond]  # may contain None\nn = litellm.token_counter(model=m, messages=[{\"content\": content}])\n\n# after\nKNOWN = (\"text\", \"image_url\", \"tool_use\", \"tool_result\", \"thinking\", \"tool_reference\")\ncontent = [c for c in content if isinstance(c, str) or (isinstance(c, dict) and c.get(\"type\") in KNOWN)]\nn = litellm.token_counter(model=m, messages=[{\"content\": content}], default_token_count=0)","handlingStrategy":"fallback","validationCode":"KNOWN = {\"text\", \"image_url\", \"tool_use\", \"tool_result\", \"thinking\", \"tool_reference\"}\ncontent = [c for c in content\n           if isinstance(c, str) or (isinstance(c, dict) and c.get(\"type\") in KNOWN)]","typeGuard":"def is_countable_content_item(item) -> bool:\n    if isinstance(item, str):\n        return True\n    return isinstance(item, dict) and item.get(\"type\") in {\n        \"text\", \"image_url\", \"tool_use\", \"tool_result\", \"thinking\", \"tool_reference\"}","tryCatchPattern":"try:\n    n = litellm.token_counter(model=m, messages=msgs)\nexcept ValueError:\n    n = litellm.token_counter(model=m, messages=msgs, default_token_count=0)","preventionTips":["Upgrade litellm promptly when providers ship new content block types.","Strip None items from comprehension-built content lists.","Use default_token_count for untrusted traffic so unknown blocks degrade to estimates."],"tags":["token-counter","content-blocks","validation","forward-compat"],"backgroundTag":null,"analyzedSha":"6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d","analyzedAt":"2026-08-15T07:12:03.035Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}