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.
This commit is contained in:
Yucheng Zhu 2026-07-31 00:58:32 -07:00
parent de55970438
commit 1db07159ce
2 changed files with 38 additions and 0 deletions

View file

@ -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

View file

@ -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",)