mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
fix(proxy): reserve budget for the Bedrock Converse prompt, not the context window
Resolving the model from the /bedrock path sends passthrough calls through optimistic budget reservation, whose tokenizer cannot walk Converse content blocks and so fell back to the model's max_input_tokens. Count those messages as text and read inferenceConfig.maxTokens so a budgeted key reserves the request's cost.
This commit is contained in:
parent
bd9593b74d
commit
533a1c959c
2 changed files with 53 additions and 15 deletions
|
|
@ -1362,12 +1362,15 @@ def _approximate_input_size(request_body: Mapping[str, object]) -> int:
|
|||
def _count_input_tokens(request_body: dict, model: str) -> int | None:
|
||||
try:
|
||||
if "messages" in request_body:
|
||||
return litellm.token_counter(
|
||||
model=model,
|
||||
messages=request_body.get("messages") or [],
|
||||
tools=request_body.get("tools"),
|
||||
tool_choice=request_body.get("tool_choice"),
|
||||
)
|
||||
try:
|
||||
return litellm.token_counter(
|
||||
model=model,
|
||||
messages=request_body.get("messages") or (),
|
||||
tools=request_body.get("tools"),
|
||||
tool_choice=request_body.get("tool_choice"),
|
||||
)
|
||||
except ValueError:
|
||||
return _count_text_tokens(model=model, text=request_body.get("messages"))
|
||||
if "prompt" in request_body:
|
||||
return _count_text_tokens(model=model, text=request_body.get("prompt"))
|
||||
if "input" in request_body:
|
||||
|
|
@ -1415,11 +1418,7 @@ def _estimate_output_tokens(
|
|||
if _is_input_only_route(route=route):
|
||||
return 0
|
||||
|
||||
requested: int | None = None
|
||||
for key in ("max_completion_tokens", "max_tokens", "max_output_tokens"):
|
||||
requested = _to_int(request_body.get(key))
|
||||
if requested is not None:
|
||||
break
|
||||
requested: Final = _requested_output_tokens(request_body)
|
||||
|
||||
# Clamp at min(requested-or-default, model_max-or-default). Two purposes:
|
||||
# (1) Without an explicit cap we still need a finite reservation so the
|
||||
|
|
@ -1430,9 +1429,19 @@ def _estimate_output_tokens(
|
|||
# at the cap — the model can only physically emit max_output_tokens
|
||||
# anyway, so reserving more is both wasteful and a DoS surface.
|
||||
model_ceiling: Final = _to_int(model_info.get("max_output_tokens")) or DEFAULT_MAX_OUTPUT_TOKENS_FALLBACK
|
||||
if requested is None:
|
||||
requested = DEFAULT_MAX_OUTPUT_TOKENS_FALLBACK
|
||||
return min(requested, model_ceiling)
|
||||
return min(DEFAULT_MAX_OUTPUT_TOKENS_FALLBACK if requested is None else requested, model_ceiling)
|
||||
|
||||
|
||||
_OUTPUT_TOKEN_FIELDS: Final = ("max_completion_tokens", "max_tokens", "max_output_tokens")
|
||||
|
||||
|
||||
def _requested_output_tokens(request_body: Mapping[str, object]) -> int | None:
|
||||
inference_config: Final = request_body.get("inferenceConfig")
|
||||
candidates: Final = (
|
||||
*(request_body.get(field) for field in _OUTPUT_TOKEN_FIELDS),
|
||||
inference_config.get("maxTokens") if isinstance(inference_config, Mapping) else None,
|
||||
)
|
||||
return next((tokens for tokens in map(_to_int, candidates) if tokens is not None), None)
|
||||
|
||||
|
||||
def _count_text_tokens(model: str, text: object) -> int:
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ 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.spend_tracking.budget_reservation import estimate_request_max_cost, reserve_budget_for_request
|
||||
from litellm.proxy.utils import ProxyLogging
|
||||
|
||||
TOKEN_COUNTING_ROUTES: Final = (
|
||||
|
|
@ -46,3 +46,32 @@ async def test_non_exempt_llm_route_still_reserves_budget():
|
|||
|
||||
assert reservation is not None
|
||||
assert reservation["reserved_cost"] > 0
|
||||
|
||||
|
||||
BEDROCK_SONNET: Final = "us.anthropic.claude-sonnet-4-6"
|
||||
CONVERSE_BODY: Final = {
|
||||
"messages": [{"role": "user", "content": [{"text": "Reply with one word: pong"}]}],
|
||||
"inferenceConfig": {"maxTokens": 5},
|
||||
}
|
||||
INVOKE_BODY: Final = {
|
||||
"anthropic_version": "bedrock-2023-05-31",
|
||||
"max_tokens": 5,
|
||||
"messages": [{"role": "user", "content": "Reply with one word: pong"}],
|
||||
}
|
||||
|
||||
|
||||
def test_bedrock_converse_body_reserves_the_prompt_not_the_context_window():
|
||||
converse_cost: Final = estimate_request_max_cost(
|
||||
request_body=CONVERSE_BODY,
|
||||
route=f"/bedrock/model/{BEDROCK_SONNET}/converse",
|
||||
llm_router=None,
|
||||
input_token_counts={},
|
||||
)
|
||||
invoke_cost: Final = estimate_request_max_cost(
|
||||
request_body=INVOKE_BODY,
|
||||
route=f"/bedrock/model/{BEDROCK_SONNET}/invoke",
|
||||
llm_router=None,
|
||||
input_token_counts={},
|
||||
)
|
||||
assert converse_cost is not None and invoke_cost is not None
|
||||
assert invoke_cost < converse_cost < 2 * invoke_cost
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue