diff --git a/litellm/integrations/otel/model/baggage.py b/litellm/integrations/otel/model/baggage.py index 0903b5ad34e..498b5dfdd13 100644 --- a/litellm/integrations/otel/model/baggage.py +++ b/litellm/integrations/otel/model/baggage.py @@ -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, ) @@ -65,6 +65,17 @@ 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. + """ + 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( identity: RequestIdentity, request_model: str | None, @@ -79,6 +90,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/litellm/integrations/otel/model/semconv.py b/litellm/integrations/otel/model/semconv.py index 1abe8ca33fa..90cd23a90be 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 b379b8bebc9..9e4279a9f5d 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,23 @@ 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_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(): 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 71be28ea485..6310a71896b 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