Commit graph

47584 commits

Author SHA1 Message Date
Mateo Wang
e0c5bb990a
Merge pull request #39835 from BerriAI/litellm_cost_map_guard
feat(ci): add the cost map guard check
2026-09-07 12:11:41 -07:00
Mateo Wang
2cfe106f9f
Merge pull request #39660 from BerriAI/litellm_bedrock_passthrough_model_access
fix(proxy): enforce key and team model access on Bedrock passthrough routes (internal copy of #34244)
2026-09-07 12:11:15 -07:00
tin-berri
1ae3216120
fix(router): preserve default heuristic updates (#40007) 2026-09-07 12:05:43 -07:00
Roman D
55fe4a7894
feat(proxy): resolve root_path per request from a configured prefix list (SERVER_ROOT_PATHS) (#35935)
* feat(proxy): resolve root_path per request from SERVER_ROOT_PATHS

One deployment can encode exactly one client-visible URL path prefix
today: SERVER_ROOT_PATH is a scalar stamped onto the app at startup, so
a pod fronting several ingress prefixes 404s every prefix but one before
any handler runs, and MCP OAuth discovery can emit only one prefix's
URLs (RFC 9728 section 3 exact-match fails for the rest).

Add an opt-in outermost ASGI middleware that matches the request path
against a configured prefix list (SERVER_ROOT_PATHS, comma-separated) on
a segment boundary and sets scope["root_path"] for that request only.
Everything downstream is stock Starlette: route matching strips
root_path so routes stay registered root-relative, and request.base_url
re-includes it, so the discovery documents' resource and the 401
challenges' resource_metadata land under the prefix the client actually
called — with no discovery-builder changes.

LazyFeatureMiddleware now strips the scope root_path (falling back to
the cached SERVER_ROOT_PATH scalar) before feature prefix matching, so
lazily-registered routers — the MCP OAuth discovery router among them —
load under per-request prefixes.

Follow-up to the routing discussion on #35226; composes with, but does
not depend on, #35576.

* fix(proxy): import Sequence from collections.abc (ruff UP035 strict-budget gate)

* review(greptile): trim implementation commentary; fixture-own MCP registry state in tests

Addresses both P2s from the first Greptile pass:
- per_request_root_path_middleware.py (and the related _lazy_features /
  proxy_server comments) cut down to the constraints the code cannot
  express, per repo comment guidance
- the new discovery tests no longer clear/repopulate the shared MCP
  registry inline; a fixture snapshots it, hands the test an empty
  registry, and restores it afterwards so no state leaks between cases

* fix(lint): mutable-ok marker on the prefix accumulator (LIT002 type-discipline gate)

* fix(proxy): tie 401 challenges and get_custom_url to the per-request root_path

The per-request root_path middleware sets scope["root_path"] to the
prefix the client actually called, but the OAuth 401 challenges
(raise_user_oauth_challenge / raise_token_exchange_challenge) still
built their resource_metadata from SERVER_ROOT_PATH. On a pod fronting
several prefixes, the challenge advertised a discovery URL under a
different prefix than the discovery document served — the two
disagreed on where the resource metadata lives, and a strict RFC 9728
client refused the challenge. Route the challenges through a small
ContextVar the middleware populates so they read the same effective
root_path Starlette resolves the request under.

The same accessor fixes get_custom_url: when a request lives under a
SERVER_ROOT_PATHS-matched prefix, request.base_url already carries it,
so appending the SERVER_ROOT_PATH scalar on top produced e.g.
/tenant-a/legacy/sso/callback — a path that does not exist. Reading
the per-request prefix instead (and relying on join_paths's tail-dedup)
keeps SSO login/callback URLs under one prefix — the one the request
actually arrived on.

Fallback: outside a request (module-load-time UI URL builders,
background tasks) the ContextVar is unset and the accessor reads
SERVER_ROOT_PATH, matching get_server_root_path() so scalar-only
deployments are byte-identical.

* fix(mcp): challenge URL under per-request prefix must route, and mock parity

Two follow-ups to the review fix that made the 401 challenge use the
per-request root_path:

1. oauth_protected_resource_path must pick the URL structure that
   actually routes for the mechanism in use:
   - The scalar SERVER_ROOT_PATH deployment registers the well-known
     routes with the prefix INSERTED (via well_known_root_suffix at
     import time), matching RFC 8414 §3. The challenge URL must use the
     same insertion or a client fetching it 404s.
   - The per-request SERVER_ROOT_PATHS deployment can't register routes
     per prefix; PerRequestRootPathMiddleware strips the prefix from
     scope["path"] and the router matches the un-inserted route. The
     URL must place the prefix BEFORE .well-known so the strip leaves a
     matching path.
   The previous fix used the insertion form for both, which 404'd the
   discovery fetch on the per-request path — the discovery doc and the
   challenge would then disagree on where the resource metadata lives,
   the very failure the review flagged. End-to-end verified: the URL
   the challenge advertises routes and the doc's `resource` field
   equals the URL the client originally called (RFC 9728 §3).

2. get_request_root_path now delegates its fallback through
   get_server_root_path() instead of reading the env directly, so every
   existing `monkeypatch.setattr("litellm.proxy.utils.get_server_root_path"`
   test override keeps working. This unstubbed the mock on the /v2/login
   test that failed on the last CI run.

Plus the lint budget: annotate the local accumulator Final, tag the
scope["root_path"] rewrite as an intentional ASGI-contract mutation,
tag the reused `path`/`root_path` rebinds in LazyFeatureMiddleware, and
add reason strings to the two new PLC0415 lazy-import noqas.

* test(mcp): pin the reviewer's expected end-state — challenge URL routes, resource matches called URL

End-to-end regression test that mounts the discoverable router + the
per-request root_path middleware, hits an MCP endpoint that raises
raise_user_oauth_challenge, fetches the resource_metadata URL the
challenge advertises, and checks the returned document's `resource`
equals the URL the client originally called (RFC 9728 §3 exact match).

Covers /tenant-a, /tenant-b, and the unprefixed path on the same app so
a regression on any prefix — challenge URL 404s, or doc emits a
different prefix than the client called — fails at this test rather
than in a strict MCP client's discovery.

---------

Co-authored-by: gym-cmd <186399764+gym-cmd@users.noreply.github.com>
2026-09-07 11:54:59 -07:00
Yuneng Jiang
9c48373bcb
Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_/guardrail-automation-testing-3ecd3d 2026-09-07 11:47:54 -07:00
Yuneng Jiang
a88002c034
test(guardrails): document audit prompt storage prerequisite 2026-09-07 11:43:32 -07:00
Yuneng Jiang
4044cd2f0c
Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_/guardrail-automation-testing-3ecd3d 2026-09-07 11:38:59 -07:00
devin-ai-integration[bot]
96c698030b
fix(spend_logs): keep partition DDL transactions alive for their statement timeout (#40098)
* fix(spend_logs): keep partition DDL transactions alive for their statement timeout

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* style: ruff format changed files

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(lint): avoid dict-literal kwargs and keep cast-ok on the cast line

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(lint): cast at the call site instead of widening PrismaClient.tx

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(spend_logs): require partition tx timeout to strictly exceed statement bound

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: jesus <jesus@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-07 11:38:53 -07:00
yuneng-jiang
4b3355bdc6
test(e2e): prove the virtual key lifecycle on every gateway replica (#40023)
* test(e2e): prove the virtual key lifecycle on every replica

Walks one virtual key through create, read, partial update, clear, enforce
and delete against a live proxy and database, reading every write back on
every gateway replica.

The management suite already had single write-then-read tests for keys, but
none of them proved that a partial /key/update leaves the untouched fields
alone, that an explicit null clears a field, or that a write is visible on
more than the one gateway that took it.

Adds read_back_everywhere to the shared ProxyClient: it polls a GET path on
every URL in PROXY_REPLICA_URLS until each replica's parsed body satisfies
the caller's predicate, and fails naming the replica that never converged.
The CLEAR sentinel in the e2e models makes an explicit JSON null expressible
in a body the transport otherwise strips of None fields.

Documents /key/update's merge patch semantics on the endpoint docstring.

* test(e2e): prove key revocation and field preservation on every replica

Applies the findings from an adversarial review of the first commit.

The delete step only checked that chat was refused on the gateway that took
the write, so it would have passed while a sibling gateway kept serving the
deleted key. It now serves one call from every replica first, so each has the
key cached and the delete has something to revoke everywhere, then polls every
replica for the refusal.

The file also carried its own poll loop that tested the deadline before
attempting, so it gave up one attempt early and skipped the attempt landing
exactly on the deadline. It now shares the harness helper, which is generic
over the polled value rather than over a parsed body, so the same loop covers
both the info read-back and the chat refusal.

The model the enforcement step registers now carries a unique marker in its
alias, matching every other deployment this suite creates, so concurrent runs
never share one model group.

The docstring sentence claimed an explicit null clears any field. It does not:
the metadata-backed fields merge into stored metadata, where a null is a silent
no-op, and only the key's own columns clear. Regenerating the dashboard types
picks up the corrected text.

* fix(e2e): delete a deployment that never becomes servable

Registering a model posts /model/new and then waits for every replica to list
it. When that wait timed out the deployment already existed in the database but
its id had never been returned, so no caller could delete it and the row
outlived the run. It is now deleted before the failure propagates.

Found by review on the key lifecycle suite, whose module fixture registers a
deployment this way, but every caller of the shared helper had the same
exposure.

* docs(e2e): drop the duplicated notes from the lifecycle docstrings

The delete method restated what the warm-up helper already explains, and the
module restated the merge patch rule that the endpoint and the request model
both document.
2026-09-07 11:30:46 -07:00
yujonglee
5f2b4d27d7
test(ocr): add SDK callback E2E parity (#40061)
* test(ocr): cover SDK callback parity

* test(ocr): compare callback kwargs
2026-09-07 11:23:12 -07:00
yuneng-jiang
9acc01efce
test(e2e/ui): cover member role and budget edits, member permission delegation, and team guardrail removal (#40042)
* test(e2e/ui): cover member role and budget edits, member permission delegation, and team guardrail removal

Three Playwright specs for the Teams flows enterprise customers hit most, each
owning its fixtures and proving the mutation through a read-back rather than a
toast.

- teamMemberEdit: an admin edits a member's team role and per-member budget,
  and both survive a reload of the Members table
- memberPermissions: a plain member is refused /key/generate for their team,
  a team admin grants it on the Member Permissions tab, and the member then
  creates a team key that serves a real completion
- teamGuardrailRemoval: clearing a team's only guardrail on the Settings tab
  really clears it, and traffic the guardrail refused starts serving again

* test(e2e/ui): make the new team specs safe to run in parallel

Fixture ids came from Date.now(), so two repeats starting in the same
millisecond minted the same user id: one got a 409 and the loser's teardown
deleted the user the other was still signed in as. Ids now carry a random
suffix.

Also move the member-permissions setup inside the cleanup-protected block so a
half-finished setup cannot leak a team, and close both browser contexts the
test opens.
2026-09-07 11:18:16 -07:00
yuneng-jiang
6dfcc46c9e
Merge pull request #40026 from BerriAI/litellm_fix_migrated_pages_sidebar_test
test(ui): make navigation smoke resilient to router refactors
2026-09-07 11:17:32 -07:00
yucheng-berri
2700ffe8c9
fix: capture provider request id in failure logging payloads (#40045)
* fix: capture provider request id in failure logging payloads

* fix: include common provider request id headers

* test: remove accidental formatter churn

* test: type provider request id cases

* test: cover azure and google request id headers

* refactor: share provider request id headers from constants

* chore: remove redundant request header comment

* test: cover provider header lookup failures
2026-09-07 11:12:34 -07:00
Mateo Wang
9275cf42ed
Merge pull request #40015 from BerriAI/litellm_fix_check_run_name_collisions
fix(ci): stop the auto-close duplicates job colliding with the required test check
2026-09-07 10:55:34 -07:00
Mateo Wang
8e7f748e8a
Merge pull request #39870 from BerriAI/litellm_lit_6917_make_check_test_tree_ruff
fix(make check): lint the test tree on tests-only changes like CI does
2026-09-07 10:55:28 -07:00
Mateo Wang
c8ef043087
Merge pull request #39981 from BerriAI/litellm_lit_7079_bridge_preserve_provider_metadata
fix: keep provider id and metadata on Responses API bridged chat completions
2026-09-07 10:55:25 -07:00
Mateo Wang
3dac0ba79b
Merge pull request #39850 from BerriAI/litellm_fix_realtime_reasoning_double_bill
fix(cost): bill realtime reasoning tokens nested in text_tokens once
2026-09-07 10:55:20 -07:00
Mateo Wang
642a0f68ae
Merge pull request #39530 from BerriAI/litellm_fix_gateway_rustls_provider
fix(ai-gateway): dial upstream WebSockets over an explicit rustls provider
2026-09-07 10:55:15 -07:00
Mateo Wang
9700f666d0
Merge pull request #39839 from BerriAI/litellm_async_remote_image_fetch
fix(async): move remote image fetches off the event loop for Snowflake, Bedrock invoke Claude, Mantle and Gemini
2026-09-07 10:55:04 -07:00
Mateo Wang
a9556c7bad
Merge pull request #39965 from BerriAI/litellm_fix_oci_cohere_stream_tool_turn_dup
fix(oci): stream Cohere tool-calling answers once
2026-09-07 10:54:59 -07:00
Mateo Wang
a676f5eb6d
Merge pull request #39800 from BerriAI/litellm_claude_md_qa_screenshots
docs(claude): have runs embed their own QA screenshots on visual changes
2026-09-07 10:54:49 -07:00
yuneng-jiang
5930549bf8
Merge pull request #40140 from BerriAI/litellm_fix_autorouter_prisma_filter
fix(router): serialize heuristic tuning quota filters for Prisma
2026-09-07 10:52:38 -07:00
yujonglee
8da735410c
test(ocr): trace callback lifecycle parity (#40063) 2026-09-07 10:44:22 -07:00
yujonglee
217cb12623
refactor(rust): remove per-request enablement arguments (#39928)
* refactor(rust): remove per-request enablement arguments

* fix(rust): remove ignored transcription enablement

* refactor(rust): remove OCR-specific bridge controls
2026-09-07 10:43:45 -07:00
Yuneng Jiang
b61ef7c232
Merge branch 'litellm_/guardrail-automation-testing-3ecd3d' into litellm_presidio_ui_user_story_e2e 2026-09-07 10:08:30 -07:00
Yuneng Jiang
f40c3842dc
fix(router): serialize heuristic tuning quota filters for Prisma 2026-09-07 10:08:19 -07:00
Yuneng Jiang
5561d2e476
Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_/guardrail-automation-testing-3ecd3d 2026-09-07 10:08:15 -07:00
yuneng-jiang
a2b7868a5b
test(ui): pin wire contracts for key, model and MCP server forms (#40019)
* test(ui): pin wire contracts for key, model and MCP server forms

Add vitest cases that pin what the key edit, key create, model edit and
MCP server edit forms put on the wire: an edited field reaches the
request with its new value, a cleared field reaches it as an explicit
null, and the dirty-only body is pinned as an expected failure until
each form moves to pickDirty. Model edit also pins the cost-map-derived
model_info fields as an expected failure.

KeyEditView hands a cleared max_budget to KeyInfoView as an empty
string and handleKeyUpdate maps it to null, so the null is pinned at
the /key/update boundary in key_info_view.test.tsx and the KeyEditView
case is an expected failure. buildEditServerPayload passes a cleared
description through as an empty string, so that case is an expected
failure too.

* test(ui): split masked model_info pins and retarget the create tracker

The model_info expected-failure case held three assertions, and it.fails
stops at the first one, so a later revamp that fixed max_input_tokens
while leaving mode leaking would still report an expected failure. Split
it into one case per pinned field group so each flips on its own.

The key create tracker asserted a body of only key_alias, which a create
can never send: key_type, user_id, duration and metadata are always
mounted. Retarget it at the real over-send, which is the Optional
Settings section adding fifteen undefined-valued keys when the user opens
it without filling anything in.
2026-09-07 09:53:18 -07:00
mubashir1osmani
a1e7293fa9
fix(mcp): apply key and team guardrails to MCP tool calls (#39629)
* fix(mcp): apply key and team guardrails to MCP tool calls

Guardrails attached to a virtual key or team were only enforced on LLM
routes. The synthetic request built for MCP tool call guardrail hooks
carried no guardrails in its metadata, so a guardrail with default_on
false never ran on tools/call even when the key explicitly listed it.
Resolve key, team, and project guardrails onto the synthetic request
with the same helper the chat path uses.

* fix(mcp): pass project metadata through without a mutable default

* fix(mcp): mark the request dict parameter mutable-ok with a reason

* test(mcp): explain the premium_user patch and tighten the helper docstring
2026-09-07 16:45:16 +00:00
Mateo Wang
1c7b13bdbf
Merge pull request #38755 from BerriAI/litellm_mistral_voxtral_tts_speech
feat(mistral): add text-to-speech support for /v1/audio/speech
2026-09-07 09:38:57 -07:00
Mateo Wang
50f54b9c7e
Merge pull request #40014 from BerriAI/litellm_lit_7036_retry_policy_400s
fix(router): skip the refusing deployment when retrying a non-transient error
2026-09-07 09:38:43 -07:00
yujonglee
728d0953af
ci: simplify Rust checks and remove wheel PR comments (#39975)
* ci: limit Rust workflows to Rust directory changes

* ci: run Rust checks when their workflow changes

* ci: report Rust wheels only for successful Rust changes

* ci: keep Rust wheel reports in the workflow summary

* ci: group Rust lint and validation jobs

* ci: keep Rust job names distinct from required lint and test checks

* ci: drop the unused Python setup from the Rust lint job

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-09-07 09:00:58 -07:00
devin-ai-integration[bot]
168a0055a2
chore(lint): stop ratcheting *-budget.json on PR branches (#39937)
Some checks failed
Unit Tests / Vertex AI (push) Has been cancelled
Unit Tests / proxy-auth (push) Has been cancelled
Unit Tests / proxy-endpoints (push) Has been cancelled
Unit Tests / proxy-extras (push) Has been cancelled
Unit Tests / proxy-infra (push) Has been cancelled
Unit Tests / proxy-server (push) Has been cancelled
Unit Tests / responses-caching-types (push) Has been cancelled
Postgres Tests / proxy-security (push) Has been cancelled
Postgres Tests / schema-migration (push) Has been cancelled
Postgres Tests / proxy-behavior (push) Has been cancelled
Unit Tests: Documentation Validation / documentation (push) Has been cancelled
Unit Tests: Proxy DB Operations / assert-shard-coverage (push) Has been cancelled
Unit Tests / caching-local (push) Has been cancelled
Unit Tests / core-utils (push) Has been cancelled
Unit Tests / enterprise-package (push) Has been cancelled
Unit Tests / enterprise-routing (push) Has been cancelled
Unit Tests / integrations (push) Has been cancelled
Unit Tests / All Other Providers (push) Has been cancelled
Unit Tests: Proxy DB Operations / auth-checks (push) Has been cancelled
Unit Tests: Proxy DB Operations / budgets (push) Has been cancelled
Unit Tests: Proxy DB Operations / custom-logging (push) Has been cancelled
Unit Tests: Proxy DB Operations / db-and-spend (push) Has been cancelled
Unit Tests: Proxy DB Operations / endpoints-and-responses (push) Has been cancelled
Unit Tests: Proxy DB Operations / guardrails-hooks (push) Has been cancelled
Unit Tests: Proxy DB Operations / jwt-and-keys (push) Has been cancelled
Unit Tests: Proxy DB Operations / key-generation (push) Has been cancelled
Unit Tests: Proxy DB Operations / logging-misc (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-runtime (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-server-core (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-utils (push) Has been cancelled
2026-09-06 12:44:19 -07:00
Yuneng Jiang
fdd1c91096
fix(e2e/ui): scope the drawer assertions to the visible, exact match
CI flagged the spec flaky twice more. Both were the same defect in different
places: the Logs drawer renders several nodes per string and the first in DOM
order is often hidden, so .first() waited 20s on an invisible element. The
entity assertions had a second problem on top, since getByText("EMAIL_ADDRESS")
substring-matched the masked prompt div, whose text contains <EMAIL_ADDRESS>,
rather than the entity chip.

Route every drawer assertion through onlyVisible, and match the entity type
and score exactly, which is what the panel renders them as: entity_type and
"Score: N.NN" each get their own span.

Verified on a live stack: 6 of 6 solo runs and the guardrails folder 5 of 5.
Mutating the analyzer to detect nothing turns the spec red on the raw address
reaching the spend log, so the assertions still carry their weight.
2026-09-06 08:34:19 -07:00
Yuneng Jiang
70f1894c28
fix(e2e/ui): wait on the visible masked prompt, not the first match
The Logs drawer renders the masked prompt in three nodes and the first one
in DOM order is hidden, so the previous commit's .first() traded a strict
mode violation for a locator that waits 20s on an invisible element. Local
runs against a warm stack failed on it every time, resolving the node 22
times and reporting "unexpected value hidden" each time.

Use onlyVisible, the helper this spec already uses for the playground
selectors, which filters to the visible node before taking the first. Solo
runs go 3 for 3 and the guardrails folder passes 5 of 5.

Also drop the 30s wait on the PII step added while chasing this: cold start
was never the cause, and it left the spec sitting on a dead locator longer.
2026-09-06 07:21:36 -07:00
Yuneng Jiang
bbc6ca6205
fix(e2e/ui): stop the masked-prompt assertion tripping strict mode
The Logs drawer renders the masked prompt in three places, so matching it
without narrowing raised a strict mode violation instead of asserting
visibility. CI caught it as a flake: the spec failed its first attempt on
b0e53bfbe4 and passed on retry, which is a locator defect rather than a
timing one and would have gone red on any run that saw all three nodes.

Narrow to the first match, matching the guardrail-name assertion above it.
The leak checks below stay on toHaveCount(0), which is unaffected by
multiple matches and is what actually proves nothing raw reached the drawer.
2026-09-06 07:04:21 -07:00
mateo-berri
5825cc7593 chore: merge litellm_internal_staging into litellm_mistral_voxtral_tts_speech
Some checks failed
LiteLLM Rust / rustfmt, clippy, test (push) Has been cancelled
LiteLLM Rust / release wheel (push) Has been cancelled
Terraform Modules / fmt, validate, test (aws) (push) Has been cancelled
Terraform Modules / fmt, validate, test (gcp) (push) Has been cancelled
2026-09-06 03:24:55 -07:00
Yuneng Jiang
b0e53bfbe4
fix(e2e/ui): fail the run when the Presidio fixture never comes up
Both readiness loops broke out on success and fell through on timeout, so a
mock Presidio server that failed to bind left the run going with nothing
serving /analyze. The guardrail then errored at request time and the failure
surfaced as an unrelated Playwright assertion in presidioUserStory.spec.ts
rather than as the missing fixture it actually was.

Fail the local runner with the port in the message, and add the matching wait
step to both CircleCI UI jobs, which had no readiness check at all.
2026-09-06 03:13:09 -07:00
mateo-berri
4edf6f4dc5 fix(ci): keep an expression matrix directive out of the comparison
`include:` or `exclude:` written as `${{ ... }}` read back as a string, and
the sweep treated that as the directive being absent, so it expanded every
combination GitHub would have dropped. A job whose `name:` holds no matrix
value then looked like it repeated one name across combinations that never
run. An absent directive still means no rows; anything that is not a list
of rows now joins the names left out of the comparison
2026-09-06 02:59:16 -07:00
Yuneng Jiang
274a489c46
test(e2e/ui): automate the RC checklist's Presidio guardrail walk
The guardrail section of the release checklist is done by hand every cut:
create a Presidio guardrail through the wizard, send a sentence with PII from
the playground, then open Logs and check the guardrail caught it. Nothing
covered that path, so a break anywhere along it surfaced only when someone
happened to repeat the steps.

Adds a spec that walks it once and turns the eyeball checks into assertions.
The strongest of them is the leak check: it reads the request back and fails if
the stored prompt still carries the raw address or number, which is what the
manual step is really looking for.

The stack gains a Presidio stand-in that answers the two routes the guardrail
calls, detecting a fixed regex set with a Luhn check on card numbers. Real
Presidio's detection quality is Presidio's business, and pinning the UI lane to
it would mean two heavy containers with spaCy models on every CI run for a test
that is about LiteLLM's integration. The real analyzer stays covered in the
Python lane. The stand-in returns the same entities at the same spans as the
real one for the checklist's sentence, and driving the real guardrail against it
produces the same record shape, so a test written against it is written against
the product's real behavior.

Making the stand-in return the text unmasked turns the spec red on the raw
address reaching the spend log, so the leak assertion reads live data.

run_e2e.sh and both CircleCI UI jobs start the stand-in alongside the mock LLM.
Its port is overridable like the others so two checkouts can run at once.
2026-09-06 02:36:46 -07:00
mateo-berri
5a27e11263 fix(ci): compare names one workflow run settles the same way
A `name:` whose only leftover expressions read a `github.` property other
than `github.job` is filled in identically for every job of the run that
publishes it, so two jobs of one workflow carrying it land on the same
check run. Those names now compare against the other jobs of their own
file instead of sitting in the blind-spot bucket. They stay out of the
comparison across files, where two workflows can run on different events
2026-09-06 02:32:50 -07:00
mateo-berri
aa2c41f489 fix(ci): stop the name-collision check failing legal workflows
An expression at `jobs.<id>.strategy` is legal on GitHub, but the model
required a mapping there, so a workflow using one made the whole file
unreadable and turned code-quality red. That job's names are now a blind
spot like any other name the sweep cannot work out offline.

A matrix whose `name:` holds no matrix value publishes that one name once
per combination, which leaves a required context just as ambiguous as two
jobs sharing a name, so it now reports instead of deduping.

A file that does not parse as one YAML document is reported the way the
module already promised, rather than escaping as a traceback.
2026-09-06 01:59:03 -07:00
Mateo Wang
02522a5441
Merge pull request #39983 from BerriAI/litellm_lit_7081_azure_ai_gpt_6_astra_pricing
feat(cost-map): add azure_ai/gpt-6-astra Foundry pricing
2026-09-06 01:27:22 -07:00
Mateo Wang
b09b7d3eb8
Merge pull request #39980 from BerriAI/litellm_lit_7048_batch_cost_row_once
fix(batches): account a batch's cost once, from the first retrieve that sees it final
2026-09-06 01:27:10 -07:00
mateo-berri
904542a559 fix(ci): stop guessing at names built from contexts the sweep cannot read
Three ways the sweep could fail a workflow GitHub would publish fine.

`github.workflow` and `github.job` were counted as fixed for the whole run, so
two jobs naming themselves after the workflow they sit in were reported as a
collision. `runner` and `vars` were wrong the same way. Drop the exception
entirely: a name still holding an expression is one GitHub resolves per job, so
it is nothing to compare, which is what the rest of the module already does.

`format()` was resolved with Python's semantics, so an attribute lookup crashed
the script and a width specifier padded a name GitHub never pads. Fill `{0}`
holes and escaped braces, and treat anything richer as unresolved.

A matrix `include` or `exclude` row holding a value that is not a scalar lost
that key and became an empty row, which excludes every combination. Report the
row instead of quietly reshaping the matrix around it.
2026-09-06 01:27:03 -07:00
mateo-berri
5ca9e26050 fix(ci): leave check-run names the sweep cannot resolve out of the comparison
A job name holding an expression the sweep could not resolve was compared as
if it were the published name. Two jobs whose names differ per matrix value or
per caller input were reported as a collision, and a matrix that was itself an
expression collapsed onto the bare job id and did the same.

Model what a job publishes as known names beside the reasons the rest stay
unknown. Anything the sweep cannot work out contributes no name and is
reported as a note instead of guessed at. An expression over contexts that are
fixed for the whole run still compares, so two jobs sharing one of those are
still caught.
2026-09-06 01:09:36 -07:00
mateo-berri
a72041b757 test(router): pin the retry skip list across attempts in a model group 2026-09-06 01:03:32 -07:00
Yuneng Jiang
b1cc1e8dae
test(ui): assert navigation through accessible links and page content 2026-09-06 00:59:18 -07:00
mateo-berri
d664ca139e chore(router): suppress the retry-skip kwargs writes and correct the filter docstring
The two writes that hand the skip list to the next attempt now carry a
`# rebind-ok` reason, which is the sanctioned escape hatch for an unavoidable
parameter mutation and matches how `log_retry` already writes into the same
kwargs dict a few lines above

`get_excluded_filtered_deployments`'s docstring said returning the unfiltered
list would re-include the deployment that just failed. The retry skip does
exactly that on purpose, so the docstring now says each caller decides what an
empty result means

The reliability registry cell the new e2e test claims is marked
`fail_before_fix: proven`: the same config returns 400 at the merge base and
200 off a sibling deployment at the tip
2026-09-06 00:53:24 -07:00
Yuneng Jiang
6b493cb61e
test(guardrails): pin the presidio spend-log record and the UI's masked-entity persistence
The presidio suite proved masking happened by reading the served answer, and the
UI suite proved the wizard's "Select All & Mask" produced a row. Neither checked
the thing an operator actually looks at afterwards: the audit trail.

Adds an e2e test asserting the spend log carries the pre_call guardrail record
for a masked request: status, provider, per-entity masked counts, and the
detected-entity list the dashboard's guardrail panel renders its scores from.
It keys off the x-litellm-applied-guardrails response header rather than masked
text in the answer, because whether the model echoes the prompt back is a model
decision, not a guardrail one. Both a mutation that stops writing the record and
one that empties the entity list turn it red.

Two records land on one log, pre_call and post_call, so the assertion selects on
mode as well as name; picking by name alone could hand it the empty post_call
record depending on write order.

On the UI side, the Presidio wizard test now reads the stored guardrail back and
asserts every persisted entity carries the MASK action. A row appearing in the
table did not prove the entity selection survived the save, so a wizard that
persisted an empty pii_entities_config would have passed.

SpendLogRow gains a typed metadata field. guardrail_response is left as object
because each provider writes its own shape there (presidio a list of entities,
bedrock an assessment object, a failed run the exception string); a union narrow
enough to be useful would fail to parse the others and break every suite that
reads a spend log. The caller validates the shape it expects with a TypeAdapter.

run_e2e.sh now pins PROXY_BASE_URL to the stack's own origin. The proxy builds
its post-login redirect from that variable when it is set, so a value inherited
from a developer's .env sent the browser off the relocated stack and the suite's
login step timed out on every port but 4000.
2026-09-06 00:43:13 -07:00