From e9df4458c7928490d5ec1dc917908107a98bba70 Mon Sep 17 00:00:00 2001 From: Yucheng He Date: Thu, 3 Sep 2026 17:17:38 -0700 Subject: [PATCH] fix(otel v2): keep the destination merge immutable The per-backend var merge seeded a plain dict and the gated exporter list a plain list, both of which the LIT budget counts. Wrap the merge in MappingProxyType and hand the exporters back as a tuple. --- litellm/integrations/otel/presets/utils.py | 8 ++++--- litellm/proxy/litellm_pre_call_utils.py | 28 +++++++++++++--------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/litellm/integrations/otel/presets/utils.py b/litellm/integrations/otel/presets/utils.py index e132b8854ab..2d7598b63c3 100644 --- a/litellm/integrations/otel/presets/utils.py +++ b/litellm/integrations/otel/presets/utils.py @@ -19,7 +19,9 @@ def ensure_mappers(mapper_names: Iterable[str], *names: str) -> list[str]: return result -def credential_gated_exporters(exporters: "Iterable[ExporterSpec]", owner: "ExporterOwner") -> "list[ExporterSpec]": +def credential_gated_exporters( + exporters: "Iterable[ExporterSpec]", owner: "ExporterOwner" +) -> "tuple[ExporterSpec, ...]": """``exporters`` with the operator's destination replaced by a header-gated one. Used when a credential-mandatory backend is asked to build without the operator's @@ -29,10 +31,10 @@ def credential_gated_exporters(exporters: "Iterable[ExporterSpec]", owner: "Expo span would be printed to stdout, and the gated spec keeps the owner so the override filter still recognises which backend this provider speaks for. """ - return [ + return ( *(spec for spec in exporters if not _is_unconfigured_placeholder(spec)), ExporterSpec(owner=owner, requires_headers=True), - ] + ) def _is_unconfigured_placeholder(spec: "ExporterSpec") -> bool: diff --git a/litellm/proxy/litellm_pre_call_utils.py b/litellm/proxy/litellm_pre_call_utils.py index 924203e9d8a..aed9933424b 100644 --- a/litellm/proxy/litellm_pre_call_utils.py +++ b/litellm/proxy/litellm_pre_call_utils.py @@ -1028,19 +1028,25 @@ def resolve_tenant_otel_destinations( if (callback := _get_validated_callback_metadata(item=item, source="otel-destination")) is not None if callback.callback_type != "failure" ) - merged: Final = { - name: { - var: value - for callback in callbacks - if callback.callback_name == name - for var, value in callback.callback_vars.items() - } - for name in dict.fromkeys(callback.callback_name for callback in callbacks) - } return tuple( destination - for name, callback_vars in merged.items() - if (destination := destination_for(name, _tenant_otel_params(callback_vars))) is not None + for name in dict.fromkeys(callback.callback_name for callback in callbacks) + if ( + destination := destination_for( + name, + _tenant_otel_params( + MappingProxyType( + { + var: value + for callback in callbacks + if callback.callback_name == name + for var, value in callback.callback_vars.items() + } + ) + ), + ) + ) + is not None )