mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-11 22:51:28 +00:00
fix(proxy): match OpenAI on empty input and skip budget reservation for token counting
/v1/responses/input_tokens returned 200 with a count for an empty
"input" ("" or []), while OpenAI returns a 400 missing_required_parameter.
The route also went through optimistic budget reservation, which is only
released by LLM success/failure callbacks that a token count never
reaches, so every call leaked a reservation until TTL expiry and could
429 real traffic. Both routes plus the /openai alias now join
/utils/token_counter in the reservation exemption set.
This commit is contained in:
parent
c7c382402a
commit
6b7159323b
4 changed files with 77 additions and 1 deletions
|
|
@ -1142,6 +1142,12 @@ async def responses_input_tokens(
|
|||
return _missing_responses_param_response("model")
|
||||
if input_value is None:
|
||||
return _missing_responses_param_response("input")
|
||||
if isinstance(input_value, (str, list)) and not input_value:
|
||||
return _responses_invalid_request_response(
|
||||
message="""One of "input" or "previous_response_id" or 'prompt' or 'conversation' must be provided.""",
|
||||
param=None,
|
||||
code="missing_required_parameter",
|
||||
)
|
||||
|
||||
try:
|
||||
payload: Final[_TokenCountPayload] = {
|
||||
|
|
|
|||
|
|
@ -172,7 +172,14 @@ async def reserve_budget_for_request(
|
|||
) -> dict | None:
|
||||
if valid_token is None or not RouteChecks.is_llm_api_route(route=route):
|
||||
return None
|
||||
if route in {"/models", "/v1/models", "/utils/token_counter"}:
|
||||
if route in {
|
||||
"/models",
|
||||
"/v1/models",
|
||||
"/utils/token_counter",
|
||||
"/responses/input_tokens",
|
||||
"/v1/responses/input_tokens",
|
||||
"/openai/v1/responses/input_tokens",
|
||||
}:
|
||||
return None
|
||||
if get_model_from_request(request_body, route, llm_router=llm_router) is None:
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -1914,6 +1914,21 @@ class TestResponsesInputTokens:
|
|||
}
|
||||
counter.assert_not_awaited()
|
||||
|
||||
@pytest.mark.parametrize("empty_input", ["", []])
|
||||
def test_empty_input_returns_openai_400(self, empty_input):
|
||||
response, counter = self._post_input_tokens({"model": "gpt-4o", "input": empty_input})
|
||||
|
||||
assert response.status_code == 400, response.text
|
||||
assert response.json() == {
|
||||
"error": {
|
||||
"message": """One of "input" or "previous_response_id" or 'prompt' or 'conversation' must be provided.""",
|
||||
"type": "invalid_request_error",
|
||||
"param": None,
|
||||
"code": "missing_required_parameter",
|
||||
}
|
||||
}
|
||||
counter.assert_not_awaited()
|
||||
|
||||
def test_invalid_tools_returns_openai_400(self):
|
||||
response, counter = self._post_input_tokens({"model": "gpt-4o", "input": "hi", "tools": "not-a-list"})
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
from typing import Final
|
||||
|
||||
import pytest
|
||||
|
||||
from litellm.caching import DualCache
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from litellm.proxy.common_utils.user_api_key_cache import UserApiKeyCache
|
||||
from litellm.proxy.spend_tracking.budget_reservation import reserve_budget_for_request
|
||||
from litellm.proxy.utils import ProxyLogging
|
||||
|
||||
TOKEN_COUNTING_ROUTES: Final = (
|
||||
"/responses/input_tokens",
|
||||
"/v1/responses/input_tokens",
|
||||
"/openai/v1/responses/input_tokens",
|
||||
"/utils/token_counter",
|
||||
)
|
||||
|
||||
|
||||
def _budgeted_token() -> UserAPIKeyAuth:
|
||||
return UserAPIKeyAuth(api_key="sk-test", token="hashed-token", max_budget=100.0, spend=0.0)
|
||||
|
||||
|
||||
async def _reserve(route: str) -> dict | None:
|
||||
return await reserve_budget_for_request(
|
||||
request_body={"model": "gpt-4o", "input": "hello"},
|
||||
route=route,
|
||||
llm_router=None,
|
||||
valid_token=_budgeted_token(),
|
||||
team_object=None,
|
||||
user_object=None,
|
||||
prisma_client=None,
|
||||
user_api_key_cache=UserApiKeyCache(),
|
||||
proxy_logging_obj=ProxyLogging(user_api_key_cache=DualCache()),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("route", TOKEN_COUNTING_ROUTES)
|
||||
async def test_token_counting_routes_are_exempt_from_budget_reservation(route):
|
||||
assert await _reserve(route) is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_non_exempt_llm_route_still_reserves_budget():
|
||||
reservation: Final = await _reserve("/v1/responses")
|
||||
|
||||
assert reservation is not None
|
||||
assert reservation["reserved_cost"] > 0
|
||||
Loading…
Add table
Reference in a new issue