{"record":{"id":"e5325b3acce4b294","repo":"BerriAI/litellm","slug":"cannot-normalize-tool-call-of-type-type-tc-nam","errorCode":null,"errorMessage":"Cannot normalize tool_call of type {type(tc).__name__}: {tc!r}","messagePattern":"Cannot normalize tool_call of type (.+?): (.+?)","errorType":"exception","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"litellm/integrations/rubrik.py","lineNumber":441,"sourceCode":"        if isinstance(tc, ChatCompletionMessageToolCall):\n            return tc\n        if isinstance(tc, dict):\n            func: Final = tc.get(\"function\") or _EMPTY_MAPPING\n            return ChatCompletionMessageToolCall(\n                id=tc.get(\"id\", \"\"),\n                type=tc.get(\"type\", \"function\"),\n                function=Function(\n                    name=func.get(\"name\", \"\"),\n                    arguments=func.get(\"arguments\", \"\"),\n                ),\n            )\n        if hasattr(tc, \"id\") and hasattr(tc, \"function\"):\n            return ChatCompletionMessageToolCall(\n                id=tc.id or \"\",\n                type=getattr(tc, \"type\", None) or \"function\",\n                function=tc.function,\n            )\n        raise TypeError(f\"Cannot normalize tool_call of type {type(tc).__name__}: {tc!r}\")\n\n    @staticmethod\n    def _join_texts(texts: Sequence[str] | None) -> str:\n        \"\"\"Join response text segments into the single content string the\n        webhook evaluates. Empty when there is no assistant text.\"\"\"\n        if not texts:\n            return \"\"\n        return \"\\n\".join(t for t in texts if t)\n\n    @staticmethod\n    def _build_response_moderation_payload(\n        tool_calls: Sequence[ChatCompletionMessageToolCall],\n        content: str,\n        request_id: str | None,\n    ) -> Mapping[str, object]:\n        \"\"\"Build an OpenAI ChatCompletion-format dict (assistant text + tool\n        calls) for the after_completion webhook.\n","sourceCodeStart":423,"sourceCodeEnd":459,"githubUrl":"https://github.com/BerriAI/litellm/blob/6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d/litellm/integrations/rubrik.py#L423-L459","documentation":"Rubrik's normalization converts a provider tool_call into ChatCompletionMessageToolCall. It handles dict-shaped tool calls ({id, function:{name,arguments}}) and object-shaped ones (has .id and .function attributes); anything else — a string, a list, an object without those attrs — hits the final raise TypeError with the offending value repr'd in the message.","triggerScenarios":"A model/provider returns tool_calls entries in an unexpected schema: a bare string function name, a partially-initialized object, or a new provider whose tool call is a pydantic model with different field names; also fabricated fixtures in tests that use the wrong shape.","commonSituations":"Routing a non-OpenAI-compatible provider through the Rubrik moderation callback; LiteLLM version lag where a newly added provider's tool-call class is not yet normalized; mutating/serializing tool call objects (e.g. after a mock) so attribute access fails.","solutions":["Capture the repr from the error message to identify the actual type, then check which provider produced it","Upgrade litellm — provider-specific tool_call normalization is actively patched","Pre-normalize tool_calls before they reach the callback (map your shape to {id, type:'function', function:{name, arguments}})","If it comes from a test fixture, fix the fixture to use a dict or ChatCompletionMessageToolCall"],"exampleFix":"# before (fixture / provider emits a bare string)\ntool_calls = [\"get_weather\"]  # TypeError: Cannot normalize tool_call of type str\n\n# after\ntool_calls = [{\n    \"id\": \"call_1\",\n    \"type\": \"function\",\n    \"function\": {\"name\": \"get_weather\", \"arguments\": \"{\\\"city\\\": \\\"SF\\\"}\"},\n}]","handlingStrategy":"type-guard","validationCode":"def is_normalizable_tool_call(tc) -> bool:\n    if isinstance(tc, dict):\n        return isinstance(tc.get(\"function\"), (dict, type(None)))\n    return hasattr(tc, \"id\") and hasattr(tc, \"function\")\n\ntool_calls = [tc for tc in raw_tool_calls if is_normalizable_tool_call(tc)]","typeGuard":"from typing import Any, TypeGuard\n\ndef is_dict_tool_call(tc: Any) -> TypeGuard[dict]:\n    return (\n        isinstance(tc, dict)\n        and isinstance(tc.get(\"function\", {}), dict)\n        and \"name\" in tc.get(\"function\", {})\n    )","tryCatchPattern":"try:\n    normalized = RubrikSecurityLogger._normalize_tool_call(tc)\nexcept TypeError as e:\n    if \"Cannot normalize tool_call\" in str(e):\n        logger.warning(\"skipping unshaped tool_call: %r\", tc)\n        normalized = None\n    else:\n        raise","preventionTips":["Filter tool_calls through a shape guard before they reach third-party callbacks","Keep litellm current when routing new/exotic providers — normalization gaps are patched quickly","In tests, generate tool calls from the provider SDK's model classes instead of hand-built strings"],"tags":["rubrik","tool-calls","normalization","provider-compat"],"backgroundTag":null,"analyzedSha":"6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d","analyzedAt":"2026-08-15T07:12:03.035Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}