BedrockGuardrail.apply_guardrail hardcoded source="INPUT" regardless of the
input_type parameter. On the non-streaming post-call path (unified_guardrail
-> OpenAIChatCompletionsHandler.process_output_response -> apply_guardrail),
the model response text was sent to Bedrock as INPUT, so guardrail policies
configured for Output (e.g. PII/NAME blocking) returned action=NONE and the
response passed through unblocked. The streaming path was unaffected because
it calls make_bedrock_api_request(source="OUTPUT", ...) directly.
Map input_type to the correct Bedrock source ("request" -> INPUT,
"response" -> OUTPUT) and build a synthetic ModelResponse for the OUTPUT
path so _create_bedrock_output_content_request produces the correct payload.
Made-with: Cursor
* bump litellm-proxy-extras version to 0.4.67
* bump litellm-proxy-extras pin to 0.4.67 in litellm pyproject
* regenerate uv.lock for litellm-proxy-extras 0.4.67
* bump litellm-enterprise version to 0.1.38
* bump litellm-enterprise pin to 0.1.38 in litellm pyproject
* regenerate uv.lock for litellm-enterprise 0.1.38
send_max_budget_alert_email previously guarded with `is not None`, which
accepts `[]` and then crashes on `recipient_emails[0]` inside
_get_email_params. The current caller (_handle_multi_threshold_max_budget_alert)
already filters empty lists upstream, but the public method signature makes
no such guarantee — a future caller passing [] would hit IndexError.
Switch to truthiness so both None and [] fall through to the single-recipient
path.
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.
The lazy import from litellm_enterprise inside _normalize_alert_emails
coupled the core proxy auth path to an optional package. Core should not
depend on enterprise, even lazily — it hides the dependency from static
analysis and inverts the intended layering.
Duplicate the 7-line parser locally. It's pure and unlikely to drift; the
enterprise copy stays where it is for its own callers.
_merge_budget_alert_email_configs previously called list() directly on each
threshold's value, which raised TypeError on null YAML values and silently
split bare strings into single characters. Both are reachable from user-
supplied global config and per-key metadata, so the crash could fire on
every authenticated request once the metadata was in place.
Route both inputs through a _normalize_alert_emails helper that delegates
to the existing _parse_email_list parser (lazy-imported, matching the
enterprise import pattern used elsewhere in proxy/). The merge body keeps
its tight Dict[str, List[str]] contract.
The Union[str, List[str]] value type was speculative — _merge_budget_alert_email_configs
always returns List[str] values, and no caller produces bare strings. Narrowing to
match the runtime guarantee resolves a mypy invariance error at auth_checks.py:3021
without adding casts or Mapping covariance.
A previous refactor added `litellm/integrations/prometheus_helpers.py` as a
sibling to the existing `litellm/integrations/prometheus_helpers/` directory
(which contains `prometheus_api.py` and has no `__init__.py`). The file
shadowed the namespace-package directory, so any deferred
`from litellm.integrations.prometheus_helpers.prometheus_api import ...`
raised `ModuleNotFoundError: 'litellm.integrations.prometheus_helpers' is
not a package` at request time.
Two runtime call sites hit that path:
- /global/spend/logs (spend_management_endpoints.py) returned plain-text 500
"Internal Server Error" for every call, breaking the Admin UI Usage tab
and programmatic consumers.
- SlackAlerting.send_fallback_stats_from_prometheus silently failed inside
its own try/except.
Fix: move prometheus_helpers.py content into prometheus_helpers/__init__.py
and delete the stray .py. The directory becomes a regular package, so both
the package-root import (from ...prometheus_helpers import X) and the
submodule import (from ...prometheus_helpers.prometheus_api import X)
resolve correctly. No call sites change.
Six CI jobs create a miniconda env with python=3.9 before installing
the project; these jobs now fail resolution because the project
requires-python is >=3.10. Bump the conda env python to 3.10 to match
the new floor.
All three dependency bumps in this PR resolve on Python 3.10, so there
is no need to jump the floor all the way to 3.11. Also restore the
py3.10-specific lunary==1.4.36 pin that was collapsed when the floor
was temporarily at 3.11.
Now that requires-python starts at 3.11, the "python_version >= '3.9'"
and ">= '3.10'" markers are unconditionally true, and the "< '3.10'"
entries for psycopg, Pillow, pyarrow, langchain, lunary, and pylint can
never resolve. Drop the dead markers and remove the unreachable pins so
the dependency list reflects what actually gets installed.
Premium fields like policies are echoed at the top level of the
/key/update response, not necessarily mirrored into metadata. Read
metadata first then fall back to the top-level property so an
intentional clear is preserved in either shape.
The /key/update response echoes top-level defaults like policies:[] into
client state. On a subsequent edit, the form resends policies:[], which
the backend treats as "user is setting policies" and blocks with a 403
enterprise check regardless of value.
Drop premium metadata fields from the update payload when the current
form value and the previously persisted value are both empty. Genuine
clears (non-empty -> empty) still pass through so premium users can
clear policies as intended.
Bumps orjson, fastapi-sso, and python-multipart to their latest releases
in the proxy extra, and raises the project python floor to 3.11 so the
updated pins can resolve. CI already runs on 3.11 / 3.12 / 3.13 and the
Docker images ship python 3.13, so the floor change aligns the declared
support range with what is actually tested and shipped.
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
`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>