fix: cap /v2/key/info batch size to bound spend-log query fan-out

This commit is contained in:
Thijmen Stavenuiter 2026-08-17 20:48:12 +02:00
parent 31130036c0
commit 9b30e73d07
2 changed files with 49 additions and 0 deletions

View file

@ -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 []

View file

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