JuliusBrussee/caveman · error · TypeError

LiteLLM metadata must be a native dictionary

Error message

LiteLLM metadata must be a native dictionary

What it means

_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).

Solutions

  1. Pass metadata as a plain JSON-object dict: metadata={"k": "v"}
  2. Convert other containers first: metadata=dict(my_mapping) or metadata=my_model.model_dump()
  3. Check which protocol you are using — for openai-responses populate litellm_metadata, otherwise metadata
  4. Remove non-JSON-serializable values (datetimes, objects) from the metadata dict

Example fix

// before
completion(scope=scope, metadata=["tag1"])

// after
completion(scope=scope, metadata={"tags": ["tag1"]})
Defensive patterns

Strategy: type-guard

Validate before calling

meta = kwargs.get("metadata") or {}
if not isinstance(meta, dict):
    kwargs["metadata"] = dict(meta) if hasattr(meta, "keys") else {}

Type guard

def is_plain_dict(v) -> bool:
    return isinstance(v, dict) and type(v) is dict

Try / catch

try:
    result = adapter.completion(scope=scope, metadata=metadata, ...)
except TypeError as e:
    if "native dictionary" in str(e):
        result = adapter.completion(scope=scope, metadata=dict(metadata or {}), ...)

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/6f729570faf42007. Report an issue: GitHub.

Appendix: source

Thrown at packages/middleware/python/caveman_middleware/litellm.py:130

    def _request(self, kwargs):
        params = kwargs.get("litellm_params")
        params = params if plain(params) else {}
        with self._lock:
            for metadata in (kwargs.get("litellm_metadata"), kwargs.get("metadata"), params.get("litellm_metadata"), params.get("metadata")):
                key = metadata.get(_KEY) if plain(metadata) else None
                request = self._requests.get(key) if type(key) is str else None
                if request and request.expires > time.monotonic():
                    return key, request
        return None

    def _tag(self, kwargs, key, protocol):
        # Responses metadata belongs to the provider's public storage contract.
        # LiteLLM keeps its own routing/auth metadata in a separate native field.
        name = "litellm_metadata" if protocol == "openai-responses" else "metadata"
        metadata = kwargs.get(name)
        if metadata is not None and not plain(metadata):
            raise TypeError("LiteLLM metadata must be a native dictionary")
        return {**kwargs, name: {**(metadata or {}), _KEY: key}}

    def _report(self, reason, logical_id=None):
        return self.runtime.report(None, reason=reason, adapter="litellm",
                                   logical_call_id=logical_id or str(uuid.uuid4()), attempt_id=str(uuid.uuid4()))

    def _passive_reason(self, method, kwargs):
        if self.runtime.mode == "off":
            return "disabled"
        if not self._version_supported:
            return "unsupported_version"
        responses = method in ("responses", "aresponses")
        source = kwargs.get("input" if responses else "messages")
        if type(source) not in ((list, str) if responses else (list,)):
            # Native async preprocessing can normalize an opaque collection.
            # Preserve its public-call behavior without claiming ownership.
            return "unsupported_shape"
        return None

View on GitHub (pinned to 3ee70a1026)