From 25a62231304330830c19c75dfba66f1cdad99e77 Mon Sep 17 00:00:00 2001 From: Ege Arhan Date: Mon, 2 Nov 2026 02:14:19 +0300 Subject: [PATCH 1/3] fix(otel): export virtual key metadata into baggage when allowlisted Fixes #49602 --- litellm/integrations/otel/model/metadata.py | 8 +++++++- .../otel/test_otel_v2_sources_of_truth.py | 12 ++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/litellm/integrations/otel/model/metadata.py b/litellm/integrations/otel/model/metadata.py index 28d6ad54607..4f76ebc56d2 100644 --- a/litellm/integrations/otel/model/metadata.py +++ b/litellm/integrations/otel/model/metadata.py @@ -152,8 +152,14 @@ class RequestIdentity: ) if (value := get(attr)) ) + raw_key_meta: Final = as_str_mapping(get("metadata") or get("user_api_key_auth_metadata")) + key_meta: Final = ( + tuple(flatten_metadata({"user_api_key_auth_metadata": raw_key_meta})) + if raw_key_meta is not None + else () + ) request_meta: Final = flatten_metadata(request_metadata) if request_metadata is not None else () - metadata: Final = MappingProxyType(dict((*request_meta, *auth_meta))) + metadata: Final = MappingProxyType(dict((*request_meta, *auth_meta, *key_meta))) return cls( team_id=as_str(get("team_id")), team_alias=as_str(get("team_alias")), diff --git a/tests/unit/integrations/otel/test_otel_v2_sources_of_truth.py b/tests/unit/integrations/otel/test_otel_v2_sources_of_truth.py index 2d0b4701c9f..4ac00ef889a 100644 --- a/tests/unit/integrations/otel/test_otel_v2_sources_of_truth.py +++ b/tests/unit/integrations/otel/test_otel_v2_sources_of_truth.py @@ -1607,6 +1607,18 @@ def test_request_identity_from_user_api_key_auth(): assert ident.metadata["user_api_key_end_user_id"] == "eu9" +def test_request_identity_from_user_api_key_auth_flattens_key_metadata(): + from litellm.integrations.otel.model.payloads import RequestIdentity + + class _Auth: + metadata = {"team": "a", "customtag": "b", "nested": {"k": "v"}} + + ident = RequestIdentity.from_user_api_key_auth(_Auth()) + assert ident.metadata["user_api_key_auth_metadata.team"] == "a" + assert ident.metadata["user_api_key_auth_metadata.customtag"] == "b" + assert ident.metadata["user_api_key_auth_metadata.nested.k"] == "v" + + # --- request-metadata translation layer (RequestContext) -------------------- # From 089e77ac19b968382dfd807c50fd6117c463f7cb Mon Sep 17 00:00:00 2001 From: Ege Arhan Date: Fri, 6 Nov 2026 03:07:50 +0300 Subject: [PATCH 2/3] style(otel): format metadata and tighten test assertions --- litellm/integrations/otel/model/metadata.py | 4 +--- .../integrations/otel/test_otel_v2_sources_of_truth.py | 10 ++++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/litellm/integrations/otel/model/metadata.py b/litellm/integrations/otel/model/metadata.py index 4f76ebc56d2..631063bbed2 100644 --- a/litellm/integrations/otel/model/metadata.py +++ b/litellm/integrations/otel/model/metadata.py @@ -154,9 +154,7 @@ class RequestIdentity: ) raw_key_meta: Final = as_str_mapping(get("metadata") or get("user_api_key_auth_metadata")) key_meta: Final = ( - tuple(flatten_metadata({"user_api_key_auth_metadata": raw_key_meta})) - if raw_key_meta is not None - else () + tuple(flatten_metadata({"user_api_key_auth_metadata": raw_key_meta})) if raw_key_meta is not None else () ) request_meta: Final = flatten_metadata(request_metadata) if request_metadata is not None else () metadata: Final = MappingProxyType(dict((*request_meta, *auth_meta, *key_meta))) diff --git a/tests/unit/integrations/otel/test_otel_v2_sources_of_truth.py b/tests/unit/integrations/otel/test_otel_v2_sources_of_truth.py index 4ac00ef889a..70b8252eedc 100644 --- a/tests/unit/integrations/otel/test_otel_v2_sources_of_truth.py +++ b/tests/unit/integrations/otel/test_otel_v2_sources_of_truth.py @@ -1613,10 +1613,12 @@ def test_request_identity_from_user_api_key_auth_flattens_key_metadata(): class _Auth: metadata = {"team": "a", "customtag": "b", "nested": {"k": "v"}} - ident = RequestIdentity.from_user_api_key_auth(_Auth()) - assert ident.metadata["user_api_key_auth_metadata.team"] == "a" - assert ident.metadata["user_api_key_auth_metadata.customtag"] == "b" - assert ident.metadata["user_api_key_auth_metadata.nested.k"] == "v" + ident: Final = RequestIdentity.from_user_api_key_auth(_Auth()) + assert ident.metadata == { + "user_api_key_auth_metadata.team": "a", + "user_api_key_auth_metadata.customtag": "b", + "user_api_key_auth_metadata.nested.k": "v", + } # --- request-metadata translation layer (RequestContext) -------------------- # From 6ffa31c196378dccac9138940d688dc2c883bc18 Mon Sep 17 00:00:00 2001 From: Jesus Velarde Date: Thu, 5 Nov 2026 16:10:41 -0800 Subject: [PATCH 3/3] test(otel): assert allowlisted key metadata end to end and drop dead auth fallback UserAPIKeyAuth has no user_api_key_auth_metadata field, so the fallback never fired. The test now uses a real UserAPIKeyAuth, compares the full promoted baggage, and checks that the default allowlist promotes no key metadata. --- litellm/integrations/otel/model/metadata.py | 7 +---- .../otel/test_otel_v2_sources_of_truth.py | 28 ++++++++++++++----- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/litellm/integrations/otel/model/metadata.py b/litellm/integrations/otel/model/metadata.py index 4f76ebc56d2..71a0f6e938f 100644 --- a/litellm/integrations/otel/model/metadata.py +++ b/litellm/integrations/otel/model/metadata.py @@ -152,12 +152,7 @@ class RequestIdentity: ) if (value := get(attr)) ) - raw_key_meta: Final = as_str_mapping(get("metadata") or get("user_api_key_auth_metadata")) - key_meta: Final = ( - tuple(flatten_metadata({"user_api_key_auth_metadata": raw_key_meta})) - if raw_key_meta is not None - else () - ) + key_meta: Final = flatten_metadata({"user_api_key_auth_metadata": get("metadata")}) request_meta: Final = flatten_metadata(request_metadata) if request_metadata is not None else () metadata: Final = MappingProxyType(dict((*request_meta, *auth_meta, *key_meta))) return cls( diff --git a/tests/unit/integrations/otel/test_otel_v2_sources_of_truth.py b/tests/unit/integrations/otel/test_otel_v2_sources_of_truth.py index 4ac00ef889a..b3703cf0770 100644 --- a/tests/unit/integrations/otel/test_otel_v2_sources_of_truth.py +++ b/tests/unit/integrations/otel/test_otel_v2_sources_of_truth.py @@ -1607,16 +1607,30 @@ def test_request_identity_from_user_api_key_auth(): assert ident.metadata["user_api_key_end_user_id"] == "eu9" -def test_request_identity_from_user_api_key_auth_flattens_key_metadata(): +def test_virtual_key_metadata_reaches_baggage_only_for_allowlisted_sub_keys(): + from litellm.integrations.otel.model.baggage import DEFAULT_BAGGAGE_METADATA_KEYS, promoted_baggage from litellm.integrations.otel.model.payloads import RequestIdentity + from litellm.proxy._types import UserAPIKeyAuth - class _Auth: - metadata = {"team": "a", "customtag": "b", "nested": {"k": "v"}} + auth: Final = UserAPIKeyAuth( + key_alias="cost-key", + metadata={"team": "a", "cost": {"center": "cc-42"}, "owner": "not-allowlisted"}, + ) + identity: Final = RequestIdentity.from_user_api_key_auth(auth) - ident = RequestIdentity.from_user_api_key_auth(_Auth()) - assert ident.metadata["user_api_key_auth_metadata.team"] == "a" - assert ident.metadata["user_api_key_auth_metadata.customtag"] == "b" - assert ident.metadata["user_api_key_auth_metadata.nested.k"] == "v" + allowlisted: Final = promoted_baggage( + identity, + None, + promoted_keys=(), + metadata_keys=("user_api_key_auth_metadata.team", "user_api_key_auth_metadata.cost.center"), + ) + default: Final = promoted_baggage(identity, None, promoted_keys=(), metadata_keys=DEFAULT_BAGGAGE_METADATA_KEYS) + + assert allowlisted == { + "litellm.metadata.user_api_key_auth_metadata.team": "a", + "litellm.metadata.user_api_key_auth_metadata.cost.center": "cc-42", + } + assert default == {"litellm.metadata.user_api_key_alias": "cost-key"} # --- request-metadata translation layer (RequestContext) -------------------- #