{"record":{"id":"6284f6b8065923ae","repo":"BerriAI/litellm","slug":"unsupported-tool-call-tool-call-must-contain-a-f","errorCode":null,"errorMessage":"Unsupported tool call {tool_call} must contain a function key","messagePattern":"Unsupported tool call (.+?) must contain a function key","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"litellm/litellm_core_utils/token_counter.py","lineNumber":423,"sourceCode":"    value: Any,\n    message: Mapping[str, Any],\n    count_function: TokenCounterFunction,\n) -> int:\n    \"\"\"\n    Count tokens contributed by an assistant message's tool/function call payload.\n\n    Handles both the modern `tool_calls` list and the legacy OpenAI\n    `function_call` dict. Only the `arguments` string is counted (matching the\n    existing tool_calls behavior); names are accounted for elsewhere via the\n    tool/function definitions and `tool_choice`.\n    \"\"\"\n    if key == \"tool_calls\":\n        if not isinstance(value, list):\n            raise ValueError(f\"Unsupported type {type(value)} for key tool_calls in message {message}\")\n        total = 0\n        for tool_call in value:\n            if \"function\" not in tool_call:\n                raise ValueError(f\"Unsupported tool call {tool_call} must contain a function key\")\n            function_arguments = tool_call[\"function\"].get(\"arguments\", \"\")\n            total += count_function(str(function_arguments))\n        return total\n    if key == \"function_call\":\n        if not isinstance(value, Mapping):\n            raise ValueError(f\"Unsupported type {type(value)} for key function_call in message {message}\")\n        return count_function(str(value.get(\"arguments\", \"\")))\n    raise ValueError(f\"Unexpected key {key!r}; expected 'tool_calls' or 'function_call'\")\n\n\ndef _count_messages(\n    params: _MessageCountParams,\n    messages: list[AllMessageValues],\n    use_default_image_token_count: bool,\n    default_token_count: int | None,\n) -> int:\n    \"\"\"\n    Count the number of tokens in a list of messages.","sourceCodeStart":405,"sourceCodeEnd":441,"githubUrl":"https://github.com/BerriAI/litellm/blob/6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d/litellm/litellm_core_utils/token_counter.py#L405-L441","documentation":"Each entry in an assistant message's tool_calls list must contain a 'function' key ({\"function\": {\"name\", \"arguments\"}}). The counter found a tool_call dict without it, so there are no arguments to count; the malformed tool_call is echoed in the error.","triggerScenarios":"tool_calls entries like {\"id\":\"1\",\"type\":\"function\"} (function omitted); entries built from an incomplete streaming delta that never finished; agent frameworks writing name/arguments at the top level instead of nested under 'function'.","commonSituations":"Reconstructing tool_calls from accumulated streaming deltas and persisting an unfinished final delta; saving LLM output as tool_calls without schema validation; framework versions that flattened the function object.","solutions":["Rebuild each tool_call as {\"id\", \"type\": \"function\", \"function\": {\"name\", \"arguments\"}} before passing messages to token_counter.","If the entry came from an unfinished stream delta, filter out entries lacking 'function' before persisting."],"exampleFix":"# before\ntool_call = {\"id\": \"call_1\", \"type\": \"function\", \"name\": \"get_weather\", \"arguments\": \"{}\"}\n\n# after\ntool_call = {\"id\": \"call_1\", \"type\": \"function\",\n             \"function\": {\"name\": \"get_weather\", \"arguments\": \"{}\"}}","handlingStrategy":"type-guard","validationCode":"clean = [tc for tc in (msg.get(\"tool_calls\") or []) if isinstance(tc, dict) and \"function\" in tc]\nmsg[\"tool_calls\"] = clean\nn = litellm.token_counter(model=m, messages=msgs)","typeGuard":"def is_complete_tool_call(tc) -> bool:\n    return isinstance(tc, dict) and isinstance(tc.get(\"function\"), dict) and \"name\" in tc[\"function\"]","tryCatchPattern":null,"preventionTips":["When assembling tool_calls from streaming deltas, persist only after the stream's finish_reason arrives.","Validate at write time (DB/queue), not at count time."],"tags":["token-counter","tool-calls","validation","schema"],"backgroundTag":null,"analyzedSha":"6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d","analyzedAt":"2026-08-15T07:12:03.035Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}