From a31a23ec18c8abb79d891433db0c3d8c27c67b72 Mon Sep 17 00:00:00 2001 From: Nishant Date: Thu, 30 Jul 2026 16:00:50 +0530 Subject: [PATCH 1/4] fix(otel): promote litellm.request.model via baggage instead of gen_ai.request.model Baggage promotion stamped canonical gen_ai.request.model onto SERVER, DB, and service spans, causing GenAI backends to misclassify non-LLM operations. Use litellm.request.model for cross-span correlation; LLM-call spans still receive gen_ai.request.model from the GenAI mapper. Fixes #35193 --- litellm/integrations/otel/model/baggage.py | 6 ++-- litellm/integrations/otel/model/semconv.py | 6 +++- .../integrations/otel/test_otel_v2_baggage.py | 31 +++++++++++++++++-- .../otel/test_otel_v2_sources_of_truth.py | 2 +- 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/litellm/integrations/otel/model/baggage.py b/litellm/integrations/otel/model/baggage.py index 0903b5ad34ea..3c2ba12c07cd 100644 --- a/litellm/integrations/otel/model/baggage.py +++ b/litellm/integrations/otel/model/baggage.py @@ -18,7 +18,7 @@ from typing import Final from litellm.integrations.otel.model.metadata import RequestIdentity -from litellm.integrations.otel.model.semconv import GenAI, LiteLLM +from litellm.integrations.otel.model.semconv import LiteLLM # Attribute key -> value extractor over (identity, request_model, # team_metadata_keys). The single definition of what may be promoted and under @@ -32,7 +32,7 @@ ), LiteLLM.KEY_HASH: lambda identity, model, team_metadata_keys: identity.key_hash, LiteLLM.END_USER: lambda identity, model, team_metadata_keys: identity.end_user, - GenAI.REQUEST_MODEL: lambda identity, model, team_metadata_keys: model, + LiteLLM.REQUEST_MODEL: lambda identity, model, team_metadata_keys: model, LiteLLM.PROVIDER_MODEL: lambda identity, model, team_metadata_keys: identity.provider_model, } @@ -44,7 +44,7 @@ LiteLLM.TEAM_ALIAS, LiteLLM.TEAM_METADATA, LiteLLM.KEY_HASH, - GenAI.REQUEST_MODEL, + LiteLLM.REQUEST_MODEL, LiteLLM.PROVIDER_MODEL, ) diff --git a/litellm/integrations/otel/model/semconv.py b/litellm/integrations/otel/model/semconv.py index 1abe8ca33fa3..90cd23a90be4 100644 --- a/litellm/integrations/otel/model/semconv.py +++ b/litellm/integrations/otel/model/semconv.py @@ -229,8 +229,12 @@ class LiteLLM: TEAM_METADATA: Final = "litellm.team.metadata" KEY_HASH: Final = "litellm.api_key.hash" END_USER: Final = "litellm.end_user.id" + # User-facing model from the inbound request. Promoted via Baggage for + # correlation on non-GenAI spans; ``gen_ai.request.model`` is stamped only + # on LLM-call spans by the GenAI mapper. + REQUEST_MODEL: Final = "litellm.request.model" # The model string litellm actually sent to the provider (the deployment's - # ``litellm_params.model``), distinct from the user-facing ``gen_ai.request.model``. + # ``litellm_params.model``), distinct from the user-facing request model. PROVIDER_MODEL: Final = "litellm.provider.model" REQUEST_STREAMING: Final = "litellm.request.streaming" GUARDRAIL_NAME: Final = "litellm.guardrail.name" diff --git a/tests/test_litellm/integrations/otel/test_otel_v2_baggage.py b/tests/test_litellm/integrations/otel/test_otel_v2_baggage.py index b379b8bebc9a..b73c536680b8 100644 --- a/tests/test_litellm/integrations/otel/test_otel_v2_baggage.py +++ b/tests/test_litellm/integrations/otel/test_otel_v2_baggage.py @@ -74,7 +74,33 @@ def test_identity_promoted_onto_every_span(): for span in spans: assert span.attributes.get(LiteLLM.TEAM_ID) == "t1" assert span.attributes.get(LiteLLM.TEAM_ALIAS) == "team one" - assert span.attributes.get(GenAI.REQUEST_MODEL) == "gpt-4o" + assert span.attributes.get(LiteLLM.REQUEST_MODEL) == "gpt-4o" + for span in spans: + if span.name == "chat gpt-4o": + assert span.attributes.get(GenAI.REQUEST_MODEL) == "gpt-4o" + else: + assert GenAI.REQUEST_MODEL not in span.attributes + + +def test_gen_ai_request_model_not_promoted_via_baggage(): + """Baggage must not stamp canonical gen_ai.request.model on non-LLM spans.""" + engine, exporter = _engine_and_exporter() + data = LLMCallSpanData.from_standard_logging_payload(_payload()) + bag = promoted_baggage(data.identity, data.request_model, BAGGAGE_PROMOTED_KEYS) + ctx = ctx_mod.set_request_baggage(bag) + + root = engine.start_span(SpanRole.PROXY_REQUEST, "POST /chat/completions", ctx) + root_ctx = ctx_mod.context_from_span(root, ctx) + engine.emit(SpanRole.SERVICE, ServiceSpanData("redis", call_type="set"), root_ctx) + root.end() + + spans = exporter.get_finished_spans() + service_span = next(s for s in spans if s.name != "POST /chat/completions") + assert service_span.attributes.get(LiteLLM.REQUEST_MODEL) == "gpt-4o" + assert GenAI.REQUEST_MODEL not in service_span.attributes + root_span = next(s for s in spans if s.name == "POST /chat/completions") + assert root_span.attributes.get(LiteLLM.REQUEST_MODEL) == "gpt-4o" + assert GenAI.REQUEST_MODEL not in root_span.attributes def test_team_metadata_promoted_only_for_allowlisted_subkeys(): @@ -99,7 +125,8 @@ def test_team_metadata_promoted_only_for_allowlisted_subkeys(): assert json.loads(span.attributes[LiteLLM.TEAM_METADATA]) == {"tier": "gold"} # provider model is distinct from the user-facing request model assert span.attributes.get(LiteLLM.PROVIDER_MODEL) == "azure/my-deployment" - assert span.attributes.get(GenAI.REQUEST_MODEL) == "gpt-4o" + assert span.attributes.get(LiteLLM.REQUEST_MODEL) == "gpt-4o" + assert GenAI.REQUEST_MODEL not in span.attributes def test_team_metadata_not_promoted_by_default(): diff --git a/tests/test_litellm/integrations/otel/test_otel_v2_sources_of_truth.py b/tests/test_litellm/integrations/otel/test_otel_v2_sources_of_truth.py index 71be28ea485b..6310a71896bd 100644 --- a/tests/test_litellm/integrations/otel/test_otel_v2_sources_of_truth.py +++ b/tests/test_litellm/integrations/otel/test_otel_v2_sources_of_truth.py @@ -698,7 +698,7 @@ def test_promoted_baggage_is_bounded_allowlist(): promoted = promoted_baggage(identity, "gpt-4o", BAGGAGE_PROMOTED_KEYS) assert promoted[LiteLLM.TEAM_ID] == "t1" assert promoted[LiteLLM.TEAM_ALIAS] == "team one" - assert promoted[GenAI.REQUEST_MODEL] == "gpt-4o" + assert promoted[LiteLLM.REQUEST_MODEL] == "gpt-4o" # allowlisted metadata sub-key is promoted under the litellm.metadata.* prefix assert promoted[f"{LiteLLM.METADATA_PREFIX}user_api_key_org_id"] == "org1" # full metadata blob is NOT promoted From 728df57accc6fafc88d2e14c515fd913531cf0f2 Mon Sep 17 00:00:00 2001 From: Nishant Date: Thu, 30 Jul 2026 20:01:29 +0530 Subject: [PATCH 2/4] fix(otel): remap legacy gen_ai.request.model baggage allowlist entries Deployments that still list gen_ai.request.model in baggage_promoted_keys keep model correlation via litellm.request.model instead of silently dropping it. --- litellm/integrations/otel/model/baggage.py | 20 ++++++++++++++++++- .../integrations/otel/test_otel_v2_baggage.py | 15 ++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/litellm/integrations/otel/model/baggage.py b/litellm/integrations/otel/model/baggage.py index 3c2ba12c07cd..bf7dea152698 100644 --- a/litellm/integrations/otel/model/baggage.py +++ b/litellm/integrations/otel/model/baggage.py @@ -18,7 +18,7 @@ from typing import Final from litellm.integrations.otel.model.metadata import RequestIdentity -from litellm.integrations.otel.model.semconv import LiteLLM +from litellm.integrations.otel.model.semconv import GenAI, LiteLLM # Attribute key -> value extractor over (identity, request_model, # team_metadata_keys). The single definition of what may be promoted and under @@ -65,6 +65,23 @@ DEFAULT_BAGGAGE_TEAM_METADATA_KEYS: Final[tuple[str, ...]] = () +def _normalize_promoted_keys(promoted_keys: tuple[str, ...]) -> tuple[str, ...]: + """Remap legacy ``gen_ai.request.model`` allowlist entries to the vendor key. + + Canonical ``gen_ai.request.model`` must not ride Baggage onto non-LLM spans. + Deployments that still list it in ``baggage_promoted_keys`` keep model + correlation via ``litellm.request.model`` instead of silently dropping it. + """ + normalized: list[str] = [] + seen: set[str] = set() + for key in promoted_keys: + remapped = LiteLLM.REQUEST_MODEL if key == GenAI.REQUEST_MODEL else key + if remapped not in seen: + seen.add(remapped) + normalized.append(remapped) + return tuple(normalized) + + def promoted_baggage( identity: RequestIdentity, request_model: str | None, @@ -79,6 +96,7 @@ def promoted_baggage( ``team_metadata_keys`` selects sub-keys of the team's metadata to promote under ``litellm.team.metadata``. Empty values are dropped. """ + promoted_keys = _normalize_promoted_keys(promoted_keys) out: dict[str, str] = {} for key, extract in _PROMOTABLE.items(): if key in promoted_keys: diff --git a/tests/test_litellm/integrations/otel/test_otel_v2_baggage.py b/tests/test_litellm/integrations/otel/test_otel_v2_baggage.py index b73c536680b8..9e4279a9f5de 100644 --- a/tests/test_litellm/integrations/otel/test_otel_v2_baggage.py +++ b/tests/test_litellm/integrations/otel/test_otel_v2_baggage.py @@ -129,6 +129,21 @@ def test_team_metadata_promoted_only_for_allowlisted_subkeys(): assert GenAI.REQUEST_MODEL not in span.attributes +def test_legacy_gen_ai_request_model_allowlist_remaps_to_vendor_key(): + """Explicit baggage_promoted_keys with gen_ai.request.model still promote + the model, but under litellm.request.model so non-LLM spans are not + misclassified as GenAI operations.""" + data = LLMCallSpanData.from_standard_logging_payload(_payload()) + bag = promoted_baggage( + data.identity, + data.request_model, + (LiteLLM.TEAM_ID, GenAI.REQUEST_MODEL), + ) + assert bag.get(LiteLLM.REQUEST_MODEL) == "gpt-4o" + assert GenAI.REQUEST_MODEL not in bag + assert bag.get(LiteLLM.TEAM_ID) == "t1" + + def test_team_metadata_not_promoted_by_default(): """The default allowlist is empty, so a team's metadata is never promoted even though its dict is present on the request.""" From 878037edba782c37b87182a65d23365f7744f3b0 Mon Sep 17 00:00:00 2001 From: Nishant Date: Thu, 30 Jul 2026 20:16:09 +0530 Subject: [PATCH 3/4] fix(otel): avoid mutable annotations in baggage key remapping LIT001 rejected list[str]/set[str] locals in _normalize_promoted_keys. Use tuple-only dedupe so the type-discipline budget stays flat. --- litellm/integrations/otel/model/baggage.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/litellm/integrations/otel/model/baggage.py b/litellm/integrations/otel/model/baggage.py index bf7dea152698..6cd7f59d963b 100644 --- a/litellm/integrations/otel/model/baggage.py +++ b/litellm/integrations/otel/model/baggage.py @@ -72,14 +72,10 @@ def _normalize_promoted_keys(promoted_keys: tuple[str, ...]) -> tuple[str, ...]: Deployments that still list it in ``baggage_promoted_keys`` keep model correlation via ``litellm.request.model`` instead of silently dropping it. """ - normalized: list[str] = [] - seen: set[str] = set() - for key in promoted_keys: - remapped = LiteLLM.REQUEST_MODEL if key == GenAI.REQUEST_MODEL else key - if remapped not in seen: - seen.add(remapped) - normalized.append(remapped) - return tuple(normalized) + remapped = tuple( + LiteLLM.REQUEST_MODEL if key == GenAI.REQUEST_MODEL else key for key in promoted_keys + ) + return tuple(key for i, key in enumerate(remapped) if key not in remapped[:i]) def promoted_baggage( From 737cc6549f195a2ea0add1e4203112c84697665b Mon Sep 17 00:00:00 2001 From: Nishant Date: Thu, 30 Jul 2026 20:30:07 +0530 Subject: [PATCH 4/4] style(otel): apply ruff format to baggage key remapping --- litellm/integrations/otel/model/baggage.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/litellm/integrations/otel/model/baggage.py b/litellm/integrations/otel/model/baggage.py index 6cd7f59d963b..498b5dfdd135 100644 --- a/litellm/integrations/otel/model/baggage.py +++ b/litellm/integrations/otel/model/baggage.py @@ -72,9 +72,7 @@ def _normalize_promoted_keys(promoted_keys: tuple[str, ...]) -> tuple[str, ...]: Deployments that still list it in ``baggage_promoted_keys`` keep model correlation via ``litellm.request.model`` instead of silently dropping it. """ - remapped = tuple( - LiteLLM.REQUEST_MODEL if key == GenAI.REQUEST_MODEL else key for key in promoted_keys - ) + remapped = tuple(LiteLLM.REQUEST_MODEL if key == GenAI.REQUEST_MODEL else key for key in promoted_keys) return tuple(key for i, key in enumerate(remapped) if key not in remapped[:i])