mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-08 22:21:35 +00:00
fix: read phoenix_project_name from team/key metadata in ArizePhoenixLogger
When phoenix_project_name is set on a team or API key via the management endpoints the proxy merges those fields into user_api_key_auth_metadata inside StandardLoggingMetadata. _get_dynamic_project_name was only checking the top-level metadata dict, so team/key values were silently dropped and traces always flowed to the default Phoenix project. Fix: also check metadata.user_api_key_auth_metadata.phoenix_project_name with priority: per-request > team/key (auth_metadata) > litellm_params > env var. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
50f88c8642
commit
9c59c908b3
2 changed files with 196 additions and 3 deletions
|
|
@ -118,18 +118,32 @@ class ArizePhoenixLogger(OpenTelemetry): # type: ignore
|
|||
"""
|
||||
Retrieve dynamic Phoenix project name from request metadata.
|
||||
|
||||
Users can set `metadata.phoenix_project_name` in their request to route
|
||||
traces to different Phoenix projects dynamically.
|
||||
Priority order (highest to lowest):
|
||||
1. Per-request metadata: ``metadata.phoenix_project_name``
|
||||
2. Team / key metadata via ``metadata.user_api_key_auth_metadata.phoenix_project_name``
|
||||
(the proxy merges both team and key metadata into ``user_api_key_auth_metadata``
|
||||
inside the standard logging payload — this is where ``phoenix_project_name`` lands
|
||||
when it is set on a team or API key via the management endpoints)
|
||||
3. ``litellm_params.metadata.phoenix_project_name`` (SDK / non-proxy usage)
|
||||
"""
|
||||
standard_logging_payload = kwargs.get("standard_logging_object")
|
||||
if isinstance(standard_logging_payload, dict):
|
||||
metadata = standard_logging_payload.get("metadata")
|
||||
if isinstance(metadata, dict):
|
||||
# 1. Per-request metadata (highest priority)
|
||||
project_name = metadata.get("phoenix_project_name")
|
||||
if project_name:
|
||||
return str(project_name)
|
||||
|
||||
# Also check litellm_params.metadata for SDK usage
|
||||
# 2. Team / key metadata — the proxy stores merged team+key metadata
|
||||
# in user_api_key_auth_metadata inside StandardLoggingMetadata.
|
||||
auth_metadata = metadata.get("user_api_key_auth_metadata")
|
||||
if isinstance(auth_metadata, dict):
|
||||
project_name = auth_metadata.get("phoenix_project_name")
|
||||
if project_name:
|
||||
return str(project_name)
|
||||
|
||||
# 3. litellm_params.metadata for direct SDK usage (non-proxy)
|
||||
litellm_params = kwargs.get("litellm_params")
|
||||
if isinstance(litellm_params, dict):
|
||||
metadata = litellm_params.get("metadata") or {}
|
||||
|
|
|
|||
|
|
@ -228,6 +228,139 @@ class TestGetDynamicProjectName:
|
|||
kwargs = {"standard_logging_object": "not-a-dict"}
|
||||
assert ArizePhoenixLogger._get_dynamic_project_name(kwargs) is None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Tests for team / key metadata path (the bug reported by Viktor O.)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def test_extracts_from_user_api_key_auth_metadata(self):
|
||||
"""
|
||||
phoenix_project_name set on a team is merged into
|
||||
user_api_key_auth_metadata by the proxy's pre-call utils.
|
||||
_get_dynamic_project_name must check that nested dict.
|
||||
"""
|
||||
kwargs = {
|
||||
"standard_logging_object": {
|
||||
"metadata": {
|
||||
"user_api_key_auth_metadata": {
|
||||
"phoenix_project_name": "team-project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert ArizePhoenixLogger._get_dynamic_project_name(kwargs) == "team-project"
|
||||
|
||||
def test_per_request_metadata_takes_priority_over_team_metadata(self):
|
||||
"""
|
||||
A per-request phoenix_project_name must win over a team-level one.
|
||||
"""
|
||||
kwargs = {
|
||||
"standard_logging_object": {
|
||||
"metadata": {
|
||||
"phoenix_project_name": "request-project",
|
||||
"user_api_key_auth_metadata": {
|
||||
"phoenix_project_name": "team-project"
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
assert ArizePhoenixLogger._get_dynamic_project_name(kwargs) == "request-project"
|
||||
|
||||
def test_falls_back_to_team_metadata_when_no_top_level_name(self):
|
||||
"""
|
||||
When there is no top-level phoenix_project_name but the team metadata
|
||||
has one, the team value must be returned.
|
||||
"""
|
||||
kwargs = {
|
||||
"standard_logging_object": {
|
||||
"metadata": {
|
||||
# no top-level phoenix_project_name
|
||||
"user_api_key_auth_metadata": {
|
||||
"phoenix_project_name": "team-project"
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
assert ArizePhoenixLogger._get_dynamic_project_name(kwargs) == "team-project"
|
||||
|
||||
def test_team_metadata_takes_priority_over_litellm_params(self):
|
||||
"""
|
||||
Team metadata (via standard_logging_object) must win over
|
||||
litellm_params.metadata (SDK-level).
|
||||
"""
|
||||
kwargs = {
|
||||
"standard_logging_object": {
|
||||
"metadata": {
|
||||
"user_api_key_auth_metadata": {
|
||||
"phoenix_project_name": "team-project"
|
||||
}
|
||||
}
|
||||
},
|
||||
"litellm_params": {
|
||||
"metadata": {"phoenix_project_name": "sdk-project"},
|
||||
},
|
||||
}
|
||||
assert ArizePhoenixLogger._get_dynamic_project_name(kwargs) == "team-project"
|
||||
|
||||
def test_empty_user_api_key_auth_metadata_does_not_crash(self):
|
||||
"""Empty auth_metadata dict must not raise and must fall through."""
|
||||
kwargs = {
|
||||
"standard_logging_object": {
|
||||
"metadata": {
|
||||
"user_api_key_auth_metadata": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert ArizePhoenixLogger._get_dynamic_project_name(kwargs) is None
|
||||
|
||||
def test_none_user_api_key_auth_metadata_does_not_crash(self):
|
||||
"""None auth_metadata must not raise and must fall through."""
|
||||
kwargs = {
|
||||
"standard_logging_object": {
|
||||
"metadata": {
|
||||
"user_api_key_auth_metadata": None
|
||||
}
|
||||
}
|
||||
}
|
||||
assert ArizePhoenixLogger._get_dynamic_project_name(kwargs) is None
|
||||
|
||||
def test_full_priority_chain_all_sources_present(self):
|
||||
"""
|
||||
Full priority chain:
|
||||
per-request > team (user_api_key_auth_metadata) > litellm_params
|
||||
"""
|
||||
# Per-request wins
|
||||
kwargs_request = {
|
||||
"standard_logging_object": {
|
||||
"metadata": {
|
||||
"phoenix_project_name": "request-project",
|
||||
"user_api_key_auth_metadata": {"phoenix_project_name": "team-project"},
|
||||
}
|
||||
},
|
||||
"litellm_params": {"metadata": {"phoenix_project_name": "sdk-project"}},
|
||||
}
|
||||
assert ArizePhoenixLogger._get_dynamic_project_name(kwargs_request) == "request-project"
|
||||
|
||||
# No per-request → team wins
|
||||
kwargs_team = {
|
||||
"standard_logging_object": {
|
||||
"metadata": {
|
||||
"user_api_key_auth_metadata": {"phoenix_project_name": "team-project"},
|
||||
}
|
||||
},
|
||||
"litellm_params": {"metadata": {"phoenix_project_name": "sdk-project"}},
|
||||
}
|
||||
assert ArizePhoenixLogger._get_dynamic_project_name(kwargs_team) == "team-project"
|
||||
|
||||
# No per-request, no team → SDK wins
|
||||
kwargs_sdk = {
|
||||
"standard_logging_object": {"metadata": {}},
|
||||
"litellm_params": {"metadata": {"phoenix_project_name": "sdk-project"}},
|
||||
}
|
||||
assert ArizePhoenixLogger._get_dynamic_project_name(kwargs_sdk) == "sdk-project"
|
||||
|
||||
# Nothing → None
|
||||
assert ArizePhoenixLogger._get_dynamic_project_name({}) is None
|
||||
|
||||
|
||||
class TestDynamicProjectNameOnSpan:
|
||||
"""set_arize_phoenix_attributes sets openinference.project.name on the span."""
|
||||
|
|
@ -253,6 +386,52 @@ class TestDynamicProjectNameOnSpan:
|
|||
|
||||
span.set_attribute.assert_called_once_with("openinference.project.name", "env-project")
|
||||
|
||||
@patch.dict("os.environ", {"PHOENIX_PROJECT_NAME": "env-fallback"}, clear=False)
|
||||
@patch("litellm.integrations.arize._utils.set_attributes")
|
||||
def test_team_metadata_project_name_sets_span_attribute(self, _mock_set_attrs):
|
||||
"""
|
||||
Regression test: phoenix_project_name from team metadata (stored in
|
||||
user_api_key_auth_metadata) must be applied to the span, not the env fallback.
|
||||
"""
|
||||
span = MagicMock()
|
||||
kwargs = {
|
||||
"standard_logging_object": {
|
||||
"metadata": {
|
||||
"user_api_key_auth_metadata": {
|
||||
"phoenix_project_name": "team-project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ArizePhoenixLogger.set_arize_phoenix_attributes(span, kwargs, response_obj=None)
|
||||
|
||||
span.set_attribute.assert_called_once_with("openinference.project.name", "team-project")
|
||||
|
||||
@patch.dict(
|
||||
"os.environ",
|
||||
{"PHOENIX_PROJECT_NAME": "env-fallback"},
|
||||
clear=False,
|
||||
)
|
||||
@patch("litellm.integrations.arize._utils.set_attributes")
|
||||
def test_per_request_overrides_team_metadata_on_span(self, _mock_set_attrs):
|
||||
"""
|
||||
Per-request phoenix_project_name must override team metadata on the span.
|
||||
"""
|
||||
span = MagicMock()
|
||||
kwargs = {
|
||||
"standard_logging_object": {
|
||||
"metadata": {
|
||||
"phoenix_project_name": "request-project",
|
||||
"user_api_key_auth_metadata": {
|
||||
"phoenix_project_name": "team-project"
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
ArizePhoenixLogger.set_arize_phoenix_attributes(span, kwargs, response_obj=None)
|
||||
|
||||
span.set_attribute.assert_called_once_with("openinference.project.name", "request-project")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue