Reserve the estimated request cost against the tags a pre-call hook added,
folded into the request's reservation, so a burst of requests cannot all
pass the read check on the same stale spend the way body tags already
cannot. Skip the post-hook check where the auth wrapper runs no
common_checks (no-auth dev mode, custom auth without
custom_auth_run_common_checks), sharing that predicate with auth.
Tag max budgets were only checked in auth against the tags in the request
body. A custom guardrail that sets metadata.tags in its pre-call hook runs
after auth, so the tag it added was charged in spend logs but never blocked.
Snapshot the request tags before pre_call_hook (once, so fallback retries
keep the original baseline), then budget-check the tags the hook added
after the proxy_server_request snapshot is refreshed, reading both
litellm_metadata and metadata since spend attribution reads both. The check
runs only on the routes auth budget-checks and skips zero-cost models like
auth does. The raised BudgetExceededError carries the resolved llm_provider
so the failure record matches the auth-path one.
The session usage collapsed duplicate query strings across turns while the
price was per turn, so two turns asking the same question paid two fees yet
reported web_search_requests 1. Sum each turn's grounding requests so the
counter matches the bill; duplicates within one turn still collapse.
A guardrail that rewrites text and hands back tool_use arguments that are
not a JSON object used to leave the text rewrite applied when the request
was rejected, so failure logging saw a half-rewritten request. Every
rejection now happens before any write to system or messages.
The router clamps a negative request_retry_count found in request metadata before counting a failure, and the proxy strips a client-supplied request_retry_count with the other router-reserved metadata fields. The rust OCR lifecycle test that trips the per-request cap now plants request_retry_count instead of attempted_retries, which the cap no longer reads since the previous commit
num_retries_per_request has always capped the retries of one request with its fallback hops included. #40930 started reading the per-hop attempted_retries counter instead, and every fallback hop restarts that counter at zero, so a request could spend a fresh retry budget on each hop and the legacy fallback cap test started seeing the hop run.
Router.log_retry now also keeps request_retry_count on the request metadata, incremented on every retry and fallback hop and never truncated the way previous_models is, and max_retries_per_request_hit reads that count. The flat retry records, the litellm_metadata coverage and caps above four from #40930 stay as they are, and the legacy test goes back to its previous_models == 0 assertion.
httpx.Headers.items() comma-joins repeated header names, so the rebuilt
response iterates multi_items() and keeps every value, matching what the
raw openai client exposes on e.response.headers
The streaming bridge restored the namespace before deciding whether a tool call was a custom tool, so a namespaced function sharing a short name with a nested custom tool streamed back as a custom_tool_call. Classify on the raw chat tool name first, the way the non-streaming path already does.
The guardrail merge only stripped the namespace prefix and grammar suffix from the ends of the edited description, so a guardrail appending text after the grammar block left the block in the member description and the chat conversion appended it a second time. Strip the first occurrence of each instead.
The proxy edge test file no longer carries any test of this change, and
the remaining diff was the scoped format gate reflowing the whole file to
the 120 limit, so it goes back to the merge base bytes
#40930 (LIT-7505) changed num_retries_per_request from a request-wide
cap to a per-model-group cap that resets on every fallback hop, and its
own comment in litellm/__init__.py names that contract. The legacy
test_async_fallbacks_max_retries_per_request still asserted the old
request-wide reading (previous_models == 0), so the CircleCI router
suite has been red on main since that merge for every run-ci PR.
The test now reads the flat RetryAttemptRecord entries the fallback
call carries and asserts the new contract directly: every record is
from the first group, the retry at attempted_retries 0 is the real
AuthenticationError, and each later attempt was refused with
"Max retries per request hit!".
The hoisted structured row keeps every text block of the top-level system
prompt, empty ones included, while the scanned texts dropped the empty ones.
Guardrails that count one text per slot then came back with more texts than
the handler could place, so their rewrite was rejected. User text blocks were
already scanned empty or not; the system prompt now matches.
A mapped litellm_proxy exception now attaches an httpx.Response that
carries the proxy's response headers whenever the handler attached a
header-less synthetic one, on every status branch and on the relay
path. BadRequestError keeps its base-class contract: .headers stays the
proxy-supplied channel, so the proxy edge keeps forwarding an upstream
proxy's headers under the llm_provider- prefix and the date and server
edge change is no longer needed.
Only image-only unscoped content stays unrecorded; text or tool content
removed by scoping is recorded as not_run even when an image sits beside it.
Also keeps the type-discipline budget flat by returning the reason from the
helper and annotating the accumulator lists _extract_inputs requires.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
main's test_one_text_per_row_over_a_system_prompt_is_rejected_by_name assumed the
top-level system prompt stays out of the scanned texts. This branch scans it, so one
text per structured row now lines up and the rewrite is applied; the test asserts that,
and a multi-block system prompt keeps the length-guard rejection covered.
The one-hour cache write fallback now takes the applied cache write rate, so an off-peak write price carries into it instead of the input rate
The cost estimate test for a cost-map model without cache prices now expects writes at the input rate, which is what the proxy bills
The recording logger in the deferred guardrail test types its callback parameters
The chat and A2A AgentCore handlers send X-Amzn-Bedrock-AgentCore-Runtime-User-Id
when runtimeUserId is set, and AWS requires bedrock-agentcore:InvokeAgentRuntimeForUser
alongside InvokeAgentRuntime on that call, so the ceiling now carries both. The role
identity policy still decides whether a given role may use it
The invalid-token test now uses a neutral example audience
A chat row whose content carried an empty text part counted two slots in the
chat completions handler while Prompt Security read one text out of the
modified row, so the structured rewrite was dropped and the request got the
named rejection. One shared helper now lists a row's slot texts and both the
slot count and the modified-row reader use it.