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:
milan 2026-07-31 19:08:18 +00:00
parent fa56283806
commit 75fa8c1b90
2 changed files with 158 additions and 6 deletions

View file

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

View file

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