{"record":{"id":"6f729570faf42007","repo":"JuliusBrussee/caveman","slug":"litellm-metadata-must-be-a-native-dictionary","errorCode":null,"errorMessage":"LiteLLM metadata must be a native dictionary","messagePattern":"LiteLLM metadata must be a native dictionary","errorType":"validation","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"packages/middleware/python/caveman_middleware/litellm.py","lineNumber":130,"sourceCode":"\n    def _request(self, kwargs):\n        params = kwargs.get(\"litellm_params\")\n        params = params if plain(params) else {}\n        with self._lock:\n            for metadata in (kwargs.get(\"litellm_metadata\"), kwargs.get(\"metadata\"), params.get(\"litellm_metadata\"), params.get(\"metadata\")):\n                key = metadata.get(_KEY) if plain(metadata) else None\n                request = self._requests.get(key) if type(key) is str else None\n                if request and request.expires > time.monotonic():\n                    return key, request\n        return None\n\n    def _tag(self, kwargs, key, protocol):\n        # Responses metadata belongs to the provider's public storage contract.\n        # LiteLLM keeps its own routing/auth metadata in a separate native field.\n        name = \"litellm_metadata\" if protocol == \"openai-responses\" else \"metadata\"\n        metadata = kwargs.get(name)\n        if metadata is not None and not plain(metadata):\n            raise TypeError(\"LiteLLM metadata must be a native dictionary\")\n        return {**kwargs, name: {**(metadata or {}), _KEY: key}}\n\n    def _report(self, reason, logical_id=None):\n        return self.runtime.report(None, reason=reason, adapter=\"litellm\",\n                                   logical_call_id=logical_id or str(uuid.uuid4()), attempt_id=str(uuid.uuid4()))\n\n    def _passive_reason(self, method, kwargs):\n        if self.runtime.mode == \"off\":\n            return \"disabled\"\n        if not self._version_supported:\n            return \"unsupported_version\"\n        responses = method in (\"responses\", \"aresponses\")\n        source = kwargs.get(\"input\" if responses else \"messages\")\n        if type(source) not in ((list, str) if responses else (list,)):\n            # Native async preprocessing can normalize an opaque collection.\n            # Preserve its public-call behavior without claiming ownership.\n            return \"unsupported_shape\"\n        return None","sourceCodeStart":112,"sourceCodeEnd":148,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/middleware/python/caveman_middleware/litellm.py#L112-L148","documentation":"_tag stores the caveman call key inside the request's metadata dict ('metadata', or 'litellm_metadata' for the openai-responses protocol). LiteLLM forwards this dict to provider APIs, so it must be a plain native dict; _tag raises TypeError if it is any other type (list, string, custom mapping, non-JSON-safe object).","triggerScenarios":"Passing metadata (or litellm_metadata for Responses-protocol calls) that is not a plain dict: a list, a str, an OrderedDict/custom mapping rejected by plain(), or a JSON-incompatible value, into completion/acompletion/responses kwargs.","commonSituations":"Reusing provider metadata structures from another SDK; passing pydantic models or dataclasses as metadata; copying metadata from a Responses call into a Chat call (wrong field name) leaving a non-dict value in place.","solutions":["Pass metadata as a plain JSON-object dict: metadata={\"k\": \"v\"}","Convert other containers first: metadata=dict(my_mapping) or metadata=my_model.model_dump()","Check which protocol you are using — for openai-responses populate litellm_metadata, otherwise metadata","Remove non-JSON-serializable values (datetimes, objects) from the metadata dict"],"exampleFix":"// before\ncompletion(scope=scope, metadata=[\"tag1\"])\n\n// after\ncompletion(scope=scope, metadata={\"tags\": [\"tag1\"]})","handlingStrategy":"type-guard","validationCode":"meta = kwargs.get(\"metadata\") or {}\nif not isinstance(meta, dict):\n    kwargs[\"metadata\"] = dict(meta) if hasattr(meta, \"keys\") else {}","typeGuard":"def is_plain_dict(v) -> bool:\n    return isinstance(v, dict) and type(v) is dict","tryCatchPattern":"try:\n    result = adapter.completion(scope=scope, metadata=metadata, ...)\nexcept TypeError as e:\n    if \"native dictionary\" in str(e):\n        result = adapter.completion(scope=scope, metadata=dict(metadata or {}), ...)","preventionTips":["Always pass JSON-object dicts as metadata — never lists, strings, or pydantic models","Use model_dump()/dataclasses.asdict() to convert structured objects first","Match the field to the protocol: 'metadata' for chat, 'litellm_metadata' for openai-responses","Keep metadata values JSON-serializable (str/int/list/dict)"],"tags":["python","type-error","metadata","litellm"],"backgroundTag":"type-mismatch","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}