* Add Rubrik as officially-supported guardrail plugin
Adds tool blocking and batch logging integration with an external Rubrik
webhook service. The plugin validates LLM tool calls against a policy
service (fail-open on errors) and batch-logs all requests/responses.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Update Rubrik docs: config.yaml as primary, env vars as fallback
Restructures the Quick Start to present config.yaml as the recommended
approach with tabbed UI, and environment variables as an alternative
fallback.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add Rubrik env vars to config_settings reference
Fixes documentation validation by adding RUBRIK_API_KEY,
RUBRIK_BATCH_SIZE, RUBRIK_SAMPLING_RATE, and RUBRIK_WEBHOOK_URL
to the environment settings reference table.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add fallback message when blocking service returns empty explanation
Prevents whitespace-only violation message when the tool blocking
service blocks tools but returns an empty content field.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- preserve existing shared backend `mode` when router deployment registration
reuses a provider/model key already in `litellm.model_cost` (prevents alias
with `mode: chat` from downgrading shared `chatgpt/gpt-5.4` from `responses`
to `chat` and triggering 403s on /v1/chat/completions)
- teach the ChatGPT Responses parser to recover `response.output_item.done`
entries when `response.completed.output` is empty
- add defensive /responses -> /chat/completions bridge fallback that
reconstructs output items from raw SSE when `raw_response.output` is empty
- regression coverage for shared alias routing, empty completed.output
parsing, and SSE bridge recovery
Closes#25403
Co-authored-by: afoninsky <andrey.afoninsky@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: reuse cached credentials in VertexAIPartnerModels instead of creating new VertexLLM per request
VertexAIPartnerModels.completion() was creating a throwaway VertexLLM()
instance on every call to get an access token, bypassing the credential
cache inherited from VertexBase. This caused a fresh token fetch for
every single request, adding significant latency overhead.
Fix: call super().__init__() to initialize VertexBase's credential cache,
and use self._ensure_access_token() instead of a new VertexLLM instance.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: apply same credential caching fix to VertexAIGemmaModels and VertexAIModelGardenModels
Same bug as VertexAIPartnerModels: both classes had `pass` in __init__
instead of `super().__init__()`, and created throwaway VertexLLM()
instances per request instead of using self._ensure_access_token().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(vertex_ai): single-flight credential refresh to prevent thundering herd
When GCP credentials expire under high concurrency, all requests
simultaneously call credentials.refresh() via asyncify, saturating the
40-thread anyio pool and blocking the proxy for 20+ seconds.
This adds:
- Per-credential asyncio.Lock in get_access_token_async for single-flight
refresh (1 coroutine refreshes, others wait on the lock)
- Background refresh when token_state is STALE (usable but near expiry),
returning the current token immediately with zero added latency
- threading.Lock on the sync get_access_token path
- Uses google-auth's TokenState enum (FRESH/STALE/INVALID) instead of
reimplementing expiry logic
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address PR review comments
- Use asyncio.create_task() instead of deprecated get_event_loop().create_task()
- Track in-flight background refresh tasks to prevent duplicate refreshes
when multiple STALE-path callers pass through the lock before the first
background task completes
- Add token validation in the STALE branch (consistent with FRESH/INVALID)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: lazy-import TokenState to avoid breaking when google-auth is not installed
Also extract helper methods to bring get_access_token_async under the
PLR0915 statement limit (50).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: apply Black formatting to test file and update uv.lock
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove user-provided project_id from log messages (CodeQL log injection)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: avoid leaking token value in error message, log type instead
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: restore uv.lock to match litellm_oss_branch
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove project_id from remaining log message (CodeQL log injection)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove remaining project_id from log and error messages
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per-user OAuth MCP requests now only skip pre-emptive 401 when a stored token is available, preserving token-reuse behavior while restoring fast PKCE kickoff for first-time or missing-token users.
`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).
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.
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>
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.
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
- url_utils.py: narrow sockaddr[0] from str|int to str via a helper with a
fail-closed isinstance check. Fixes the two mypy errors introduced by
the SSRF hardening without masking unexpected stdlib behavior.
- key_management_endpoints.py: restore the documented team member_permissions
path for /key/update. The cross-key admin check added to close the
cross-org rewrite attack was over-broad: it rejected non-admin team
members even when can_team_member_execute_key_management_endpoint had
already validated their team membership and /key/update grant. Now skip
the admin check when the key has a team_id and the change is non-budget
(membership + permission already enforced above). Budget/spend changes
still require team/org admin. The cross-org attack remains blocked:
an outside org admin fails the earlier team membership check.
- test_logging_redaction_e2e_test.py: rename and rewrite two parametrized
tests to assert that request-body turn_off_message_logging has no effect.
Reflects the intentional removal of turn_off_message_logging from
_supported_callback_params so the caller cannot override admin logging
policy via the request body.
- test_key_management_endpoints.py: add two tests covering the restored
team member permission path — one positive (non-budget update succeeds
for a team member with /key/update grant), one negative (max_budget
change still rejected without admin role).