mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-05 02:41:56 +00:00
fix(proxy): report callback entries requests ignore as unhealthy in /key/health
Entries that fail AddTeamCallback validation (for example a key entry without callback_vars) are dropped by the request-time resolver. /key/health used to fall through to the next level and call the key healthy with no callbacks. It now returns unhealthy with the offending entry and the validation error Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
8ca3812e99
commit
edea717f3c
2 changed files with 79 additions and 0 deletions
|
|
@ -26,6 +26,7 @@ from typing import TYPE_CHECKING, Any, Final, Literal, Optional, Protocol, TypeV
|
|||
import fastapi
|
||||
import yaml
|
||||
from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request, status
|
||||
from pydantic import ValidationError as PydanticValidationError
|
||||
from typing_extensions import ReadOnly, TypedDict
|
||||
|
||||
import litellm
|
||||
|
|
@ -7019,6 +7020,7 @@ async def key_health(
|
|||
```
|
||||
"""
|
||||
from litellm.proxy.litellm_pre_call_utils import (
|
||||
KeyAndTeamLoggingSettings,
|
||||
_get_dynamic_logging_metadata, # pyright: ignore[reportPrivateUsage] # the request-time resolver; the health check must report the same callbacks a request would use
|
||||
)
|
||||
from litellm.proxy.proxy_server import proxy_config
|
||||
|
|
@ -7028,6 +7030,22 @@ async def key_health(
|
|||
if key_metadata and "logging" in key_metadata:
|
||||
_raise_if_key_logging_missing_callback_name(decrypt_callback_vars(key_metadata)["logging"])
|
||||
|
||||
configured_entries: Final = (
|
||||
KeyAndTeamLoggingSettings.get_key_dynamic_logging_settings(user_api_key_dict)
|
||||
or KeyAndTeamLoggingSettings.get_team_dynamic_logging_settings(user_api_key_dict)
|
||||
or ()
|
||||
)
|
||||
invalid_entries: Final = _describe_invalid_callback_entries(configured_entries)
|
||||
if invalid_entries is not None:
|
||||
return KeyHealthResponse(
|
||||
key="unhealthy",
|
||||
logging_callbacks=LoggingCallbackStatus(
|
||||
callbacks=_configured_callback_names(configured_entries),
|
||||
status="unhealthy",
|
||||
details=invalid_entries,
|
||||
),
|
||||
)
|
||||
|
||||
callback_settings: Final = _get_dynamic_logging_metadata(
|
||||
user_api_key_dict=user_api_key_dict, proxy_config=proxy_config
|
||||
)
|
||||
|
|
@ -7091,6 +7109,32 @@ def _raise_if_key_logging_missing_callback_name(key_logging: Sequence[Mapping[st
|
|||
raise ValueError("callback_name is required in key_logging")
|
||||
|
||||
|
||||
def _configured_callback_names(entries: Sequence[Mapping[str, object]]) -> tuple[str, ...]:
|
||||
return tuple(dict.fromkeys(name for entry in entries if isinstance(name := entry.get("callback_name"), str)))
|
||||
|
||||
|
||||
def _describe_invalid_callback_entries(entries: Sequence[Mapping[str, object]]) -> str | None:
|
||||
errors: Final = tuple(
|
||||
f"{entry.get('callback_name')}: {error}"
|
||||
for entry in entries
|
||||
if (error := _callback_entry_error(entry)) is not None
|
||||
)
|
||||
if not errors:
|
||||
return None
|
||||
return f"Invalid callback metadata, requests ignore these entries: {'; '.join(errors)}"
|
||||
|
||||
|
||||
def _callback_entry_error(entry: Mapping[str, object]) -> str | None:
|
||||
try:
|
||||
AddTeamCallback.model_validate(entry)
|
||||
except PydanticValidationError as e:
|
||||
return ", ".join(
|
||||
f"{'.'.join(str(part) for part in err['loc'])} {err['msg']}"
|
||||
for err in e.errors(include_url=False, include_input=False)
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
async def flush_gcs_and_describe_failures(gcs_logger: CustomLogger | None) -> str | None:
|
||||
from litellm.integrations.gcs_bucket.gcs_bucket import GCSBucketLogger
|
||||
|
||||
|
|
|
|||
|
|
@ -18240,6 +18240,41 @@ async def test_key_health_without_any_effective_callbacks_reports_healthy_and_se
|
|||
test_logging.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"caller",
|
||||
[
|
||||
UserAPIKeyAuth(api_key="sk-1", metadata={"logging": [{"callback_name": "langfuse"}]}),
|
||||
UserAPIKeyAuth(
|
||||
api_key="sk-1",
|
||||
metadata={"logging": []},
|
||||
team_metadata={"logging": [{"callback_name": "langfuse", "callback_vars": {"not_a_var": "x"}}]},
|
||||
),
|
||||
],
|
||||
ids=["key entry without callback_vars", "team entry with an unknown callback var"],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_key_health_reports_callback_entries_requests_ignore_instead_of_calling_them_healthy(
|
||||
caller: UserAPIKeyAuth,
|
||||
):
|
||||
from litellm.proxy.management_endpoints.key_management_endpoints import key_health
|
||||
|
||||
with (
|
||||
patch("litellm.proxy.proxy_server.proxy_config", _default_team_gcs_proxy_config("team-gcs")), # test-quality-ok: key_health reads the module-level proxy config
|
||||
patch( # test-quality-ok: the mock completion behind test_key_logging needs a running proxy
|
||||
"litellm.proxy.management_endpoints.key_management_endpoints.test_key_logging", AsyncMock()
|
||||
) as test_logging,
|
||||
):
|
||||
response = await key_health(request=MagicMock(), user_api_key_dict=caller)
|
||||
|
||||
assert response["key"] == "unhealthy"
|
||||
logging_callbacks: Final = response["logging_callbacks"]
|
||||
assert logging_callbacks is not None
|
||||
assert logging_callbacks["callbacks"] == ("langfuse",)
|
||||
assert logging_callbacks["status"] == "unhealthy"
|
||||
assert "langfuse:" in (logging_callbacks["details"] or "")
|
||||
test_logging.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_key_health_rejects_key_logging_entries_without_a_callback_name():
|
||||
from litellm.proxy.management_endpoints.key_management_endpoints import key_health
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue