mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-08 22:21:35 +00:00
fix: cap /v2/key/info batch size to bound spend-log query fan-out
This commit is contained in:
parent
31130036c0
commit
9b30e73d07
2 changed files with 49 additions and 0 deletions
|
|
@ -3604,6 +3604,11 @@ async def _budget_limits_with_usage(budget_limits: object, api_key_hash: str) ->
|
|||
]
|
||||
|
||||
|
||||
# Caps per-request fan-out: each key with budget windows costs one spend-counter
|
||||
# read (worst case a SpendLogs aggregation) per window.
|
||||
MAX_KEY_INFO_KEYS_PER_REQUEST: Final = 100
|
||||
|
||||
|
||||
@router.post(
|
||||
"/v2/key/info",
|
||||
tags=["key management"],
|
||||
|
|
@ -3643,6 +3648,17 @@ async def info_key_fn_v2(
|
|||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail={"message": "Malformed request. No keys passed in."},
|
||||
)
|
||||
requested_key_count: Final = len(data.keys or []) + len(data.key_aliases or [])
|
||||
if requested_key_count > MAX_KEY_INFO_KEYS_PER_REQUEST:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail={
|
||||
"message": (
|
||||
f"Too many keys requested: {requested_key_count}. "
|
||||
f"At most {MAX_KEY_INFO_KEYS_PER_REQUEST} keys and key_aliases combined per request."
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
# Resolve key_aliases to tokens so we never pass token=None (unbounded query)
|
||||
tokens_to_query: Final = list(data.keys) if data.keys else []
|
||||
|
|
|
|||
|
|
@ -13995,6 +13995,39 @@ async def test_info_key_fn_v2_budget_limits_includes_current_spend(monkeypatch):
|
|||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_info_key_fn_v2_rejects_oversized_batch(monkeypatch):
|
||||
"""/v2/key/info must reject over-cap batches before doing any DB work."""
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from litellm.proxy._types import KeyRequest, ProxyException
|
||||
from litellm.proxy.management_endpoints.key_management_endpoints import (
|
||||
MAX_KEY_INFO_KEYS_PER_REQUEST,
|
||||
info_key_fn_v2,
|
||||
)
|
||||
|
||||
mock_prisma_client = AsyncMock()
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.user_api_key_cache", AsyncMock())
|
||||
|
||||
user_api_key_dict = UserAPIKeyAuth(
|
||||
user_role=LitellmUserRoles.PROXY_ADMIN,
|
||||
api_key="sk-admin-batch-cap",
|
||||
)
|
||||
|
||||
with pytest.raises(ProxyException) as exc_info:
|
||||
await info_key_fn_v2(
|
||||
data=KeyRequest(
|
||||
keys=[f"hash-{i}" for i in range(MAX_KEY_INFO_KEYS_PER_REQUEST)],
|
||||
key_aliases=["alias-over-cap"],
|
||||
),
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
)
|
||||
|
||||
assert exc_info.value.code == "422"
|
||||
mock_prisma_client.get_data.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_budget_limits_with_usage_json_string_input(monkeypatch):
|
||||
"""budget_limits stored as a JSON string should be parsed and annotated."""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue