Move async_log_success_event pipeline construction into
_build_success_event_pipeline_operations so the async hook stays
under Ruff's max-statement limit.
Made-with: Cursor
Addresses Greptile review feedback:
- Clarify LiteLLM_Reserved_Metadata_Fields comment to describe both
preserve-on-omit and reject-on-change behaviors.
- Treat explicit null as a change attempt so callers trying to clear
service_account_id get a 400 instead of a silent no-op.
`should_create_missing_views()` had `and result[0]["reltuples"]` which is
falsy when reltuples=0. On a fresh empty PostgreSQL table, CREATE INDEX sets
reltuples=0, causing the guard to return False and skip view creation entirely.
Views like MonthlyGlobalSpendPerKey are never created, and the
/global/spend/logs endpoint returns 500.
Fix: change to `and result[0]["reltuples"] is not None` so reltuples=0
(empty table) and reltuples=-1 (unanalyzed table) both correctly return True.
Also harden test_vertex_ai.py to return None instead of crashing with
JSONDecodeError when the spend-logs endpoint returns a non-JSON 500 response,
and add unit tests covering all three reltuples branches (0, -1, positive).
Without this, project-level model_tpm_limit was silently acting as an
RPM cap — the pre-call +1 sentinel was tracked but the actual token
count was never added after a successful call.
Extracts user_api_key_project_id from standard_logging_metadata and
adds a model_per_project pipeline operation matching the existing
model_per_team and model_per_organization patterns.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
test_virtual_key_max_budget_alert_check_per_key_overrides_global asserted
override semantics but the implementation does additive merge. Renamed test
and updated assertion to match: per-key and global thresholds are unioned,
not replaced.
/key/update and /user/update wholesale-replaced the metadata JSON column
whenever a caller passed a `metadata` field, silently dropping
service_account_id. The pre-call check in litellm_pre_call_utils.py then
stopped treating the key as a service account and bypassed
service_account_settings.enforced_params.
Add LiteLLM_Reserved_Metadata_Fields and have prepare_metadata_fields
preserve these keys from the existing row when the caller omits them.
Reject attempts to change an already-set value (400) since rebinding
service_account_id would break spend attribution.
Set extra_headers explicitly in initialValues instead of relying on
a useEffect setFieldValue call that races with Antd form initialization.
Also avoid sending empty array on submit so the backend's exclude_none
doesn't overwrite stored values.
The edit page was calling POST /mcp-rest/test/tools/list (the temp-session
endpoint that requires inline credentials) on mount. Since fetchTools
deliberately omits credentials from the request body, any server with
auth_type api_key/bearer_token/basic/authorization would 422.
Switch to GET /mcp-rest/tools/list?server_id=... which looks up stored
credentials on the backend — no inline creds needed for saved servers.
Editing a model in the Admin UI (e.g. to change credential) unconditionally
included input_cost_per_token: 0 and output_cost_per_token: 0 in the PATCH
payload, overriding built-in pricing from model_prices_and_context_window.json.
Guard cost fields with form.isFieldTouched so they are only sent when the
user explicitly modifies them. Intentional $0 cost (for budget bypass) still
works because the guard checks touched + non-null, not non-zero.
- Add early return guard in _handle_multi_threshold_max_budget_alert
for None max_budget_alert_emails and max_budget
- Add explicit type annotation on alert_email_config in auth_checks
Fixes SyntaxError at pytest collection time caused by leftover
<<<<<<<, =======, >>>>>>> markers in test_bedrock_common_utils.py.
Keeps the assertion matching the model under test
(claude-haiku-4-5-20251001-v1:0).
Project-level model rpm/tpm limits stored in project_metadata were never
checked during rate limit enforcement — only model-level limits applied.
Adds _add_project_model_rate_limit_descriptor_from_metadata() to the v3
limiter (mirrors the existing team metadata path) and calls it in
async_pre_call_hook, creating a model_per_project descriptor keyed as
"{project_id}:{model}" with the project's configured limits.
Also extends get_model_rate_limit_from_metadata's Literal to accept
"project_metadata" and adds get_project_model_rpm/tpm_limit helpers.
Fixes: LIT-2317
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Guard empty recipients in _handle_multi_threshold_max_budget_alert:
log warning and skip instead of falling through to old path error loop
- Widen max_budget_alert_emails type to Dict[str, Union[str, List[str]]]
to match _parse_email_list runtime behavior (accepts comma-separated strings)
- Pre-filter asyncio.create_task with min threshold check to avoid
unnecessary task allocation on every request when spend is below
all configured thresholds
When litellm.max_end_user_budget_id is configured, implicitly-created end users
(via /chat/completions) have budget_id=NULL in the DB since the default budget
is only applied in-memory. The budget reset job filtered by budget_id, so these
users were never reset and eventually permanently blocked.
Fix: when the default budget is in the reset list, also query for and reset
end users with budget_id=NULL and spend > 0. This keeps the hot auth path
unchanged (no DB writes on every request).
Fixes#22019
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add `default_key_max_budget_alert_emails` litellm_settings config as
global fallback for all virtual keys (per-key metadata takes priority)
- Fix crash when key has no user_id/user_email by passing recipient email
to _get_email_params (same pattern as team soft budget path)
- Use owner email for greeting, falling back to key_alias or token
- Rename setting from default_max_budget_alert_emails to
default_key_max_budget_alert_emails for clarity
Previously, _apply_default_budget_to_end_user() only set the budget in-memory,
leaving budget_id NULL in the database. This caused the budget reset job to skip
these users since it filters by budget_id. Now the function also persists
budget_id via a Prisma update call (non-fatal on failure).
Fixes#22019
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
_virtual_key_max_budget_check raises BudgetExceededError when spend
crosses max_budget, which meant the 100% threshold in the multi-threshold
email config never got a chance to fire — the request that pushes spend
over 100% was rejected before the alert check ran. Reorder so the alert
check runs first; enforcement still raises right after.
Users can set metadata.max_budget_alert_emails as a JSON map of threshold
percentages to email recipients on virtual keys. When configured, the email
handler loops over each threshold, checks per-threshold dedup cache, and
sends to the configured recipients (auto-including the key owner's email).
When no map is set, the existing single 80% threshold behavior is preserved
unchanged. Teams support is out of scope for this v0.
async_post_call_failure_hook passed only supported_enum_labels to
prometheus_label_factory; enum_values is required. The call was unused
because _inc_labeled_counter builds labels internally.
Reformat Prometheus-related modules and passthrough/copilot helpers for CI.
Made-with: Cursor
Unit Tests: Proxy DB Operations / proxy-db (auth-checks, tests/proxy_unit_tests/test_auth_checks.py tests/proxy_unit_tests/test_user_api_key_auth.py, 20, 8) (push) Waiting to run
Unit Tests: Proxy DB Operations / proxy-db (remaining, tests/proxy_unit_tests --ignore=tests/proxy_unit_tests/test_key_generate_prisma.py --ignore=tests/proxy_unit_tests/test_auth_checks.py --ignore=tests/proxy_unit_tests/test_user_api_key_auth.py, 30, 8) (push) Waiting to run
When prisma migrate deploy reports 'No pending migrations to apply' the DB
already matches schema — running _resolve_all_migrations (migrate diff +
prisma db execute) adds 25+ seconds unnecessarily, causing the proxy to
miss the 90-second startup timeout in test_litellm_proxy_server_config_no_general_settings.
page_utils.test.ts enforces that every menuGroups entry has a matching
description and vice versa. The left nav uses 'skills' but page_metadata.ts
still had 'claude-code-plugins', causing two test failures.