chore: sync upstream 2026-08-22 - #209
Merged
shudonglin merged 404 commits intoAug 22, 2026
Merged
Conversation
…erriAI#37662) * feat(ui): let admins supply a dark-mode variant of their custom logo A deployment branded through UI_LOGO_PATH got its light artwork on the dark sidebar, and there was nothing an admin could set to change that. Adds UI_LOGO_PATH_DARK, exposed as the logo_url_dark theme setting and a second field on the UI theme page. /get_image now walks an ordered list of candidates for the requested theme and serves the first usable one: the dark logo, then the light logo, then the bundled default. Falling through rather than failing is the point. An admin who never sets a dark logo keeps their own light one instead of reverting to LiteLLM's, and a dark logo that goes missing later degrades to their light logo rather than dropping their branding entirely. * fix(ui): recover from a dark logo the browser cannot load A dark logo given as an http(s) URL is loaded by the browser straight from the sidebar, so it never passes through the proxy's fallback chain. A URL that 404s left a broken image where the admin's light logo should have been, while the same logo given as a local path fell back cleanly. The sidebar now remembers the dark URL that failed and drops to the light logo, matching how the proxy resolves an unusable dark logo and how the provider Logo component already handles a broken image.
…iAI#37519) * fix(proxy): run pre-call guardrails on batch input file uploads POST /v1/files with purpose=batch was the only route in files_endpoints that never reached pre_call_hook, so guardrails did not see batch content at all and records reached the provider unscanned. Stream the uploaded JSONL a record at a time and run each record's body through the existing pre_call_hook dispatch under the call type its url maps to, so guardrail resolution, key and team config, and the per-endpoint translations are reused rather than reimplemented. The hook gains a guardrails_only mode for this, since the same callback loop also drives rate limiters, budget hooks, prompt templates and hanging-request alerting, none of which should fire once per record. A guardrail that blocks raises its own exception, which propagates untouched so its status code survives. A record a guardrail would rewrite, a record that cannot be parsed, and a record whose url cannot be scanned all reject the upload, since silently skipping any of them is the bypass this is meant to close. Per-record redaction lands separately. The scan only runs when a guardrail that actually runs pre_call, or a guardrail pipeline, is configured, so deployments without one are byte for byte unchanged. * fix(proxy): compare the dict a batch guardrail returns, not the one it was given async_pre_call_hook may return a replacement dict instead of mutating its input, and process_pre_call_hook_response then makes that replacement the request. The scan only inspected the dict it passed in, so a guardrail that redacts by returning a copy was treated as a no-op and its record uploaded unchanged. * fix(proxy): treat a missing batch body key as different from a null one Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * docs(proxy): document the guardrails_only mode on pre_call_hook * fix(proxy): resolve a batch record's scan type from its body when the url is unfamiliar The scanner only accepted five exact urls, but callers write that field by hand and the provider transformers are far more permissive: bedrock treats any non-empty url as chat and vertex strips query strings and trailing slashes. Uploads that work today would have started failing the moment a pre-call guardrail was configured. Normalize the url before lookup and fall back to the body shape when it is unfamiliar, so a record we can still read is a record we still scan. Only a body with no messages, prompt or input is now refused, and the error says so instead of listing urls that were never the whole set. Also pins the default side of the guardrails_only gate: the hanging-request alert and prompt templating are asserted to still fire when the flag is absent. * refactor(proxy): drop batch guardrail checks the upload validation already makes check_batch_file_upload now runs first and rejects a line that does not parse, a line that is not an object, and a line missing custom_id, method, url or body, so the guardrail scan can rely on all four. Its own parse handling was unreachable through the endpoint and is gone, along with the tests for it. What is left is the case that validation does not cover, a body whose value is not an object, since it only checks that the key is present. * fix(proxy): resolve a batch record's call type from the url path, not the whole url A record naming its route in full, which is how callers actually write batch files, matched no known route, so it fell through to the body shape. A Responses record carries `input`, and that reads as an embedding, so the record was scanned as the wrong call type and any guardrail scoped to chat or Responses skipped it while the upload was accepted. Chat records survived only because their body shape happens to map back to the same call type. The url is now reduced to its path before matching. Guardrails that pick their policy from a request header, such as noma choosing an application id, saw no headers at all during the scan and fell back to a default, so a batch record could be evaluated under a different policy than the same content sent online. The sanitized headers the proxy already stores in request metadata now travel with the scan. Also drops the bare `dict` annotation, the unreachable non-dict branch on the guardrail chain's own return, and the type alias that was missing its `TypeAlias`, which together were failing the lint gate. * fix(proxy): give each batch record its own copy of the scan metadata The narrowed metadata was handed to every record as a shallow copy, so `headers` and `tags` stayed shared with the upload request and with the other records in the same window. A guardrail that writes into one of those in place, which several do to record their own bookkeeping, would have its write show up in every record scanned after it and in the request itself. The narrowing already removed the values that cannot be copied, so each record now gets a deep copy. --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…I#37669) * feat(ui): add a light/dark/system theme toggle The dashboard already carried a full `.dark` palette, dark-aware surfaces and a dark logo variant, but nothing ever put the `dark` class on the document, so none of it could be reached. next-themes now owns that class: it reads the stored choice, falls back to the OS preference, and stamps the class from an inline script before first paint so there is no light flash on load. The toggle is a three-way System / Light / Dark control in the account menu, in both the sidebar menu and the older navbar dropdown, so it is reachable from the gateway dashboard, chat and the model hub alike. useIsDarkMode watched the root element with a MutationObserver purely to answer a question next-themes now answers directly, so it goes, and useSyntaxTheme reads resolvedTheme instead. The toaster follows the resolved theme too. * feat(ui): move the theme control to the top bar and default to light The toggle now lives in the header toolbar of both shells, the gateway dashboard's DashboardHeader and the older full-width Navbar, where it replaces the placeholder comment that had been holding its spot. It reads better there as a single icon button with a System / Light / Dark menu than as a segmented row buried in the account popover, so the account menus lose their theme row. Dark mode is still being rolled out, so an install that has never touched the control now stays light instead of following the OS. System is still a choice, just no longer the default. While dark is active the toolbar carries a small Experimental badge, so nobody mistakes an unstyled surface for a bug. * fix(ui): serve the dark logo in the legacy navbar too The sidebar already paired its logo with a dark variant, but the full-width navbar kept a single light-only image. That did not matter while dark mode was unreachable; now that the toggle sits in that shell's own top bar, the white JPEG slab lands on a dark bar. It gets the same two-image swap the sidebar uses, and a test that pins the pairing so the two shells cannot drift apart again.
…-key identity The canonical request already folds params and form into the digest, but nothing asserted it, so dropping either from canonicalize() left all 92 fixture tests green. Two GETs differing only in query string, or two uploads differing only in a form field, would share a replay pool and FIFO-pop each other's recorded response. Resolves LIT-5890
Three fixes on the Postgres token-auth path found by a live risk pass: Pre-encoded connection components no longer double-escape. The user, database name, and schema used to be interpolated raw, so encoding an already-encoded DATABASE_USER like svc%40corp turned it into svc%2540corp and Postgres rejected the login with P1010. Decoding before encoding is idempotent, so a pre-encoded value comes out byte for byte as it went in while a raw UPN still gets encoded. An unreadable IAM_TOKEN_DB_AUTH or AZURE_POSTGRESQL_AUTH now fails startup naming the variable and the value. Reading a typo like "enabled" as off would silently downgrade an operator from token auth to password auth, and the first sign of it would be the server refusing the connection. The proactive refresh loop floors its sleep at 30 seconds. azure-identity hands back its cached token when a renewal fails inside its own window, so a token whose expiry never advances used to compute a zero sleep and spin the loop, re-minting and recreating the Prisma query engine every pass. Co-authored-by: David Balatoni <balcsida@gmail.com>
…s calls completion() strips the litellm routing prefix before it dispatches to the responses bridge, but responses() runs get_llm_provider() again, so a model id that itself starts with the provider name lost a second prefix and reached the provider as a name it does not know. Handing responses() the prefixed model back makes its own resolve a no-op: across the 3061 cost map entries, 76 reach the responses bridge and only the four perplexity Agent API models change.
…ting the file (BerriAI#37561) * feat(proxy): redact or drop individual batch records instead of rejecting the file A single record tripping a guardrail rejected the whole upload, which is unusable for a file holding thousands of rows. A record a guardrail rewrites is now submitted in its rewritten form, a record it blocks is left out, and the create response reports every changed record by both custom_id and line so a caller can reconcile against the file it sent. The same outcome is written to the proxy log and to request metadata, so it is not visible only to the caller. A rewritten record goes straight to a spool and only its offset is carried, so a masking guardrail touching most rows of a large upload does not build a second copy of the file on the heap, and the rewrite runs off the event loop the way the sibling full-file validation does. Both proxy-injected metadata keys are captured from the record and restored exactly, including an explicit null, so a masked row keeps the tags that decide how it is attributed. A record is dropped only when a guardrail judged its content. `GuardrailRaisedException` now carries `blocked_content` for that, because half its raise sites in the repo signal an unreachable or unparseable backend under a fail-closed policy, and treating those as blocks would turn "refuse this request" into "drop this record and submit the rest". The default is off, so a raise that does not say what it means aborts the upload instead of silently shrinking the file. * fix(proxy): only drop a batch record on a verdict the guardrail actually reached A guardrail that reports a technical failure as an HTTPException carrying a block status was read as a content block, so an unreachable backend under a fail-closed policy quietly shrank the file instead of failing the upload. Two in-tree integrations do exactly that, and one of them defaults to fail-closed, so the broken configuration was the default one. Such an exception is raised `from` the underlying error, which is a deliberate statement that something else caused it, and no content verdict in the repo is raised that way, so the chain now settles it. Implicit context is left alone, since a block raised inside an unrelated `except` would read as a failure. Two annotation errors in the same family: the one GuardrailRaisedException subclass in tree never opted into blocked_content, so a real block took the whole upload down with it, and straiker's block helper is reached both from its verdict and from its fail-closed handler, so it claimed a verdict for an outage. The helper now takes the flag from its caller. A record could also opt itself out of the chain. Guardrail selection reads a body-level `guardrails` key ahead of the proxy-injected list, and online that key can only add to the key and team selection, never replace it, so a batch record naming an empty list skipped every guardrail that was not default_on and was still reported as scanned. Every injected key is now stripped before dispatch and restored afterwards. A guardrail that reroutes a record to another model is honoured on the online path by rewriting the model, which the scan read as a rewrite and submitted in the same file, sending content to the provider the reroute existed to avoid. Every record of a batch file goes to one provider, so the upload is refused instead, naming the line. The scan spool is closed on the paths that never read it back. * fix(proxy): give the scan the metadata bag guardrails actually read, and close its spools The narrowed request metadata was installed under `litellm_metadata` only, but a record is scanned as the chat request it describes, and the guardrails that pick a policy from a request header read `metadata` instead. Noma choosing an application and Aim choosing a user both look there, so the header allowlist added for them did not reach either one and a batch record was still evaluated under the fallback policy. The scan metadata now goes into both bags, which are both stripped and restored, so neither survives into the record that ships. The scan spool was closed on the paths that abort, which are exactly the paths where it is empty, and left open on the one path where it holds the rewritten records. Nothing closed the rewrite output either, where before this feature the uploaded handle belonged to Starlette. The upload now owns both and closes them however it exits. * fix(proxy): register the scan spool before the rewrite can fail The scan spool was added to the request's cleanup list only after the rewrite returned, so a rewrite that raised, which for a spilled file can be as ordinary as the disk filling up, jumped to the handler with the list still empty and left the scan's own handle open. The rewrite also left its half-written output behind on that path, since nothing owns that handle until it is returned. Both now close.
…erriAI#37680) The Experimental badge sat in the top bar next to the icon, which read as if the whole theme control were experimental and cost toolbar width for a caveat that only applies once. It moves into the menu as a Beta tag on the Dark entry, where it labels exactly the choice it is about and is visible before the choice is made rather than only after.
) ruff.toml excludes tests/* from `ruff check`, so nothing has ever checked the test tree for names that do not exist. That matters more in tests than in product code: a NameError inside a test whose body is wrapped in `except Exception: pass` is swallowed, and the test reports green forever. Adds ruff-tests.toml selecting F821 alone, wired into the lint workflow and `make lint-ruff`, and clears every existing violation: - 4 tests interpolated an unbound `e` into a `pytest.fail` message reached only on the failure path, so the NameError, not the assertion, is what ran. test_llm_guard_error_raising is the worst: it passes today with content safety disabled entirely. It now asserts the 400 and its detail body. - 5 sites construct BaseExceptionGroup, a 3.11 builtin, in a tree that still supports 3.10. Guarded behind the exceptiongroup backport that anyio already pulls in below 3.11. - 9 missing imports (json, openai, Any, Final, HTTPException), including one in a helper that catches HTTPException by a name it never imported, so the challenge path it exists to detect raises NameError instead. - 5 annotations naming types imported inside the function body, hoisted to module scope or TYPE_CHECKING. - 2 blocks of dead code: everything after a pytest.fail in test_claude_agent_sdk, and an unused helper in test_end_users calling a function defined in a different module. - 1 error-path f-string in the router-settings doc test that masked the real FileNotFoundError behind a NameError. Only F821 for now. Widening the select list means ratcheting thousands of pre-existing findings, so rules go in one at a time with their violations already fixed.
…itellm_block_unpriced_models # Conflicts: # litellm/proxy/auth/auth_checks.py # tests/test_litellm/proxy/auth/test_auth_checks.py # tests/test_litellm/proxy/management_endpoints/test_cost_tracking_settings.py # ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/cost_tracking_settings.tsx # ui/litellm-dashboard/src/lib/http/schema.d.ts
AKS workload identity injects AZURE_CLIENT_ID, AZURE_TENANT_ID, and AZURE_FEDERATED_TOKEN_FILE into the pod, and never a client secret. Reading that bare client id as a managed identity sent the pod to IMDS, which has no identity attached to it, so the token request failed and the federated token was never exchanged. AZURE_FEDERATED_TOKEN_FILE now wins over the bare client id and infers DefaultAzureCredential, whose chain reaches WorkloadIdentityCredential before ManagedIdentityCredential. DefaultAzureCredential passes AZURE_CLIENT_ID to both legs, so a plain user-assigned managed identity still reaches the same identity it does today. This is the credential path Azure recommends for passwordless Postgres on AKS, and it also fixes the Azure OpenAI token provider, which infers its credential the same way.
…er_edge_replay feat(e2e): move record/replay to the provider edge (LIT-5745)
…itellm_azure_postgres_entra_auth
…th instead of 500
A deployment that overrides any cost_per field, including at zero, now counts as priced so it is not blocked as unpriced Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…evin.ai/proxy/github.com/BerriAI/litellm into litellm_block_unpriced_models
…authorization (BerriAI#37672) store_user_oauth_credential refused to overwrite any existing row that did not decode as an OAuth2 payload, which conflated two states: a live BYOK secret that reads back as plaintext, and ciphertext written under a LITELLM_SALT_KEY the proxy no longer holds. The second is unrecoverable by any caller, so refusing preserved nothing and instead wedged the user out of the OAuth flow permanently, since re-authorizing is their only recovery. The guard now raises only when the existing value is genuinely readable. An undecryptable row is logged and replaced by the newly authorized token. Both read paths were equally silent: get_user_oauth_credential and list_user_oauth_credentials (which backs the bulk prefetch) each dropped an undecryptable row indistinguishably from "user never authorized", so an operator saw an upstream 401 and no hint that a credential had failed to decrypt. Both now warn with the user and server ids, never the stored value.
…ix (BerriAI#37668) MCP egress prefixed the configured scheme unconditionally, but callers legitimately supply both a bare token (from a stored credential) and an already-schemed value (passed through from the caller's x-mcp-auth or Authorization header). The second shape produced Authorization: Bearer Bearer <jwt>, which upstream servers reject as a malformed token. It presented intermittently because a resolved stored credential arrives via extra_headers and overwrites the doubled header, so only users without one always failed. strip_auth_scheme drops one leading scheme before the header is rebuilt. It matches the scheme case-insensitively per RFC 7235 and requires a credential behind it, so both a token that merely begins with the scheme text and a scheme with nothing behind it are left intact. MCPAuth.authorization stays verbatim because that auth type means the caller owns the whole header value. For MCPAuth.basic the normalization has to happen in update_auth_value rather than at header-build time: to_basic_auth has already encoded the whole "Basic <credentials>" string by then, so no prefix is left to find. A schemed value whose remainder decodes is already encoded and is reused; one that does not decode is the bare pair with the scheme written in front of it, and is encoded rather than forwarded as an invalid header. The same doubling reached OpenAPI-backed servers through _format_byok_openapi_auth_header. A non-BYOK server short-circuits _resolve_byok_mcp_auth_header, so that formatter also receives the deprecated global x-mcp-auth, which is already a complete header value.
…37709) `assert False` inside a `try:` raises AssertionError, which the `except Exception` right below it catches, so several tests reported green no matter what the code did. `pytest.fail` raises Failed, a BaseException, and escapes. A bare `a == b` statement is evaluated and discarded. Nine of those sat in tests, and one was comparing against a model name the router never produces. Selects B011, B015, B018, PT015, PLR0133 and PLW0127 in ruff-tests.toml alongside F821, with all 50 existing violations fixed, so no budget file or ratchet is needed. CI already runs this config over tests/.
…itellm_model_registry_consolidated_20260820_wt
…erriAI#37589) Coverage is the single biggest time lever on the unit shards: the legacy no-coverage workflow ran the same directory in about 5 minutes against 11 to 13 with coverage on. coverage.py's sys.monitoring backend (PEP 669) is the cheapest core it ships, and it is not in use here today. It has to be asked for explicitly. coverage 7.14 only defaults to sysmon from Python 3.14 (`SYSMON_DEFAULT = CPYTHON and PYVERSION >= (3, 14)`) and these shards pin 3.12, so without `COVERAGE_CORE` they get the slow tracer. The audit left open whether sysmon survives turning on branch coverage. It does not, at this Python. coverage gates branch measurement under sysmon on `branch_right_left`, which needs newer than 3.14.0a5; on 3.12 it refuses and falls back to the default core with a `no-sysmon` warning. Verified directly against Python 3.12.13 with coverage 7.14.0: $ COVERAGE_CORE=sysmon python -m coverage run --branch --source=. run.py CoverageWarning: Can't use core=sysmon: sys.monitoring can't measure branches in this version, using default core (no-sysmon) So this speedup and `branch = true` are mutually exclusive until the runners move to 3.14. Nothing here turns branch coverage on, so the two never collide in this change, but whoever does turn it on is choosing to give this back.
…riAI#37600) * test: merge three stranded twins into the files that shadow them The second mirror's last four files each share a filename with a live test, so the previous commit could not move them. Three of the four turn out to be plain additions: their classes collide with nothing in the live file, so the tests are extra coverage that has sat unrun rather than a competing version of anything. Appending them takes the three files from 156 collected tests to 196, and all 196 pass. The 40 recovered are 13 OCI cases covering key normalization, credential validation, complete-URL building and image-url transformation, 15 management-endpoint cases covering empty-value handling and the premium check, and 12 DeepSeek thinking-parameter cases. One assertion had to change. test_map_reasoning_effort_none_does_not_enable_thinking asserted that reasoning_effort='none' leaves no thinking key, while the handler maps it to {'type': 'disabled'} on purpose, documented in map_openai_params as the OpenAI-style way to ask for thinking off. The test's stated intent holds, since disabled does not enable anything, so it now asserts the disabled mapping instead of the key's absence. Two imports moved to module scope for the appended code, and no live test was touched. test_discoverable_endpoints.py is the one left. Its twin grew from 1268 lines to 9434, 25 of its assertions fail against today's code, and only 5 of its 19 tests have no counterpart, so deciding what survives that rewrite is a judgement about the endpoints rather than a merge. The allowlist now holds exactly that file and that reasoning. * test(oci): stop the OCI suite reading credentials from the environment validate_environment falls back to os.environ for every OCI credential and only defaults the region when OCI_REGION is unset, so on a machine with OCI configured the missing-credential test finds credentials it never passed and the default-region test builds a URL for the ambient region. The suite then passes or fails depending on who runs it. A fixture drops the seven OCI variables for the four classes this branch added and for TestOCIChatConfig, which had the same dependency before any of this and fails the same way: with OCI_USER and friends exported, two of its cases fail on origin/litellm_internal_staging today. clean env: 83 passed ambient OCI env: 83 passed Same numbers either way, where the pre-existing file gave 68 passed / 2 failed under the second.
…le (BerriAI#37608) * test(ci): reject coverage-allowlist entries that no longer match a file * fix(ci): match a dockerfile allowlist entry the way the census exempts one
…med (BerriAI#37616) * feat(ci): assert .github/workflows holds only workflows, correctly named * style(tests): annotate the hygiene test module's names with Final * fix(ci): report a .yaml workflow as a naming finding, not a stray GitHub reads .yml and .yaml alike, so WF001 telling you to move a valid .yaml workflow to .github/scripts/ was wrong advice. WF001 now covers only files that are not workflows at all, and the .yml spelling this directory keeps moves to WF004, which says to rename rather than relocate. WF001 also never looked into subdirectories, since GitHub does not read them either; the message now says so. The directory is injected rather than read off a module constant, so the cases are testable without monkeypatching.
…rriAI/litellm into litellm_block_unpriced_models # Conflicts: # litellm/proxy/auth/auth_checks.py # tests/test_litellm/proxy/auth/test_auth_checks.py
…hrink (BerriAI#37621) * feat(ci): freeze the conftest save/restore inventory so it can only shrink * fix(ci): resolve the named constant a conftest save loop iterates * fix(ci): match the snapshot shape instead of a list of blessed dict names * feat(ci): fail a branch that clears TQ violations without lowering the ceiling A limit that only ever falls is not the same as one that falls when it can. Clearing violations and leaving the ceiling above the new count let the same violations return later under a limit nobody moved, so the gate now fails on that and names `make lint-budget-update` as the fix. It needs both head below base and head below limit, so headroom already in the base is never blamed on the branch that happens to run next. Drops the seeded-rule exemption from the ratchet along with it. Its stated reason was that the base tree predates a rule introduced on this branch, but base counts are measured with the current checker, so such a rule is counted at the base too and its grandfathered total was never at risk of reading as fixed. Removing the exemption is what lets a newly seeded rule ratchet like the six that came before it. The base scan is skipped when the branch touches neither the test tree nor the checker, since neither count can have moved.
…I#37704) JSON-RPC 2.0 types `id` as string, integer or null, but LiteLLMSendMessageResponse annotated it as a bare required `str`. Pydantic v2 dropped v1's int-to-str coercion, so an upstream agent echoing an integer id was rejected outright, and a null id, which section 5 requires for an error that cannot be correlated to a request, was rejected too. Both surfaced as -32603 with a pydantic ValidationError in the message: five distinct 500s on /a2a/{agent_id}, across message/send and tasks/get. Everything around the model already handled the full union: the endpoint reads the id off the body as Any, its helpers are typed `str | int | None`, the error builder takes `object`, and the streaming path passes the id through untouched. The response model was the only narrowing left. Backfilling an id the agent omitted keeps the caller's type too, since JSON-RPC requires the response id to equal the request id and a caller that sent 7 cannot correlate a response carrying "7". `bool` is excluded from the integer half even though it subclasses `int`, so a boolean id is stringified rather than relayed as 1 or 0, where it would collide with a real integer id another in-flight request may be using.
…erriAI#37811) Eight validators in that module decide what a request body may say, and none of them was asserted anywhere. Reversing any one of the eight left the file green. Cover them at the API boundary: a JWT issuer must pick audience validation or opt out, a temp budget needs both halves, an empty max budget reads as no limit, an organization member can only take a role the organization has, an LLM-backed injection check needs the call it would make, and four server-only markers are never taken from the caller. The injection case builds each incomplete body as its own value rather than deleting a key out of the one it is iterating.
…7812) Rebuilding a streamed response and pricing it is the path a spend row comes from, and nothing asserted it end to end. Reversing either half of the usage the provider reported left the file green. Three cases: the rebuilt response bills the usage the last chunk carried, streaming and not streaming bill the same usage the same, and a stream that reported no usage is still billed rather than dropped. The cost is asserted against the catalog prices the run itself reads, with a non-zero guard in front of it so an all-zeros lookup cannot satisfy it vacuously. Pinning the dollar figure as a literal would have made a routine gpt-4o price update fail a test about usage reconstruction.
…rriAI#37813) Six helpers in `litellm/proxy/utils.py` decide the usage a failed request records, and none of them is named anywhere in the suite. Two of their decisions could be reversed with the file still green: a request with nothing countable in it lifted as a zero-token usage, and a request that never reached a provider billed for input it never sent. Twelve cases asserting those contracts directly, plus a canary pinning the literal no-upstream-call key the module branches on, so a rename cannot pass silently.
…est path branches on (BerriAI#37814) The Responses WebSocket path, the pre-call deployment hook and the per-frame project quota hook are all selected by small predicates that nothing asserted directly. Mutating those four decisions left 4 of 6 mutants alive against the mapped test file. Cover them at the boundary: the rust WebSocket path needs both the openai provider and the rust flag, a plain CustomLogger must not advertise a pre-call deployment hook while an overriding or inheriting one must, and only callbacks that actually expose a callable enforce_project_io_token_quota_for_frame reach the WebSocket loop. Kill rate on those four decisions goes 2/6 -> 6/6; the file goes 66 -> 75 passing.
BerriAI#37825) * fix(ci): stop the mutation report publishing a score it never measured Run 32475268575 was the first dispatch of this workflow since May. Every setup step passed and mutmut generated all 48 mutant files, so the suspected zero-mutants bug is not what stops it. It dies in the stats phase, where mutmut times the configured test set once up front. That set included tests/proxy_behavior/management/, a behaviour tier that talks to a real seeded database, so the run ended having mutated nothing. Narrow tests_dir to the unit tier that maps to paths_to_mutate. Run 32476663383 proved a Postgres service is not enough on its own: with a schema but no seed rows the same test fails on a foreign key instead, and a mutation score is only meaningful against the tests that claim to cover the mutated code. The second half is the one that matters. With no results at all, mutation_report.py printed "No surviving mutants, the test suite caught every mutation" and exited 0, so a run that mutated nothing published a perfect score. It now separates no survivors from no results, says which it got, and exits 1. * fix(ci): count mutmut's multi-word verdicts as results The verdict capture was `\w+`, so it matched only single-word statuses. mutmut's status_by_exit_code table has four that are not: `no tests`, `not checked`, `caught by type check` and `check was interrupted by user`. A finished run made entirely of those parsed as zero results, which is exactly the state this script now treats as an unfinished run, so it would have failed a run that had in fact completed. The regression test asserting `reported == 2` on a three-verdict fixture was codifying that, and now asserts 3. A second test walks all four multi-word statuses and checks the report does not call the run unfinished. Caught by Greptile on BerriAI#37825. * fix(ci): keep the saml tests out of the mutmut stats phase Run 32477695014 got past the database blocker and ran 208 of the configured tests, then ended on one error: test_saml_sso.py builds an x509 certificate in a fixture, and inside mutmut's mutants/ sandbox cryptography's hash classes are imported under a second identity, so .sign() rejects the SHA256 instance with "Algorithm must be a registered hash algorithm". That is a property of the sandbox, not of the tests or the code being mutated, and one erroring test ends the stats phase before a single mutant runs. * fix(ci): only claim a clean sweep when something was shown to be killed `mutmut results` skips killed mutants by design, so its silence means either that everything was killed or that nothing ran. Counting the verdicts it does print cannot tell those apart, which left the report still able to say the suite caught every mutation on a run whose mutants were all `no tests` or `not checked`. The clean-sweep sentence is now gated on mutmut-cicd-stats.json reporting a non-zero killed count, which is the only signal that positively distinguishes the two. Without it the report says so in as many words and main returns 1. A run with zero kills and a stats file says that too. The test asserting a non-killed run was not called unfinished was codifying the same confusion; it is replaced by three that pin each branch. Caught by Greptile on BerriAI#37825. * fix(ci): treat stats that count survivors the report never listed as untrusted clean_sweep_is_provable passed on any positive kill count, so a stats file reporting 48 killed and 3 survived, next to a `mutmut results` that listed no survivors, still published a clean sweep. The two sources contradict each other there, and neither one is worth believing. It now requires the stats file to agree that nothing survived, and the report says which disagreement it found. * fix(ci): refuse a clean sweep while mutants never reached the tests A run can end with kills, no survivors, and a pile of mutants marked no tests, skipped, suspicious, timeout or segfault. Those never got put in front of the suite, so "caught every mutation" says more than the run measured. The verdict now names which of them it found and withholds the pass, and the status list those five come from is one constant the summary and the verdict share. * fix(ci): read anything that is not a kill or a survivor as unresolved The unresolved statuses were a list of five, so a run ending in a status the reporter had never met, "check was interrupted by user" among them, still counted as a clean sweep. The rule is now the other way round: killed, survived and total are the keys with a meaning here, and every other non-zero count is a mutant that did not reach the tests, whatever mutmut chose to call it.
…erriAI#37840) Twenty tests in test_request_metadata.py assigned the global directly and leaned on an autouse fixture to put it back afterwards. monkeypatch.setattr does both jobs at the point of use, so each test now says what it sets and the fixture that existed only to undo them goes away.
…lobals (BerriAI#37842) Fifteen tests assigned litellm.audit_log_callbacks, s3_callback_params or s3_audit_callback_params directly and leaned on two autouse fixtures to put them back. monkeypatch.setattr does that at the point of use, so each test now says what it sets, including the one that swaps the value mid-test to prove the cache does not serve the stale params. The fixtures keep only the work monkeypatch cannot do: the per-test empty callback list, and clearing the logger and audit caches around each test.
BerriAI#37806) * test: use monkeypatch.setenv for env writes in tests/test_litellm `os.environ["X"] = v` inside a test leaks the value into every test that runs after it in the same worker, so ordering decides the result. 262 of those writes across 40 files now go through pytest's `monkeypatch` fixture, which restores the previous value at teardown. The rewrite skips any test that a mock.patch-family decorator wraps, any test with defaulted positional parameters, any test whose own name is called directly elsewhere, and rebinds nothing inside nested defs, because in each of those cases appending a fixture parameter changes what pytest or mock binds. Ratchets the TQ004 ceiling from 768 to 506. * fix(test): delete the key through monkeypatch instead of popping it first Five tests popped a key straight out of `os.environ`, ran, then restored it with `monkeypatch.setenv`. By the time monkeypatch saw the name it was already gone, so it recorded "absent" as the value to go back to and deleted the key at teardown. On a worker that inherited a real `RESEND_API_KEY`, `SENDGRID_API_KEY`, `UI_PASSWORD`, `LITELLM_SALT_KEY` or `OPENAI_API_KEY`, every test after the first one ran without it. `monkeypatch.delenv(..., raising=False)` removes the key and restores whatever was there, so the try/finally the manual restore needed goes with it. * chore(test): leave the two cost-calc files to the PR that rewrites them fully Both files are also in BerriAI#37815, which converts the module-global writes as well as the env writes and folds them into one fixture. Two PRs rewriting the same lines differently is a conflict nobody benefits from resolving, so this one drops back to staging on those two and keeps the other 39. TQ004 clears 200 here instead of 275; the rest moves with BerriAI#37815.
…erriAI#37832) Both datadog test files hand-roll what monkeypatch.setenv already does: read the old value, write the test value, put the old one back on the way out. The cost management fixture checks the old value for truthiness rather than for None, so an operator running the suite with DD_API_KEY set to the empty string gets it deleted rather than restored. Starting from DD_API_KEY="" and running test_init leaves it None on the current file, and "" after this. 13 raw os.environ writes become monkeypatch.setenv, the two fixtures stop being yield fixtures because there is nothing left to do on the way out, and the now unused os import goes with them. 27 tests pass across the two files, 88 across tests/test_litellm/integrations/datadog.
…suites (BerriAI#37815) * test(cost-calc): stop 182 global writes leaking out of the cost-calc suites Across test_cost_calculator.py and llm_cost_calc/test_llm_cost_calc_utils.py, 58 tests opened by setting LITELLM_LOCAL_MODEL_COST_MAP in os.environ and replacing litellm.model_cost, and none of them put the env var back. The second file already had a _local_model_cost_map fixture doing it by hand with a try/finally, so both idioms sat in the same file. Keep that fixture, give it monkeypatch, and have every one of those tests ask for it. The margin and discount tests drop their hand-rolled copy-then-restore in favour of monkeypatch.setattr, which also puts the global back when an assertion fails part way through. Both files also drop a sys.path.insert whose argument resolves outside the repo, so it was never what made the imports work. TQ003 1077 -> 1075, TQ004 768 -> 693, TQ005 2836 -> 2731, and the budget ceilings come down with them. * fix(test): make the streamed-cost tests load the map they assert against The local_cost_map fixture set LITELLM_LOCAL_MODEL_COST_MAP but never reloaded litellm.model_cost, and reading the variable is not what loads the map. So the three streaming-cost tests billed against whatever map the process happened to be holding, and their hardcoded prices only held when something else had already swapped in the checked-in one. This branch stops the cost-calc tests leaking that map, which left test_main billing at the ambient prices instead. The fixture now loads the map it names, so the prices these tests assert hold on their own.
… around (BerriAI#37822) Seventeen tests in this file save a litellm module global, open a try, write it, and restore it in a finally. Four more sit behind autouse fixtures that reset the flag to a hard-coded False rather than to whatever it was. monkeypatch.setattr does all of that, so the capture, the try and the finally come out and the test body loses a level of indentation. The alias-format fixtures stop guessing the value they are restoring to. Also drops the sys.path.insert, whose argument resolves four levels above the repo, so it was never what made the imports work. TQ003 1077 -> 1076 and TQ005 2836 -> 2796, and the budget ceilings come down with them. 443 tests pass either way; the conftest snapshot was already catching these globals, so this is about not needing it.
…altime tests scaffold around (BerriAI#37826) * test(policy-engine): unwind the callback global the pipeline tests scaffold around Every one of the 16 tests in this file set litellm.callbacks by hand, each wrapping its body in a try/finally to put the old value back, and each capturing that old value with a .copy() first. That is 32 TQ005 violations and about 70 lines of scaffolding to say what monkeypatch.setattr says in one. The write also sat outside the try, so the block that restores it did not cover the statement that changed it. 16 tests pass either way, and litellm.callbacks reads restored on both sides, because the conftest snapshot already lists it. The point is that these tests stop depending on that snapshot to clean up after them. * test(realtime): unwind the same callback global in the realtime streaming tests Same global, same shape as the previous commit. 25 writes to litellm.callbacks, 2 of them wrapped in a try/finally that resets to [] rather than to the old value, and 12 tests that write it with no protection at all. monkeypatch.setattr replaces all of them, and the sys.path.insert with its now-unused os and sys imports goes too. Both sides read restored here as well, for the same reason as the previous commit: litellm.callbacks is in the conftest snapshot. What changes is that these tests no longer lean on it. 101 tests pass in this file, 16 in the policy engine one. * style(realtime): wrap the one signature the monkeypatch param pushed past 120
…failure (BerriAI#37828) Onyx, prompt security, hiddenlayer, repelloai and deepkeep all write straight to os.environ and unset again at the bottom of each test. None of the five has a try/finally, so the moment a test fails it returns to the runner with the keys still set and whatever runs next in that worker inherits them. Raising inside test_onyx_guard_with_custom_timeout_from_kwargs on the current files leaves ONYX_API_BASE and ONYX_API_KEY behind; doing the same in test_hiddenlayer_config_saas leaves HIDDENLAYER_API_BASE. Both come back clean after this. 89 raw writes and the hand-rolled deletes become monkeypatch calls. The class-level setup_method and teardown_method pair in the onyx file, sweeping the same three keys twice, becomes one autouse fixture. The sys.path.insert lines and their now-unused imports go too, and litellm.set_verbose = True, which only turned global debug logging on for whatever ran next, is dropped rather than restored. test_onyx_guard_config and test_prompt_security_guard_config asserted nothing at all, so they could only fail by raising. Each now pins what init_guardrails_v2 produces: exactly one guardrail of the right class on litellm.callbacks, carrying the configured name, default_on and hook. The zero-assert tests in the other three are left alone; those are a judgement about each guardrail rather than a mechanical sweep. tests/test_litellm/proxy/guardrails passes at 2873.
…stem and fallback tests (BerriAI#37915) The mid-conversation system tests prime the prompt cache by re-sending an identical /v1/messages body until its usage shows the full prefix read back three times in a row. The e2e stack runs with the litellm response cache on, so every resend after the first is served from redis with the first call's usage and the streak can never form; the three unflagged-model tests have failed on every litellm-e2e build since the consecutive-read check landed. Send cache: {"no-cache": true} on RichMessagesRequest, as test_cache_control already does, so each resend reaches the provider. The two fallback tests sent the same "say hi" / max_tokens=16 body to the gpt-5.5 fallback, so one empty (finish_reason=length) completion served the second test from the response cache and failed both. Give each test a unique prompt and leave gpt-5.5 enough tokens to emit text.
…on (BerriAI#37912) The provider is published in lockstep with LiteLLM: every dev, rc and stable release mirrors terraform/provider/ from the release commit and tags it with the LiteLLM version, alongside the aws/google modules. The 0.x line ends at 0.4.0, and the CHANGELOG headings no longer drive a release. RELEASING.md describes the new flow and how to recover a version whose goreleaser run failed; README gains a Versioning section with the re-pin note for anyone on `~> 0.4`; CHANGELOG records the change under Unreleased. goreleaser gets `prerelease: auto` so a v1.99.0-dev.1 / -rc.1 tag in the mirror is marked as a pre-release instead of becoming the repo's latest release. The registry ingests it either way.
…e session (BerriAI#37834) test_zai_provider.py set LITELLM_LOCAL_MODEL_COST_MAP and litellm.model_cost directly and never put them back, so every test that ran after it in the same process saw a local cost map instead of the real one. The two respx tests did the same to litellm.disable_aiohttp_transport with no restore at all. Both now go through monkeypatch, which restores on teardown including when the test fails. The cost-map setup moves into a fixture requested by exactly the five tests that read the cost map.
…BerriAI#37831) Ten tests set litellm.s3_callback_params by hand. Four of them reset it to None on the last line of the test body, which only runs when the test passes; the other six wrap the body in try/finally to put the old value back. Raising inside test_s3_verify_false_handling on the current file leaves the whole callback config, bucket, endpoint and keys, set in the process for whatever runs next. monkeypatch.setattr covers both shapes and restores on failure, so the 28 TQ005 violations and the try/finally scaffolding come out together. 51 tests pass, and the wider tests/test_litellm/integrations tree is unchanged. The five TQ002 mock-echo tests in this file are left alone; those need a judgement about what S3 logging should assert, not a mechanical sweep.
…globals (BerriAI#37839) Nine tests in test_http_handler.py captured litellm.disable_aiohttp_transport, force_ipv4, ssl_ecdh_curve or the request_timeout pair, wrapped their whole body in a try, and put the value back in a finally. monkeypatch.setattr does all of that, so the captures, the try and the finally go away and the bodies lose a level of indentation. The class-scoped restore_request_timeout fixture existed only for that same bookkeeping and goes with them. litellm.in_memory_llm_clients_cache is left alone on purpose: the eviction tests assert a handler is garbage collected, and monkeypatch holds the replaced value alive until teardown, which keeps the weakref they check from clearing.
…cy flag (BerriAI#37841) Seven tests captured litellm.use_legacy_interactions_schema, wrapped their body in a try, and put it back in a finally. monkeypatch.setattr does that, so the capture, the try and the finally go and the bodies lose an indentation level. The remaining hand-rolled restores stay. They hold the flag only across the iterator's constructor and put it back before the test iterates, so handing them to monkeypatch would widen that window to the whole test and change what the streaming assertions run against.
…BerriAI#37844) Fifteen tests opened with litellm.set_verbose = True and never put it back, so the flag stayed on for everything that ran after them in the same process. Nothing in the file reads the output it produces: there is no caplog, no capsys and no assertion on a log line, so the flag was left over from debugging. Deleting it beats restoring it, since restoring keeps the noise.
# Conflicts: # .github/scripts/run_llm_translation_tests.py # .github/workflows/triage_rollout_heads_up.yml
The -X theirs merge silently dropped several fork-only pieces that don't textually conflict with upstream's own edits to the same files: - litellm/router.py lost RoutingArgs (upstream's own enum, still referenced at 3 call sites), plus the fork-only mid-stream refusal-hold machinery (_RefusalStreamHold, _ResponsesRefusalStreamHold, _fallback_dispatch_exception, and their helpers) that content_policy_fallbacks depends on. - enterprise/litellm_enterprise/proxy/common_utils/check_batch_cost.py grew a second, upstream-authored claim layer (_claim_job_for_costing plus a duplicate _release_job_claim) alongside the fork's existing fenced one, the same recurring conflation documented in .github/fork-patches.txt for the 2026-08-09/08-14/08-21 syncs. Python keeps only the last definition of a duplicate method name, so the fork's status="pricing"-fenced _release_job_claim was silently shadowed. Removed the duplicate layer and restored the single fenced claim/release path with the CLAIM_LOST sentinel. - litellm/integrations/anthropic_cache_control_hook.py had a ~50-line block of module-level constants/functions duplicated, and _apply_message_injections lost its default_control parameter while its call sites and internal use of it survived (NameError on every call). - litellm/proxy/prisma_migration.py lost the LITELLM_PRISMA_CLIENT_PREBAKED skip for the standalone entrypoint's runtime `prisma generate` call. Also bumped type-discipline-budget.json's LIT001/LIT006/LIT011 ceilings to the verified current totals (22945/1073/5617): every newly flagged site is byte-identical to upstream/litellm_internal_staging (confirmed via type_discipline_gate.py --base origin/litellm_internal_staging), so this is upstream's own growth, not fork-introduced debt; make lint-budget-update cannot raise an over-budget rule, only lower one. Everything else fork-patches.txt documents was verified already intact.
…ew rules Today's sync added F811/PT017/RUF043 to ruff-tests.toml's lint.select, and F811 immediately caught 2 pre-existing duplicate definitions in upstream's own test files (both byte-identical to origin/litellm_internal_staging, so the definitions predate this sync and were simply invisible before): - test_scim_v2_endpoints.py re-imported ProxyException on its own line after already importing it in the earlier _types import block. - test_openai_passthrough_logging_handler.py defined test_is_openai_embeddings_route twice; the later definition (with classic Azure deployment-path coverage) silently shadowed the earlier one, which pytest never ran. Merged the earlier definition's non-redundant assertions (bare openai.azure.com/cognitiveservices.azure.com hosts without the /openai/ prefix, a deployment path missing ?api-version=, and the proxy's own passthrough path prefix) into the surviving definition instead of just deleting it, so no coverage is lost.
…ertions Same recurring conflation as check_batch_cost.py itself: this test file is shared with upstream (which ships its own simpler TestMultiPodBatchCostClaim expecting a single unfenced batch_processed flip), and today's -X theirs merge reverted the fork's adaptations back to upstream's shape wherever they sit in a region upstream also touched historically - the status="pricing" fencing in FakeManagedObjectRow's matching, the _classify() journal helper that distinguishes claim/release/finalize/mark writes, and the "mark" step in the expected journals. CI on the previous push caught this directly: 5 failures in TestMultiPodBatchCostClaim, all expecting the unfenced upstream write shape that check_batch_cost.py (already fixed to the fenced fork design) no longer produces. Restored verbatim from origin/litellm_internal_staging; confirmed via diff math that upstream added nothing new to this file this cycle (current-vs-upstream line count == origin-vs-upstream minus exactly the lines this restore reintroduces), so nothing upstream-new is lost.
model_prices_and_context_window.json and its backup counterpart each ended up with two entries for gemini-3.1-flash-lite-image, gemini/gemini-3.1-flash-lite-image, and vertex_ai/gemini-3.1-flash-lite-image: the fork's pre-sync entry and upstream's independently-updated one, added in different parts of the file so the merge kept both with no conflict. JSON parsers keep only the last occurrence, so this was silently dropping upstream's newer pricing (batch rates, cache-read cost, video modality support) in favor of the stale fork copy wherever a caller happened to load the file in a way that surfaced the first entry. Kept each key's second (upstream-current) occurrence, matching upstream/litellm_internal_staging exactly, and removed the stale first one. Caught by tests/test_litellm/test_model_prices_schema.py::test_price_map_has_no_duplicate_keys.
…drails_only walk Upstream's own new content-enforcer feature (has_content_enforcer, enforces_request_content, the _policy_pipelines/_pipeline_managed_guardrail_names helpers) landed cleanly in ProxyLogging.pre_call_hook, but the merge left a stray "not guardrails_only and" prefix on the CustomLogger dispatch branch's condition, textually adjacent to a pre-existing fork check. That prefix skips the whole elif whenever guardrails_only=True, which contradicts the very next clause upstream added right after it: "(not guardrails_only or _callback.enforces_request_content)" exists specifically so a content-enforcing CustomLogger (not a CustomGuardrail, so it has no other hook to run through) still runs during a guardrails-only walk. With the stray prefix, no enforces_request_content logger could ever run in that walk. Caught by CI: tests/test_litellm/proxy/utils/proxy_logging/test_pre_call_hook.py:: test_a_content_enforcer_runs_in_both_walks[True] and tests/test_litellm/proxy/openai_files_endpoint/test_batch_guardrails.py:: test_a_real_non_guardrail_enforcement_hook_drops_its_record (the batch-scan path calls pre_call_hook with guardrails_only=True, and its prompt-injection detection hook is a CustomLogger with enforces_request_content=True). Removing the prefix makes the file byte-identical to upstream/litellm_internal_staging.
When merging the deleted duplicate test_is_openai_embeddings_route definition's unique assertions into the surviving one (previous commit 2cafe94), I kept its assumption that a classic Azure deployment path missing ?api-version= is not recognized as embeddings. That assumption predates this sync's classic-Azure-deployment-path support: the current is_openai_embeddings_route only checks _in_openai_scope() and a path suffix of "/embeddings" (see its own docstring), never the query string, so the assertion was simply wrong against the current implementation and CI caught it immediately.
…ed total CI's own basedpyright run (this sandbox can't provision .venv-typecheck locally, see prior commits) found reportUnnecessaryCast at 118 against a 117 ceiling, 1 over relative to origin/litellm_internal_staging (dc53c1f, the exact base scripts/type_check_gate.py compares against). This file is shared with upstream, so the same class of upstream-growth bump documented throughout .github/fork-patches.txt for every other ruff-strict/type-discipline/basedpyright rule; make lint-budget-update cannot raise an over-budget rule, only lower one.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Full -X theirs sync of BerriAI/litellm litellm_internal_staging (526 commits behind).
Merge conflicts
Resolved 2 conflicts by taking upstream's side:
.github/scripts/run_llm_translation_tests.py(add/add, both sides independently added the same script with different quote-style formatting) and.github/workflows/triage_rollout_heads_up.yml(upstream deleted it as part of moving scripts out ofworkflows/; not tracked in fork-patches.txt, so no fork-only content to preserve).Fixes landed on top of the merge
Verified every fork-patches.txt entry against the merged tree. Most were already intact; the merge silently dropped or duplicated these:
RoutingArgs(upstream's own enum, still referenced at 3 call sites) and the entire fork-only mid-stream refusal-hold machinery (_RefusalStreamHold,_ResponsesRefusalStreamHold,_fallback_dispatch_exception, and their helper functions) thatcontent_policy_fallbacksdepends on. Restored the whole block verbatim fromorigin/litellm_internal_staging._claim_job_for_costing/_release_job_claimpair landed again this cycle, stacking on top of the fork's fenced claim layer. Deleted the duplicate layer and restored the single fenced claim/release path with theCLAIM_LOSTsentinel, matching the design PR chore: sync upstream 2026-08-21 #208 landed yesterday. Its test file (test_check_batch_cost.py) had the same reversion and needed the same restoration.default_controlparameter threading inAnthropicCacheControlHook._apply_message_injectionsand removed a duplicated ~50-line block of module-level constants/functions the merge had copy-pasted.LITELLM_PRISMA_CLIENT_PREBAKEDenv-var skip inlitellm/proxy/prisma_migration.py(upstream's own PR fix(proxy): fail the standalone prisma migration entrypoint on migration errors BerriAI/litellm#37692 turns a previously-silentprisma generatefailure into a hard one under a non-root runtime uid).not guardrails_only andprefix inProxyLogging.pre_call_hook(litellm/proxy/utils.py) that broke upstream's own brand-new content-enforcer feature: it skipped the whole CustomLogger dispatch branch wheneverguardrails_only=True, directly contradicting the very next clause upstream added to support exactly that case. Removing it makes the file byte-identical to upstream.type-discipline-budget.json's LIT001/LIT006/LIT011 ceilings (22945/1073/5617) andbasedpyright-code-budget.json'sreportUnnecessaryCastceiling (118) to CI-verified totals: every flagged site traces to upstream's own growth relative toorigin/litellm_internal_staging, not fork debt.ruff-tests.tomlchange surfaced (it added F811/PT017/RUF043 tolint.select): a redundantProxyExceptionre-import intest_scim_v2_endpoints.py, and a duplicatetest_is_openai_embeddings_routeintest_openai_passthrough_logging_handler.py. Merged the older definition's non-redundant assertions into the surviving one, then fixed one of those assertions (a stale expectation about?api-version=mattering) once CI caught that it no longer matched the current implementation.gemini-3.1-flash-lite-image,gemini/gemini-3.1-flash-lite-image,vertex_ai/gemini-3.1-flash-lite-image) that ended up with two entries each inmodel_prices_and_context_window.jsonand its backup counterpart, silently dropping upstream's newer pricing wherever a caller surfaced the first occurrence. Kept each key's upstream-current entry.The ruff-strict and test-quality budget gates pass unchanged, with no new ceilings needed there.
Linear ticket