mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
fix(proxy): return team callbacks stored under metadata.logging from GET /team/{team_id}/callback
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
fa56283806
commit
75fa8c1b90
2 changed files with 158 additions and 6 deletions
|
|
@ -9,6 +9,7 @@ import copy
|
|||
import json
|
||||
import traceback
|
||||
from datetime import datetime, timezone
|
||||
from functools import reduce
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Header, HTTPException, Request, status
|
||||
|
|
@ -27,7 +28,10 @@ from litellm.proxy._types import (
|
|||
UserAPIKeyAuth,
|
||||
)
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
from litellm.proxy.common_utils.callback_utils import encrypt_callback_vars
|
||||
from litellm.proxy.common_utils.callback_utils import (
|
||||
decrypt_callback_vars,
|
||||
encrypt_callback_vars,
|
||||
)
|
||||
from litellm.proxy.management_endpoints.team_endpoints import _verify_team_access
|
||||
from litellm.proxy.management_helpers.utils import management_endpoint_wrapper
|
||||
from litellm.repositories.team_repository import TeamRepository
|
||||
|
|
@ -385,6 +389,48 @@ async def disable_team_logging(
|
|||
)
|
||||
|
||||
|
||||
def _team_callback_metadata_from_metadata(team_metadata: Any) -> TeamCallbackMetadata:
|
||||
"""Build the effective ``TeamCallbackMetadata`` from a team's stored metadata.
|
||||
|
||||
Mirrors the request-time precedence in
|
||||
``litellm_pre_call_utils._get_dynamic_logging_metadata``: callbacks added via
|
||||
``POST /team/{team_id}/callback`` live under ``team_metadata["logging"]`` (a
|
||||
list of ``AddTeamCallback`` dicts), while older rows carry a pre-aggregated
|
||||
``team_metadata["callback_settings"]``. Prefer ``logging`` and fall back to
|
||||
``callback_settings``, decrypting stored callback_vars first.
|
||||
"""
|
||||
from litellm.proxy.litellm_pre_call_utils import (
|
||||
_get_validated_callback_metadata,
|
||||
convert_key_logging_metadata_to_callback,
|
||||
)
|
||||
|
||||
if not isinstance(team_metadata, dict):
|
||||
return TeamCallbackMetadata()
|
||||
|
||||
decrypted_metadata = decrypt_callback_vars(team_metadata)
|
||||
|
||||
logging_entries = decrypted_metadata.get("logging")
|
||||
if isinstance(logging_entries, list) and logging_entries:
|
||||
callbacks = tuple(
|
||||
callback
|
||||
for item in logging_entries
|
||||
if (callback := _get_validated_callback_metadata(item=item, source="team-level")) is not None
|
||||
)
|
||||
if callbacks:
|
||||
return reduce(
|
||||
lambda acc, callback: convert_key_logging_metadata_to_callback(
|
||||
data=callback, team_callback_settings_obj=acc
|
||||
),
|
||||
callbacks,
|
||||
TeamCallbackMetadata(),
|
||||
)
|
||||
|
||||
callback_settings = decrypted_metadata.get("callback_settings")
|
||||
if isinstance(callback_settings, dict):
|
||||
return TeamCallbackMetadata(**callback_settings)
|
||||
return TeamCallbackMetadata()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/team/{team_id:path}/callback",
|
||||
tags=["team management"],
|
||||
|
|
@ -443,11 +489,7 @@ async def get_team_callbacks(
|
|||
)
|
||||
|
||||
# Retrieve team callback settings from metadata
|
||||
team_metadata = _existing_team.metadata
|
||||
team_callback_settings = team_metadata.get("callback_settings", {})
|
||||
|
||||
# Convert to TeamCallbackMetadata object for consistent structure
|
||||
team_callback_settings_obj = TeamCallbackMetadata(**team_callback_settings)
|
||||
team_callback_settings_obj = _team_callback_metadata_from_metadata(_existing_team.metadata)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
|
|
|
|||
|
|
@ -459,3 +459,113 @@ async def test_add_team_callbacks_writes_encrypted_callback_vars(monkeypatch):
|
|||
recovered = decrypt_callback_vars(written)["logging"][0]["callback_vars"]
|
||||
assert recovered["langfuse_secret_key"] == "sk-lf-real-secret"
|
||||
assert recovered["langfuse_public_key"] == "pk-lf-real-public"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_team_callbacks_returns_callbacks_stored_under_logging():
|
||||
"""GET must surface callbacks written by POST, which persists them under
|
||||
``metadata["logging"]`` rather than the deprecated ``callback_settings``."""
|
||||
mock_prisma = _patch_prisma(
|
||||
_team_row(
|
||||
team_id="team-1",
|
||||
metadata={
|
||||
"logging": [
|
||||
{
|
||||
"callback_name": "langsmith",
|
||||
"callback_type": "success",
|
||||
"callback_vars": {
|
||||
"langsmith_api_key": "ls-key",
|
||||
"langsmith_project": "proj",
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
with patch("litellm.proxy.proxy_server.prisma_client", mock_prisma):
|
||||
response = await get_team_callbacks(
|
||||
http_request=MagicMock(spec=Request),
|
||||
team_id="team-1",
|
||||
user_api_key_dict=_admin_auth(),
|
||||
)
|
||||
|
||||
data = response["data"]
|
||||
assert data["success_callbacks"] == ["langsmith"]
|
||||
assert data["failure_callbacks"] == []
|
||||
assert data["callback_vars"]["langsmith_project"] == "proj"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_then_get_team_callbacks_round_trip(monkeypatch):
|
||||
"""End-to-end regression: a callback added via POST must appear in the GET
|
||||
response with decrypted callback_vars, matching what the proxy applies."""
|
||||
monkeypatch.setenv("LITELLM_SALT_KEY", "test-salt-32-bytes-aaaaaaaaaaaaaa")
|
||||
mock_prisma = _patch_prisma(_team_row(team_id="team-1", metadata={"logging": []}))
|
||||
|
||||
with (
|
||||
patch("litellm.proxy.proxy_server.prisma_client", mock_prisma),
|
||||
patch("litellm.proxy.proxy_server.litellm_proxy_admin_name", "admin"),
|
||||
patch("litellm.proxy.proxy_server.master_key", None),
|
||||
):
|
||||
await add_team_callbacks(
|
||||
data=AddTeamCallback(
|
||||
callback_name="langfuse",
|
||||
callback_type="success",
|
||||
callback_vars={
|
||||
"langfuse_public_key": "pk-lf-real-public",
|
||||
"langfuse_secret_key": "sk-lf-real-secret",
|
||||
},
|
||||
),
|
||||
http_request=MagicMock(spec=Request),
|
||||
team_id="team-1",
|
||||
user_api_key_dict=_admin_auth(),
|
||||
litellm_changed_by=None,
|
||||
)
|
||||
|
||||
written_metadata = json.loads(
|
||||
mock_prisma.db.litellm_teamtable.update.await_args.kwargs["data"]["metadata"]
|
||||
)
|
||||
mock_prisma.get_data = AsyncMock(
|
||||
return_value=_team_row(team_id="team-1", metadata=written_metadata)
|
||||
)
|
||||
|
||||
response = await get_team_callbacks(
|
||||
http_request=MagicMock(spec=Request),
|
||||
team_id="team-1",
|
||||
user_api_key_dict=_admin_auth(),
|
||||
)
|
||||
|
||||
data = response["data"]
|
||||
assert data["success_callbacks"] == ["langfuse"]
|
||||
assert data["callback_vars"]["langfuse_public_key"] == "pk-lf-real-public"
|
||||
assert data["callback_vars"]["langfuse_secret_key"] == "sk-lf-real-secret"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_team_callbacks_falls_back_to_legacy_callback_settings():
|
||||
"""Rows written in the deprecated ``callback_settings`` shape must still be
|
||||
returned so pre-existing teams keep working."""
|
||||
mock_prisma = _patch_prisma(
|
||||
_team_row(
|
||||
team_id="team-1",
|
||||
metadata={
|
||||
"callback_settings": {
|
||||
"success_callback": ["langfuse"],
|
||||
"failure_callback": [],
|
||||
"callback_vars": {"langfuse_public_key": "pk-legacy"},
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
with patch("litellm.proxy.proxy_server.prisma_client", mock_prisma):
|
||||
response = await get_team_callbacks(
|
||||
http_request=MagicMock(spec=Request),
|
||||
team_id="team-1",
|
||||
user_api_key_dict=_admin_auth(),
|
||||
)
|
||||
|
||||
data = response["data"]
|
||||
assert data["success_callbacks"] == ["langfuse"]
|
||||
assert data["callback_vars"]["langfuse_public_key"] == "pk-legacy"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue