{"record":{"id":"65d2910fb4703e7e","repo":"BerriAI/litellm","slug":"otel-attributes-must-be-a-mapping-with-optional-i","errorCode":null,"errorMessage":"otel.attributes must be a mapping with optional 'include_list' / 'exclude_list', got {type(value).__name__}","messagePattern":"otel\\.attributes must be a mapping with optional 'include_list' / 'exclude_list', got (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"litellm/integrations/opentelemetry.py","lineNumber":161,"sourceCode":"        \"gen_ai.request.model\",\n        \"gen_ai.framework\",\n        \"hidden_params\",\n    )\n    + tuple(f\"metadata.{key}\" for key in METRIC_METADATA_KEYS)\n)\n\n\n@dataclass(frozen=True)\nclass OTELMetricAttributeFilter:\n    include_list: list[str] | None = None\n    exclude_list: list[str] | None = None\n\n\ndef _build_metric_attribute_filter(value: Any) -> OTELMetricAttributeFilter:\n    if isinstance(value, OTELMetricAttributeFilter):\n        return value\n    if not isinstance(value, dict):\n        raise ValueError(\n            \"otel.attributes must be a mapping with optional 'include_list' / \"\n            f\"'exclude_list', got {type(value).__name__}\"\n        )\n    return OTELMetricAttributeFilter(\n        include_list=value.get(\"include_list\"),\n        exclude_list=value.get(\"exclude_list\"),\n    )\n\n\ndef _resolve_metric_attribute_filter(\n    attributes: OTELMetricAttributeFilter | None,\n) -> tuple[frozenset[str] | None, frozenset[str] | None]:\n    if attributes is None:\n        return None, None\n    include: Final = attributes.include_list or None\n    exclude: Final = attributes.exclude_list or None\n    if include and exclude:\n        raise ValueError(\"otel.attributes: include_list and exclude_list are mutually exclusive\")","sourceCodeStart":143,"sourceCodeEnd":179,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/integrations/opentelemetry.py#L143-L179","documentation":"The OpenTelemetry callback accepts a metric-attribute cardinality filter (otel.attributes / the attributes= kwarg on OpenTelemetry(...)). _build_metric_attribute_filter only accepts a mapping (dict) with optional 'include_list' and 'exclude_list' keys; anything else — a bare list, a string, None-like sentinels that are not the dataclass, numbers — raises ValueError naming the offending type. This is fail-fast type validation of proxy/litellm config before any metric is recorded.","triggerScenarios":"Passing OpenTelemetry(attributes=[...]) or configuring otel.attributes in config.yaml/callback_settings as a YAML list (e.g. attributes: [gen_ai.system]) or a comma-separated string instead of a dict like {include_list: [...]}; also programmatic constructions that forward an untyped value. Raised eagerly at logger construction when an explicit config/override is supplied, or when the proxy later populates callback_settings.otel.attributes.","commonSituations":"Copy-pasting an env-var-style comma-separated list into the YAML key; assuming the filter takes the same list shape as baggage_team_metadata_keys (which does accept a list); schema drift after upgrading litellm when otel.attributes changed from list-style to mapping-style; YAML indentation turning the mapping into a list of strings.","solutions":["Change the value to a mapping: otel.attributes: {include_list: [gen_ai.system, gen_ai.request.model]} (or exclude_list)","If you meant to allowlist team metadata keys instead, use the baggage/team-metadata config which does accept a list or comma-separated string","After fixing, restart and confirm no ValueError at OpenTelemetry(...) construction"],"exampleFix":"# before (list -> ValueError: got list)\nlitellm_settings:\n  callbacks: [otel]\ncallbacks:\n  otel:\n    attributes: [gen_ai.system]\n# after (mapping)\ncallbacks:\n  otel:\n    attributes:\n      include_list: [gen_ai.system]","handlingStrategy":"type-guard","validationCode":"import os\n\nraw = os.getenv('OTEL_ATTRIBUTES')  # if sourced from env, parse first\nvalue = raw if raw is None else __import__('json').loads(raw)\nif value is not None and not isinstance(value, dict):\n    raise SystemExit(f'otel.attributes must be a mapping, got {type(value).__name__}')","typeGuard":"def is_valid_otel_attributes(value) -> bool:\n    \"\"\"Narrow otel.attributes config to the accepted mapping shape.\"\"\"\n    return isinstance(value, dict) and set(value) <= {'include_list', 'exclude_list'}","tryCatchPattern":"try:\n    otel_cb = OpenTelemetry(attributes=config['otel']['attributes'])\nexcept ValueError as e:\n    raise ConfigError(f'otel callback rejected config: {e}') from e","preventionTips":["Schema-validate config.yaml against the expected otel.attributes mapping shape in CI","Keep the mapping/list distinction in mind: attributes is a dict, baggage_team_metadata_keys accepts a list","Use YAML anchors from a reviewed example block instead of hand-writing new otel config","Prefer explicit keys (include_list:/exclude_list:) over bare values so YAML cannot coerce to a list"],"tags":["opentelemetry","otel","configuration","type-validation","metrics"],"backgroundTag":"config-type-mismatch","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}