BerriAI/litellm · error · ValueError
otel.attributes: include_list and exclude_list are mutually…
Error message
otel.attributes: include_list and exclude_list are mutually exclusive
What it means
The otel.attributes metric filter supports either an include_list (only these attributes are kept on metrics) or an exclude_list (drop these), never both. _resolve_metric_attribute_filter raises ValueError when both resolve to non-empty lists, because applying two opposite cardinality policies to the same metric set is ambiguous. Empty/falsy lists are treated as absent, so one side must actually be omitted or empty.
Solutions
- Pick one mode: keep include_list and delete exclude_list, or vice versa
- For 'allow most but drop a few', use exclude_list alone with the attributes you want removed
- For 'keep only a few', use include_list alone
- Re-apply config and restart; if the error appears on first metric rather than startup, remember lazy resolution means the bad config was in the proxy's callback_settings, not just the constructor args
Example fix
# before (both -> ValueError) attributes: include_list: [gen_ai.system] exclude_list: [metadata.user_api_key_team_id] # after (single mode) attributes: include_list: [gen_ai.system, gen_ai.request.model]
Defensive patterns
Strategy: validation
Validate before calling
attrs = config['otel'].get('attributes') or {}
include, exclude = attrs.get('include_list'), attrs.get('exclude_list')
if include and exclude:
raise SystemExit('otel.attributes: set only include_list OR exclude_list, not both') Type guard
def has_single_filter_mode(attrs: dict) -> bool:
"""True when at most one of include_list/exclude_list is non-empty."""
return bool(attrs.get('include_list')) != bool(attrs.get('exclude_list')) or not (
attrs.get('include_list') or attrs.get('exclude_list')
) Try / catch
try:
otel_cb = OpenTelemetry(attributes=attrs)
except ValueError as e:
if 'mutually exclusive' in str(e):
# drop the weaker policy and keep the stricter include_list
attrs.pop('exclude_list', None)
otel_cb = OpenTelemetry(attributes=attrs)
else:
raise Prevention
- Document the either/or policy next to the otel.attributes block in your config repo
- Lint config merges (Helm overlay + base) for both keys appearing together
- Decide cardinality strategy once (allowlist vs denylist) and standardize across environments
- Remember lazy validation: proxy-populated callback_settings may only fail on the first metric, so smoke-test a metric emission after config changes
When it happens
Trigger: Configuring otel.attributes (or attributes= on OpenTelemetry(...)) with both keys populated, e.g. {include_list: [gen_ai.system], exclude_list: [metadata.user_api_key_team_id]}. Validation runs eagerly when an explicit config is passed at construction, or lazily on the first metric record when the proxy populates callback_settings.otel.attributes after the logger is built.
Common situations: Merging two example configs (one using include, one exclude) during a merge/refactor; incrementally tightening cardinality by adding exclude_list on top of an existing include_list instead of editing it; misunderstanding the filter as ordered rules (include-then-exclude pipeline) rather than mutually exclusive modes.
Related errors
- otel.attributes: is a structural token-usage discriminator…
- otel.attributes: unknown attribute name(s)
- otel.attributes must be a mapping with optional…
- unknown mapper name ; known
- OpenTelemetry OTLP gRPC metric exporter is not available…
AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18).
Data as JSON: /api/errors/c4b039b37149e566.
Report an issue: GitHub.
Appendix: source
Thrown at litellm/integrations/opentelemetry.py:179
raise ValueError(
"otel.attributes must be a mapping with optional 'include_list' / "
f"'exclude_list', got {type(value).__name__}"
)
return OTELMetricAttributeFilter(
include_list=value.get("include_list"),
exclude_list=value.get("exclude_list"),
)
def _resolve_metric_attribute_filter(
attributes: OTELMetricAttributeFilter | None,
) -> tuple[frozenset[str] | None, frozenset[str] | None]:
if attributes is None:
return None, None
include: Final = attributes.include_list or None
exclude: Final = attributes.exclude_list or None
if include and exclude:
raise ValueError("otel.attributes: include_list and exclude_list are mutually exclusive")
requested: Final = include or exclude or []
if TOKEN_TYPE_ATTRIBUTE in requested:
raise ValueError(
f"otel.attributes: {TOKEN_TYPE_ATTRIBUTE} is a structural token-usage discriminator and cannot be filtered"
)
unknown: Final = sorted(name for name in requested if name not in VALID_METRIC_ATTRIBUTE_NAMES)
if unknown:
raise ValueError(
f"otel.attributes: unknown attribute name(s) {unknown}. Valid names: {sorted(VALID_METRIC_ATTRIBUTE_NAMES)}"
)
return (
frozenset(include) if include else None,
frozenset(exclude) if exclude else None,
)
def _normalize_team_metadata_keys(value: Any) -> list[str]:
"""Coerce a team-metadata allowlist from a list or comma-separated string.View on GitHub (pinned to 77b7c6c40c)