From 1db07159cec23b0554cdd768a57e23bf6c67ff78 Mon Sep 17 00:00:00 2001 From: Yucheng Zhu Date: Fri, 31 Jul 2026 00:58:32 -0700 Subject: [PATCH] fix(otel/v2): gate destination disclosure on the v2 flag resolved_logging_exporter_names backs the resolved_logging_exporters field on /team/info and /organization/info (and the team/org UI views that render it). It mirrored the request-time resolver's access selection but, unlike the resolver, did not check is_otel_v2_enabled. With LITELLM_OTEL_V2 off the resolver returns nothing, yet the disclosure still listed granted destinations, so the info endpoints and UI advertised exporters that receive no traces while the feature is inert. Gate the disclosure on the same flag so it matches what actually exports. --- .../logging_exporter_access.py | 7 +++++ .../test_logging_exporter_access.py | 31 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/litellm/proxy/management_endpoints/logging_exporter_access.py b/litellm/proxy/management_endpoints/logging_exporter_access.py index 5eaffa55773..7507bdaf6be 100644 --- a/litellm/proxy/management_endpoints/logging_exporter_access.py +++ b/litellm/proxy/management_endpoints/logging_exporter_access.py @@ -13,6 +13,7 @@ one-element scope built with ``identity_scope``. from pydantic import BaseModel, ConfigDict, ValidationError import litellm +from litellm.integrations.otel.model.config import is_otel_v2_enabled from litellm.models.credentials import CredentialAccess, CredentialInfo @@ -76,7 +77,13 @@ def resolved_logging_exporter_names( Mirrors the request-time resolver's selection: a logging destination is included when its ``access`` grants the identity. Names only; endpoints, headers, and the access map itself stay proxy-admin information. + + Gated on ``is_otel_v2_enabled`` for parity with the resolver, which returns nothing + when the flag is off: disclosing a destination the request path would never fire + would claim traces are exported when none are. """ + if not is_otel_v2_enabled(): + return () team_ids, org_ids = identity_scope(team_id, org_id) selected = tuple( credential.credential_name diff --git a/tests/test_litellm/proxy/management_endpoints/test_logging_exporter_access.py b/tests/test_litellm/proxy/management_endpoints/test_logging_exporter_access.py index e5bab96e9f0..996455fc9ac 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_logging_exporter_access.py +++ b/tests/test_litellm/proxy/management_endpoints/test_logging_exporter_access.py @@ -20,6 +20,19 @@ from litellm.proxy.management_endpoints.logging_exporter_access import ( resolved_logging_exporter_names, ) +import pytest + +from litellm.integrations.otel.model.config import is_otel_v2_enabled + + +@pytest.fixture(autouse=True) +def _reset_otel_v2_flag_cache(): + """``is_otel_v2_enabled`` is lru-cached; clear it around each test so ``LITELLM_OTEL_V2`` + toggles take effect and don't leak between tests.""" + is_otel_v2_enabled.cache_clear() + yield + is_otel_v2_enabled.cache_clear() + # --- is_logging_credential: the access-validation + merge gate --------------- @@ -183,6 +196,7 @@ def test_resolved_names_are_access_only(monkeypatch): """A destination name appears iff its access grants the (team_id, org_id). Included: team-granted, org-granted, global. Excluded: empty-access, granted-but-not-logging (provider) credentials, access for another team.""" + monkeypatch.setenv("LITELLM_OTEL_V2", "true") monkeypatch.setattr( litellm, "credential_list", @@ -200,6 +214,7 @@ def test_resolved_names_are_access_only(monkeypatch): def test_resolved_names_empty_scope_gets_global_only(monkeypatch): + monkeypatch.setenv("LITELLM_OTEL_V2", "true") monkeypatch.setattr( litellm, "credential_list", @@ -212,5 +227,21 @@ def test_resolved_names_empty_scope_gets_global_only(monkeypatch): def test_resolved_names_empty_registry(monkeypatch): + monkeypatch.setenv("LITELLM_OTEL_V2", "true") monkeypatch.setattr(litellm, "credential_list", []) assert resolved_logging_exporter_names("t1", "o1") == () + + +def test_resolved_names_gated_off_when_v2_disabled(monkeypatch): + """Disclosure mirrors the resolver, which returns nothing with the v2 flag off. + A granting destination is disclosed only when ``LITELLM_OTEL_V2`` is enabled, so + ``/team/info`` never claims traces are exported while the feature is inert.""" + monkeypatch.setattr(litellm, "credential_list", [_cred("team-granted", access={"teams": ["t1"]})]) + + monkeypatch.setenv("LITELLM_OTEL_V2", "false") + is_otel_v2_enabled.cache_clear() + assert resolved_logging_exporter_names("t1", None) == () + + monkeypatch.setenv("LITELLM_OTEL_V2", "true") + is_otel_v2_enabled.cache_clear() + assert resolved_logging_exporter_names("t1", None) == ("team-granted",)