chore(release): backport #30480, #30543, #30542, #30573 to stable/1.89.x and cut 1.89.3 - #30888
Conversation
…30480) * fix(integrations): cap Anthropic cache_control injection at 4 blocks Respect Anthropic's 4 cache_control breakpoint limit by counting client-supplied blocks, skipping messages that already carry cache_control, and stopping further auto-injection once the limit is reached. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(integrations): reserve cache slot for tool_config and short-circuit cap Address review feedback on the cache_control cap: break out of the injection loop before resolving target indices once the limit is reached, and reserve one of the four breakpoint slots when a tool_config injection point is present so the cachePoint appended by the Bedrock transform does not push the total past Anthropic's limit. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> (cherry picked from commit fc9d789)
…30543) * fix(guardrails): run pre_call hook once for model-level guardrails A CustomGuardrail attached to a deployment via litellm_params.guardrails gets its async_pre_call_hook invoked twice per request: once by the proxy pre-call loop and again by async_pre_call_deployment_hook after the router spreads the model-level guardrails into the top-level request kwargs. Record in request metadata that the proxy pre-call loop already ran a given guardrail, and have the deployment hook skip it when the marker is present. Direct-SDK usage never runs the proxy loop, so the deployment hook stays the sole invocation there and still fires exactly once. The marker key is stripped from untrusted caller metadata so a request body cannot suppress a model-only guardrail by pre-seeding it. * fix(guardrails): mark pre_call dedup on the post-hook request data Record the exactly-once marker after async_pre_call_hook runs, on the data object that flows downstream, rather than before it. A guardrail whose hook returns a brand-new request dict (instead of mutating or spreading the one it received) would otherwise discard the marker, letting the deployment hook re-run the guardrail a second time. (cherry picked from commit 4faeabc)
…0542) * fix(guardrails): stop re-initializing DB guardrails on every poll InMemoryGuardrailHandler._has_guardrail_params_changed compared the in-memory LitellmParams against the raw dict loaded from the DB. The in-memory side carries every field default and coerces enums via model_dump(), while the DB side only holds the keys originally stored, so the two shapes never compared equal and the guardrail was rebuilt on every poll cycle. Each rebuild created a fresh instance, but delete_in_memory_guardrail only removed the old callback from litellm.callbacks. Request handling promotes guardrail callbacks into the success/failure/async lists, so the previous instance stayed referenced there and instances accumulated. Normalize both sides through LitellmParams(...).model_dump() before diffing, and purge the callback from every callback list on delete. * refactor(guardrails): narrow params-normalization fallback to ValidationError The comparison normalizer caught a bare Exception and silently fell back to the raw dict, which hid the cause and quietly degraded the affected guardrail back to re-initializing on every poll. Catch only the ValidationError that LitellmParams construction can raise, log a warning so the offending row is diagnosable, and let any other error surface instead of being swallowed. * refactor(callbacks): add remove_callback_from_all_lists helper to manager Move the knowledge of which callback lists a callback can be promoted into out of the guardrail registry and into LoggingCallbackManager, where the rest of the callback-list bookkeeping already lives. delete_in_memory_guardrail now delegates to the new helper instead of iterating the lists itself. (cherry picked from commit 9fa74ad)
* fix(guardrails): return 400 not 500 when AIM blocks a request AIM guardrail blocks raised a bare HTTPException whose type and param serialized as the literal string "None", which broke OpenAI-SDK error parsing for downstream consumers. Switching AIM to raise a ProxyException surfaced a second bug: the shared error funnel re-derived the HTTP status from a nonexistent status_code attribute and downgraded the 400 to a 500. The funnel now honors an already-normalized ProxyException rather than rebuilding it, and ProxyException is excluded from llm_exceptions alerting so a content-policy block no longer pages on-call as an LLM API failure Resolves LIT-3751 * fix(guardrails): route all AIM rejection paths through ProxyException The block-action fix left two AIM rejection paths raising a bare HTTPException: the multimodal anonymize rejection and the output-side block. Both serialized type and param as the literal string "None", the same malformed shape the block fix removed. Funnel all three through a shared _rejection helper so they return a conformant OpenAI error body. The output block carries content_policy_violation; the multimodal rejection stays a plain invalid_request_error because it is a usage error, not a policy violation Resolves LIT-3751 * fix(guardrails): record AIM ProxyException blocks in failure logs Switching AIM blocks from HTTPException to ProxyException made _is_proxy_only_llm_api_error return False for them, so _handle_logging_proxy_only_error was skipped and the blocked prompt was dropped from the configured failure loggers. Classify ProxyException as a proxy-only error alongside HTTPException so guardrail blocks are recorded again, matching the prior behavior. The llm_exceptions alert suppression is a separate check and stays in place Resolves LIT-3751 * style(guardrails): use str | None over Optional[str] in AIM _rejection * style(guardrails): collapse AIM _rejection signature per black (cherry picked from commit b5fcd85)
|
|
Greptile SummaryThis backport cherry-picks four guardrail and integration reliability fixes onto
Confidence Score: 4/5The four fixes are well-scoped and well-tested; no DB schema, auth, or dependency changes are included, and the most delicate path (ProxyException short-circuit in the error funnel) is covered by direct unit tests and a live-proxy gauntlet. The changes are narrow bug fixes with strong regression coverage (246 passing, zero new failures). The reserved_blocks heuristic could be wrong if multiple tool_config entries are supplied in a single request, and the header-merge order in the ProxyException short-circuit could silently drop guardrail-supplied headers if they collide with funnel headers in a future scenario. Neither condition arises with current usage. The reserved_blocks slot calculation in anthropic_cache_control_hook.py and the header merge ordering in common_request_processing.py are the two spots worth a second look.
|
| Filename | Overview |
|---|---|
| litellm/integrations/anthropic_cache_control_hook.py | Refactors message-level cache_control injection to enforce Anthropic's 4-block limit, count client-supplied breakpoints, and skip messages that already carry cache_control; also fixes the previously-broken role-based injection (the old loop reassigned a local variable and never modified the list) |
| litellm/integrations/custom_guardrail.py | Adds per-process secret token and mark/check helpers so the proxy pre_call loop and the router deployment hook together invoke async_pre_call_hook exactly once per request; PRE_CALL_EXECUTED_GUARDRAILS_KEY imported from constants.py as required |
| litellm/proxy/common_request_processing.py | Inserts ProxyException short-circuit before the HTTPException branch so already-normalized guardrail rejections raise with their original 400 status instead of being re-derived to 500; headers are merged in |
| litellm/proxy/guardrails/guardrail_hooks/aim/aim.py | Replaces all three HTTPException raises with a conformant ProxyException via the new _rejection helper; removes FastAPI import from a non-proxy integration file, aligning with the project rule |
| litellm/proxy/guardrails/guardrail_registry.py | Fixes two bugs: normalizes both sides of the params comparison through LitellmParams so unchanged DB guardrails are not rebuilt on every poll; purges deleted guardrail callbacks from all five callback lists |
| litellm/proxy/utils.py | Calls mark_pre_call_hook_ran after the proxy pre_call loop executes a guardrail; extends the llm_exceptions and proxy-only logging classifier to also match ProxyException alongside HTTPException |
| litellm/proxy/policy_engine/pipeline_executor.py | Marks the pre_call hook on both the input data dict and the possibly-fresh response dict so the deployment-level dedup check works when hooks return a new dict rather than mutating in-place |
| litellm/litellm_core_utils/logging_callback_manager.py | Adds remove_callback_from_all_lists helper used by guardrail_registry to purge stale instances from every callback list on deletion |
| litellm/proxy/litellm_pre_call_utils.py | Adds PRE_CALL_EXECUTED_GUARDRAILS_KEY to _UNTRUSTED_METADATA_CONTROL_FIELDS so a caller cannot inject the marker through request metadata to bypass guardrail checks |
| litellm/proxy/common_utils/callback_utils.py | Adds PRE_CALL_EXECUTED_GUARDRAILS_KEY to LITELLM_PROXY_INTERNAL_METADATA_KEYS so the key is stripped from outbound requests |
| litellm/constants.py | Adds PRE_CALL_EXECUTED_GUARDRAILS_KEY sentinel to constants.py per the project's sentinel-in-constants rule |
| tests/test_litellm/proxy/test_common_request_processing.py | Adds test_already_normalized_proxy_exception_is_honored covering the ProxyException short-circuit; staging-only neighbor tests excluded as documented |
| tests/test_litellm/proxy/test_proxy_utils.py | Adds TestPostCallFailureHookLLMExceptionAlerting and TestPostCallFailureHookProxyExceptionLogging verifying ProxyException is excluded from alerting but still drives failure logging |
| tests/local_testing/test_aim_guardrails.py | Updates exception assertions from HTTPException to ProxyException; adds three new tests for output block, multimodal anonymize rejection, and AIM conformance; all HTTP calls are mocked |
| tests/test_litellm/proxy/guardrails/test_guardrail_registry.py | Adds comprehensive tests for LitellmParams normalization comparison, all-list deletion, and repeated-sync accumulation regression |
Reviews (1): Last reviewed commit: "chore: refresh uv.lock for 1.89.3" | Re-trigger Greptile
| messages=processed_messages, | ||
| max_blocks=MAX_CACHE_CONTROL_BLOCKS - reserved_blocks, | ||
| ) | ||
|
|
||
| # Pass through non-message injection points for provider-specific handling | ||
| if remaining_points: | ||
| non_default_params["cache_control_injection_points"] = remaining_points | ||
|
|
||
| return model, processed_messages, non_default_params | ||
|
|
There was a problem hiding this comment.
reserved_blocks assumes exactly one cachePoint per non-message injection point
reserved_blocks is set to 1 whenever any tool_config point exists in remaining_points, regardless of how many there are. In practice a single tool_config field can only carry one cachePoint so this is currently harmless, but if a caller passes multiple tool_config entries the over-count would prevent only one message-level block from injecting while the transform later appends N cachePoints, potentially pushing the total past Anthropic's limit. A comment here noting the one-slot-per-unique-non-message-location assumption would help future contributors understand why counting distinct locations matters.
| if isinstance(e, ProxyException): | ||
| e.headers = { | ||
| **e.headers, | ||
| **{k: v if isinstance(v, str) else str(v) for k, v in headers.items()}, | ||
| } | ||
| raise e |
There was a problem hiding this comment.
Funnel headers may overwrite ProxyException's own headers
The merge is {**e.headers, **funnel_headers}, so any key present in both dicts resolves to the funnel value. For a guardrail 400, e.headers is typically {}, so there is no practical conflict today. If a future guardrail sets a response header (e.g., a custom X-Blocked-By header) and the funnel accumulates a same-named key from a callback, the guardrail's value would be silently dropped. Reversing the merge order to {**funnel_headers, **e.headers} would let the ProxyException's own headers win.
| # Per-process secret tagging each recorded marker. The deployment hook only | ||
| # honors markers carrying this token, so a caller cannot forge the metadata | ||
| # field to suppress a guardrail on the direct-SDK path that never reaches the |
There was a problem hiding this comment.
_PRE_CALL_EXECUTED_TOKEN is a module-level secret, not in constants.py
The project rule (5f7d9a68) requires sentinel-like module-level variables to live in constants.py. _PRE_CALL_EXECUTED_TOKEN is a per-process secrets.token_hex value, not a static string, so it differs from the canonical sentinel example. However, keeping it unexported in custom_guardrail.py (rather than in constants.py) actually strengthens the security property: any code that imports from constants.py would gain access to the token, widening the attack surface. The companion key name PRE_CALL_EXECUTED_GUARDRAILS_KEY is correctly placed in constants.py. Consider adding an inline comment explaining why this value is intentionally kept module-private rather than in constants.py.
Rule Used: What: Require sentinel-like variables (e.g., `_NEG... (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
….3) (#171) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/berriai/litellm](https://images.chainguard.dev/directory/image/wolfi-base/overview) ([source](https://github.com/BerriAI/litellm)) | patch | `v1.89.2` → `v1.89.3` | --- ### Release Notes <details> <summary>BerriAI/litellm (ghcr.io/berriai/litellm)</summary> ### [`v1.89.3`](https://github.com/BerriAI/litellm/releases/tag/v1.89.3) [Compare Source](BerriAI/litellm@v1.89.3...v1.89.3) ##### Verify Docker Image Signature All LiteLLM Docker images are signed with [cosign](https://docs.sigstore.dev/cosign/overview/). Every release is signed with the same key introduced in [commit `0112e53`](BerriAI/litellm@0112e53). **Verify using the pinned commit hash (recommended):** A commit hash is cryptographically immutable, so this is the strongest way to ensure you are using the original signing key: ```bash cosign verify \ --key https://raw.githubusercontent.com/BerriAI/litellm/0112e53046018d726492c814b3644b7d376029d0/cosign.pub \ ghcr.io/berriai/litellm:v1.89.3 ``` **Verify using the release tag (convenience):** Tags are protected in this repository and resolve to the same key. This option is easier to read but relies on tag protection rules: ```bash cosign verify \ --key https://raw.githubusercontent.com/BerriAI/litellm/v1.89.3/cosign.pub \ ghcr.io/berriai/litellm:v1.89.3 ``` Expected output: ``` The following checks were performed on each of these signatures: - The cosign claims were validated - The signatures were verified against the specified public key ``` *** ##### What's Changed - chore(release): backport [#​30480](BerriAI/litellm#30480), [#​30543](BerriAI/litellm#30543), [#​30542](BerriAI/litellm#30542), [#​30573](BerriAI/litellm#30573) to stable/1.89.x and cut 1.89.3 by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​30888](BerriAI/litellm#30888) **Full Changelog**: <BerriAI/litellm@v1.89.2...v1.89.3> ### [`v1.89.3`](https://github.com/BerriAI/litellm/releases/tag/v1.89.3) [Compare Source](BerriAI/litellm@v1.89.2...v1.89.3) ##### Verify Docker Image Signature All LiteLLM Docker images are signed with [cosign](https://docs.sigstore.dev/cosign/overview/). Every release is signed with the same key introduced in [commit `0112e53`](BerriAI/litellm@0112e53). **Verify using the pinned commit hash (recommended):** A commit hash is cryptographically immutable, so this is the strongest way to ensure you are using the original signing key: ```bash cosign verify \ --key https://raw.githubusercontent.com/BerriAI/litellm/0112e53046018d726492c814b3644b7d376029d0/cosign.pub \ ghcr.io/berriai/litellm:v1.89.3 ``` **Verify using the release tag (convenience):** Tags are protected in this repository and resolve to the same key. This option is easier to read but relies on tag protection rules: ```bash cosign verify \ --key https://raw.githubusercontent.com/BerriAI/litellm/v1.89.3/cosign.pub \ ghcr.io/berriai/litellm:v1.89.3 ``` Expected output: ``` The following checks were performed on each of these signatures: - The cosign claims were validated - The signatures were verified against the specified public key ``` *** ##### What's Changed - chore(release): backport [#​30480](BerriAI/litellm#30480), [#​30543](BerriAI/litellm#30543), [#​30542](BerriAI/litellm#30542), [#​30573](BerriAI/litellm#30573) to stable/1.89.x and cut 1.89.3 by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​30888](BerriAI/litellm#30888) **Full Changelog**: <BerriAI/litellm@v1.89.2...v1.89.3> </details> --- ### Configuration 📅 **Schedule**: (in timezone Europe/London) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yMzIuMSIsInVwZGF0ZWRJblZlciI6IjQzLjIzMi4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZS9jb250YWluZXIiLCJ0eXBlL3BhdGNoIl19--> Reviewed-on: https://forgejo.hayden.moe/hayden/phoebe/pulls/171
….3) (#352) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/berriai/litellm](https://images.chainguard.dev/directory/image/wolfi-base/overview) ([source](https://github.com/BerriAI/litellm)) | patch | `v1.89.2` → `v1.89.3` | --- ### Release Notes <details> <summary>BerriAI/litellm (ghcr.io/berriai/litellm)</summary> ### [`v1.89.3`](https://github.com/BerriAI/litellm/releases/tag/v1.89.3) [Compare Source](BerriAI/litellm@v1.89.3...v1.89.3) ##### Verify Docker Image Signature All LiteLLM Docker images are signed with [cosign](https://docs.sigstore.dev/cosign/overview/). Every release is signed with the same key introduced in [commit `0112e53`](BerriAI/litellm@0112e53). **Verify using the pinned commit hash (recommended):** A commit hash is cryptographically immutable, so this is the strongest way to ensure you are using the original signing key: ```bash cosign verify \ --key https://raw.githubusercontent.com/BerriAI/litellm/0112e53046018d726492c814b3644b7d376029d0/cosign.pub \ ghcr.io/berriai/litellm:v1.89.3 ``` **Verify using the release tag (convenience):** Tags are protected in this repository and resolve to the same key. This option is easier to read but relies on tag protection rules: ```bash cosign verify \ --key https://raw.githubusercontent.com/BerriAI/litellm/v1.89.3/cosign.pub \ ghcr.io/berriai/litellm:v1.89.3 ``` Expected output: ``` The following checks were performed on each of these signatures: - The cosign claims were validated - The signatures were verified against the specified public key ``` *** ##### What's Changed - chore(release): backport [#​30480](BerriAI/litellm#30480), [#​30543](BerriAI/litellm#30543), [#​30542](BerriAI/litellm#30542), [#​30573](BerriAI/litellm#30573) to stable/1.89.x and cut 1.89.3 by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​30888](BerriAI/litellm#30888) **Full Changelog**: <BerriAI/litellm@v1.89.2...v1.89.3> ### [`v1.89.3`](https://github.com/BerriAI/litellm/releases/tag/v1.89.3) [Compare Source](BerriAI/litellm@v1.89.2...v1.89.3) ##### Verify Docker Image Signature All LiteLLM Docker images are signed with [cosign](https://docs.sigstore.dev/cosign/overview/). Every release is signed with the same key introduced in [commit `0112e53`](BerriAI/litellm@0112e53). **Verify using the pinned commit hash (recommended):** A commit hash is cryptographically immutable, so this is the strongest way to ensure you are using the original signing key: ```bash cosign verify \ --key https://raw.githubusercontent.com/BerriAI/litellm/0112e53046018d726492c814b3644b7d376029d0/cosign.pub \ ghcr.io/berriai/litellm:v1.89.3 ``` **Verify using the release tag (convenience):** Tags are protected in this repository and resolve to the same key. This option is easier to read but relies on tag protection rules: ```bash cosign verify \ --key https://raw.githubusercontent.com/BerriAI/litellm/v1.89.3/cosign.pub \ ghcr.io/berriai/litellm:v1.89.3 ``` Expected output: ``` The following checks were performed on each of these signatures: - The cosign claims were validated - The signatures were verified against the specified public key ``` *** ##### What's Changed - chore(release): backport [#​30480](BerriAI/litellm#30480), [#​30543](BerriAI/litellm#30543), [#​30542](BerriAI/litellm#30542), [#​30573](BerriAI/litellm#30573) to stable/1.89.x and cut 1.89.3 by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​30888](BerriAI/litellm#30888) **Full Changelog**: <BerriAI/litellm@v1.89.2...v1.89.3> </details> --- ### Configuration 📅 **Schedule**: (in timezone America/New_York) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yMjQuMCIsInVwZGF0ZWRJblZlciI6IjQzLjIzNC4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZS9jb250YWluZXIiLCJ0eXBlL3BhdGNoIl19--> Reviewed-on: https://git.greyrock.io/greyrock-labs/home-ops/pulls/352
…to v1.89.3 (#216) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [https://github.com/BerriAI/litellm.git](https://github.com/BerriAI/litellm) | patch | `v1.89.2` → `v1.89.3` | --- ### Release Notes <details> <summary>BerriAI/litellm (https://github.com/BerriAI/litellm.git)</summary> ### [`v1.89.3`](https://github.com/BerriAI/litellm/releases/tag/v1.89.3) [Compare Source](BerriAI/litellm@v1.89.2...v1.89.3) #### Verify Docker Image Signature All LiteLLM Docker images are signed with [cosign](https://docs.sigstore.dev/cosign/overview/). Every release is signed with the same key introduced in [commit `0112e53`](BerriAI/litellm@0112e53). **Verify using the pinned commit hash (recommended):** A commit hash is cryptographically immutable, so this is the strongest way to ensure you are using the original signing key: ```bash cosign verify \ --key https://raw.githubusercontent.com/BerriAI/litellm/0112e53046018d726492c814b3644b7d376029d0/cosign.pub \ ghcr.io/berriai/litellm:v1.89.3 ``` **Verify using the release tag (convenience):** Tags are protected in this repository and resolve to the same key. This option is easier to read but relies on tag protection rules: ```bash cosign verify \ --key https://raw.githubusercontent.com/BerriAI/litellm/v1.89.3/cosign.pub \ ghcr.io/berriai/litellm:v1.89.3 ``` Expected output: ``` The following checks were performed on each of these signatures: - The cosign claims were validated - The signatures were verified against the specified public key ``` *** #### What's Changed - chore(release): backport [#​30480](BerriAI/litellm#30480), [#​30543](BerriAI/litellm#30543), [#​30542](BerriAI/litellm#30542), [#​30573](BerriAI/litellm#30573) to stable/1.89.x and cut 1.89.3 by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​30888](BerriAI/litellm#30888) **Full Changelog**: <BerriAI/litellm@v1.89.2...v1.89.3> </details> --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yMjAuMCIsInVwZGF0ZWRJblZlciI6IjQzLjIyMC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=--> Co-authored-by: Renovate Bot <renovate@bhamm-lab.com> Reviewed-on: https://codeberg.org/blake-hamm/bhamm-lab/pulls/216
Relevant issues
Backports four already-merged guardrail and integration fixes from
litellm_internal_stagingontostable/1.89.xand cuts 1.89.3. All four are guardrail/integration reliability fixes; none touch DB schema, dependencies, or auth defaults. Datadog 413-split (#29444) was requested too but is already on this line (shipped in v1.89.0), so it is not re-applied.What is included
In cherry-pick (original staging merge) order:
Then the version bump to 1.89.3 and the uv.lock refresh.
Adaptation notes
#30573 is the only adapted pick; the other three applied patch-identical to their staging source (verified by patch-id). The pick's own added lines are byte-identical to staging. The divergences are dropped staging-only neighbors this line does not carry:
self._apply_router_cooldown_retry_after(headers, e)context line. That helper, and the router-cooldown retry-after feature, landed on staging after 1.89.x was cut and are absent here; the line was context in the diff, not part of the fix. The identical ProxyException short-circuit is inserted immediately before theif isinstance(e, HTTPException)branch, reusing theheadersdict the funnel already builds on this line. The staging-only helper call is omittedpatch, which on staging is imported on the same line that the dropped SMTP change added, sopatchwas restored tofrom unittest.mock import MagicMock, patchto match staging. This is a stdlib import the picked tests already use; no production code or test assertion changedKnown noise on this line
Targeted tests run as a delta against a pre-pick baseline. One failure pre-exists on stable/1.89.x and is unrelated to this PR:
0.0.0.0but getslocalhost; host-normalization, present before any pickEverything else is green. Baseline: 219 passed, 1 failed. With the picks: 246 passed, 1 failed (same known-noise test). Zero new failures, +27 passing from the picks' own regression tests.
Screenshots / Proof of Fix
Live proxy on the picked code, real Anthropic API (claude-haiku-4-5), real spend.
Sanity floor, unchanged from the pre-pick baseline:
#30480 cache_control cap, live:
Call A stays on claude (<=4 blocks reached Anthropic) while Call B falls back (>4 reached Anthropic), so the only thing that changed Call A's outcome is the cap. Together they show the fix caps auto-injection and does not strip client-supplied breakpoints.
#30573, #30542, #30543 are guardrail-internal fixes whose live reproducers need an AIM API key and a Postgres-backed guardrail harness that this environment does not have. They are verified by their own regression tests passing on this line (in the 246-passed delta above) and by the gauntlet below.
Gauntlet (behavioral, deep, universal): SURVIVED, zero verified regression findings. It independently re-ran the decisive checks on the picked tree and live-validated each fix: the #30573 funnel short-circuit returns HTTP 400 for a ProxyException with a generic-exception 500 negative control, the dropped staging-only helper is referenced nowhere, #30543 resists forged and wrong-token suppression markers while honoring the genuine per-process token, #30480 caps at 4 while preserving client blocks, and #30542 purges across all five callback lists. Targeted-test delta: 0 new failures vs baseline.