BerriAI/litellm · error · TypeError

Cannot normalize tool_call of type

Error message

Cannot normalize tool_call of type {type(tc).__name__}: {tc!r}

What it means

To evaluate tool calls, Rubrik normalizes each item to ChatCompletionMessageToolCall, accepting either a dict containing 'function' (with name/arguments) or an object exposing .id and .function attributes; every other shape — a bare string, an Anthropic-style content block, a partial streaming chunk, a dataclass without those fields — hits the final raise as TypeError with the offending type name and repr.

Solutions

  1. Log the repr from the exception to identify the concrete type being passed
  2. Update litellm — normalization coverage for provider-specific tool-call shapes improves across versions
  3. As a workaround, exclude such models from the rubrik response-moderation (after_completion) mode until supported
  4. Post-process the response so tool calls are plain OpenAI dicts ({'id','type','function':{'name','arguments'}}) before guardrails see them

Example fix

# before (guardrail receives an Anthropic-style content block)
# TypeError: Cannot normalize tool_call of type ContentBlock: ...

# after (convert to OpenAI shape before the response reaches guardrails)
tool_calls = [
    {'id': tc.id, 'type': 'function', 'function': {'name': tc.name, 'arguments': tc.input}}
    for tc in response.content if getattr(tc, 'type', '') == 'tool_use'
]
Defensive patterns

Strategy: type-guard

Validate before calling

def is_normalizable_tool_call(tc: object) -> bool:
    if isinstance(tc, dict):
        return isinstance(tc.get('function'), (dict,)) or 'function' in tc
    return hasattr(tc, 'id') and hasattr(tc, 'function')

if not all(is_normalizable_tool_call(tc) for tc in tool_calls):
    raise TypeError('tool_calls contain a shape the rubrik guardrail cannot normalize')

Type guard

from typing import Any

def to_openai_tool_call(tc: Any) -> dict | None:
    if isinstance(tc, dict) and 'function' in tc:
        return tc
    if hasattr(tc, 'id') and hasattr(tc, 'function'):
        return {'id': tc.id or '', 'type': getattr(tc, 'type', 'function'), 'function': tc.function}
    return None  # caller decides: convert or skip moderation for this call

Try / catch

try:
    await rubrik.async_moderation(...)
except TypeError as e:
    if 'Cannot normalize tool_call' in str(e):
        logger.warning('skipping rubrik tool-call moderation for unsupported shape: %s', e)
    else:
        raise

Prevention

When it happens

Trigger: A provider or gateway returns tool calls in a non-OpenAI shape that reaches the Rubrik response-moderation path: string fragments of arguments, content-block objects, or custom objects lacking id/function; a router/custom post-processor mutating the response into an unusual structure before guardrails run.

Common situations: Using tools=... with the rubrik after_completion guardrail on providers whose tool-call representations differ from OpenAI's; middleware converting responses; litellm versions before broader normalization coverage.

Related errors


AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18). Data as JSON: /api/errors/e5325b3acce4b294. Report an issue: GitHub.

Appendix: source

Thrown at litellm/integrations/rubrik.py:441

        if isinstance(tc, ChatCompletionMessageToolCall):
            return tc
        if isinstance(tc, dict):
            func: Final = tc.get("function") or _EMPTY_MAPPING
            return ChatCompletionMessageToolCall(
                id=tc.get("id", ""),
                type=tc.get("type", "function"),
                function=Function(
                    name=func.get("name", ""),
                    arguments=func.get("arguments", ""),
                ),
            )
        if hasattr(tc, "id") and hasattr(tc, "function"):
            return ChatCompletionMessageToolCall(
                id=tc.id or "",
                type=getattr(tc, "type", None) or "function",
                function=tc.function,
            )
        raise TypeError(f"Cannot normalize tool_call of type {type(tc).__name__}: {tc!r}")

    @staticmethod
    def _join_texts(texts: Sequence[str] | None) -> str:
        """Join response text segments into the single content string the
        webhook evaluates. Empty when there is no assistant text."""
        if not texts:
            return ""
        return "\n".join(t for t in texts if t)

    @staticmethod
    def _build_response_moderation_payload(
        tool_calls: Sequence[ChatCompletionMessageToolCall],
        content: str,
        request_id: str | None,
    ) -> Mapping[str, object]:
        """Build an OpenAI ChatCompletion-format dict (assistant text + tool
        calls) for the after_completion webhook.

View on GitHub (pinned to 77b7c6c40c)