BerriAI/litellm · error · ValueError

Unsupported type {type(value)} for key tool_calls in message

Error message

Unsupported type {type(value)} for key tool_calls in message {message}

What it means

While counting an assistant message's tool_calls payload, the value under the 'tool_calls' key is not a list. The OpenAI schema requires tool_calls to be a list of {id, type, function} objects; anything else (dict, string, None) cannot be iterated, so the counter raises with the offending type and the full message echoed.

Source

Thrown at litellm/litellm_core_utils/token_counter.py:419


def _count_function_call_tokens(
    key: str,
    value: Any,
    message: Mapping[str, Any],
    count_function: TokenCounterFunction,
) -> int:
    """
    Count tokens contributed by an assistant message's tool/function call payload.

    Handles both the modern `tool_calls` list and the legacy OpenAI
    `function_call` dict. Only the `arguments` string is counted (matching the
    existing tool_calls behavior); names are accounted for elsewhere via the
    tool/function definitions and `tool_choice`.
    """
    if key == "tool_calls":
        if not isinstance(value, list):
            raise ValueError(f"Unsupported type {type(value)} for key tool_calls in message {message}")
        total = 0
        for tool_call in value:
            if "function" not in tool_call:
                raise ValueError(f"Unsupported tool call {tool_call} must contain a function key")
            function_arguments = tool_call["function"].get("arguments", "")
            total += count_function(str(function_arguments))
        return total
    if key == "function_call":
        if not isinstance(value, Mapping):
            raise ValueError(f"Unsupported type {type(value)} for key function_call in message {message}")
        return count_function(str(value.get("arguments", "")))
    raise ValueError(f"Unexpected key {key!r}; expected 'tool_calls' or 'function_call'")


def _count_messages(
    params: _MessageCountParams,
    messages: list[AllMessageValues],
    use_default_image_token_count: bool,

View on GitHub (pinned to 6c2dcb801b)

Solutions

  1. Normalize to a list: always a list of tool-call objects (wrap a lone dict, drop None).
  2. Fix the producer: validate tool_calls against the OpenAI schema before persisting messages.

Example fix

# before
msg = {"role": "assistant", "tool_calls": {"id": "1", "function": {"name": "get_weather", "arguments": "{}"}}}

# after
msg = {"role": "assistant", "tool_calls": [{"id": "1", "type": "function", "function": {"name": "get_weather", "arguments": "{}"}}]}
Defensive patterns

Strategy: type-guard

Type guard

def is_valid_tool_calls(value) -> bool:
    return value is None or (isinstance(value, list) and all(
        isinstance(tc, dict) and "function" in tc for tc in value))

Try / catch

try:
    n = litellm.token_counter(model=m, messages=msgs)
except ValueError as e:
    if "tool_calls" in str(e):
        msgs = normalize_tool_calls(msgs)  # wrap dict->list, drop None
        n = litellm.token_counter(model=m, messages=msgs)
    else:
        raise

Prevention

When it happens

Trigger: An assistant message with tool_calls as a dict instead of a list (e.g. {"role":"assistant","tool_calls":{"function":...}}), tool_calls=null, or a message where tool_calls was serialized to a JSON string before reaching token_counter or cost calculation.

Common situations: Hand-constructed agent conversation histories; messages round-tripped through a queue/DB that mutated the shape; third-party frameworks emitting a single tool-call object instead of a list; None tool_calls on the final assistant message.

Related errors


AI-assisted analysis of BerriAI/litellm@6c2dcb801b (2026-08-15). Data as JSON: /api/errors/eb3c8a8ae3bdca2f. Report an issue: GitHub.