BerriAI/litellm · error · ValueError

otel.attributes: is a structural token-usage discriminator…

Error message

otel.attributes: {TOKEN_TYPE_ATTRIBUTE} is a structural token-usage discriminator and cannot be filtered

What it means

The otel.attributes filter may not include or exclude the attribute gen_ai.token.type (TOKEN_TYPE_ATTRIBUTE). That attribute is the structural discriminator that splits token usage metrics into input/output/reasoning/cache series; filtering it out would collapse distinct series onto the same labels and corrupt the counts. _resolve_metric_attribute_filter raises ValueError whenever the requested name appears in the active include or exclude list.

Solutions

  1. Remove gen_ai.token.type from whichever list it appears in — there is no override; it is structural by design
  2. Restrict cardinality via other valid names: gen_ai.operation.name, gen_ai.provider.name, gen_ai.system, gen_ai.request.model, gen_ai.framework, hidden_params, or metadata.<key> entries
  3. If token-type series themselves are too high-cardinality for your backend, the filter is the wrong tool — reduce at the metrics backend (recording rules/aggregation) instead

Example fix

# before
attributes:
  exclude_list: [gen_ai.token.type, metadata.requester_metadata]
# after
attributes:
  exclude_list: [metadata.requester_metadata]
Defensive patterns

Strategy: validation

Validate before calling

TOKEN_TYPE = 'gen_ai.token.type'
attrs = config['otel'].get('attributes') or {}
for mode in ('include_list', 'exclude_list'):
    names = attrs.get(mode) or []
    if TOKEN_TYPE in names:
        raise SystemExit(f'{TOKEN_TYPE} is structural and cannot appear in otel.attributes.{mode}')

Type guard

def filters_token_type(attrs: dict) -> bool:
    """True when the config attempts to filter gen_ai.token.type."""
    return 'gen_ai.token.type' in ((attrs.get('include_list') or []) + (attrs.get('exclude_list') or []))

Try / catch

try:
    otel_cb = OpenTelemetry(attributes=attrs)
except ValueError as e:
    if 'cannot be filtered' in str(e):
        attrs['include_list'] = [n for n in attrs.get('include_list', []) if n != 'gen_ai.token.type']
        attrs['exclude_list'] = [n for n in attrs.get('exclude_list', []) if n != 'gen_ai.token.type']
        otel_cb = OpenTelemetry(attributes=attrs)
    else:
        raise

Prevention

When it happens

Trigger: Listing gen_ai.token.type in include_list or exclude_list of otel.attributes, e.g. {include_list: [gen_ai.token.type, gen_ai.system]}. Validation fires eagerly for explicit constructor config or on first metric record for proxy-populated callback_settings.otel.attributes.

Common situations: Operators attempting aggressive cardinality reduction who sweep 'everything gen_ai.*' into exclude_list via copy-paste of the full attribute list from docs/metrics explorer; treating gen_ai.token.type as a normal label like provider or model; wildcard-style configs expanded by a templating system (Helm/Ansible) that includes all known attribute names.

Related errors


AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18). Data as JSON: /api/errors/1f10363ca0dbc15f. Report an issue: GitHub.

Appendix: source

Thrown at litellm/integrations/opentelemetry.py:182

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

    config.yaml passes a YAML list; an env var passes a comma-separated string.
    Both collapse to a list of stripped, non-empty keys.

View on GitHub (pinned to 77b7c6c40c)