Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ef0be3a
perf(auth): prefetch user, team, membership, org and project in one M…
yassin-berriai Sep 12, 2026
8acc1ff
refactor(auth): keep prefetch and spend batch collections immutable
yassin-berriai Sep 12, 2026
01f3443
perf(auth): let the cold spend-counter reseed reuse the admission MGE…
yassin-berriai Sep 12, 2026
a52ef03
perf(auth): prefetch referenced auth objects only after the key's mod…
yassin-berriai Sep 12, 2026
03bdb75
Merge remote-tracking branch 'origin/litellm_internal_staging' into l…
yassin-berriai Sep 12, 2026
69e2017
test(auth): give the prefetch-ordering test's patches their test-qual…
yassin-berriai Sep 12, 2026
006ab3d
test(auth): move the real-Postgres prefetch join test to the proxy_be…
yassin-berriai Sep 12, 2026
897d561
fix(auth): read NULL nested permission and budget lists as [] in the …
yassin-berriai Sep 12, 2026
4c5bcbb
perf(proxy): batch post-call spend counter reads and carry budget sta…
yassin-berriai Sep 12, 2026
6b2ae1e
perf(proxy): reconcile the budget reservation and the post-call warm …
yassin-berriai Sep 12, 2026
6012071
chore(proxy): drop the dataclass import left behind by the PendingSpe…
yassin-berriai Sep 12, 2026
4148bd8
fix(types): import Self from typing_extensions so the proxy imports o…
yassin-berriai Sep 12, 2026
f7e03fb
test(proxy): use a neutral organization alias in the carried budget s…
yassin-berriai Sep 12, 2026
189c563
test(proxy): cover recorded and forgotten spend counter values in the…
yassin-berriai Sep 12, 2026
01c66d5
test(caching): assert async_set_cache_pipeline_with_ttls keeps per-en…
yassin-berriai Sep 12, 2026
a84c4cc
Merge branch 'litellm_auth_single_io_lookup' into litellm_post_call_s…
yassin-berriai Sep 12, 2026
df2a305
refactor(proxy): type the reservation entry carried through reconcile…
yassin-berriai Sep 12, 2026
e75b541
fix(auth): map the model table's aliases column to model_aliases in t…
yassin-berriai Sep 12, 2026
0626401
Merge branch 'litellm_auth_single_io_lookup' into litellm_post_call_s…
yassin-berriai Sep 12, 2026
b4f81a7
Merge remote-tracking branch 'origin/litellm_internal_staging' into l…
yassin-berriai Sep 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 21 additions & 8 deletions litellm/caching/redis_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -1273,6 +1273,24 @@ async def batch_cache_write(self, key, value, **kwargs):
if len(self.redis_batch_writing_buffer) >= self.redis_flush_size:
await self.flush_cache_buffer() # logging done in here

@staticmethod
async def _incrbyfloat_with_ttl(
_redis_client: "Redis", key: str, value: float, ttl: int | None, refresh_ttl: bool
) -> float:
"""INCRBYFLOAT plus its TTL command in one round trip; a third only when an unexpiring key needs an EXPIRE."""
if ttl is None:
return await _redis_client.incrbyfloat(name=key, amount=value)
async with _redis_client.pipeline(transaction=False) as pipe:
pipe.incrbyfloat(name=key, amount=value)
if refresh_ttl:
pipe.expire(key, ttl)
else:
pipe.ttl(key)
result, ttl_or_expire = await pipe.execute()
if not refresh_ttl and ttl_or_expire == -1:
await _redis_client.expire(key, ttl)
return float(result)

@_redis_circuit_breaker_guard
async def async_increment(
self,
Expand All @@ -1289,14 +1307,9 @@ async def async_increment(
_used_ttl: Final = self.get_ttl(ttl=ttl)
key = self.check_and_fix_namespace(key=key)
try:
result: Final = await _redis_client.incrbyfloat(name=key, amount=value)
if _used_ttl is not None:
if refresh_ttl:
await _redis_client.expire(key, _used_ttl)
else:
current_ttl: Final = await _redis_client.ttl(key)
if current_ttl == -1:
await _redis_client.expire(key, _used_ttl)
result: Final = await self._incrbyfloat_with_ttl(
_redis_client, key=key, value=value, ttl=_used_ttl, refresh_ttl=refresh_ttl
)

## LOGGING ##
end_time = time.time()
Expand Down
78 changes: 69 additions & 9 deletions litellm/integrations/prometheus.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@
_sanitize_prometheus_label_value,
validate_prometheus_deployment_and_latency_caller_identity,
)
from litellm.types.proxy.carried_budget_state import (
KeyBudgetSnapshot,
OrgBudgetSnapshot,
TeamBudgetSnapshot,
UserBudgetSnapshot,
)
from litellm.types.utils import (
StandardLoggingGuardrailInformation,
StandardLoggingPayload,
Expand Down Expand Up @@ -1941,6 +1947,8 @@ async def _increment_remaining_budget_metrics(

_user_spend: Final = _metadata.get("user_api_key_user_spend", None)
_user_max_budget: Final = _metadata.get("user_api_key_user_max_budget", None)
_user_email: Final = _metadata.get("user_api_key_user_email", None)
_org_alias: Final = _metadata.get("user_api_key_org_alias", None)

# Bound the per-request budget-metric emission so that slow Redis/DB
# lookups under load cannot consume the whole LoggingWorker watchdog
Expand All @@ -1957,23 +1965,29 @@ async def _increment_remaining_budget_metrics(
response_cost=response_cost,
key_max_budget=_api_key_max_budget,
key_spend=_api_key_spend,
carried=KeyBudgetSnapshot.from_metadata(_metadata),
),
self._set_team_budget_metrics_after_api_request(
user_api_team=user_api_team,
user_api_team_alias=user_api_team_alias,
team_spend=_team_spend,
team_max_budget=_team_max_budget,
response_cost=response_cost,
carried=TeamBudgetSnapshot.from_metadata(_metadata),
),
self._set_user_budget_metrics_after_api_request(
user_id=user_id,
user_spend=_user_spend,
user_max_budget=_user_max_budget,
response_cost=response_cost,
carried=UserBudgetSnapshot.from_metadata(_metadata),
user_email=_user_email if isinstance(_user_email, str) else None,
),
self._set_org_budget_metrics_after_api_request(
org_id=user_api_key_org_id,
response_cost=response_cost,
carried=OrgBudgetSnapshot.from_metadata(_metadata),
org_alias=_org_alias if isinstance(_org_alias, str) else None,
),
return_exceptions=True,
)
Expand Down Expand Up @@ -3821,6 +3835,7 @@ async def _set_team_budget_metrics_after_api_request(
team_spend: float | None,
team_max_budget: float | None,
response_cost: float,
carried: TeamBudgetSnapshot | None = None,
):
"""
Set team budget metrics after an LLM API request
Expand All @@ -3839,6 +3854,7 @@ async def _set_team_budget_metrics_after_api_request(
spend=team_spend,
max_budget=team_max_budget,
response_cost=response_cost,
carried=carried,
)

self._set_team_budget_metrics(team_object)
Expand All @@ -3850,18 +3866,26 @@ async def _assemble_team_object(
spend: float | None,
max_budget: float | None,
response_cost: float,
carried: TeamBudgetSnapshot | None = None,
) -> LiteLLM_TeamTable:
"""
Assemble a LiteLLM_TeamTable object

for fields not available in metadata, we fetch from db
Fields not available in metadata:
- `budget_reset_at`
``budget_reset_at`` comes from the auth-carried snapshot when the request has one,
otherwise from the team lookup
"""
from litellm.proxy.auth.auth_checks import get_team_object
from litellm.proxy.proxy_server import prisma_client, user_api_key_cache

_total_team_spend: Final = (spend or 0) + response_cost
if carried is not None:
return LiteLLM_TeamTable(
team_id=team_id,
team_alias=team_alias,
spend=_total_team_spend,
max_budget=max_budget if max_budget is not None else carried.max_budget,
budget_reset_at=carried.budget_reset_at,
)
team_object: Final = LiteLLM_TeamTable(
team_id=team_id,
team_alias=team_alias,
Expand Down Expand Up @@ -3946,11 +3970,13 @@ async def _set_org_budget_metrics_after_api_request(
self,
org_id: str | None,
response_cost: float,
carried: OrgBudgetSnapshot | None = None,
org_alias: str | None = None,
):
"""
Set org budget metrics after an LLM API request

- Fetches org info via cache (get_org_object)
- Uses the auth-carried org budget when the request has one, else fetches via get_org_object
- Sets org budget metrics
"""
if isinstance(self.litellm_remaining_org_budget_metric, NoOpMetric):
Expand All @@ -3959,6 +3985,16 @@ async def _set_org_budget_metrics_after_api_request(
if not org_id:
return

if carried is not None:
self._set_org_budget_metrics(
org_id=org_id,
org_alias=org_alias or "",
spend=carried.spend + response_cost,
max_budget=carried.max_budget,
budget_reset_at=None,
)
return

from litellm.proxy.auth.auth_checks import get_org_object
from litellm.proxy.proxy_server import prisma_client, user_api_key_cache

Expand All @@ -3979,15 +4015,14 @@ async def _set_org_budget_metrics_after_api_request(
if org_info is None:
return

org_alias: Final = org_info.organization_alias or ""
_total_org_spend: Final = (org_info.spend or 0.0) + response_cost
budget_table: Final = org_info.litellm_budget_table
max_budget: Final = budget_table.max_budget if budget_table else None
budget_reset_at: Final = getattr(budget_table, "budget_reset_at", None) if budget_table else None

self._set_org_budget_metrics(
org_id=org_id,
org_alias=org_alias,
org_alias=org_info.organization_alias or "",
spend=_total_org_spend,
max_budget=max_budget,
budget_reset_at=budget_reset_at,
Expand Down Expand Up @@ -4084,6 +4119,7 @@ async def _set_api_key_budget_metrics_after_api_request(
response_cost: float,
key_max_budget: float | None,
key_spend: float | None,
carried: KeyBudgetSnapshot | None = None,
):
if isinstance(self.litellm_remaining_api_key_budget_metric, NoOpMetric):
return
Expand All @@ -4095,6 +4131,7 @@ async def _set_api_key_budget_metrics_after_api_request(
key_max_budget=key_max_budget,
key_spend=key_spend,
response_cost=response_cost,
carried=carried,
)
self._set_key_budget_metrics(user_api_key_dict)

Expand All @@ -4105,6 +4142,7 @@ async def _assemble_key_object(
key_max_budget: float | None,
key_spend: float | None,
response_cost: float,
carried: KeyBudgetSnapshot | None = None,
) -> UserAPIKeyAuth:
"""
Assemble a UserAPIKeyAuth object
Expand All @@ -4113,6 +4151,14 @@ async def _assemble_key_object(
from litellm.proxy.proxy_server import prisma_client, user_api_key_cache

_total_key_spend: Final = (key_spend or 0) + response_cost
if carried is not None:
return UserAPIKeyAuth(
token=user_api_key,
key_alias=user_api_key_alias,
max_budget=key_max_budget,
spend=_total_key_spend,
budget_reset_at=carried.budget_reset_at,
)
user_api_key_dict: Final = UserAPIKeyAuth(
token=user_api_key,
key_alias=user_api_key_alias,
Expand Down Expand Up @@ -4140,6 +4186,8 @@ async def _set_user_budget_metrics_after_api_request(
user_spend: float | None,
user_max_budget: float | None,
response_cost: float,
carried: UserBudgetSnapshot | None = None,
user_email: str | None = None,
):
"""
Set user budget metrics after an LLM API request
Expand All @@ -4157,6 +4205,8 @@ async def _set_user_budget_metrics_after_api_request(
spend=user_spend,
max_budget=user_max_budget,
response_cost=response_cost,
carried=carried,
user_email=user_email,
)

self._set_user_budget_metrics(user_object)
Expand All @@ -4167,18 +4217,28 @@ async def _assemble_user_object(
spend: float | None,
max_budget: float | None,
response_cost: float,
carried: UserBudgetSnapshot | None = None,
user_email: str | None = None,
) -> LiteLLM_UserTable:
"""
Assemble a LiteLLM_UserTable object

for fields not available in metadata, we fetch from db
Fields not available in metadata:
- `budget_reset_at`
``budget_reset_at`` and ``user_alias`` come from the auth-carried snapshot when the
request has one, otherwise from the user lookup
"""
from litellm.proxy.auth.auth_checks import get_user_object
from litellm.proxy.proxy_server import prisma_client, user_api_key_cache

_total_user_spend: Final = (spend or 0) + response_cost
if carried is not None:
return LiteLLM_UserTable(
user_id=user_id,
spend=_total_user_spend,
max_budget=max_budget if max_budget is not None else carried.max_budget,
budget_reset_at=carried.budget_reset_at,
user_alias=carried.user_alias,
user_email=user_email,
)
user_object: Final = LiteLLM_UserTable(
user_id=user_id,
spend=_total_user_spend,
Expand Down
8 changes: 8 additions & 0 deletions litellm/proxy/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@
MCPTransportType,
)
from litellm.types.mcp_server.mcp_server_manager import MCPInfo
from litellm.types.proxy.carried_budget_state import (
OrgBudgetSnapshot,
TeamBudgetSnapshot,
UserBudgetSnapshot,
)
from litellm.types.proxy.control_plane_endpoints import WorkerRegistryEntry
from litellm.types.router import RouterErrors, UpdateRouterConfig
from litellm.types.secret_managers.main import KeyManagementSystem
Expand Down Expand Up @@ -3106,6 +3111,9 @@ class UserAPIKeyAuth(LiteLLM_VerificationTokenView): # the expected response ob
),
)
budget_reservation: dict[str, Any] | None = Field(default=None, exclude=True)
team_budget_snapshot: TeamBudgetSnapshot | None = Field(default=None, exclude=True)
user_budget_snapshot: UserBudgetSnapshot | None = Field(default=None, exclude=True)
org_budget_snapshot: OrgBudgetSnapshot | None = Field(default=None, exclude=True)
matched_model_access_groups: list[str] | None = Field(default=None, exclude=True)
budget_throttle_pct: float | None = Field(default=None, exclude=True)
user: Any | None = None # Expanded user object when expand=user is used
Expand Down
3 changes: 3 additions & 0 deletions litellm/proxy/auth/auth_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
)
from litellm.proxy.route_llm_request import route_request
from litellm.proxy.spend_tracking.budget_reservation import get_budget_window_start
from litellm.proxy.spend_tracking.carried_budget_state import carry_organization_budget_state
from litellm.proxy.utils import PrismaClient, ProxyLogging, log_db_metrics
from litellm.repositories.budget_repository import BudgetRepository
from litellm.repositories.object_permission_repository import ObjectPermissionRepository
Expand Down Expand Up @@ -5635,6 +5636,8 @@ async def _organization_max_budget_check(
if org_table is None:
return

carry_organization_budget_state(valid_token=valid_token, org_table=org_table)

# Get max_budget from organization's budget table
org_max_budget: float | None = None
if org_table.litellm_budget_table is not None:
Expand Down
6 changes: 6 additions & 0 deletions litellm/proxy/auth/user_api_key_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
)
from litellm.proxy.db.exception_handler import PrismaDBExceptionHandler
from litellm.proxy.litellm_pre_call_utils import LiteLLMProxyRequestSetup
from litellm.proxy.spend_tracking.carried_budget_state import carry_team_and_user_budget_state
from litellm.proxy.spend_tracking.spend_counter_batch import (
bind_admission_counter_keys,
release_spend_counter_batch,
Expand Down Expand Up @@ -2631,6 +2632,11 @@ async def _run_centralized_common_checks(
None if isinstance(end_user_result, BaseException) else end_user_result
)
global_proxy_spend: float | None = None if isinstance(global_spend_result, BaseException) else global_spend_result
carry_team_and_user_budget_state(
valid_token=user_api_key_auth_obj,
team_object=team_object,
user_object=user_object,
)

if user_api_key_auth_obj.org_id is None and team_object is not None and team_object.organization_id is not None:
user_api_key_auth_obj.org_id = team_object.organization_id
Expand Down
16 changes: 10 additions & 6 deletions litellm/proxy/db/spend_counter_reseed.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from litellm.litellm_core_utils.duration_parser import duration_in_seconds
from litellm.proxy._types import Litellm_EntityType
from litellm.proxy.db.db_lookup_gate import db_lookup_gate
from litellm.proxy.spend_tracking.spend_counter_batch import active_spend_counter_batch
from litellm.proxy.spend_tracking.spend_counter_batch import read_batched_spend_counter, record_spend_counter_value
from litellm.repositories.organization_repository import OrganizationRepository
from litellm.repositories.table_repositories import (
BudgetWindowSpendRepository,
Expand Down Expand Up @@ -197,9 +197,8 @@ def _is_key_or_team_window_counter(counter_key: str) -> bool:

@staticmethod
async def _read_active_batch(counter_key: str) -> tuple[float | None, bool] | None:
"""The request's admission MGET already answered for this counter; a Redis miss there is authoritative."""
batch: Final = active_spend_counter_batch()
return None if batch is None else await batch.read(counter_key)
"""The request's MGET answers for this counter; a Redis miss there is authoritative."""
return await read_batched_spend_counter(counter_key)

@staticmethod
async def coalesced(
Expand Down Expand Up @@ -263,6 +262,7 @@ async def coalesced(
key=counter_key,
value=current_value,
)
record_spend_counter_value(counter_key, current_value)
else:
cached_spend: Final = spend_counter_cache.in_memory_cache.get_cache(key=counter_key)
seeded_spend: Final = max(db_spend, float(cached_spend)) if cached_spend is not None else db_spend
Expand Down Expand Up @@ -414,8 +414,11 @@ async def coalesced_window(
) -> float | None:
lock: Final = await SpendCounterReseed._get_lock(counter_key)
async with lock:
redis_clean_miss = False
if spend_counter_cache.redis_cache is not None:
batched: Final = await SpendCounterReseed._read_active_batch(counter_key)
if batched is not None and batched[0] is not None:
return batched[0]
redis_clean_miss = batched is not None
if spend_counter_cache.redis_cache is not None and not redis_clean_miss:
try:
val = await spend_counter_cache.redis_cache.async_get_cache(key=counter_key)
if val is not None:
Expand Down Expand Up @@ -459,6 +462,7 @@ async def coalesced_window(
key=counter_key,
value=current_value,
)
record_spend_counter_value(counter_key, float(current_value))
else:
cached_spend: Final = spend_counter_cache.in_memory_cache.get_cache(key=counter_key)
seeded_spend: Final = (
Expand Down
Loading
Loading