{"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/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/integrations/rubrik.py#L423-L459","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Log the repr from the exception to identify the concrete type being passed","Update litellm — normalization coverage for provider-specific tool-call shapes improves across versions","As a workaround, exclude such models from the rubrik response-moderation (after_completion) mode until supported","Post-process the response so tool calls are plain OpenAI dicts ({'id','type','function':{'name','arguments'}}) before guardrails see them"],"exampleFix":"# before (guardrail receives an Anthropic-style content block)\n# TypeError: Cannot normalize tool_call of type ContentBlock: ...\n\n# after (convert to OpenAI shape before the response reaches guardrails)\ntool_calls = [\n    {'id': tc.id, 'type': 'function', 'function': {'name': tc.name, 'arguments': tc.input}}\n    for tc in response.content if getattr(tc, 'type', '') == 'tool_use'\n]","handlingStrategy":"type-guard","validationCode":"def is_normalizable_tool_call(tc: object) -> bool:\n    if isinstance(tc, dict):\n        return isinstance(tc.get('function'), (dict,)) or 'function' in tc\n    return hasattr(tc, 'id') and hasattr(tc, 'function')\n\nif not all(is_normalizable_tool_call(tc) for tc in tool_calls):\n    raise TypeError('tool_calls contain a shape the rubrik guardrail cannot normalize')","typeGuard":"from typing import Any\n\ndef to_openai_tool_call(tc: Any) -> dict | None:\n    if isinstance(tc, dict) and 'function' in tc:\n        return tc\n    if hasattr(tc, 'id') and hasattr(tc, 'function'):\n        return {'id': tc.id or '', 'type': getattr(tc, 'type', 'function'), 'function': tc.function}\n    return None  # caller decides: convert or skip moderation for this call","tryCatchPattern":"try:\n    await rubrik.async_moderation(...)\nexcept TypeError as e:\n    if 'Cannot normalize tool_call' in str(e):\n        logger.warning('skipping rubrik tool-call moderation for unsupported shape: %s', e)\n    else:\n        raise","preventionTips":["Normalize provider tool calls to OpenAI dicts before guardrail processing","Pin a recent litellm version — normalization coverage expands over releases","Log the offending repr once to identify which provider emits the unsupported shape"],"tags":["rubrik","guardrail","tool-calls","type-error","normalization"],"backgroundTag":"unexpected-tool-call-format","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}