fix(proxy): reconcile project quota reservations

This commit is contained in:
Shivi Jain 2026-08-14 23:31:18 +05:30
parent 312d12fe0c
commit 2b23295f82
7 changed files with 407 additions and 341 deletions

View file

@ -57,7 +57,7 @@
"limit": 5707
},
"reportMissingTypeArgument": {
"limit": 15642
"limit": 15641
},
"reportMissingTypeStubs": {
"limit": 40
@ -108,7 +108,7 @@
"limit": 39237
},
"reportUnknownParameterType": {
"limit": 19969
"limit": 19968
},
"reportUnknownVariableType": {
"limit": 30881
@ -132,7 +132,7 @@
"limit": 27
},
"reportUnusedClass": {
"limit": 23
"limit": 22
},
"reportUnusedFunction": {
"limit": 139

View file

@ -2,7 +2,7 @@ import asyncio
import json
import os
import ssl
from collections.abc import AsyncIterator, Coroutine, Iterator, Mapping
from collections.abc import AsyncIterator, Coroutine, Iterator, Mapping, Sequence
from contextlib import asynccontextmanager
from functools import lru_cache
from types import ModuleType
@ -69,6 +69,7 @@ from litellm.llms.custom_httpx.http_handler import (
from litellm.responses.streaming_iterator import (
BaseResponsesAPIStreamingIterator,
MockResponsesAPIStreamingIterator,
ProjectQuotaCallback,
ResponsesAPIStreamingIterator,
ResponsesWebSocketStreaming,
SyncResponsesAPIStreamingIterator,
@ -252,7 +253,7 @@ def _has_pre_call_deployment_hook(logging_obj: LiteLLMLoggingObj) -> bool:
return False
def _collect_ws_project_quota_callbacks() -> list:
def _collect_ws_project_quota_callbacks() -> tuple[ProjectQuotaCallback, ...]:
"""Duck-type discover proxy hooks exposing per-frame project ITPM/OTPM
enforcement, so the Responses WebSocket loop can charge every
``response.create`` frame, not just the connection's first one.
@ -261,19 +262,16 @@ def _collect_ws_project_quota_callbacks() -> list:
proxy hook directly) to avoid a layering violation (SDK importing from
the proxy layer).
"""
try:
import litellm as _litellm
import litellm as _litellm
return [
cb for cb in _litellm.callbacks if callable(getattr(cb, "enforce_project_io_token_quota_for_frame", None))
]
except Exception as exc: # noqa: BLE001 - discovery must not block the connection
verbose_logger.warning(
"Responses WebSocket: failed to collect project quota callbacks — "
"per-frame ITPM/OTPM enforcement will be skipped. Error: %s",
exc,
)
return []
callbacks: Final = cast( # cast-ok: callback registry is inspected before protocol use
Sequence[object], _litellm.callbacks
)
return tuple(
cast(ProjectQuotaCallback, callback) # cast-ok: required callback method is callable
for callback in callbacks
if callable(getattr(callback, "enforce_project_io_token_quota_for_frame", None))
)
class BaseLLMHTTPHandler:

View file

@ -18,11 +18,12 @@ Quick summary:
"""
import json
from collections.abc import Iterable
from collections.abc import Iterable, Mapping, Sequence
from types import MappingProxyType
from typing import TYPE_CHECKING, Any, Final, Literal, NoReturn
from fastapi import HTTPException
from pydantic import BaseModel
from pydantic import BaseModel, TypeAdapter
import litellm
from litellm._logging import verbose_proxy_logger
@ -77,6 +78,9 @@ else:
RateLimitDescriptor = dict[str, Any]
_BATCH_BODY_ADAPTER: Final = TypeAdapter(dict[str, object])
class BatchFileUsage(BaseModel):
"""
Internal model for batch file usage tracking, used for batch rate limiting
@ -214,7 +218,7 @@ class _PROXY_BatchRateLimiter(CustomLogger):
tpm_limit_type=None,
model_has_failures=False,
)
self.parallel_request_limiter._add_project_io_token_rate_limit_descriptors_from_metadata(
self.parallel_request_limiter.add_project_io_token_rate_limit_descriptors_from_metadata(
user_api_key_dict=user_api_key_dict,
requested_model=self._get_batch_routing_model(data),
descriptors=descriptors,
@ -314,7 +318,7 @@ class _PROXY_BatchRateLimiter(CustomLogger):
def _estimate_entry_output_tokens(
self,
entry: dict,
entry: Mapping[str, object],
min_configured_otpm_limit: int | None,
) -> int:
"""Conservative per-row output-token estimate for the project OTPM reservation.
@ -325,16 +329,21 @@ class _PROXY_BatchRateLimiter(CustomLogger):
that omits ``max_tokens`` can't be used to bypass OTPM the way an
unbounded streaming request could.
"""
body: Final = entry.get("body", {}) or {}
raw_body: Final = entry.get("body")
body: Final[Mapping[str, object]] = (
MappingProxyType(_BATCH_BODY_ADAPTER.validate_python(raw_body))
if isinstance(raw_body, Mapping)
else MappingProxyType({}) # mutable-ok: immediately frozen empty fallback
)
if body.get("input") is not None and body.get("messages") is None and body.get("prompt") is None:
return 0 # embeddings: no output tokens
explicit_cap = body.get("max_tokens", body.get("max_completion_tokens"))
explicit_cap: Final = body.get("max_tokens", body.get("max_completion_tokens"))
if explicit_cap is not None:
try:
return max(0, int(explicit_cap))
except (TypeError, ValueError):
pass
return self.parallel_request_limiter._no_max_tokens_output_floor(min_configured_otpm_limit)
return self.parallel_request_limiter.no_max_tokens_output_floor(min_configured_otpm_limit)
@staticmethod
def _has_applicable_batch_rate_limits(
@ -446,7 +455,7 @@ class _PROXY_BatchRateLimiter(CustomLogger):
f"Limit resets at: {reset_time_formatted}"
)
else: # tokens
batch_token_count = (
batch_token_count: Final = (
batch_usage.output_tokens
if descriptor.get("key") == PROJECT_OTPM_DESCRIPTOR_KEY
else batch_usage.total_tokens
@ -496,8 +505,8 @@ class _PROXY_BatchRateLimiter(CustomLogger):
data=data,
)
increments: Final[list[dict[Literal["requests", "tokens"], int]]] = [
{
increments: Final = [ # mutable-ok: atomic limiter API requires mutable increment records
{ # mutable-ok: atomic limiter API requires mutable increment records
"requests": batch_usage.request_count,
"tokens": batch_usage.output_tokens
if d.get("key") == PROJECT_OTPM_DESCRIPTOR_KEY
@ -530,7 +539,7 @@ class _PROXY_BatchRateLimiter(CustomLogger):
custom_llm_provider: Literal["openai", "azure", "vertex_ai"] = "openai",
user_api_key_dict: UserAPIKeyAuth | None = None,
data: dict | None = None,
descriptors: list["RateLimitDescriptor"] | None = None,
descriptors: Sequence["RateLimitDescriptor"] | None = None,
) -> BatchFileUsage:
"""
Count number of requests and tokens in a batch input file.
@ -545,13 +554,14 @@ class _PROXY_BatchRateLimiter(CustomLogger):
Returns:
BatchFileUsage with total_tokens, output_tokens, and request_count
"""
otpm_limits: Final = [
otpm_limits: Final = tuple(
int(v)
for d in (descriptors or [])
for d in (descriptors or ())
if d.get("key") == PROJECT_OTPM_DESCRIPTOR_KEY
for v in [(d.get("rate_limit") or {}).get("tokens_per_unit")]
for rate_limit in (d.get("rate_limit"),)
for v in (rate_limit.get("tokens_per_unit") if rate_limit is not None else None,)
if v is not None
]
)
min_configured_otpm_limit: Final = min(otpm_limits) if otpm_limits else None
try:
# Check if this is a managed file (base64 encoded unified file ID)
@ -605,7 +615,7 @@ class _PROXY_BatchRateLimiter(CustomLogger):
# the counter can't measure is estimated, not hard-rejected.
models: Final[set] = set()
total_tokens = 0
output_tokens = 0
output_tokens = 0 # rebind-ok: accumulated per JSONL row in the loop below
request_count = 0
for raw_line in _iter_batch_input_lines(file_content_bytes):
request_count += 1
@ -613,9 +623,7 @@ class _PROXY_BatchRateLimiter(CustomLogger):
entry = json.loads(raw_line)
except Exception:
total_tokens += _estimate_batch_entry_tokens(raw_line)
output_tokens += self.parallel_request_limiter._no_max_tokens_output_floor(
min_configured_otpm_limit
)
output_tokens += self.parallel_request_limiter.no_max_tokens_output_floor(min_configured_otpm_limit)
continue
if isinstance(entry, dict):
model = (entry.get("body") or {}).get("model")
@ -623,9 +631,7 @@ class _PROXY_BatchRateLimiter(CustomLogger):
models.add(model)
output_tokens += self._estimate_entry_output_tokens(entry, min_configured_otpm_limit)
else:
output_tokens += self.parallel_request_limiter._no_max_tokens_output_floor(
min_configured_otpm_limit
)
output_tokens += self.parallel_request_limiter.no_max_tokens_output_floor(min_configured_otpm_limit)
try:
total_tokens += _count_entry_tokens(entry)
except Exception:

View file

@ -22,7 +22,7 @@ from typing import (
TypedDict,
)
from typing_extensions import NotRequired
from typing_extensions import NotRequired, ReadOnly
from litellm import DualCache
from litellm._logging import verbose_proxy_logger
@ -69,6 +69,7 @@ else:
Span = Any
InternalUsageCache = Any
BATCH_RATE_LIMITER_SCRIPT: Final = """
local results = {}
local now = tonumber(ARGV[1])
@ -413,7 +414,7 @@ class RateLimitStatus(TypedDict):
class RateLimitResponse(TypedDict):
overall_code: str
statuses: list[RateLimitStatus]
reservation_windows: NotRequired[frozenset[tuple[str, str, Literal["redis", "local"]]]]
reservation_windows: NotRequired[ReadOnly[frozenset[tuple[str, str, Literal["redis", "local"]]]]]
class ReservationAwareIncrementOperation(RedisPipelineIncrementOperation):
@ -452,6 +453,7 @@ class AtomicCounterMeta(TypedDict):
class AtomicCounterState(TypedDict):
window_expired: bool
current: int
window_start: ReadOnly[str]
DescriptorAtomicGroup: TypeAlias = tuple[list[str], list[int], list[AtomicCounterMeta]]
@ -639,7 +641,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
return self._time_provider()
@staticmethod
def _no_max_tokens_output_floor(
def no_max_tokens_output_floor(
min_configured_tpm_limit: int | None,
) -> int:
"""Output-budget floor used when the request omits max_tokens.
@ -670,7 +672,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
data: object,
call_type: str | None,
) -> Mapping[str, object] | None:
contents = data.get("contents") if isinstance(data, dict) else None
contents: Final = data.get("contents") if isinstance(data, dict) else None
if (
not isinstance(data, dict)
or call_type not in GOOGLE_GENAI_NATIVE_CALL_TYPES
@ -679,7 +681,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
return None
from litellm.google_genai.adapters.transformation import GoogleGenAIAdapter
config = data.get("config") if "config" in data else data.get("generationConfig")
config: Final = data.get("config") if "config" in data else data.get("generationConfig")
return GoogleGenAIAdapter().translate_generate_content_to_completion(
model=data.get("model") if isinstance(data.get("model"), str) else "",
contents=contents,
@ -696,15 +698,17 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
if not isinstance(data, dict):
return None
if call_type in GOOGLE_GENAI_NATIVE_CALL_TYPES:
config = data.get("config") if "config" in data else data.get("generationConfig")
values = tuple(
int(config[field])
config: Final = data.get("config") if "config" in data else data.get("generationConfig")
google_cap_values: Final = tuple(
int(raw_value)
for field in ("maxOutputTokens", "max_output_tokens")
if isinstance(config, dict) and isinstance(config.get(field), (int, float, str))
if isinstance(config, dict)
for raw_value in (config.get(field),)
if isinstance(raw_value, (int, float, str))
)
return max(values, default=None)
return max(google_cap_values, default=None)
if call_type in RESPONSES_API_CALL_TYPES:
value = data.get("max_output_tokens")
value: Final = data.get("max_output_tokens")
if value is None:
return None
if not isinstance(value, (int, float, str)):
@ -712,13 +716,18 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
return max(RESPONSES_API_MIN_OUTPUT_TOKENS, int(value))
if call_type in EMBEDDING_API_CALL_TYPES:
return None
fields = (
fields: Final = (
("max_tokens", "max_completion_tokens")
if call_type
else ("max_tokens", "max_completion_tokens", "max_output_tokens")
)
values = tuple(int(data[field]) for field in fields if isinstance(data.get(field), (int, float, str)))
return max(values, default=None)
output_cap_values: Final = tuple(
int(raw_value)
for field in fields
for raw_value in (data.get(field),)
if isinstance(raw_value, (int, float, str))
)
return max(output_cap_values, default=None)
@classmethod
def _has_explicit_output_cap(cls, data: object, call_type: str | None) -> bool:
@ -733,18 +742,18 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
def _get_output_candidate_count(data: object, call_type: str | None = None) -> int:
if not isinstance(data, dict):
return 1
config = (
config: Final = (
(data.get("config") if "config" in data else data.get("generationConfig"))
if call_type in GOOGLE_GENAI_NATIVE_CALL_TYPES
else None
)
candidate_values = (
candidate_values: Final = (
data.get("n"),
data.get("best_of"),
config.get("candidateCount") if isinstance(config, dict) else None,
config.get("candidate_count") if isinstance(config, dict) else None,
)
candidate_count = 1
candidate_count = 1 # rebind-ok: running maximum across candidate-count aliases
for value in candidate_values:
try:
candidate_count = max(candidate_count, int(value or 1))
@ -774,31 +783,34 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
"""
if not isinstance(data, dict):
return
capped_floor = _PROXY_MaxParallelRequestsHandler_v3._no_max_tokens_output_floor(min_configured_limit)
if call_type in RESPONSES_API_CALL_TYPES:
capped_floor = max(capped_floor, RESPONSES_API_MIN_OUTPUT_TOKENS)
baseline_floor = DEFAULT_MAX_TOKENS_ESTIMATE // _TPM_FLOOR_FRACTION
is_embedding = _PROXY_MaxParallelRequestsHandler_v3._is_embedding_request(data, call_type)
base_capped_floor: Final = _PROXY_MaxParallelRequestsHandler_v3.no_max_tokens_output_floor(min_configured_limit)
capped_floor: Final = (
max(base_capped_floor, RESPONSES_API_MIN_OUTPUT_TOKENS)
if call_type in RESPONSES_API_CALL_TYPES
else base_capped_floor
)
baseline_floor: Final = DEFAULT_MAX_TOKENS_ESTIMATE // _TPM_FLOOR_FRACTION
is_embedding: Final = _PROXY_MaxParallelRequestsHandler_v3._is_embedding_request(data, call_type)
if (
capped_floor >= baseline_floor
or _PROXY_MaxParallelRequestsHandler_v3._has_explicit_output_cap(data, call_type)
or is_embedding
):
return
effective_cap = max(capped_floor, configured_output_tokens or 0)
effective_cap: Final = max(capped_floor, configured_output_tokens or 0)
if call_type in GOOGLE_GENAI_NATIVE_CALL_TYPES:
config_field = "config" if "config" in data or "generationConfig" not in data else "generationConfig"
config = data.get(config_field)
config_field: Final = "config" if "config" in data or "generationConfig" not in data else "generationConfig"
config: Final = data.get(config_field)
if config is None or isinstance(config, dict):
data[config_field] = { # mutable-ok: downstream native routing requires a mutable request config
data[config_field] = { # rebind-ok: routed request needs cap # mutable-ok: downstream needs dict
**(config or {}), # mutable-ok: downstream native routing requires a mutable request config
"maxOutputTokens": effective_cap,
}
return
cap_field = "max_output_tokens" if call_type in RESPONSES_API_CALL_TYPES else "max_tokens"
existing_cap = data.get(cap_field)
cap_field: Final = "max_output_tokens" if call_type in RESPONSES_API_CALL_TYPES else "max_tokens"
existing_cap: Final = data.get(cap_field)
if existing_cap is None or effective_cap < existing_cap:
data[cap_field] = effective_cap
data[cap_field] = effective_cap # rebind-ok: downstream routing requires the bounded output cap
def _estimate_tokens_for_request(
self,
@ -878,69 +890,57 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
return 0, 0
translated_data: Final = self._translate_google_genai_native_request(data, call_type)
estimable_data: Final = translated_data if translated_data is not None else data
messages = estimable_data.get("messages")
prompt = estimable_data.get("prompt")
input_text = estimable_data.get("input")
selected_fields: Final[tuple[object | None, object | None, object | None]] = (
(None, None, estimable_data.get("input"))
if call_type in RESPONSES_API_CALL_TYPES or call_type in EMBEDDING_API_CALL_TYPES
else (None, estimable_data.get("prompt"), None)
if call_type in TEXT_COMPLETION_API_CALL_TYPES
else (estimable_data.get("messages"), None, None)
if call_type
else (
estimable_data.get("messages"),
estimable_data.get("prompt"),
estimable_data.get("input"),
)
)
messages, prompt, input_text = selected_fields
if call_type in RESPONSES_API_CALL_TYPES or call_type in EMBEDDING_API_CALL_TYPES:
messages = None
prompt = None
elif call_type in TEXT_COMPLETION_API_CALL_TYPES:
messages = None
input_text = None
elif call_type:
prompt = None
input_text = None
match (messages, prompt, input_text):
case (selected_messages, _, _) if selected_messages:
total_chars = len(get_str_from_messages(selected_messages))
case (_, str() as selected_prompt, _):
total_chars = len(selected_prompt)
case (_, list() as selected_prompt, _):
total_chars = sum(len(str(item)) for item in selected_prompt)
case (_, _, str() as selected_input):
total_chars = len(selected_input)
case (_, _, list() as selected_input):
total_chars = sum(len(str(item)) for item in selected_input)
case _:
total_chars = 0
total_chars: Final = (
len(get_str_from_messages(messages))
if isinstance(messages, list) and messages
else len(prompt)
if isinstance(prompt, str)
else sum(len(str(item)) for item in prompt)
if isinstance(prompt, list)
else len(input_text)
if isinstance(input_text, str)
else sum(len(str(item)) for item in input_text)
if isinstance(input_text, list)
else 0
)
estimated_input_tokens: Final = max(1, total_chars // DEFAULT_CHARS_PER_TOKEN) if total_chars > 0 else 0
explicit_max_tokens: Final = self._get_explicit_output_cap(data, call_type)
is_embedding: Final = self._is_embedding_request(data, call_type)
match (explicit_max_tokens, is_embedding):
case (_, True):
max_tokens_estimate = 0
case (mt, _) if mt is not None:
max_tokens_estimate = mt
case _ if total_chars == 0 and configured_output_tokens is None:
# Fully contentless request (no messages, prompt, or input).
# Don't apply the conservative output-budget floor here — it
# would over-reserve and could push small TPM limits into a
# false 429. The caller floors at 1 so backpressure still
# applies once the counter is at limit.
max_tokens_estimate = 0
case _:
# No max_tokens specified — reserve at least the input size with a
# conservative floor so a stream of small concurrent requests can't
# collectively bypass the limit. Cap the floor by a fraction of
# the smallest TPM limit this request will be charged against,
# so a small per-tenant TPM cap can't be tripped by the floor
# alone.
output_floor = self._no_max_tokens_output_floor(min_configured_tpm_limit)
if call_type in RESPONSES_API_CALL_TYPES:
output_floor = max(output_floor, RESPONSES_API_MIN_OUTPUT_TOKENS)
max_tokens_estimate = (
configured_output_tokens
if configured_output_tokens is not None
else max(estimated_input_tokens, output_floor)
)
base_output_floor: Final = self.no_max_tokens_output_floor(min_configured_tpm_limit)
output_floor: Final = (
max(base_output_floor, RESPONSES_API_MIN_OUTPUT_TOKENS)
if call_type in RESPONSES_API_CALL_TYPES
else base_output_floor
)
max_tokens_estimate: Final = (
0
if is_embedding or (explicit_max_tokens is None and total_chars == 0 and configured_output_tokens is None)
else explicit_max_tokens
if explicit_max_tokens is not None
else configured_output_tokens
if configured_output_tokens is not None
else max(estimated_input_tokens, output_floor)
)
max_tokens_estimate *= self._get_output_candidate_count(data, call_type)
return estimated_input_tokens, max_tokens_estimate
return estimated_input_tokens, max_tokens_estimate * self._get_output_candidate_count(data, call_type)
def _is_redis_cluster(self) -> bool:
"""
@ -1755,7 +1755,10 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
mid-loop, refund applied increments and fall back to in-memory.
"""
if not descriptor_groups:
return RateLimitResponse(overall_code="OK", statuses=[])
return RateLimitResponse(
overall_code="OK",
statuses=[], # mutable-ok: response contract requires a status list
)
applied: Final[list[list[AtomicCounterMeta]]] = []
statuses: Final[list[RateLimitStatus]] = []
raw: list[CacheCounterValue]
@ -1946,7 +1949,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
],
)
descriptor_state.append(
{
{ # mutable-ok: local atomic-counter state is updated during pass two
"window_expired": window_expired,
"current": current_counter,
"window_start": str(now_int if window_expired else int(window_start)),
@ -2096,21 +2099,18 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
configured, or if the reservation failed), for the caller to stash
for post-call reconciliation.
"""
itpm_descriptors = [ # mutable-ok: atomic limiter API requires lists
itpm_descriptors: Final = [ # mutable-ok: atomic limiter API requires lists
d for d in descriptors if d["key"] == PROJECT_ITPM_DESCRIPTOR_KEY
]
otpm_descriptors = [ # mutable-ok: atomic limiter API requires lists
otpm_descriptors: Final = [ # mutable-ok: atomic limiter API requires lists
d for d in descriptors if d["key"] == PROJECT_OTPM_DESCRIPTOR_KEY
]
if not itpm_descriptors and not otpm_descriptors:
return RateLimitResponse(overall_code="OK", statuses=[]), 0, 0 # mutable-ok: response contract uses a list
itpm_response: RateLimitResponse | None = None
itpm_reserved = 0
if itpm_descriptors:
itpm_response = await self.atomic_check_and_increment_by_n(
itpm_response: Final = (
await self.atomic_check_and_increment_by_n(
descriptors=itpm_descriptors,
increments=[ # mutable-ok: atomic limiter API requires mutable increment records
{"tokens": estimated_input_tokens} # mutable-ok: atomic limiter increment record
@ -2118,12 +2118,15 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
],
parent_otel_span=parent_otel_span,
)
if itpm_response["overall_code"] == "OVER_LIMIT":
return itpm_response, 0, 0
itpm_reserved = estimated_input_tokens
if itpm_descriptors
else None
)
if itpm_response is not None and itpm_response["overall_code"] == "OVER_LIMIT":
return itpm_response, 0, 0
itpm_reserved: Final = estimated_input_tokens if itpm_response is not None else 0
if otpm_descriptors:
otpm_response = await self.atomic_check_and_increment_by_n(
otpm_response: Final = await self.atomic_check_and_increment_by_n(
descriptors=otpm_descriptors,
increments=[ # mutable-ok: atomic limiter API requires mutable increment records
{"tokens": estimated_output_tokens} # mutable-ok: atomic limiter increment record
@ -2142,7 +2145,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
parent_otel_span=parent_otel_span,
)
return otpm_response, 0, 0
statuses = (
statuses: Final = (
[ # mutable-ok: response contract uses a list
*itpm_response["statuses"],
*otpm_response["statuses"],
@ -2172,7 +2175,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
async def enforce_project_io_token_quota_for_frame(
self,
user_api_key_dict: UserAPIKeyAuth,
user_api_key_dict: UserAPIKeyAuth | None,
requested_model: str | None,
estimated_input_tokens: int,
estimated_output_tokens: int,
@ -2188,8 +2191,10 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
like the batch rate limiter -- this charges the estimate immediately
and never refunds it.
"""
descriptors: Final[list[RateLimitDescriptor]] = []
self._add_project_io_token_rate_limit_descriptors_from_metadata(
if user_api_key_dict is None:
return
descriptors: Final[list[RateLimitDescriptor]] = [] # mutable-ok: descriptor helper appends in place
self.add_project_io_token_rate_limit_descriptors_from_metadata(
user_api_key_dict=user_api_key_dict,
requested_model=requested_model,
descriptors=descriptors,
@ -2911,7 +2916,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
)
)
def _add_project_io_token_rate_limit_descriptors_from_metadata(
def add_project_io_token_rate_limit_descriptors_from_metadata(
self,
user_api_key_dict: UserAPIKeyAuth,
requested_model: str | None,
@ -2926,22 +2931,22 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
if requested_model is None or user_api_key_dict.project_id is None:
return
itpm_limit_for_project_model = (
itpm_limit_for_project_model: Final = (
get_model_rate_limit_from_metadata(user_api_key_dict, "project_metadata", "model_itpm_limit")
or {} # mutable-ok: metadata helper returns an optional mapping
)
otpm_limit_for_project_model = (
otpm_limit_for_project_model: Final = (
get_model_rate_limit_from_metadata(user_api_key_dict, "project_metadata", "model_otpm_limit")
or {} # mutable-ok: metadata helper returns an optional mapping
)
model_itpm_limit = itpm_limit_for_project_model.get(requested_model)
model_otpm_limit = otpm_limit_for_project_model.get(requested_model)
model_itpm_limit: Final = itpm_limit_for_project_model.get(requested_model)
model_otpm_limit: Final = otpm_limit_for_project_model.get(requested_model)
if model_itpm_limit is None and model_otpm_limit is None:
return
descriptor_value = f"{user_api_key_dict.project_id}:{requested_model}"
descriptor_value: Final = f"{user_api_key_dict.project_id}:{requested_model}"
if model_itpm_limit is not None:
descriptors.append(
RateLimitDescriptor(
@ -3026,10 +3031,10 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
"""
if not isinstance(block, dict):
return DEFAULT_AUDIO_TOKEN_ESTIMATE
input_audio = block.get("input_audio")
b64_data = input_audio.get("data") if isinstance(input_audio, dict) else None
input_audio: Final = block.get("input_audio")
b64_data: Final = input_audio.get("data") if isinstance(input_audio, dict) else None
if b64_data and isinstance(b64_data, str):
decoded_bytes = len(b64_data) * 3 // 4
decoded_bytes: Final = len(b64_data) * 3 // 4
return max(decoded_bytes // _AUDIO_BYTES_PER_TOKEN, DEFAULT_AUDIO_TOKEN_ESTIMATE)
return DEFAULT_AUDIO_TOKEN_ESTIMATE
@ -3042,17 +3047,15 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
"""
if not isinstance(messages, list):
return 0
total = 0
for message in messages:
content = message.get("content") if isinstance(message, dict) else None
if not isinstance(content, list):
continue
total += sum(
cls._estimate_audio_block_tokens(block)
for block in content
if isinstance(block, dict) and block.get("type") == "input_audio"
)
return total
return sum(
cls._estimate_audio_block_tokens(block)
for message in messages
if isinstance(message, dict)
for content in (message.get("content"),)
if isinstance(content, list)
for block in content
if isinstance(block, dict) and block.get("type") == "input_audio"
)
@staticmethod
def _strip_audio_content_blocks(messages: object) -> object:
@ -3066,7 +3069,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
"""
if not isinstance(messages, list):
return messages
sanitized = [] # mutable-ok: token_counter requires a list of message dicts
sanitized: Final[list[object]] = [] # mutable-ok: token_counter requires a list of message dicts
for message in messages:
if not isinstance(message, dict):
sanitized.append(message)
@ -3127,7 +3130,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
@classmethod
def _contains_image_content(cls, value: object) -> bool:
if isinstance(value, dict):
media_type = value.get("media_type") or value.get("mime_type")
media_type: Final = value.get("media_type") or value.get("mime_type")
return (
value.get("type") in ("image", "image_url", "input_image")
or (isinstance(media_type, str) and media_type.startswith("image/"))
@ -3159,9 +3162,9 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
@staticmethod
def _rerank_input_to_text(data: Mapping[str, object]) -> str:
documents = data.get("documents")
document_items: Sequence[object] = documents if isinstance(documents, list) else () # pyright: ignore[reportUnknownVariableType] # rerank documents are validated runtime JSON
input_parts: tuple[object, ...] = ( # pyright: ignore[reportUnknownVariableType] # list narrowing preserves unknown JSON element types
documents: Final = data.get("documents")
document_items: Final[Sequence[object]] = documents if isinstance(documents, list) else () # pyright: ignore[reportUnknownVariableType] # rerank documents are validated runtime JSON
input_parts: Final[tuple[object, ...]] = ( # pyright: ignore[reportUnknownVariableType] # list narrowing preserves unknown JSON element types
data.get("query"),
*document_items,
)
@ -3199,39 +3202,45 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
if not isinstance(data, dict):
return 0
selected_text = None
countable_tools = data.get("tools")
countable_tool_choice = data.get("tool_choice")
if call_type in RESPONSES_API_CALL_TYPES:
messages = self._responses_input_to_chat_messages(data)
elif (translated_request := self._translate_google_genai_native_request(data, call_type)) is not None:
messages = translated_request.get("messages")
countable_tools = translated_request.get("tools")
countable_tool_choice = translated_request.get("tool_choice")
elif self._is_embedding_request(data, call_type):
messages = None
selected_text = data.get("input")
pretokenized_input_tokens = self._count_pretokenized_embedding_input(selected_text)
if pretokenized_input_tokens is not None:
return pretokenized_input_tokens
elif call_type in RERANK_API_CALL_TYPES:
messages = None
selected_text = self._rerank_input_to_text(data) # pyright: ignore[reportUnknownArgumentType] # proxy request bodies are runtime-validated JSON
elif call_type in TEXT_COMPLETION_API_CALL_TYPES:
messages = None
selected_text = data.get("prompt")
else:
messages = data.get("messages")
if messages is None:
selected_text = data.get("prompt")
if messages is None and selected_text is None:
selected_text = data.get("input")
is_responses_request: Final = call_type in RESPONSES_API_CALL_TYPES
translated_request: Final = (
None if is_responses_request else self._translate_google_genai_native_request(data, call_type)
)
is_embedding_request: Final = self._is_embedding_request(data, call_type)
embedding_text: Final = data.get("input") if is_embedding_request else None
pretokenized_input_tokens: Final = (
self._count_pretokenized_embedding_input(embedding_text) if is_embedding_request else None
)
if pretokenized_input_tokens is not None:
return pretokenized_input_tokens
audio_token_estimate = self._estimate_audio_content_tokens(messages)
countable_messages = self._strip_audio_content_blocks(messages) if audio_token_estimate > 0 else messages
prompt: Final = data.get("prompt")
fallback_text: Final = prompt if prompt is not None else data.get("input")
selected_inputs: Final[tuple[object | None, object | None, object | None, object | None]] = (
(self._responses_input_to_chat_messages(data), None, data.get("tools"), data.get("tool_choice"))
if is_responses_request
else (
translated_request.get("messages"),
None,
translated_request.get("tools"),
translated_request.get("tool_choice"),
)
if translated_request is not None
else (None, embedding_text, data.get("tools"), data.get("tool_choice"))
if is_embedding_request
else (None, self._rerank_input_to_text(data), data.get("tools"), data.get("tool_choice"))
if call_type in RERANK_API_CALL_TYPES
else (None, prompt, data.get("tools"), data.get("tool_choice"))
if call_type in TEXT_COMPLETION_API_CALL_TYPES
else (data.get("messages"), fallback_text, data.get("tools"), data.get("tool_choice"))
)
messages, selected_text, countable_tools, countable_tool_choice = selected_inputs
audio_token_estimate: Final = self._estimate_audio_content_tokens(messages)
countable_messages: Final = self._strip_audio_content_blocks(messages) if audio_token_estimate > 0 else messages
try:
estimate = max(
estimate: Final = max(
0,
int(
token_counter(
@ -3245,7 +3254,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
),
)
return estimate + audio_token_estimate
except Exception: # noqa: BLE001 - any tokenizer/model-resolution/transform failure degrades to the cheap estimate, never a 500
except Exception: # noqa: BLE001 # tokenizer failures degrade to the cheap estimate
if call_type in RERANK_API_CALL_TYPES and isinstance(selected_text, str):
return max(0, len(selected_text) // DEFAULT_CHARS_PER_TOKEN)
estimated_input_tokens, _ = self._estimate_input_and_output_tokens(data=data, call_type=call_type)
@ -3272,14 +3281,14 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
"""
if not isinstance(data, dict):
return
stash = claim_request_stash_for_data(data)
io_token_descriptors = [ # mutable-ok: reservation API requires descriptor lists
stash: Final = claim_request_stash_for_data(data)
io_token_descriptors: Final = [ # mutable-ok: reservation API requires descriptor lists
d for d in descriptors if d["key"] in (PROJECT_ITPM_DESCRIPTOR_KEY, PROJECT_OTPM_DESCRIPTOR_KEY)
]
if not io_token_descriptors:
return
configured_otpm_limits = [ # mutable-ok: min calculation materializes validated limits
configured_otpm_limits: Final = [ # mutable-ok: min calculation materializes validated limits
int(v)
for d in io_token_descriptors
if d["key"] == PROJECT_OTPM_DESCRIPTOR_KEY
@ -3290,8 +3299,8 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
]
if v is not None
]
min_configured_otpm_limit = min(configured_otpm_limits) if configured_otpm_limits else None
configured_itpm_limits = [ # mutable-ok: min calculation materializes validated limits
min_configured_otpm_limit: Final = min(configured_otpm_limits) if configured_otpm_limits else None
configured_itpm_limits: Final = [ # mutable-ok: min calculation materializes validated limits
int(v)
for d in io_token_descriptors
if d["key"] == PROJECT_ITPM_DESCRIPTOR_KEY
@ -3302,14 +3311,14 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
]
if v is not None
]
min_configured_itpm_limit = min(configured_itpm_limits) if configured_itpm_limits else None
min_configured_itpm_limit: Final = min(configured_itpm_limits) if configured_itpm_limits else None
_, estimated_output_tokens = self._estimate_input_and_output_tokens(
_, raw_estimated_output_tokens = self._estimate_input_and_output_tokens(
data=data,
min_configured_tpm_limit=min_configured_otpm_limit,
call_type=call_type,
)
estimated_input_tokens = (
raw_estimated_input_tokens: Final = (
min_configured_itpm_limit
if min_configured_itpm_limit is not None
and (
@ -3319,9 +3328,12 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
)
else self._estimate_precise_input_tokens(data=data, model=requested_model, call_type=call_type)
)
estimated_input_tokens = max(estimated_input_tokens, 1)
if not self._has_explicit_output_cap(data, call_type):
estimated_output_tokens = max(estimated_output_tokens, 1)
estimated_input_tokens: Final = max(raw_estimated_input_tokens, 1)
estimated_output_tokens: Final = (
raw_estimated_output_tokens
if self._has_explicit_output_cap(data, call_type)
else max(raw_estimated_output_tokens, 1)
)
# Hard-cap generation length so an unbounded response can't overshoot
# the OTPM budget before post-call reconciliation runs, mirroring the
@ -3353,7 +3365,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
parent_otel_span=user_api_key_dict.parent_otel_span,
)
stash.reservation_released = True
acquisition = stash.parallel_slot
acquisition: Final = stash.parallel_slot
if acquisition is not None:
await self._release_parallel_request_slots(
acquisition=acquisition,
@ -3367,9 +3379,9 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
)
if itpm_reserved > 0:
itpm_scopes = [ # mutable-ok: request stash freezes the collected scopes
itpm_scopes: Final = tuple(
(d["key"], d["value"]) for d in io_token_descriptors if d["key"] == PROJECT_ITPM_DESCRIPTOR_KEY
]
)
stash.itpm_reserved_tokens = itpm_reserved
stash.itpm_reserved_scopes = frozenset(itpm_scopes)
stash.itpm_reserved_window_identities = frozenset(
@ -3378,9 +3390,9 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
if "model_per_project_itpm" in counter_key
)
if otpm_reserved > 0:
otpm_scopes = [ # mutable-ok: request stash freezes the collected scopes
otpm_scopes: Final = tuple(
(d["key"], d["value"]) for d in io_token_descriptors if d["key"] == PROJECT_OTPM_DESCRIPTOR_KEY
]
)
stash.otpm_reserved_tokens = otpm_reserved
stash.otpm_reserved_scopes = frozenset(otpm_scopes)
stash.otpm_reserved_window_identities = frozenset(
@ -3473,7 +3485,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
requested_model=requested_model,
descriptors=descriptors,
)
self._add_project_io_token_rate_limit_descriptors_from_metadata(
self.add_project_io_token_rate_limit_descriptors_from_metadata(
user_api_key_dict=user_api_key_dict,
requested_model=requested_model,
descriptors=descriptors,
@ -3546,8 +3558,8 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
# limit. Stays empty/0 whenever no combined-TPM reservation was
# made (or it was over limit, in which case execution never
# reaches the ITPM/OTPM block -- `_handle_rate_limit_error` raises).
tpm_reservation_scopes: Sequence[tuple[str, str]] = ()
tpm_reservation_amount = 0
tpm_reservation_scopes: Sequence[tuple[str, str]] = () # rebind-ok: set after successful reservation
tpm_reservation_amount = 0 # rebind-ok: set after successful reservation
if has_tpm_limits and self.tpm_reservation_enabled:
min_configured_tpm_limit: Final = min(configured_tpm_limits)
@ -3633,8 +3645,10 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
)
is not None
)
tpm_reservation_scopes = tuple(stash.reserved_scopes)
tpm_reservation_amount = estimated_tokens
tpm_reservation_scopes = tuple( # rebind-ok: record successful reservation scopes
stash.reserved_scopes
)
tpm_reservation_amount = estimated_tokens # rebind-ok: record successful reservation amount
# Merge TPM statuses into the stored rate-limit response
# so x-ratelimit-{key}-remaining-tokens / -limit-tokens
@ -3648,7 +3662,6 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
verbose_proxy_logger.debug(
"TPM tokens reserved: %s for model %s", estimated_tokens, requested_model
)
await self._reserve_project_io_tokens_or_raise(
descriptors=descriptors,
data=data,
@ -3734,7 +3747,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
return total_tokens
@staticmethod
def _aggregate_only_total_tokens(usage: Usage | dict | None) -> int:
def _aggregate_only_total_tokens(usage: Usage | ResponseAPIUsage | Mapping[str, object] | None) -> int:
"""Total for usage that carries no input/output split, else 0.
A source that can only report one number for the whole request (a
@ -3744,24 +3757,43 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
uncharged, which is how pass-through traffic slips past a TPM limit
it is supposed to share.
"""
if isinstance(usage, Usage):
prompt_tokens, completion_tokens, total_tokens = (
usage.prompt_tokens or 0,
usage.completion_tokens or 0,
usage.total_tokens or 0,
)
elif isinstance(usage, dict):
prompt_tokens, completion_tokens, total_tokens = (
usage.get("prompt_tokens") or 0,
usage.get("completion_tokens") or 0,
if usage is None:
return 0
token_counts: Final = (
(usage.prompt_tokens or 0, usage.completion_tokens or 0, usage.total_tokens or 0)
if isinstance(usage, Usage)
else (usage.input_tokens or 0, usage.output_tokens or 0, usage.total_tokens or 0)
if isinstance(usage, ResponseAPIUsage)
else (
usage.get("prompt_tokens") or usage.get("input_tokens") or 0,
usage.get("completion_tokens") or usage.get("output_tokens") or 0,
usage.get("total_tokens") or 0,
)
else:
return 0
if prompt_tokens or completion_tokens:
)
prompt_tokens, completion_tokens, total_tokens = token_counts
if prompt_tokens or completion_tokens or not isinstance(total_tokens, int):
return 0
return total_tokens
@staticmethod
def _response_usage(
response_obj: object,
) -> Usage | ResponseAPIUsage | Mapping[str, object] | None:
if isinstance(response_obj, (Usage, ResponseAPIUsage)):
return response_obj
if isinstance(
response_obj,
(ModelResponse, EmbeddingResponse, TextCompletionResponse, BaseLiteLLMOpenAIResponseObject),
):
usage: Final = getattr(response_obj, "usage", None)
return usage if isinstance(usage, (Usage, ResponseAPIUsage, dict)) else None
if isinstance(response_obj, dict):
nested_usage: Final = response_obj.get("usage")
if isinstance(nested_usage, (Usage, ResponseAPIUsage, dict)):
return nested_usage
return response_obj
return None
async def _execute_token_increment_script(
self,
pipeline_operations: list["RedisPipelineIncrementOperation"],
@ -3884,15 +3916,18 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
if self.window_guarded_token_increment_script is not None:
try:
await self.window_guarded_token_increment_script(
keys=[window_key, operation["key"]],
args=[
keys=[ # mutable-ok: Redis script interface requires a key list
window_key,
operation["key"],
],
args=[ # mutable-ok: Redis script interface requires an argument list
expected_window_start,
operation["increment_value"],
operation["ttl"] or 0,
],
)
continue
except Exception as e: # noqa: BLE001 - any Redis/Lua failure degrades to the plain increment fallback, never a 500
except Exception as e: # noqa: BLE001 # Redis failures use the plain increment fallback
verbose_proxy_logger.warning(
"Window-guarded token adjustment failed for %s: %s",
operation["key"],
@ -3978,16 +4013,16 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
if not isinstance(response_obj, RerankResponse) or response_obj.meta is None:
return None
rerank_tokens = response_obj.meta.get("tokens") # pyright: ignore[reportUnknownMemberType] # TypedDict's optional generic metadata widens get overloads
rerank_tokens: Final = response_obj.meta.get("tokens") # pyright: ignore[reportUnknownMemberType] # TypedDict's optional generic metadata widens get overloads
if rerank_tokens is not None:
input_tokens = rerank_tokens.get("input_tokens") or 0 # pyright: ignore[reportUnknownMemberType] # token fields are typed integers despite the generic get overload
output_tokens = rerank_tokens.get("output_tokens") or 0 # pyright: ignore[reportUnknownMemberType] # token fields are typed integers despite the generic get overload
input_tokens: Final = rerank_tokens.get("input_tokens") or 0 # pyright: ignore[reportUnknownMemberType] # token fields are typed integers despite the generic get overload
output_tokens: Final = rerank_tokens.get("output_tokens") or 0 # pyright: ignore[reportUnknownMemberType] # token fields are typed integers despite the generic get overload
if input_tokens or output_tokens:
return max(0, input_tokens), max(0, output_tokens), True
billed_units = response_obj.meta.get("billed_units") # pyright: ignore[reportUnknownMemberType] # TypedDict's optional generic metadata widens get overloads
billed_units: Final = response_obj.meta.get("billed_units") # pyright: ignore[reportUnknownMemberType] # TypedDict's optional generic metadata widens get overloads
if billed_units is not None:
total_tokens = billed_units.get("total_tokens") or 0 # pyright: ignore[reportUnknownMemberType] # billed total is a typed integer despite the generic get overload
total_tokens: Final = billed_units.get("total_tokens") or 0 # pyright: ignore[reportUnknownMemberType] # billed total is a typed integer despite the generic get overload
if total_tokens:
return max(0, total_tokens), 0, True
return None
@ -4003,68 +4038,57 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
but they're untouched everywhere else (cost/usage logging still sees
the full prompt token count).
"""
rerank_usage = self._resolve_rerank_token_usage(response_obj)
rerank_usage: Final = self._resolve_rerank_token_usage(response_obj)
if rerank_usage is not None:
return rerank_usage
usage: object | None = None
if isinstance(response_obj, (Usage, ResponseAPIUsage)):
usage = response_obj
elif isinstance(
response_obj,
(ModelResponse, EmbeddingResponse, TextCompletionResponse, BaseLiteLLMOpenAIResponseObject),
):
usage = getattr(response_obj, "usage", None)
elif isinstance(response_obj, dict):
usage = response_obj.get("usage")
if usage is None and any(
key in response_obj
for key in (
"prompt_tokens",
"completion_tokens",
"input_tokens",
"output_tokens",
)
):
usage = response_obj
usage: Final = self._response_usage(response_obj)
if isinstance(usage, Usage):
prompt_tokens = usage.prompt_tokens or 0
completion_tokens = usage.completion_tokens or 0
cached_tokens = 0
if usage.prompt_tokens_details is not None:
cached_tokens = getattr(usage.prompt_tokens_details, "cached_tokens", 0) or 0
elif isinstance(usage, ResponseAPIUsage):
# Responses API usage uses input_tokens/output_tokens instead of
# prompt_tokens/completion_tokens.
prompt_tokens = usage.input_tokens or 0
completion_tokens = usage.output_tokens or 0
cached_tokens = 0
if usage.input_tokens_details is not None:
cached_tokens = usage.input_tokens_details.cached_tokens or 0
elif isinstance(usage, dict):
prompt_tokens = usage.get("prompt_tokens") or usage.get("input_tokens") or 0
completion_tokens = usage.get("completion_tokens") or usage.get("output_tokens") or 0
prompt_details = (
usage.get("prompt_tokens_details")
or usage.get("input_tokens_details")
or {} # mutable-ok: usage details are optional mappings
prompt_tokens: Final = usage.prompt_tokens or 0
completion_tokens: Final = usage.completion_tokens or 0
cached_tokens: Final = (
getattr(usage.prompt_tokens_details, "cached_tokens", 0) or 0
if usage.prompt_tokens_details is not None
else 0
)
cached_tokens = (
(prompt_details.get("cached_tokens", 0) or 0) if isinstance(prompt_details, dict) else 0
) or (usage.get("cache_read_input_tokens") or 0)
else:
return 0, 0, False
if prompt_tokens == 0 and completion_tokens == 0:
return 0, 0, False
return max(0, prompt_tokens - cached_tokens), completion_tokens, True
if prompt_tokens == 0 and completion_tokens == 0:
return 0, 0, False
return max(0, prompt_tokens - cached_tokens), completion_tokens, True
if isinstance(usage, ResponseAPIUsage):
response_input_tokens: Final = usage.input_tokens or 0
response_output_tokens: Final = usage.output_tokens or 0
response_cached_tokens: Final = (
usage.input_tokens_details.cached_tokens or 0 if usage.input_tokens_details is not None else 0
)
if response_input_tokens == 0 and response_output_tokens == 0:
return 0, 0, False
return max(0, response_input_tokens - response_cached_tokens), response_output_tokens, True
if isinstance(usage, Mapping):
raw_prompt_tokens: Final = usage.get("prompt_tokens") or usage.get("input_tokens") or 0
raw_completion_tokens: Final = usage.get("completion_tokens") or usage.get("output_tokens") or 0
mapped_prompt_tokens: Final = raw_prompt_tokens if isinstance(raw_prompt_tokens, int) else 0
mapped_completion_tokens: Final = raw_completion_tokens if isinstance(raw_completion_tokens, int) else 0
prompt_details: Final = usage.get("prompt_tokens_details") or usage.get("input_tokens_details")
raw_cached_tokens: Final = (
(prompt_details.get("cached_tokens", 0) if isinstance(prompt_details, dict) else 0)
or usage.get("cache_read_input_tokens")
or 0
)
mapped_cached_tokens: Final = raw_cached_tokens if isinstance(raw_cached_tokens, int) else 0
if mapped_prompt_tokens == 0 and mapped_completion_tokens == 0:
return 0, 0, False
return max(0, mapped_prompt_tokens - mapped_cached_tokens), mapped_completion_tokens, True
return 0, 0, False
def _build_io_token_reservation_ops(
self,
kwargs: object,
response_obj: object,
) -> list[RedisPipelineIncrementOperation] | tuple[ReservationAwareIncrementOperation, ...]:
) -> Sequence[RedisPipelineIncrementOperation]:
"""
Reconcile project ITPM/OTPM reservations to actual usage on success:
ITPM to billable input tokens, OTPM to actual completion tokens.
@ -4075,25 +4099,33 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
"""
if not isinstance(kwargs, dict):
return ()
stash = get_request_stash_for_call(_call_id_from_callback_kwargs(kwargs))
stash: Final = get_request_stash_for_call(_call_id_from_callback_kwargs(kwargs))
if stash is None:
return ()
itpm_reserved = stash.itpm_reserved_tokens
otpm_reserved = stash.otpm_reserved_tokens
itpm_reserved: Final = stash.itpm_reserved_tokens
otpm_reserved: Final = stash.otpm_reserved_tokens
if itpm_reserved <= 0 and otpm_reserved <= 0:
return ()
billable_input, completion_tokens, usage_resolved = self._resolve_io_token_reconcile_usage(response_obj)
if not usage_resolved:
billable_input, completion_tokens, usage_resolved = self._resolve_io_token_reconcile_usage(
kwargs.get("combined_usage_object")
)
if not usage_resolved:
if not stash.reservation_released:
return ()
billable_input = itpm_reserved
completion_tokens = otpm_reserved
response_usage: Final = self._resolve_io_token_reconcile_usage(response_obj)
combined_usage: Final = self._resolve_io_token_reconcile_usage(kwargs.get("combined_usage_object"))
aggregate_total: Final = self._aggregate_only_total_tokens(
self._response_usage(response_obj)
) or self._aggregate_only_total_tokens(self._response_usage(kwargs.get("combined_usage_object")))
if not response_usage[2] and not combined_usage[2] and aggregate_total <= 0 and not stash.reservation_released:
return ()
resolved_usage: Final = (
response_usage
if response_usage[2]
else combined_usage
if combined_usage[2]
else (aggregate_total, aggregate_total, True)
if aggregate_total > 0
else (itpm_reserved, otpm_reserved, False)
)
billable_input, completion_tokens, _ = resolved_usage
if stash.reservation_released or (
not stash.itpm_reserved_window_identities and not stash.otpm_reserved_window_identities
@ -4110,24 +4142,28 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
reserved_tokens=0 if stash.reservation_released else otpm_reserved,
)
itpm_ops: Sequence[ReservationAwareIncrementOperation] = ()
if itpm_reserved > 0:
itpm_ops = self._build_project_reservation_ops(
itpm_ops: Final[Sequence[ReservationAwareIncrementOperation]] = (
self._build_project_reservation_ops(
targets=tuple(stash.itpm_reserved_scopes),
reserved_scopes=frozenset() if stash.reservation_released else stash.itpm_reserved_scopes,
actual_tokens=billable_input,
reserved_tokens=itpm_reserved,
reservation_window_identities=stash.itpm_reserved_window_identities,
)
otpm_ops: Sequence[ReservationAwareIncrementOperation] = ()
if otpm_reserved > 0:
otpm_ops = self._build_project_reservation_ops(
if itpm_reserved > 0
else ()
)
otpm_ops: Final[Sequence[ReservationAwareIncrementOperation]] = (
self._build_project_reservation_ops(
targets=tuple(stash.otpm_reserved_scopes),
reserved_scopes=frozenset() if stash.reservation_released else stash.otpm_reserved_scopes,
actual_tokens=completion_tokens,
reserved_tokens=otpm_reserved,
reservation_window_identities=stash.otpm_reserved_window_identities,
)
if otpm_reserved > 0
else ()
)
return tuple((*itpm_ops, *otpm_ops))
def _collect_tpm_scope_targets(
@ -4522,14 +4558,11 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
# already released it (proxy-level rejection that also bubbles up
# here as an LLM-error callback). max_parallel_requests is its
# own counter and is always decremented per call.
if stash is None or stash.reservation_released:
reserved_tokens = 0
itpm_reserved = 0
otpm_reserved = 0
else:
reserved_tokens = stash.reserved_tokens
itpm_reserved = stash.itpm_reserved_tokens
otpm_reserved = stash.otpm_reserved_tokens
reserved_tokens, itpm_reserved, otpm_reserved = (
(0, 0, 0)
if stash is None or stash.reservation_released
else (stash.reserved_tokens, stash.itpm_reserved_tokens, stash.otpm_reserved_tokens)
)
if stash is not None and reserved_tokens > 0:
verbose_proxy_logger.debug("Releasing reserved TPM tokens on failure: %s", reserved_tokens)

View file

@ -50,6 +50,16 @@ if TYPE_CHECKING:
)
class ProjectQuotaCallback(Protocol):
async def enforce_project_io_token_quota_for_frame(
self,
user_api_key_dict: UserAPIKeyAuth | None,
requested_model: str | None,
estimated_input_tokens: int,
estimated_output_tokens: int,
) -> None: ...
@lru_cache(maxsize=1)
def _get_openai_response_types():
from litellm.types.llms import openai as openai_types
@ -1345,11 +1355,19 @@ def _extract_frame_quota_estimate_inputs(msg_obj: Mapping[str, object]) -> tuple
"""
nested: Final = msg_obj.get("response")
params: Final[Mapping[str, object]] = (
nested if _is_json_object(nested) and nested else {k: v for k, v in msg_obj.items() if k != "type"}
nested
if _is_json_object(nested) and nested
else MappingProxyType( # mutable-ok: immediately frozen filtered frame
{k: v for k, v in msg_obj.items() if k != "type"}
)
)
text_parts: list[str] = [] # mutable-ok: local accumulator built in one pass, not shared
def _collect_text(value: object) -> None:
text_parts: Final[list[str]] = [] # mutable-ok: local accumulator built in one pass, not shared
pending: Final[list[object]] = [ # mutable-ok: explicit worklist avoids recursion
params.get("input"),
params.get("instructions"),
]
while pending:
value = pending.pop()
if isinstance(value, str):
text_parts.append(value)
elif _is_json_array(value):
@ -1357,20 +1375,17 @@ def _extract_frame_quota_estimate_inputs(msg_obj: Mapping[str, object]) -> tuple
if isinstance(item, str):
text_parts.append(item)
elif _is_json_object(item):
_collect_text(item.get("content"))
_collect_text(item.get("text"))
_collect_text(params.get("input"))
_collect_text(params.get("instructions"))
pending.append(item.get("content"))
pending.append(item.get("text"))
total_chars: Final = sum(len(part) for part in text_parts)
estimated_input_tokens: Final = max(1, total_chars // _FRAME_CHARS_PER_TOKEN_ESTIMATE) if total_chars else 0
max_output_tokens = params.get("max_output_tokens")
max_output_tokens: Final = params.get("max_output_tokens")
return estimated_input_tokens, max_output_tokens if isinstance(max_output_tokens, int) else None
async def _enforce_frame_project_quota(
quota_callbacks: Sequence[Any],
quota_callbacks: Sequence[ProjectQuotaCallback],
user_api_key_dict: UserAPIKeyAuth | None,
model: str | None,
raw_message: str,
@ -1387,7 +1402,7 @@ async def _enforce_frame_project_quota(
if not _is_json_object(msg_obj) or msg_obj.get("type") != "response.create":
return
estimated_input_tokens, explicit_max_output_tokens = _extract_frame_quota_estimate_inputs(msg_obj)
estimated_output_tokens = (
estimated_output_tokens: Final = (
explicit_max_output_tokens if explicit_max_output_tokens is not None else _FRAME_NO_MAX_OUTPUT_TOKENS_FLOOR
)
for callback in quota_callbacks:
@ -1433,7 +1448,7 @@ class ResponsesWebSocketStreaming:
first_message: str | None = None,
guardrail_callbacks: list[Any] | None = None,
output_guardrail_callbacks: list[PresidioGuardrailCallback] | None = None,
quota_callbacks: list[Any] | None = None,
quota_callbacks: Sequence[ProjectQuotaCallback] | None = None,
authorized_model: str | None = None,
):
self.websocket = websocket
@ -1446,7 +1461,7 @@ class ResponsesWebSocketStreaming:
self.first_message = first_message
self.guardrail_callbacks: list[Any] = guardrail_callbacks or []
self.output_guardrail_callbacks: list[PresidioGuardrailCallback] = output_guardrail_callbacks or []
self.quota_callbacks: list[Any] = quota_callbacks or []
self.quota_callbacks: tuple[ProjectQuotaCallback, ...] = tuple(quota_callbacks) if quota_callbacks else ()
# Model name authorized at connection time; enforced on every
# response.create frame to prevent deployment-substitution attacks.
self.authorized_model: str | None = authorized_model
@ -1870,9 +1885,17 @@ class ResponsesWebSocketStreaming:
except RateLimitError as e:
try:
await self.websocket.send_text(
json.dumps({"type": "error", "error": {"type": "rate_limit_exceeded", "message": str(e)}})
json.dumps( # mutable-ok: WebSocket wire payload requires JSON objects
{ # mutable-ok: WebSocket wire payload requires JSON objects
"type": "error",
"error": { # mutable-ok: nested WebSocket error object
"type": "rate_limit_exceeded",
"message": str(e),
},
}
)
)
except Exception: # noqa: BLE001, S110 - best-effort notification, client may already be gone
except Exception: # noqa: BLE001, S110 # client may already be gone
pass
return False
return True
@ -1969,7 +1992,7 @@ class ManagedResponsesWebSocketHandler:
timeout: float | None = None,
custom_llm_provider: str | None = None,
first_message: str | None = None,
quota_callbacks: list[Any] | None = None,
quota_callbacks: Sequence[ProjectQuotaCallback] | None = None,
**kwargs: object,
) -> None:
self.websocket = websocket
@ -1986,7 +2009,7 @@ class ManagedResponsesWebSocketHandler:
self.custom_llm_provider = custom_llm_provider
self._connection_provider = self._resolve_provider(model) or custom_llm_provider
self.first_message = first_message
self.quota_callbacks: list[Any] = quota_callbacks or []
self.quota_callbacks: tuple[ProjectQuotaCallback, ...] = tuple(quota_callbacks) if quota_callbacks else ()
# Carry through safe pass-through kwargs (e.g. extra_headers)
self.extra_kwargs: dict[str, object] = {k: v for k, v in kwargs.items() if k not in _MANAGED_WS_SKIP_KWARGS}
# In-memory session history: response_id → full accumulated message list.

View file

@ -3582,18 +3582,24 @@ async def test_streaming_combined_usage_reconciles_project_io_reservations(
assert [operation["increment_value"] for operation in otpm_adjustments] == [-45]
def test_aggregate_only_combined_usage_keeps_project_io_reservations(rate_limiter):
def test_aggregate_only_combined_usage_reconciles_project_io_reservations(rate_limiter):
handler, _cache = rate_limiter
stash = get_or_create_request_stash()
stash.itpm_reserved_tokens = 100
stash.itpm_reserved_scopes = frozenset(
{(PROJECT_ITPM_DESCRIPTOR_KEY, "project:model")}
)
stash.otpm_reserved_tokens = 80
stash.otpm_reserved_scopes = frozenset(
{(PROJECT_OTPM_DESCRIPTOR_KEY, "project:model")}
)
kwargs = {
"combined_usage_object": Usage(total_tokens=55),
}
assert handler._build_io_token_reservation_ops(kwargs, object()) == ()
operations = handler._build_io_token_reservation_ops(kwargs, object())
assert [operation["increment_value"] for operation in operations] == [-45, -25]
def test_raw_split_usage_dict_reconciles_project_io_tokens(rate_limiter):

View file

@ -1,9 +1,9 @@
{
"LIT001": {
"limit": 22941
"limit": 22936
},
"LIT002": {
"limit": 27139
"limit": 27133
},
"LIT003": {
"limit": 269
@ -27,10 +27,10 @@
"limit": 0
},
"LIT010": {
"limit": 16716
"limit": 16701
},
"LIT011": {
"limit": 5596
"limit": 5595
},
"LIT012": {
"limit": 4519