Commit graph

39428 commits

Author SHA1 Message Date
mateo-berri
1434ad3cc6
fix(jwt): guard litellm_jwtauth access in auth_jwt global path
JWTHandler() can be constructed without update_environment() being
called (tests do this directly), in which case self.litellm_jwtauth
does not exist. Accessing it raises AttributeError before getattr can
fall back. Use the same safe pattern other call sites use.
2026-05-21 03:52:11 +00:00
Cursor Agent
86cf3efd16
Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_feat/v1.84.0-mcp-gateway-jwt-auth
Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>
2026-05-21 03:50:06 +00:00
mateo-berri
91f44f3de9
fix(jwt,mcp): clarify issuers fallthrough + add TTL on mcp permission cache
- LiteLLM_JWTAuth.issuers docs now state explicitly that unlisted
  issuers fall back to the global JWT_AUDIENCE/JWT_ISSUER path; the
  field is additive routing, not an allow-list. Matches actual
  control flow in handle_jwt.auth_jwt and the regression tests
  asserting backwards compatibility with the global JWKS path.
- MCPRequestHandler._get_{org,agent}_object_permission now pass
  ttl=DEFAULT_MANAGEMENT_OBJECT_IN_MEMORY_CACHE_TTL on async_set_cache,
  mirroring the auth_checks.py pattern so the cache TTL is explicit
  on both DualCache layers.
2026-05-21 03:48:35 +00:00
mateo-berri
7905e996bd
fix(mcp,jwt): drop unneeded async helper + suppress misleading unscoped JWT warning
- _build_oauth_authorization_server_response: revert to sync (no awaits in body).
  The function only does dict construction and synchronous registry lookups;
  async added coroutine creation overhead per discovery call without need.
- _build_decode_kwargs: accept has_issuer_config so the global path's
  'JWT auth is unscoped' warning is suppressed when LiteLLM_JWTAuth.issuers
  provides per-issuer scoping. Previously the warning fired spuriously for
  admins who intentionally use only the new issuers config.
2026-05-21 03:42:53 +00:00
mateo-berri
31f8c56cb7
fix(mcp,tests): assert cold-start helper directly for aggregate /mcp
Threading client_ip into _target_servers_delegate_auth_to_upstream
made get_mcp_server_by_name(name, client_ip=...) also fire from the
delegate-auth check, so the call_args_list assertion on
client_ip-in-kwargs no longer uniquely signals a cold-start lookup.
Patch _is_mcp_passthrough_cold_start and assert it is not invoked,
which is the actual contract the test is pinning.
2026-05-21 03:38:27 +00:00
yuneng-jiang
37ef8d9059
fix(proxy): hydrate wildcard discovery credentials (#28284) (#28419)
* fix(proxy): hydrate wildcard discovery credentials

* fix(proxy): constrain wildcard credential hydration

Co-authored-by: Dibyo Mukherjee <dibyo@adobe.com>
2026-05-20 20:03:05 -07:00
yuneng-jiang
79a5a7abad
feat(tests): behavior-pinning harness + Key Tier-1 matrix (#28321)
* test(proxy_behavior): scaffold session-scoped async ASGI client + liveness smoke

Slice 2 of the management-endpoints behavior-pinning effort. New top-level dir
tests/proxy_behavior/management/ outside every existing pytest glob.

conftest.py initialises the proxy app once per session against the DATABASE_URL
the harness boots Postgres at, wraps it in httpx.AsyncClient via in-process
ASGITransport. The one smoke test asserts /health/liveliness returns 200, which
exercises the full FastAPI middleware stack against a real app — no mocks.

Plan: https://www.notion.so/36643b8acdab8128a581ced0f6a4744d

* test(proxy_behavior): connect prisma via real lifespan; key/generate de-risk

Slice 3 of the management-endpoints behavior-pinning effort. The fixture now
enters the real FastAPI lifespan (proxy_startup_event) instead of just calling
initialize() — that is where prisma_client is connected, password migration is
kicked off, and the rest of the startup wiring runs.

Tests pin the loop to the session scope so the AsyncClient created in the
session fixture and the prisma connection opened in the lifespan share the
same loop as the test bodies.

New de-risk smoke: POST /key/generate with the master key returns 200, the
returned sk- token resolves to a hashed row in LiteLLM_VerificationToken, and
the cleartext token is never stored. Proves auth + handler + helper + prisma
all wire together end-to-end against a real Postgres.

Plan: https://www.notion.so/36643b8acdab8128a581ced0f6a4744d

* test(proxy_behavior): seed 8-actor read-world for the authz matrix

Slice 4 of the management-endpoints behavior-pinning effort. New
``actors.py`` defines the actor enum + seeds an immutable world (2 orgs,
2 teams, 8 users, 8 verification tokens) under the ``behavior-pin-``
prefix so the rows are identifiable in psql and ``_wipe_world`` is
targeted.

Each actor key is created with its cleartext form generated locally and
its hashed form (via ``litellm.proxy.utils.hash_token``) stored in
``LiteLLM_VerificationToken`` — so the real ``user_api_key_auth`` accepts
the cleartext bearer token. Roles, ``team_id``, ``organization_id``, and
the service-account metadata flag are all set on the seeded rows so the
auth layer resolves the same scopes a real proxy would.

The session-scoped ``world`` fixture re-seeds at session start (idempotent
via wipe-then-create), and the smoke test confirms each of the 8 actor
keys can call ``/key/info`` on itself and receive its own row back.

Plan: https://www.notion.so/36643b8acdab8128a581ced0f6a4744d

* test(proxy_behavior): per-test scratch namespace + targeted delete_many teardown

Slice 5 of the management-endpoints behavior-pinning effort. Adds the
``scratch`` function-scoped fixture: each test gets a uuid4-derived
namespace prefix, tags writes with it (``key_alias``, ``team_alias``,
``user_id``, ``budget_id``), and the fixture teardown ``delete_many``-s
any row whose namespace column starts with that prefix.

Cleanup uses Prisma model methods only (no raw SQL, per CLAUDE.md) and
orders deletes children-before-parents to avoid FK conflicts. The Slice 3
de-risk smoke is migrated onto the same fixture so it stops accumulating
untagged tokens across repeated local runs.

Smoke proves both halves of the contract: one test writes a scratch-tagged
key and asserts it lands; a second test runs after the first's teardown
and asserts no rows in the scratch namespace survived.

Plan: https://www.notion.so/36643b8acdab8128a581ced0f6a4744d

* test(proxy_behavior): codify G3 (strict-import grep) as a pytest item

Slice 6 of the management-endpoints behavior-pinning effort. Two new tests
walk every .py file under tests/proxy_behavior/ and assert:

  * no ``from litellm.proxy.management_endpoints`` import — the suite is
    deliberately constrained to the HTTP boundary so it survives handler
    refactors;
  * no ``mock``/``patch`` on ``user_api_key_auth`` — mocking auth is the
    structural failure mode of the existing 11k-line mock suite, and the
    point of this harness is that the real auth layer runs.

Codifying G3 as a CI test removes the "did someone forget to check the
PR-description checklist" failure mode.

Plan: https://www.notion.so/36643b8acdab8128a581ced0f6a4744d

* style(proxy_behavior): apply black to G3 grep test

Follow-up to 6f588c753b — line-length fixes only, no behavior change.

* test(proxy_behavior): pin /key/generate authz matrix (18 scenarios)

Slice 7 of the management-endpoints behavior-pinning effort. Parametrized
matrix across two axes: actor (8 seeded) × target scope (self, team_alpha
in org_a, team_beta in org_b). 18 scenarios after dropping non-applicable
combos. Whole-suite wall-time stays at ~4.7s (well under the 10-min G2
budget for the eventual CI job).

While pinning, the test surfaced one seed gap: ``_get_user_in_team`` reads
``members_with_roles`` (a JSON list of ``{user_id, role}``), not the plain
``members`` String[]. Both columns are now populated in the seed to match
what the real ``/team/new`` handler would produce.

Expected status codes are intentionally heterogeneous (200, 400, 401)
because the current handler emits different statuses depending on which
check fails first (role gate, team-member-perm gate, "not assigned"
check). Pinning the *observed* codes — not what they "should" be — is
exactly the regression signal we want.

Plan: https://www.notion.so/36643b8acdab8128a581ced0f6a4744d

* test(proxy_behavior): pin /key/info authz matrix (24 scenarios)

Slice 8 of the management-endpoints behavior-pinning effort. 8 actors ×
3 target keys (own, OWNER's key in org_a, CROSS_ORG_USER's key in org_b)
covering self-read, same-team-peer read, and cross-org read.

Notable pinned behaviors (intentionally surfaced for review, not "fixed"):

  * ORG_ADMIN gets 403 on individual key info even within their own org
    — visibility is scoped to "your own keys" + "your team's keys", not
    "your org's keys".
  * Same-team peers (INTERNAL_USER, UNRELATED_SAME_ORG, SERVICE_ACCOUNT)
    DO see each other's keys. Whether that is desired is for the team
    to decide; this PR only pins the existing behavior so unintentional
    changes flip the matrix red.

Wall-time is unchanged (~4.3s for the slice on its own).

Plan: https://www.notion.so/36643b8acdab8128a581ced0f6a4744d

* test(proxy_behavior): pin /key/list default-visibility matrix (8 scenarios)

Slice 9 of the management-endpoints behavior-pinning effort. For /key/list
the response IS the matrix: each of the 8 seeded actors calls the endpoint
with default filters and the test asserts set-equality between the returned
visible-token set (filtered to seeded tokens only, so unrelated rows can't
flap the assertion) and a pinned expected actor-set.

Pinned default visibility:

  * PROXY_ADMIN sees all 8 actors' keys.
  * Every other actor sees only their own key — including ORG_ADMIN
    (which had broader expectations going in but currently behaves
    same-as-internal-user for /key/list defaults) and TEAM_ADMIN (no
    team-aggregation without include_team_keys=true).

Future changes that broaden or narrow any single actor's default
visibility will turn this matrix red — exactly the regression signal we
want. Parameter-driven views (include_team_keys, filters) are deferred to
Slice 13 / PR2 follow-up.

Plan: https://www.notion.so/36643b8acdab8128a581ced0f6a4744d

* test(proxy_behavior): pin /key/update authz matrix + mutation re-read (21 scenarios)

Slice 10 of the management-endpoints behavior-pinning effort. 8 actors ×
3 target shapes (self-owned, OWNER-scoped in org_a/team_alpha,
CROSS_ORG_USER-scoped in org_b/team_beta) = 21 applicable scenarios.

Each test:
  1. Master-key-seeds a fresh scratch key with the target's (user_id,
     team_id) scope (so the read-world stays untouched).
  2. Has the actor under test POST /key/update flipping ``models`` to
     a known marker list.
  3. Asserts the status code AND the DB row's ``models`` field — present
     when 200, unchanged otherwise — so a handler that silently mutates
     on a denied response surfaces red.

Observed gating (pinned, not endorsed):

  * PROXY_ADMIN bypasses every check.
  * ORG_ADMIN is blocked by an early role gate, always 401.
  * Every other (INTERNAL_USER-rolesed) actor hits one of three failure
    modes — 403 "user can only create keys for themselves", 403
    "only proxy admins, team admins, or org admins", or 401
    "team_member_permission_error" — depending on whether they own the
    target and whether they're a team admin / member of its team.

Plan: https://www.notion.so/36643b8acdab8128a581ced0f6a4744d

* test(proxy_behavior): pin /key/regenerate authz matrix + rotation contract (22 scenarios)

Slice 11 of the management-endpoints behavior-pinning effort. 21 matrix
scenarios (8 actors × 3 target shapes, minus the cross_org/owner combo
that exists in the seed but isn't applicable) plus one smoke for the
``/key/{key:path}/regenerate`` route registration.

On 200 outcomes the test verifies the full rotation contract:
  * the regenerate response key differs from the old cleartext,
  * the OLD cleartext returns 401 on a follow-up ``/key/info``,
  * the NEW cleartext returns 200 on a follow-up ``/key/info``.

On denied outcomes the test verifies the OLD cleartext still works —
catching any handler that mutates the token row on a failed call.

Pinned authz divergence vs /key/update: regenerate routes most denials
through the team-member-perm 401 path rather than the role-gate 403
path. The matrices for both endpoints are now in tree side-by-side, so
any future refactor that "harmonises" the codes will turn one of the two
red.

Plan: https://www.notion.so/36643b8acdab8128a581ced0f6a4744d

* test(proxy_behavior): pin /key/delete authz matrix + post-delete contract (21 scenarios)

Slice 12 of the management-endpoints behavior-pinning effort. Mirrors
slices 10/11. On success: cleartext can no longer authenticate
(handles both hard-delete and soft-delete to LiteLLM_DeletedVerificationToken).
On denial: row survives and cleartext still authenticates.

Notable behavior gap with /key/update: same-team peers (internal_user,
unrelated_same_org, etc.) get 403 on /key/delete for OWNER's key — i.e.
cannot delete each other's keys — whereas they CAN read each other's
keys (Slice 8). Delete is stricter than read. Pinned as-is.

Cumulative whole-suite wall-time is 5.9s for all 128 tests on the local
runner — well under the 10-min G2 budget for the CI job in Slice 13.

Plan: https://www.notion.so/36643b8acdab8128a581ced0f6a4744d

* ci(proxy-mgmt-behavior): add PR-triggered workflow for the behavior suite

Slice 13 of the management-endpoints behavior-pinning effort. New
workflow ``test-unit-proxy-mgmt-behavior.yml`` fires ``on: pull_request``
for the same branch set every other proxy unit-test workflow watches
(main, litellm_internal_staging, litellm_oss_branch, litellm_**).

It delegates to the existing reusable ``_test-unit-services-base.yml``
with ``enable-postgres: true``, which already provisions a postgres:14
service container and runs ``prisma db push`` against it before pytest
collects. ``reruns: 0`` because a behavior-pinning matrix that needs
reruns is itself a regression — flakes are signal.

``timeout-minutes: 15`` gives generous headroom over the local 5.9s
whole-suite wall-time; the binding G2 budget is 10 min.

Plan: https://www.notion.so/36643b8acdab8128a581ced0f6a4744d

* docs(proxy_behavior): G4 regression-replay table for Key Tier-1

Slice 14 of the management-endpoints behavior-pinning effort. Documents
the regression-replay verification methodology + a 12-row table mapping
recent fix-PRs touching key_management_endpoints.py to the catching
scenarios in the PR1 matrix.

One canonical RED→GREEN cycle is captured verbatim — c7c3df2b02
"extend /key/update admin check to non-budget fields". Under the
parent-of-fix code, 6 scenarios in test_key_update.py flip from 200 to
403; under HEAD code, all 21 pass. The handler swap is the only change
between the two runs, confirming the matrix catches the behavior shift
the fix introduced.

The table also calls out 4 genuine coverage gaps deferred to PR2/PR3:
404-on-missing-key, budget-limit counter assertions, /key/regenerate
upperbound enforcement, and /key/list filter-param views.

Plan: https://www.notion.so/36643b8acdab8128a581ced0f6a4744d

* chore(mutmut): include the behavior suite in tests_dir + G5 triage stub

Slice 15 of the management-endpoints behavior-pinning effort. Appends
``tests/proxy_behavior/management/`` to ``[tool.mutmut].tests_dir`` so
the existing mutation-test workflow runs against both the legacy mock
suite AND the new behavior suite — the latter is where the regression
signal will actually surface.

Adds a stub at ``tests/proxy_behavior/management/mutmut_triage/pr1.md``
documenting the G5 triage protocol (zero unreviewed survivors in the 6
Tier-1 handler functions) and a placeholder baseline-metrics table to
fill in after the first manually-triggered mutmut run completes — runs
take hours and run on a manual cadence, so PR1 ships with the wiring +
protocol, not the numbers. The actual baseline is recorded in a
follow-up once ``gh workflow run mutation-test.yml`` finishes.

The kill rate stays telemetry-only, never a gate. G5 (per-survivor
classification) is the binding mutation gate.

Plan: https://www.notion.so/36643b8acdab8128a581ced0f6a4744d

* docs(proxy_behavior): suite README with local-repro + conventions + gates

Slice 16 of the management-endpoints behavior-pinning effort. The README
documents:

  * The same three commands the CI workflow runs locally (BYO-DATABASE_URL,
    no new tooling).
  * Suite layout — what each test file covers, which slice it lands.
  * The asyncio loop_scope convention required for session fixtures
    (httpx AsyncClient + prisma connection) to share a loop with each
    test body.
  * G3 strict-import convention + the test that enforces it.
  * Read-world vs scratch-world fixture conventions.
  * Behavior-pinning philosophy: pin observed codes; flag, don't judge.
  * Where each G1–G5 + PR1.M1–M3 gate's evidence lives.

Plan: https://www.notion.so/36643b8acdab8128a581ced0f6a4744d

* ci(proxy-mgmt-behavior): drop xdist (workers=0) to fix seed race

First run on PR #28321 failed with UniqueViolation on
``behavior-pin-budget`` plus cascading missing-membership FK errors. Both
xdist workers entered ``seed_world()`` concurrently against the shared
Postgres service container; whichever lost the race left the world in a
half-seeded state and downstream tests ran against missing
team_membership rows.

Whole-suite wall-time is ~7s sequentially, so disabling xdist here costs
nothing — and the seed itself is the wrong place to add per-worker
isolation (the world is intentionally shared so set-equality assertions
in /key/list have a deterministic expected set).

* ci(proxy-mgmt-behavior): seed scratch keys via proxy_admin actor, not master

Second CI run failed: ``/key/generate`` with explicit ``user_id`` returned
403 "User can only create keys for themselves. Got user_id=X, Your ID=None"
in every test that called ``_create_scratch_key`` with a per-actor user_id.
The bare master key's auth path was producing ``user_id=None`` in the
fresh CI Postgres, which doesn't trigger the PROXY_ADMIN bypass in
``_user_can_only_create_keys_for_themselves`` reliably. Locally the same
master key path worked, masking the issue.

Fix: every ``_create_scratch_key`` helper now takes a seeder cleartext
and the test bodies pass ``world.keys[Actor.PROXY_ADMIN].cleartext``.
That actor was seeded with ``user_role=PROXY_ADMIN`` AND a concrete
``user_id``, so the bypass fires deterministically in both environments.

No behavior shift in the matrices themselves — all 128 scenarios still
pass locally; only the setup helper's auth identity changed.

The bare-master smoke (test_smoke + test_scratch_teardown) is intentionally
left on the master key path: those tests don't pass ``user_id`` in the
body so they don't hit the user_id-mismatch gate.

* ci(proxy-mgmt-behavior): diag — run world-seed test first + bump max-failures

Third CI run failed identically: seeded PROXY_ADMIN actor's auth resolves
to ``user_id=None`` even though the DB row has the right ``user_id``. The
suite was aborting at maxfail=10 inside test_key_delete, so test_world_seed
(which would tell us whether the seed itself is reachable) never ran in CI.

Two diagnostic moves on this push, no behavior change:

  * Rename ``test_world_seed.py`` → ``test_aaa_world_seed.py`` so it's
    the first collected file. If it passes in CI we know the seed is
    fine and the bug lives downstream; if it fails the same way the
    bug is in the auth resolution path.
  * Bump ``max-failures`` to 200 for this workflow so we see the full
    failure surface instead of stopping at the first cascading setup
    error. Will tighten back down once the suite is green.

Adds one new test ``test_proxy_admin_actor_can_create_keys_for_others``
that explicitly exercises the PROXY_ADMIN bypass via /key/generate with
an explicit user_id — the same shape the matrix setup helper uses but
without the matrix machinery muddying the diagnostic.

* ci(proxy-mgmt-behavior): await LiteLLM_VerificationTokenView creation in fixture

Fourth CI run still failed because the proxy's lifespan kicks off
``prisma_client.check_view_exists()`` as a fire-and-forget background
task — that task is what creates ``LiteLLM_VerificationTokenView``, the
SQL view ``user_api_key_auth`` queries to resolve a token to its
user_id / user_role / team.

On a fresh Postgres (CI), the first test races the background task. The
view doesn't exist when the first auth call runs, the resolver falls
through to a degraded path that returns ``user_id=None``, and every
matrix test that depends on the seeded actor's identity then fails
confusingly with "Got user_id=X, Your ID=None" 403s. Locally the view
persists across pytest runs so the race is invisible.

Fix: await ``prisma_client.check_view_exists()`` explicitly inside the
session ``proxy_app`` fixture, after the lifespan enters but before the
fixture yields. Deterministic regardless of whether the underlying DB is
fresh (CI) or warm (local).

* ci(proxy-mgmt-behavior): widen diagnostic to dump token / user / view shape

The fifth CI run isolated the failure to ``/key/generate`` with explicit
user_id while ``/key/info`` works for the same seeded PROXY_ADMIN actor.
The auth context's user_id is None even though the DB row has it set.

This commit widens the diagnostic test: on failure, dump the raw token
row's user_id, the user row's user_role, and what
``LiteLLM_VerificationTokenView`` actually returns for the seeded token.
If the view returns user_id=None we know the view shape is the problem;
if the view returns the right user_id we know it's a downstream code
path stripping it.

* ci(proxy-mgmt-behavior): unambiguous diagnostic view query

Previous diagnostic's raw SQL had an ambiguous user_id column from
joining the view with the user table, so the diagnostic itself crashed
before printing useful state. Simplified to query just the view's columns.

* ci(proxy-mgmt-behavior): add auth-resolver chain diagnostic

Six runs and the underlying data (token row, user row, view row) all
verified correct in CI, but auth still returns user_id=None. This
diagnostic calls the resolver primitives directly:

  1. ``prisma.get_data(table_name="combined_view")`` → raw view object
  2. ``get_key_object(...)`` → cached/DB UserAPIKeyAuth
  3. ``get_user_object(...)`` → LiteLLM_UserTable row
  4. ``_is_user_proxy_admin`` / ``_get_user_role``

and prints each intermediate via captured stdout (-s). Whichever step
returns None/False in CI is where the chain breaks. Imports come from
``litellm.proxy.auth`` (not management_endpoints), so G3 still passes.

* ci(proxy-mgmt-behavior): set LITELLM_MASTER_KEY env so lifespan doesn't wipe it

Real root cause of every CI run that returned ``Your ID=None`` for the
seeded actors:

  * In ``initialize()``, ``master_key`` is set from the config YAML's
    ``general_settings.master_key`` (load_config code path at
    proxy_server.py:4174).
  * Then the FastAPI lifespan (``proxy_startup_event``) runs and at line
    776 does ``master_key = get_secret_str("LITELLM_MASTER_KEY")``,
    which UNCONDITIONALLY overwrites the global.
  * In CI the env var is unset, so the post-lifespan ``master_key`` is
    None.

Downstream every auth path degrades: master-key requests don't bypass
because ``secrets.compare_digest(api_key, None)`` raises and is caught
to ``is_master_key_valid=False``; seeded-actor requests cache a
``UserAPIKeyAuth`` whose ``user_role`` never resolves through the
PROXY_ADMIN bypass; ``_is_allowed_to_make_key_request`` then hits the
``user_id`` mismatch path with ``Your ID=None``.

Locally my shell happened to have ``LITELLM_MASTER_KEY`` set from a prior
session, which is why every local run was green and CI red — exactly the
"don't generalize from your environment to CI" memory.

Fix: ``os.environ.setdefault("LITELLM_MASTER_KEY", MASTER_KEY)`` and
``os.environ.setdefault("CONFIG_FILE_PATH", config_path)`` before
entering the lifespan, so its re-read produces the same value as
``initialize()``.

Whole-suite still green locally (130 tests, ~6.4s).

* ci(proxy-mgmt-behavior): force premium_user=True so /key/regenerate isn't gated

Ninth CI run cleared every ``Your ID=None`` failure (the master_key env
fix worked end-to-end) and exposed the next thin layer of failures:
``/key/regenerate`` returns 500 "Regenerating Virtual Keys is an
Enterprise feature" in CI because the proxy can't see a
``LITELLM_LICENSE``. Locally my license is set, so the matrix passes.

The behavior matrix is supposed to pin authz, not licensing — so flip
``proxy_server.premium_user = True`` directly, both before and after the
lifespan (the lifespan re-runs ``_license_check.is_premium()`` and would
otherwise reset it). With premium gating disabled, the regenerate matrix
exercises the same authz path /key/update does.

Whole-suite still green locally (130 tests, ~6.3s).

* test(proxy_behavior): trim debug diagnostics, restore default max-failures

Followup to the CI-bring-up sequence: now that the suite is green in CI
(130 → 129 tests after this trim; 156s wall-time on ubuntu-latest), drop
the diagnostic noise left over from debugging the master_key wipe:

  * Rename ``test_aaa_world_seed.py`` back to ``test_world_seed.py`` —
    no longer needs to run first.
  * Remove ``test_auth_resolver_returns_correct_user_id_and_role`` —
    that test reached into private auth helpers to localize the bug
    between the DB and ``UserAPIKeyAuth``; it has served its purpose
    and isn't HTTP-boundary.
  * Keep ``test_proxy_admin_actor_can_create_keys_for_others`` (without
    the failure-time dump) — it's a real authz contract that pins the
    PROXY_ADMIN bypass on /key/generate, and would catch a regression
    of the same conftest interaction this sequence revealed.
  * Drop the workflow's ``max-failures: 200`` override — that was a
    debug aid for seeing the full failure surface in CI. Default of 10
    is right for a stable suite.

* chore(proxy_behavior): drop empty mutmut triage stub, fold protocol into README

The mutmut_triage/pr1.md file was a placeholder for numbers and
classifications that don't exist yet — the first mutmut run is a manual
follow-up. Empty stubs aren't evidence; deleting it.

The G5 protocol (run the workflow, triage survivors in the six Tier-1
handler functions, kill-or-accept-with-reason, zero unreviewed) moves
into the suite README's "Gate evidence" block. The real triage file
will land alongside the first mutmut follow-up.

pyproject.toml's [tool.mutmut].tests_dir entry stays — that's the
one-line wiring that makes the existing (manual-trigger) mutation-test
workflow include our suite next time someone runs it. Comment updated
to drop the dead file reference.

* chore(proxy_behavior): drop README + trim comments

Removes the suite README — its contents (local repro, layout, conventions)
were either restated by the file structure or already covered by the
workflow YAML and pyproject.toml. Trims docstrings and inline comments
across every test file to keep only non-obvious WHY (the masking
``_get_user_in_team`` reads, the LiteLLM_VerificationTokenView models-can't-
be-NULL gotcha, the org_admin/peer-visibility surprise, the rotation
contract).

Suite still 129 green locally.

* test(proxy_behavior): address Greptile review — env force, pagination, dedup

- conftest: force LITELLM_MASTER_KEY / CONFIG_FILE_PATH unconditionally
  instead of setdefault. An ambient LITELLM_MASTER_KEY with a different
  value would make the proxy authenticate on that key while the tests
  still send MASTER_KEY → silent 401s.
- test_key_list: paginate /key/list instead of a single size=100 request.
  size is capped at 100 by the endpoint, so on a non-fresh DB a single
  page could truncate PROXY_ADMIN's view and a seeded key could fall off
  the page. Walk total_pages.
- conftest: hoist the duplicated _create_scratch_key helper (copy-pasted
  and already diverged across test_key_{update,regenerate,delete}.py)
  into a single shared create_scratch_key.
- Delete regression_replay/README.md — G4 regression-replay evidence
  belongs in the PR description, not a committed doc file (repo docs
  policy + the effort's own plan both say so). Content moved to the PR.
2026-05-20 19:27:44 -07:00
yuneng-jiang
e23d06dda4
test(realtime): expect session.created as xAI realtime initial event (#28424)
xAI's Grok Voice Agent API now sends session.created as its first
realtime event (matching OpenAI), followed by conversation.created.
The E2E canary pinned the old conversation.created value and failed.

LiteLLM's xAI realtime path is a verbatim passthrough (provider_config
is None, raw forwarding), so the event ordering is xAI's own — no
transformation on our side. Update the pinned expected value and the
now-stale comments to match the current API behavior.
2026-05-20 19:01:31 -07:00
yuneng-jiang
f99fb5f27f
chore(ci): merge dev branch (#28314)
* chore(proxy): strict media-type match for form bodies (#27939)

* chore(proxy): strict media-type match for form bodies

``_read_request_body`` and ``get_request_body`` routed on
``"form" in content_type`` / ``"multipart/form-data" in content_type``,
which match any header containing the literal — ``application/form-json``,
``multiform/anything``, ``application/json; xform=1``. Starlette's
``request.form()`` returns an empty ``FormData`` for any non-canonical
type without consuming the body, so the auth-time pre-read saw ``{}``
and skipped the banned-param check while the handler's later
``request.body()`` saw the original JSON payload.

Parse the media type per RFC 7231 (substring before ``;``, trimmed,
lowercased) and accept only ``application/x-www-form-urlencoded`` and
``multipart/form-data``. Replace both substring sites with the shared
``_is_form_content_type`` helper.

Tests pin: case/whitespace/charset variants of the two real types
match; ``application/form-json`` and similar substring-match traps
fall through to the JSON parse path; real form POSTs continue to
route through ``request.form()``.

* chore(proxy): extract _is_json_content_type symmetric helper

Mirror ``_is_form_content_type`` for the JSON branch of
``get_request_body`` so both classifications share the same media-type
normalisation (strip params, trim, lowercase) and any future change
to the parsing rules has one place to update.

Adds tests for ``_is_json_content_type`` and for ``get_request_body``
covering the canonical JSON / form / unsupported / non-POST paths.

* chore(proxy): surface form-parse failures instead of caching empty body

Starlette's ``request.form()`` raises ``MultiPartException`` /
``ValueError`` / ``AssertionError`` on malformed multipart input
(missing boundary, malformed chunk encoding, etc.). The outer
``except Exception: return {}`` swallowed every form-parse failure
and cached an empty parsed body — auth-time pre-reads saw ``{}`` and
skipped every banned-param check while a later raw-body re-read in
the handler still saw the original payload. Same TOCTOU shape as the
substring-match bypass: the auth gate and the handler don't agree on
what the body is.

Wrap ``request.form()`` in a narrow ``try`` that converts any parse
failure to a 400 ``ProxyException``. The outer broad ``except`` is
retained for unrelated unexpected errors but no longer covers
form-parse-side bypass shapes.

Adds a regression test parametrised over the exception classes
Starlette can raise from ``request.form()``.

* chore(proxy): drop redundant _is_json_content_type test class

``_is_json_content_type`` is a 3-line wrapper around the shared
``_normalize_media_type`` helper. Positive coverage lives in
``TestGetRequestBody.test_json_with_charset_param_parses_as_json``;
negative coverage is covered transitively by
``TestIsFormContentType``'s non-form parametrize matrix (anything that
isn't a form type falls through to the JSON branch).

* chore(proxy): carry ASGI path into WebSocket auth synthetic Request (#27940)

``user_api_key_auth_websocket`` built a synthetic ``Request`` with a
two-key scope (``type`` + ``headers``) and set ``request._url =
websocket.url``. ``get_request_route`` reads ``scope.get("path", ...)``
and falls back to ``request.url.path`` only when ``path`` is absent.
For the WebSocket flow that fallback fires and resolves to the
Host-header-derived value (Starlette reconstructs ``websocket.url``
from the Host header), so a malformed Host collapses the resolved
route and lets the auth gate compare against the wrong value.

Carry the ASGI scope's ``path``, ``root_path``, and ``app_root_path``
into the synthetic scope so the lookup never reaches the fallback on
the legitimate path.

Regression test pins that the request handed to ``user_api_key_auth``
has ``scope["path"]`` equal to the ASGI scope's path.

---------

Co-authored-by: stuxf <70670632+stuxf@users.noreply.github.com>
2026-05-20 17:47:33 -07:00
Yassin Kortam
35520adb4f
fix: serialize guardrail_response to JSON in OTEL traces (#28362)
* fix: serialize guardrail_response to JSON in OTEL traces

Guardrail spans previously set the `guardrail_response` attribute via
`safe_set_attribute`, which let dict payloads reach the OTEL exporter as
Python repr strings. Downstream log pipelines could not parse those as
JSON, breaking metric creation from guardrail traces.

Serialize `guardrail_response` with `safe_dumps` before setting the
attribute, matching how `masked_entity_count` is already handled.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: cover dict-serialization and None-skip for guardrail_response

Address Greptile feedback on #28362 — add explicit coverage for the
two behavioral guarantees of this fix:

- Dict payloads (the OpenAI moderation case in the report) reach the
  span as a JSON string, not a Python repr.
- ``None`` guardrail_response skips the attribute entirely, so no
  ``"null"`` leaks into traces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Yassin Kortam <yassinkortam@g.ucla.edu>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 17:34:36 -07:00
Sameer Kankute
988196911a
Litellm oss staging 1 (#28337)
* feat: add Xiaomi MiMo-V2.5-Pro and MiMo-V2.5 OpenRouter model entries (#27700)

Squash-merged by litellm-agent from TorvaldUtne's PR.

* fix(ui): trim whitespace from MCP inspector tool call inputs (#28203)

Co-authored-by: shin-berri <shin-laptop@berri.ai>
Co-authored-by: yuneng-jiang <yuneng@berri.ai>

* gemini-3.1-flash-lite pricing (#27933)

* feat(model_prices): add gemini-3.1-flash-lite pricing with standard/batch/flex/priority tiers

* fix pricing

* add service tier

---------

Co-authored-by: shin-berri <shin-laptop@berri.ai>

* fix: incorrect /v1/agents request example (#28131)

* fix(anthropic): accept dict-shape reasoning_effort from Responses bridge (#28201)

* fix(anthropic): accept dict-shape reasoning_effort from Responses bridge

Issue #28196 — the Responses->Chat parser (transformation.py:184-200) keeps the full dict as reasoning_effort when summary is set; that branch was added in #25359. But the Anthropic transformation here still guarded on isinstance(value, str), silently dropping the param. Result: callers using the standard Reasoning(effort, summary) OpenAI-shaped object on Anthropic lose thinking entirely (0 reasoning_tokens, no thinking_blocks).

Coerce dict -> string before mapping. Same shape tolerance that gpt_5_transformation._normalize_reasoning_effort_for_chat_completion already implements. summary is irrelevant for Anthropic's thinking_blocks.

Adds two regression tests: one parametrized over string + dict shapes (with and without summary), one covering unparseable dict inputs (drops silently, no crash).

* test(anthropic): add non-adaptive model coverage for dict-shape reasoning_effort

Per Greptile feedback on PR #28198: the original regression test only exercised the adaptive (4.6+) path. Add a parametrized test for the non-adaptive branch (claude-sonnet-4-5) verifying that dict-shape reasoning_effort still maps to thinking.type='enabled' + budget_tokens, and that output_config is NOT set on pre-4.6 models.

* test(anthropic): convert unparseable-dict test to @pytest.mark.parametrize

Per @greptile-apps inline review on PR #28201 — matches the parametrize style of the two adjacent dict-shape tests and produces clearer failure messages (test ID per case instead of one collapsing for-loop).

* feat: add pricing entry for openrouter/google/gemini-3.1-flash-lite (#28280)

Squash-merged by litellm-agent from ro31337's PR.

* fix(router): wrap aresponses streaming iterator for mid-stream fallbacks (#28215)

Squash-merged by litellm-agent from cwang-otto's PR.

* fix(router): unblock staging — mypy + coverage for aresponses streaming fallback (#28318)

Squash-merged by litellm-agent from cwang-otto's PR.

* fix(responses): forward timeout on completion transformation path (Anthropic, Bedrock, Vertex) (#28133)

Squash-merged by litellm-agent from cwang-otto's PR.

* feat(ui): add pause/resume Switch to the models table (#28151)

Squash-merged by litellm-agent from Cyberfilo's PR.

* fix(responses): merge sync completion kwargs to avoid duplicate keys

Double-splatting litellm_completion_request and kwargs raised TypeError
when metadata or service_tier were set. Match the async merge pattern.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Use proxy base URL for CLI SSO form action (#28271)

Co-authored-by: shin-berri <shin-laptop@berri.ai>
Co-authored-by: yuneng-jiang <yuneng@berri.ai>

* fix(tests): add mistral/ministral-8b-2512 to cost map and backfill in conftest

Mistral rotated the 'mistral/mistral-tiny' alias to return
'ministral-8b-2512' as the response model, which was missing from the
cost map. This caused test_completion_mistral_api and
test_completion_mistral_api_modified_input to fail in
litellm.completion_cost lookup.

- Add mistral/ministral-8b-2512 entry to both the in-tree
  model_prices_and_context_window.json and the bundled
  litellm/model_prices_and_context_window_backup.json (mirrors the
  existing openrouter/mistralai/ministral-8b-2512 pricing).

- litellm.model_cost is loaded at import time from the URL pinned to
  main, so the new backup entry isn't visible at test runtime until
  it also lands on main. Backfill any entries missing from the
  remote-fetched map into litellm.model_cost in the local_testing
  conftest so cost-calculator lookups succeed on this branch.

* fix(tests): drop unnecessary del of conftest backfill loop vars

* fix(router): harden streaming fallback wrapper for bridge iterators

- FallbackResponsesStreamWrapper now uses getattr fallbacks when copying
  attributes from the source iterator. The bridge path
  (LiteLLMCompletionStreamingIterator used by Anthropic/Bedrock/Vertex)
  does not call super().__init__ and is missing response, logging_obj
  (it uses litellm_logging_obj), responses_api_provider_config,
  start_time, request_data, call_type, and _hidden_params. Previously,
  wrapper construction raised AttributeError for any streaming fallback
  on the bridge path.
- _aresponses_with_streaming_fallbacks now deep-copies the
  litellm_metadata (and metadata) dicts into fallback_kwargs. The
  primary attempt mutates this dict in place via
  _update_kwargs_with_deployment, so a shallow copy of kwargs was
  leaking primary-deployment fields (deployment, model_info, api_base)
  into the mid-stream fallback request.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix(router): use safe_deep_copy for fallback metadata snapshot

The ban_copy_deepcopy_kwargs CI check rejects copy.deepcopy() on any
variable whose name contains 'kwargs' (incl. fallback_kwargs). Swap
the two copy.deepcopy(fallback_kwargs[...]) calls for safe_deep_copy,
which handles non-picklable values (OTEL spans, etc.) by per-key
deepcopy with fallback to the original reference.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* test(ci): skip chronically flaky build_and_test integration tests

Both tests have been failing on every recent run of build_and_test
against this PR's HEAD (1686967, 1688402, 1689993, 1690877), and the
same two tests also fail intermittently on unrelated commits and other
branches, independent of any code change in this PR (which only touches
router fallback wrappers, the Anthropic Responses bridge, and unrelated
UI/cost-map files).

- tests.test_spend_logs.test_spend_logs: /spend/logs?request_id=...
  returns 500 even after a 20s wait for the spend log to be written.
  Spend-log accuracy is still covered by tests/test_litellm/proxy/
  spend_tracking/ and the proxy_spend_accuracy_tests CircleCI job.

- tests.test_team_members.test_add_multiple_members: /team/info?team_id=
  ... intermittently returns 404/400 mid-loop after add_team_member
  calls in the same fixture-created team. Single-member coverage in
  test_add_single_member already exercises the same endpoints, and
  team-member CRUD has dedicated unit coverage under
  tests/test_litellm/proxy/management_endpoints/.

Skipping unblocks the build_and_test job until the underlying race in
the dockerized integration setup is root-caused.

* fix: preserve explicit timeout=0 in responses API handler

Use 'timeout if timeout is not None else request_timeout' instead of
'timeout or request_timeout' so an explicit timeout=0/0.0 isn't silently
replaced by the default request_timeout.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix(ui): guard model_info access in pause Switch with optional chaining

* fix(ui): guard model_info access in pause Switch onChange handler

Mirror the optional-chaining guard already applied to the isPausing
check so a config-model row with a missing model_info cannot throw
when the toggle's onChange fires.

---------

Co-authored-by: TorvaldUtne <78661304+TorvaldUtne@users.noreply.github.com>
Co-authored-by: oss-agent-shin <ext-agent-shin@berri.ai>
Co-authored-by: shin-berri <shin-laptop@berri.ai>
Co-authored-by: yuneng-jiang <yuneng@berri.ai>
Co-authored-by: mubashir1osmani <mubashir.osmani777@gmail.com>
Co-authored-by: Isha <72744901+IshaMeera@users.noreply.github.com>
Co-authored-by: cwang-otto <chengxuan.wang@ottotheagent.com>
Co-authored-by: Roman Pushkin <roman.pushkin@gmail.com>
Co-authored-by: Filippo Menghi <113345637+Cyberfilo@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: boarder7395 <37314943+boarder7395@users.noreply.github.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Yassin Kortam <yassin@berri.ai>
2026-05-20 17:27:03 -07:00
Yassin Kortam
2f9ac77b24
fix(proxy): expose Prisma idle/connect timeout + extra DB URL params (#28395)
* fix(proxy): expose Prisma idle/connect timeout + extra DB URL params

Operators have reported large numbers of idle Prisma connections that
never get closed. The proxy already forwards `connection_limit` and
`pool_timeout` to the DATABASE_URL, but had no knob for capping idle
or slow connections. Add three new `general_settings` keys that thread
through to the DATABASE_URL / DIRECT_URL query string:

- `database_connect_timeout`  -> Prisma `connect_timeout`
- `database_socket_timeout`   -> Prisma `socket_timeout` (the main
  knob for closing idle connections from the LiteLLM side)
- `database_extra_connection_params` -> untyped passthrough dict for
  any other Prisma URL param (`pgbouncer`, `statement_cache_size`,
  `sslmode`, ...); keys here override LiteLLM defaults.

Refactors the duplicated DATABASE_URL/DIRECT_URL param dicts into a
single `_build_db_connection_url_params` helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Update litellm/proxy/proxy_cli.py

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

---------

Co-authored-by: Yassin Kortam <yassinkortam@g.ucla.edu>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-05-20 17:19:24 -07:00
Mateo Wang
8acf64e16c
fix(interactions): never drop streamed text deltas; always emit terminal completion (#28394)
* fix(interactions): never drop streamed text deltas; always emit terminal completion

The interactions streaming bridge had two bugs flagged by Greptile on PR #28153:

1. The first OutputTextDeltaEvent (and the second, when no ResponseCreatedEvent
   precedes the deltas) was consumed to emit a synthetic interaction.created /
   step.start event, but the chunk's text payload was never forwarded as a
   step.delta. The text only reappeared in the terminal step.stop, which
   defeats the purpose of incremental streaming.

2. When the upstream Responses API stream ended via StopIteration without a
   ResponseCompletedEvent, the iterator emitted step.stop but never the
   terminal interaction.completed event carrying the full collected text.

This refactors the iterator to translate each upstream chunk into a list of
events (instead of a single event) and buffers them in a deque. A text delta
now expands into [interaction.created, step.start, step.delta] on the first
chunk so no token is dropped, and the StopIteration / StopAsyncIteration
fallback always flushes a terminal interaction.completed event when one
hasn't already been sent.

Both behaviors are covered by new unit tests:
- test_no_text_token_is_dropped_during_streaming
- test_response_created_then_text_delta_emits_step_start_and_delta
- test_stop_iteration_fallback_emits_completion_event
- test_response_completed_emits_stop_then_completion (no double-emit)

Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>

* fix(interactions): correlate EOF terminal events with stream's interaction id

The StopIteration fallback path previously built the terminal step.stop /
interaction.completed events with id=None (legacy content.stop) and a
memory-address fallback string (interaction.completed), neither of which
matched the item_id used by the earlier interaction.created / step.start /
step.delta events in the same stream. Downstream consumers correlating
events by id would see a mismatch.

Persist the interaction id derived from the first upstream chunk (item_id
on an OutputTextDeltaEvent, or response.id on a ResponseCreatedEvent) and
reuse it when flushing the terminal events on EOF.

Author: mateo-berri <277851410+mateo-berri@users.noreply.github.com>

* ci(windows): raise UV_HTTP_TIMEOUT to 300s for uv sync

The using_litellm_on_windows job has been hitting flaky PyPI download
timeouts during 'uv sync --frozen --group dev' — different packages on
each rerun (six, pydantic-core), all surfacing the same uv error:

  Failed to download distribution due to network timeout.
  Try increasing UV_HTTP_TIMEOUT (current value: 30s).

uv's default 30s per-request timeout is too tight for the Windows runner
on this project (50+ deps, several multi-MB wheels), so bump it to 300s
to let slow individual downloads complete instead of failing the build.

* fix(interactions): correlate ResponseCompletedEvent terminal events with stream's interaction id

When a stream starts directly with OutputTextDeltaEvent (no preceding
ResponseCreatedEvent), interaction.created carries item_id while
interaction.completed previously carried response.id from
ResponseCompletedEvent. The two ids can differ, leaving consumers that
correlate events by id unable to match the start and completion events.

Fall back to self._interaction_id (set on the first chunk that derives
an id) before response.id, mirroring the EOF terminal path.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>
2026-05-20 16:41:40 -07:00
Sameer Kankute
718c4637a8
feat(mcp): allow native MCP OAuth support for cursor (#28327)
* feat(mcp): allow native MCP OAuth redirect URIs (cursor://)

Discoverable OAuth /authorize rejected cursor:// callbacks because
validate_trusted_redirect_uri only accepted http/https. Add an
allowlisted native path with a built-in Cursor default and optional
MCP_TRUSTED_NATIVE_REDIRECT_URIS env for other clients.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): address Greptile native redirect URI review

Lowercase paths in normalizer so env allowlist entries match case-
insensitively. Tighten wildcard prefix matching to reject sibling
paths (e.g. callback-2) unless the prefix ends with /.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): reject query params on native OAuth redirect URIs

Greptile: normalization stripped query strings before allowlist compare,
so cursor://.../callback?injected=... could pass validation. Reject any
native redirect_uri with a query component (same as fragments).

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(model_cost_map): add mistral/ministral-8b-2512 entry

Mistral rotated the 'mistral/mistral-tiny' alias to return
'ministral-8b-2512' as the response model, which is not in the cost map.
This caused test_completion_mistral_api and
test_completion_mistral_api_modified_input to fail in
completion_cost lookup. Add the entry mirroring the existing
openrouter/mistralai/ministral-8b-2512 pricing.

* fix(mcp): lowercase default native redirect URIs

Make _parse_trusted_native_redirect_uris apply the same lowercasing
to built-in defaults as it does to env-var entries.

* fix(tests): backfill local model_cost into remote-fetched map

litellm.model_cost is loaded at import time from the URL pinned to main,
so pricing entries that exist only in this branch (e.g.
mistral/ministral-8b-2512, freshly added because Mistral now returns this
id from mistral-tiny) are absent at test time and completion_cost lookups
raise. Backfill the in-tree backup so cassette-driven cost calculations
resolve against the entries that ship with the branch under test.

Fixes the local_testing_part1 failures on test_completion_mistral_api and
test_completion_mistral_api_modified_input.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
2026-05-20 15:28:44 -07:00
Sameer Kankute
fecf212d70
fix(vertex_ai): omit function_call id on Vertex Gemini 3.5+ tool turns (#28324)
* fix(vertex_ai): omit function_call id on Vertex Gemini 3.5+ tool turns

Vertex AI rejects `id` on function_call/function_response parts; only Google AI Studio accepts it for Gemini 3.5+ strict tool matching.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Update litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* fix(vertex_ai): forward custom_llm_provider in context caching

Pass custom_llm_provider through to _gemini_convert_messages_with_history
in the context caching path so Gemini 3.5+ tool-call `id` forwarding
behaves consistently between cached and non-cached completions on Google
AI Studio.

Co-authored-by: Claude <claude@anthropic.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <claude@anthropic.com>
2026-05-20 14:52:50 -07:00
ryan-crabbe-berri
f82ff7ee2a
test(ui-e2e): admin key creation with a specific proxy model (#28365)
* test(ui-e2e): add admin key creation with a specific proxy model

Adds Playwright coverage for creating a key (no team) scoped to a single
proxy model, complementing the existing All-Proxy-Models test. Uses a
DOM-dispatched click on the antd dropdown option since the popup
animation can render the option outside the viewport.

* test(ui-e2e): verify scoped key works against mock /chat/completions

Extend the "Create a key with a specific proxy model" test to extract
the new key from the success modal and POST to /chat/completions for
the scoped model, asserting 200 and the mock response body. Without
this the test could pass even if the model selection failed to register.
2026-05-20 13:37:17 -07:00
Sameer Kankute
f3a669fc5d
feat(interactions): migrate to Google Interactions API steps schema (May 2026) (#28153)
* feat(interactions): migrate to Google Interactions API steps schema (May 2026)

Default to Api-Revision: 2026-05-20 (new `steps` schema). Add
`litellm.use_legacy_interactions_schema` global flag that sends
Api-Revision: 2026-05-07 for operators who need the legacy `outputs`
schema until June 8, 2026.

- Inject Api-Revision header in GoogleAIStudioInteractionsConfig.validate_environment()
- Auto-coalesce response_mime_type → response_format and image_config migration on new schema
- Add steps field to InteractionsAPIResponse and InteractionsAPIStreamingResponse
- Add StepStart/StepDelta/StepStop/InteractionCreated/etc. SSE event types
- Update streaming completion detection to handle interaction.completed event
- Bridge transformer populates both outputs and steps fields
- Bridge streaming iterator emits new-schema events by default

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(interactions): address greptile review feedback

- Avoid mutating caller's generation_config dict by shallow-copying
  before popping image_config, preventing silent failures on retries
- Skip schema key in response_format when response_format is None to
  avoid sending schema: null to the Google Interactions API
- Remove delta field from step.stop events (new schema only); the
  StepStop model has no delta field and sending it duplicates already-
  streamed text and breaks spec-conformant clients

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(proxy): parse use_legacy_interactions_schema string values safely

bool("false") returns True in Python, so quoted YAML values like
"false" or "False" silently activated the legacy Interactions API
schema. Match the env-var parsing pattern in litellm/__init__.py by
treating string inputs as true only when they equal "true" (case
insensitive).

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix(interactions): only set object/id/delta on step.stop for legacy schema

StepStop (new schema) has no object, id, or delta fields. Setting them
unconditionally caused spec-breaking extra fields on new-schema step.stop
events in all four construction sites (sync/async × main-loop/StopIteration).

Legacy content.stop still receives id, object, and delta unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(interactions): stabilize streaming bridge schema, dict aliasing, and lost first delta

- Capture use_legacy_interactions_schema once at iterator construction so
  all events emitted by a single stream use a consistent schema, even if
  the global flag is mutated mid-stream.
- Check for the buffered interaction.complete/completed event before the
  finished check in __next__/__anext__ so the final completion event
  (which carries the full collected text in steps) is not dropped after
  self.finished is set.
- Copy text content entries before appending to both outputs and the
  steps content list to avoid shared mutable dict aliasing between the
  two response fields.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix tests

* fix greptile review

* fix(interactions): address Greptile P1 review on schema coalescing and legacy deltas

Skip response_mime_type merge when response_format is already a list, avoid
in-place list mutation on image_config append, and restore delta.type on
legacy content.delta events.

Co-authored-by: Cursor <cursoragent@cursor.com>

* style(interactions): black-format gemini transformation.py

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Yassin Kortam <yassin@berri.ai>
Co-authored-by: Claude <noreply@anthropic.com>
2026-05-20 13:32:12 -07:00
Sameer Kankute
68efe6970c
fix(mcp): JWT on tools/list and REST tools/call server resolution (#28227)
* fix(mcp): JWT on tools/list, REST server_id resolution, tool_server_mismatch

Sign outbound MCP JWTs for list_mcp_tools and inject headers on the tools/list
path. Resolve server_id on /mcp-rest/tools/call and return 403 tool_server_mismatch
when the tool does not belong to the requested server. Default missing arguments to {}.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): restrict list JWTs to mcp:tools/list and default REST arguments to {}

- List-only JWTs (call_type=list_mcp_tools) no longer carry the broad
  mcp:tools/call scope. _build_scope() now emits only mcp:tools/list
  when no tool name is provided, mirroring the existing least-privilege
  rule that tool-call JWTs omit mcp:tools/list.
- REST /tools/call now defaults a missing 'arguments' field to {} so
  execute_mcp_tool() and downstream **arguments / .keys() calls don't
  receive None and crash with TypeError/AttributeError.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix(mcp): validate tool/server in call_tool; skip JWT signer when not configured or static auth present

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix(mcp): align tests and mypy with user_api_key_auth on tools/list

Update mocks for the new _get_tools_from_server parameter, mock server
registry in REST access-denied test, and narrow static_headers for mypy.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(test): accept user_api_key_auth in get_tools_from_mcp_servers mock

The side_effect for the all-servers case did not accept the new kwarg,
so tools/list returned an empty list.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): fail fast for unknown tools when server mapping exists

Server-name fallback in call_tool must not open an upstream session when
the tool is absent from a populated mapping. Update the HTTP transport test
to register a known tool before asserting not-found behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix mypy

* Fix mypy

* fix(mcp): preserve tools/call scope on missing tool name; pass user_api_key_auth in list_tools

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix(mcp): match alias/server_name in _resolve_mcp_server_for_tool_call

The registry lookup in _resolve_mcp_server_for_tool_call previously only
compared candidate.name against the provided server_name, but tool name
prefixes can be derived from a server's alias or server_name (see
get_server_prefix). When the tool→server mapping is empty/stale (cold
start, dynamic tools), the lookup would fail for alias-configured
servers even though get_mcp_server_by_name (used by the REST path)
matches alias, server_name, and name.

Match the same priority of identifiers in both the registry pass and
the unprefixed fallback so the MCP protocol call_tool path is
consistent with the REST path.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix(mcp): reuse proxy_logging DualCache in inject_mcp_jwt_headers_for_upstream

Instead of allocating a fresh DualCache() on every tools/list invocation,
prefer the shared proxy_logging_obj.internal_usage_cache.dual_cache when
available. The cache argument is currently unused by MCPJWTSigner, but
sharing the proxy's cache avoids per-call allocation overhead and matches
the cache identity used elsewhere in the proxy hook plumbing — so any
future per-request state stored in cache will survive across list calls.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(mcp): return 403 ip_filtering for IP-restricted servers in tools/call name lookup

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix(test): accept user_api_key_auth kwarg in list_tools mocks

The proxy-infra job was failing on four TestMCPServerManager tests because
the mock_get_tools_from_server stubs did not accept the new
user_api_key_auth keyword argument that list_tools now forwards to
_get_tools_from_server. Add the kwarg to each stub so list_tools can call
through cleanly.

Co-authored-by: Claude <claude@anthropic.com>

* fix(mcp): skip JWT injection when per-user mcp_auth_header is set

MCPClient._get_auth_headers() applies extra_headers AFTER writing
Authorization from auth_value, so an injected JWT silently overwrites
the user's per-server OAuth token. Guard the JWT signer with
'not mcp_auth_header' so per-user OAuth (and any dict-form per-user
auth) takes precedence, mirroring the existing static_headers guard.

Adds a regression test that the signer's inject helper is not called
when mcp_auth_header is supplied.

* fix(mcp): skip JWT injection when extra_headers already has Authorization

When a server uses per-user OAuth tokens, the resolved token is passed
into _get_tools_from_server via extra_headers. The JWT injection guard
only checked mcp_auth_header and the server's static headers, so the
signer would silently overwrite the user's OAuth Authorization header.

Add a check for an existing Authorization entry in extra_headers so
caller-supplied per-user OAuth tokens take precedence over JWT signing.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* test(mcp): cover JWT signer + tool-call resolution branches

Adds unit tests for the new MCPServerManager helpers (_resolve_mcp_server_for_tool_call,
_resolve_oauth2_headers_for_tool_call) and the new MCPJWTSigner paths
(_build_scope call_type branches and inject_mcp_jwt_headers_for_upstream).
Brings patch coverage above the auto target without changing behavior.

Co-authored-by: Claude <claude@anthropic.com>

* fix(mcp): retry tool-server lookup with prefixed name in REST mismatch check

When the REST /mcp-rest/tools/call path sends a raw tool name plus
requested_server_id, _get_mcp_server_from_tool_name(name) can return
None if the mapping only stores the prefixed form. That bypassed the
tool_server_mismatch 403 guard and let the call fall through to
trusting requested_server.

Retry the lookup with every known prefix of the requested server so
the mismatch check fires whenever the tool is actually registered.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix(mcp): always reject unknown tools in server-name fallback

Defense-in-depth: _resolve_mcp_server_for_tool_call previously skipped
the unknown-tool check whenever the per-server mapping had no entries
yet (cold start, OAuth2 lazy listing, or upstream listing failure),
allowing arbitrary tool names to reach upstream servers.

Tighten the check so the server-name fallback always rejects tool
names not present in the mapping. Callers must call list_tools first
(standard MCP flow) before tools/call can resolve. Removes the
now-unused _mapping_has_tools_for_server helper and adds an
explicit empty-mapping rejection test alongside the existing
populated-mapping rejection test.

Co-authored-by: Sameer Kankute <sameer@berri.ai>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Yassin Kortam <yassin@berri.ai>
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude (greptile subagent) <claude-greptile-bot@anthropic.com>
2026-05-20 13:31:44 -07:00
ryan-crabbe-berri
fb73995c40
feat(ui): team passthrough routes create parity + edit load fix (#28098)
* feat(ui): team allowed_passthrough_routes create parity + edit load fix

Add the Allowed Pass Through Routes selector to the create-team modal
(previously only on the edit form), and fix the edit form silently
dropping the field: it lives under team metadata, so initialValues must
read info.metadata.allowed_passthrough_routes — otherwise the selector
renders empty and saving wipes admin-set routes. Both selectors are
gated to premium proxy admins, mirroring the server-side gate.

Resolves LIT-3019

* fix(ui): persist team allowed_passthrough_routes edits on save

The edit form loaded the selector but the save path never wrote it back:
allowed_passthrough_routes stayed in the raw metadata JSON textarea and
parsedMetadata (from that textarea) always won, so selector edits were
silently discarded. Strip it from the textarea initialValues and overlay
values.allowed_passthrough_routes into updateData.metadata, mirroring how
guardrails is handled.

Resolves LIT-3019

* fix(ui): preserve team passthrough routes for non-proxy-admins on save

Only proxy admins may set allowed_passthrough_routes (server-side gate).
For non-proxy-admins, write the team's stored value back into metadata
instead of the form value, so saving an unrelated setting can't silently
wipe routes; omit the key entirely when the team never had any.

Resolves LIT-3019
2026-05-20 12:35:49 -07:00
ryan-crabbe-berri
5a00cb1592
test(e2e): migrate runner to uv, add All Proxy Models key test (#28313)
* chore(e2e): migrate runner to uv, add All Proxy Models key test

Switches the local e2e runner (run_e2e.sh) from poetry to uv to match
the rest of the repo and CI. Adds a Playwright test for creating an
admin key with no team selected (all-proxy-models flow), a SLOWMO env
hook for headed debugging, and a MIGRATION_TRACKING.md doc that maps
the manual UI QA checklist to e2e tests so future migration work has
a single source of truth.

* chore(e2e): address greptile feedback

- Remove MIGRATION_TRACKING.md (docs belong in litellm-docs repo)
- playwright.config.ts: fall back to 0 when SLOWMO is non-numeric
  (parseInt returns NaN, which Playwright accepts silently)
- run_e2e.sh: add --frozen to uv sync for CI determinism
2026-05-20 12:35:29 -07:00
ryan-crabbe-berri
2eeca2d096
fix(ui): restore log filter loading indicator (#28282)
When a new filter is applied to spend logs, React Query's keepPreviousData
left stale rows on screen for 10–15s with no indication that a fetch was
in progress. The previous custom isFilteringResults flag was removed in
the #25847 toolbar refactor and only partially restored on the Fetch
button. Use React Query's isPlaceholderData to discriminate a real
filter change (queryKey changed, data not yet arrived) from a same-key
live-tail refetch, and feed it into the existing isLoading prop on the
toolbar pagination text and the table body. Live-tail polls still keep
previous rows without flicker.

Co-authored-by: Ryan <ryan@Ryans-MBP.localdomain>
2026-05-20 12:35:06 -07:00
Sameer Kankute
7f563b2593
fix(router): use forwarded model_id for native Azure container IDs (#27921)
* fix(router): use forwarded model_id for native Azure container IDs in _init_containers_api_endpoints

Azure code-interpreter containers return provider-native IDs (cntr_ + hex)
that carry no LiteLLM routing payload, so _decode_container_id returns
model_id=None. The router was falling through to call the handler directly,
bypassing _ageneric_api_call_with_fallbacks and leaving api_base=None for
Azure deployments. Fall back to the model_id forwarded from the proxy
ownership check so deployment credentials are always applied.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(azure-containers): strip /openai/responses path from api_base in AzureContainerConfig.get_complete_url

When a deployment's api_base is the responses endpoint URL
(e.g. .../openai/responses?api-version=...), AzureContainerConfig was
appending /openai/containers on top of it, producing the broken path
.../openai/responses/openai/containers. Azure returns 404 for that URL
while the correct path is .../openai/containers.

Strip any /openai/responses suffix from api_base before constructing
the containers URL so the resource root is always used as the starting point.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(azure-containers): prefer api-version from api_base URL over deployment's api_version

The deployment's api_version (e.g. 2024-08-01-preview) targets the chat/responses
API and is too old for the containers API, which requires 2025-04-01-preview.
The responses endpoint api_base already carries the correct api-version in its
query string. Extract it and use it for the containers URL, overriding the
stale deployment-level version.

Fixes DELETE and file-upload operations returning 404 due to wrong api-version.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(containers): pass params=None instead of params={} to httpx to preserve api-version

httpx erases a URL's query-string when params={} (empty dict) is passed,
silently stripping ?api-version=2025-04-01-preview from every container
POST/DELETE request. Azure's GET endpoints tolerate a missing api-version;
POST (upload) and DELETE are strict, so those returned 404.

Fix: use `params or None` in container_handler._async_handle and
llm_http_handler.async_container_delete_handler (and all sibling container
handlers) so that an empty params dict falls back to None, leaving httpx to
preserve the URL's existing query string intact.

Adds a regression test that directly documents the httpx behaviour.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(router): remove elif model_id branch from _init_containers_api_endpoints

Two reviewer findings addressed:

1. Truncated comment on the model_id fallback line — now complete.

2. Security: the elif branch that fired when container_id was absent allowed
   any authenticated caller to supply model_id in a POST /v1/containers body
   and route the request through an arbitrary deployment UUID, bypassing the
   model-level access checks that only validate `model`. Removed the elif
   branch; operations without container_id (create, list) route by the
   caller-supplied `model` field as before. model_id forwarding is kept only
   inside the container_id block, where the proxy ownership check has already
   validated the container before forwarding the deployment ID.

Adds a regression test pinning the security boundary: no-container-id path
calls original_function directly even when model_id is in kwargs.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(containers): validate proxy-to-router model_id forwarding for managed IDs

Add test_regression_get_container_forwarding_params_sets_model_id_for_managed_id
to verify that get_container_forwarding_params (the proxy-side half of the Azure
routing fix) correctly extracts and forwards model_id from a LiteLLM-managed
encoded container ID.

This closes the gap identified by Greptile P1: the previous regression test
only injected model_id as a direct kwarg, validating the router in isolation.
The new test exercises the actual proxy-to-router data flow through
ownership.get_container_forwarding_params, confirming that kwargs["model_id"]
is populated before _init_containers_api_endpoints is reached.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(azure-containers): tighten endpoint-path strip to endswith match

Use path.endswith() instead of path.find() for _AZURE_ENDPOINT_PATHS so
the suffix strip only fires when api_base actually ends with one of the
endpoint-specific path suffixes. This is the more precise check greptile
flagged on the original find()-based implementation.

* Fix sync container handler to preserve URL query string

Mirror the async path fix: pass None instead of an empty params dict so
httpx does not strip the URL's existing query string (e.g.
?api-version=...), which is required for Azure container routing.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix(azure-containers): strip trailing slash before endpoint suffix match

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix(containers): recover model_id from stored encoded id for native Azure container IDs

get_container_forwarding_params previously only set model_id when the
user-supplied container_id was a LiteLLM-managed encoded id. For native
upstream IDs (e.g. Azure 'cntr_<hex>') the decode fails and model_id was
never forwarded — making the router-side fallback in
_init_containers_api_endpoints unreachable in production.

Fall back to the stored 'unified_object_id' on the ownership row, which
is the encoded form captured at create time when the router selected a
specific deployment. Decoding that yields the deployment model_id and
restores router-based credential application (api_base, api_key) for
retrieve/delete and container-file operations on native IDs.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Yassin Kortam <yassin@berri.ai>
2026-05-20 12:32:34 -07:00
Sameer Kankute
183092d797
fix(proxy): normalize batch file IDs before ManagedObjectTable write (#28339)
* fix(proxy): normalize batch file IDs before ManagedObjectTable write

Run post_call_success_hook before update_batch_in_database on retrieve/cancel,
and ensure_batch_response_managed_file_ids so file_object never stores raw
provider output_file_id or error_file_id.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(proxy): address Greptile review on batch file ID normalization

Remove redundant resolve_* calls after update_batch_in_database and rename
loop variable to avoid shadowing hidden_params unified_file_id.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(tests): add mistral/ministral-8b-2512 to cost map and backfill in conftest

Mistral rotated the 'mistral/mistral-tiny' alias to return
'ministral-8b-2512' as the response model, which was missing from the
cost map. This caused test_completion_mistral_api and
test_completion_mistral_api_modified_input to fail in
litellm.completion_cost lookup.

- Add mistral/ministral-8b-2512 entry to both the in-tree
  model_prices_and_context_window.json and the bundled
  litellm/model_prices_and_context_window_backup.json (mirrors the
  existing openrouter/mistralai/ministral-8b-2512 pricing).

- litellm.model_cost is loaded at import time from the URL pinned to
  main, so the new backup entry isn't visible at test runtime until
  it also lands on main. Backfill any entries missing from the
  remote-fetched map into litellm.model_cost in the local_testing
  conftest so cost-calculator lookups succeed on this branch.

* fix(tests): drop unnecessary del of conftest backfill loop vars

* fix: resolve batch response file IDs even when status unchanged

The status-unchanged early return in update_batch_in_database was
skipping ensure_batch_response_managed_file_ids, leaving raw provider
input_file_id (and other raw IDs) in the user-facing response when
polling an in-progress batch. Move the in-place file ID normalization
above the early return so the response always carries unified managed
IDs while still skipping the DB write when nothing changed.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* test(batches): cover ensure_batch_response_managed_file_ids branches

Add tests for the previously-uncovered paths in
ensure_batch_response_managed_file_ids: error_file_id normalization,
swallowed conversion errors, UserAPIKeyAuth fallback from
db_batch_object, model_name resolution from unified_file_id, and early
returns when managed_files_obj, model_id, or auth context are missing.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Yassin Kortam <yassin@berri.ai>
Co-authored-by: Claude <noreply@anthropic.com>
2026-05-20 12:13:56 -07:00
Cursor Agent
7ba78155d7
fix(mcp): restrict passthrough probe challenge to OAuth passthrough servers
The probe filter previously matched any server with Authorization in
extra_headers, including gateway-managed OAuth2 servers. Those would
then receive the resource_metadata= WWW-Authenticate challenge meant
for pass-through servers, instead of the authorization_uri= challenge
pointing at the gateway AS metadata. Use srv.is_oauth_passthrough so
only genuine pass-through servers get the resource-metadata challenge.

Co-authored-by: Yassin Kortam <yassin@berri.ai>
2026-05-20 18:32:41 +00:00
Cursor Agent
719adfd043
fix(mcp): handle passthrough OAuth metadata and startup auth errors
- discoverable_endpoints: For pass-through MCP servers, when upstream
  oauth-protected-resource returns a non-200/non-dict response, raise
  HTTP 502 instead of falling through to default gateway metadata.
  Falling through would direct MCP clients at the gateway, which is
  not the authorization server for pass-through configs.

- mcp_server_manager: Wrap _get_tools_from_server in startup tool name
  mapping with try/except. Since _get_tools_from_server now re-raises
  MCPUpstreamAuthError, an upstream 401 from a pass-through server at
  startup (when no user token is present) would otherwise abort the
  loop and leave subsequent servers unmapped.

Co-authored-by: Yassin Kortam <yassin@berri.ai>
2026-05-20 18:19:28 +00:00
Cursor Agent
d1d9ff0121
refactor(mcp): dedupe MCPUpstreamAuthError->HTTPException + thread client_ip into delegate-auth gate
Co-authored-by: Yassin Kortam <yassin@berri.ai>
2026-05-20 18:07:36 +00:00
milan-berri
0fb710400f
fix(spend_counter): seed Redis counter via SET NX to prevent cross-pod double-seed (#27854)
* fix(spend_counter): seed Redis counter via SET NX to prevent cross-pod double-seed

Symptom
-------
Customers on multi-pod deployments see team `spend` jump to ~2x (or N x
the pod count) shortly after a Redis cache miss / TTL expiry, triggering
spurious "Budget Crossed" alerts and blocked requests until the value is
manually reset.

Root cause
----------
`SpendCounterReseed.coalesced` warmed the primary spend counter by
calling `redis.async_increment(key, value=db_spend, refresh_ttl=True)`,
which lowers to Redis `INCRBYFLOAT`. That is additive, not idempotent.

The per-counter `asyncio.Lock` only coalesces seeders inside one
process. With N pods sharing one Redis, on a cold key (cold start, TTL
expiry, manual delete) every pod independently passes its lock + Redis
re-check, reads the same `db_spend`, and issues `INCRBYFLOAT db_spend`.
Final value: N x db_spend.

Fix
---
Use `redis.async_set_cache(key, value=db_spend, nx=True)` for the seed.
SET NX is atomic across pods: exactly one writer initializes the key;
losers read the winner's value via `async_get_cache`. This is the same
idiom already used by `coalesced_window` in the same file, so the two
seed paths are now consistent.

Per-request deltas continue to use `INCRBYFLOAT` (correct - additive
behaviour is what we want for increments, not for initial seed).

Verification
------------
Live two-process repro against the same Postgres + Redis (DB
spend = 506):

  Unpatched: 4/4 runs -> Redis counter = ~1012  (~2 x db_spend)
  Patched:  12/12 runs -> Redis counter = ~506

Unit tests (`test_proxy_server.py`):

- New `test_primary_spend_counter_redis_concurrent_seed_does_not_double_seed`
  patches `_get_lock` to return a fresh lock per caller (otherwise the
  per-process lock masks the race), races two `coalesced` calls, and
  asserts final = 506 with exactly one of two SET NX attempts winning.
- 4 existing tests updated for the new seed contract (SET NX for the
  seed, INCRBYFLOAT only for the per-request delta).
- Full `spend_counter or reseed or budget` slice: 22 passed.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(spend_counter): make SET NX mock atomic so loser branch is exercised

Greptile flagged that `redis_set_cache` in
test_primary_spend_counter_redis_concurrent_seed_does_not_double_seed
placed `await asyncio.sleep(0)` AFTER the NX membership check. Both
concurrent tasks observed an empty `redis_store`, passed the guard, and
both returned True - so the loser branch (else: read back winner's value)
was never exercised.

Fix the mock to model real atomic Redis SET NX:

- Yield BEFORE the membership check so two concurrent callers interleave
  the way real SET NX does (first to resume runs check + write atomically
  and wins; second resumes after the key exists and loses).
- Track set_cache return values; assert sorted([loser, winner]) so we
  know exactly one task wins and one loses.
- Track async_get_cache calls that happen AFTER at least one SET NX has
  completed; assert at least one such read - that is the loser-path
  fallback (`current_value = float(cached)` when seeded is False).

Verified by temporarily reverting the mock to the old order: the test
now fails with `expected exactly one SET NX winner and one loser, got
[True, True]`, exactly the failure mode Greptile described.

No production code change.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(spend_counter): mock async_set_cache to populate redis_store in concurrent read+write test

`test_concurrent_read_and_write_paths_share_one_db_query` mocks
`async_increment` to populate the in-memory `redis_store`, but did not
mock `async_set_cache`. After the SET-NX seed change in `coalesced()`,
the seed step writes via `async_set_cache(nx=True)` (default AsyncMock,
no `redis_store` write), so the simulated Redis stays empty after the
first reseed. The second `get_current_spend` then sees a clean Redis
miss, re-enters the DB read path, and the test fails with
`expected 1 DB query, got 2`.

Fix: add a `redis_set_cache` side_effect that updates `redis_store` on
`nx=True` (and rejects when the key already exists), matching the
pattern used by the four sibling tests fixed in this branch's first
commit. Pre-existing assertions are unchanged.

Full `tests/test_litellm/proxy/test_proxy_server.py`: 158 passed.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-20 10:57:08 -07:00
Cursor Agent
f0eb54ea9f
fix(mcp): correct passthrough probe 401 + slashed-name cold start parser
- _check_passthrough_upstream_auth now emits
  'Bearer resource_metadata="..."' pointing at the gateway's
  oauth-protected-resource well-known URL, mirroring the
  pre-emptive 401 path. Pass-through servers don't use the gateway
  as an authorization server, so the previous 'authorization_uri='
  challenge sent clients to the wrong metadata endpoint.

- _parse_mcp_server_names_from_path now accepts server names that
  contain a single slash (e.g. custom_solutions/user_123), mirroring
  MCPRequestHandler._extract_target_server_names_from_path. Without
  this, the cold-start bypass missed slashed-name servers and the
  generic admission error propagated instead of the spec-compliant
  401 challenge.

- _is_mcp_passthrough_cold_start drops the unused scope parameter
  from its signature.

Co-authored-by: Yassin Kortam <yassin@berri.ai>
2026-05-20 17:52:55 +00:00
Claude
ee38ba16e3
fix(mcp,tests): sync stubs and cold-start assertions with delegate-check
The merge of base-branch _target_servers_delegate_auth_to_upstream
into process_mcp_request inserts an additional
get_mcp_server_by_name(name) lookup ahead of the cold-start path,
which breaks two test patterns:

1. lookup_by_name(name) side-effect stubs in
   TestMCPDelegateAuthToUpstream are called positionally by the
   delegate check, then again by the cold-start path with
   client_ip=... — raising TypeError: unexpected keyword argument
   'client_ip'. Accept **_kwargs to match the real signature.

2. TestMCPPassthroughColdStartAdmission assertions count the lookup
   exactly once with client_ip=..., but the delegate check now adds
   a positional-only call ahead of it. Switch assert_called_once_with
   to assert_any_call for the cold-start invocation, and assert
   client_ip was *not* passed for the aggregate /mcp test where
   cold-start must not fire.

Both updates align with CLAUDE.md guidance to keep monkeypatch stubs in
sync with the real signature when an optional parameter is added.

Co-authored-by: Claude <claude@anthropic.com>
2026-05-20 17:42:30 +00:00
Cursor Agent
78cb412419
fix(mcp): tighten cold-start bypass against CSV paths + dedupe upstream auth probe
- Return None from _parse_mcp_server_names_from_path for CSV multi-server
  paths (/mcp/a,b). The regex previously truncated at the first comma and
  silently passed a single server name to the cold-start gate.
- Switch _is_mcp_passthrough_cold_start to all-targets semantics, matching
  _target_servers_use_oauth2: one non-passthrough target in a co-targeted
  set must not flip the anonymous-admission bypass open for the others.
- Drop the redundant HTTPStatusError block in _extract_upstream_auth_failure
  - any HTTPStatusError carries a .response, so the preceding generic block
  already handles 401/403 detection.

Co-authored-by: Yassin Kortam <yassin@berri.ai>
2026-05-20 17:32:46 +00:00
Claude
326c6bb84f
fix(mcp): add upstream auth pre-flight in SSE handler
Mirror handle_streamable_http_mcp by calling _check_passthrough_upstream_auth
after the cold-start 401 emitter so expired/invalid upstream tokens surface a
proper 401 + WWW-Authenticate challenge before the SSE session commits 200
headers, instead of letting list_tools silently return [] when the upstream
rejects the token.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-20 17:31:20 +00:00
Cursor Agent
a86f6e7eb6
fix(mcp): surface MCPUpstreamAuthError as 401 in SSE/HTTP transport handlers
Both handle_sse_mcp and handle_streamable_http_mcp only caught
HTTPException to preserve 401 + WWW-Authenticate challenges, but
MCPUpstreamAuthError (raised when a pass-through server's upstream
rejects a bearer token mid-session) inherits from Exception. It was
falling through to the generic handler and surfacing as an opaque 500.

Mirror the REST endpoint behavior: translate MCPUpstreamAuthError into
an HTTPException(status_code=e.status_code) with the upstream
www-authenticate header so standards-compliant MCP clients trigger the
upstream OAuth flow.

Co-authored-by: Yassin Kortam <yassin@berri.ai>
2026-05-20 17:18:22 +00:00
Cursor Agent
5d8f74f1c1
fix(jwt): strip internal _litellm_* claims in global JWKS auth path
Prevents identity spoofing where a token signed by the global JWKS
could inject _litellm_jwt_issuer and other _litellm_* claims that
downstream getters trust. The issuer-scoped path already strips these
via _apply_issuer_claim_mappings; mirror that behavior for the global
fallback path.

Co-authored-by: Yassin Kortam <yassin@berri.ai>
2026-05-20 17:06:37 +00:00
Claude
6f82537c35
Merge branch 'litellm_internal_staging' into litellm_feat/v1.84.0-mcp-gateway-jwt-auth
Resolved conflict in tests/local_testing/conftest.py by keeping the
branch-specific comment that describes the ministral-8b-2512 backfill
case (this branch adds that entry).

Co-authored-by: Claude <claude@anthropic.com>
2026-05-20 17:06:12 +00:00
Sameer Kankute
99a63d5180
feat(gemini): add gemini-3.1-flash-lite model cost map (#28320)
* feat(gemini): add gemini-3.1-flash-lite model cost map entries

Co-authored-by: Cursor <cursoragent@cursor.com>

* Update model_prices_and_context_window.json

* Update source URL for model pricing information

* Sync source URL for gemini-3.1-flash-lite in backup JSON

* fix(model_cost_map): add mistral/ministral-8b-2512 entry

Mistral rotated the 'mistral/mistral-tiny' alias to return
'ministral-8b-2512' as the response model, which is not in the cost map.
This caused test_completion_mistral_api and
test_completion_mistral_api_modified_input to fail in
completion_cost lookup. Add the entry mirroring the existing
openrouter/mistralai/ministral-8b-2512 pricing.

* test(cost_calculator): assert output_cost_per_reasoning_token for gemini-3.1-flash-lite

* fix(tests): backfill local backup entries into runtime model_cost

litellm.model_cost is loaded from LITELLM_MODEL_COST_MAP_URL (pinned to
main) at import time, so any pricing entries added to the in-tree backup
on this branch aren't visible at test runtime until they also land on
main. The Mistral cassette currently returns model=ministral-8b-2512
and the cost-calculator lookup in test_completion_mistral_api /
test_completion_mistral_api_modified_input fails despite the entry
existing in the local backup. Backfill missing backup entries into
litellm.model_cost in the local_testing conftest so these lookups
succeed against the cassette state the branch is being tested with.

* fix(tests): guard conftest backfill against empty local cost map

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-05-20 10:03:14 -07:00
Claude
a6492bf424
fix(auth): address remaining greptile style findings
- get_request_route: require root_path to match whole path segments before
  stripping, so '/apifoo' isn't truncated to 'foo' when root_path='/api'.
- get_mcp_client_ip: collapse the two trusted-proxy validation branches into
  a single is_request_from_trusted_proxy call so the return value drives
  control flow instead of being discarded for the side-effect warning.

Co-authored-by: Claude <claude@anthropic.com>
2026-05-20 16:59:07 +00:00
Claude
1e5053adf1
fix(mcp,auth): restore client_ip in oauth2 target check, drop from delegate check
The merge of staging into the PR branch (d42a66adb6) misplaced the
client_ip=client_ip kwarg: it landed inside _target_servers_delegate_auth_to_upstream
(which never accepted client_ip and isn't called with it), while the
sibling _target_servers_use_oauth2 has client_ip in its signature but
stopped passing it through to get_mcp_server_by_name. That left ruff
flagging F821 on the undefined name and lint failing.

Move client_ip back into _target_servers_use_oauth2's lookup (matching
the call site that already forwards IPAddressUtils.get_mcp_client_ip)
and drop it from _target_servers_delegate_auth_to_upstream so its body
matches its signature again.
2026-05-20 16:37:02 +00:00
Claude
488fcc25e1
fix(jwt,mcp): fall back to global JWKS on unknown issuer; prune fetch locks
- handle_jwt._get_configured_issuer now returns None for tokens whose 'iss'
  is not in the configured issuers list, letting auth_jwt fall through to
  the legacy JWT_PUBLIC_KEY_URL path instead of hard-raising. This keeps
  existing tokens from non-configured IdPs working when an operator adds
  the new 'issuers' list to a live deployment.

- discoverable_endpoints._prune_oauth_metadata_cache now also prunes
  entries in _OAUTH_METADATA_FETCH_LOCKS whose cache entry has been
  evicted and whose lock isn't currently held, bounding the locks dict
  to match the cache it guards.

Co-authored-by: Claude <claude@anthropic.com>
2026-05-20 16:35:23 +00:00
Claude
22e9064673
fix(mcp,jwt): address greptile concurrency and code-quality concerns
- _apply_issuer_claim_mappings now builds a new dict and reads from the
  original token, rather than mutating its input. The change is
  behaviour-preserving (caller passes a fresh jwt.decode result), but
  avoids the surprise-mutation pattern flagged by greptile.
- is_network_error uses isinstance(exc, httpx.TransportError) instead of
  matching type(exc).__name__ against a hand-maintained string set, so
  ReadError / WriteError / ProxyError / etc. are also treated as
  transport-level failures and surfaced as HTTP 502.
- fetch_upstream_oauth_protected_resource now coalesces concurrent
  discovery requests per (server_id, resource_url) through an
  asyncio.Lock so concurrent .well-known calls share a single upstream
  fetch + cache write.
- Drop the redundant 'if trusted_ranges:' branch in get_mcp_client_ip;
  it is always true on the path that reaches it (the prior 'if not
  trusted_ranges:' early-returns).

Co-authored-by: Claude <claude@anthropic.com>
2026-05-20 16:19:16 +00:00
Claude
087a4da116
fix(tests): backfill local model_cost into remote-fetched map
litellm.model_cost is loaded at import time from LITELLM_MODEL_COST_MAP_URL
(pinned to main), so pricing entries that exist only in this branch (e.g.
mistral/ministral-8b-2512, freshly added because Mistral's API now returns
this id from mistral-tiny) are absent at test time and completion_cost
lookups raise 'This model isn't mapped yet'. Backfill the in-tree backup
into litellm.model_cost in the local_testing conftest so cassette-driven
cost calculations resolve against the entries that ship with the branch
under test.

Fixes local_testing_part1 failures on test_completion_mistral_api and
test_completion_mistral_api_modified_input.
2026-05-20 16:17:56 +00:00
Claude
fc31a07013
Merge remote-tracking branch 'origin/litellm_feat/v1.84.0-mcp-gateway-jwt-auth' into litellm_feat/v1.84.0-mcp-gateway-jwt-auth
# Conflicts:
#	litellm/proxy/management_endpoints/mcp_management_endpoints.py
2026-05-20 16:00:27 +00:00
Claude
d42a66adb6
Merge branch 'litellm_internal_staging' into litellm_feat/v1.84.0-mcp-gateway-jwt-auth
Resolved unrelated-history merge using e1fc955464 (the original base of upstream PR #28008) as the 3-way merge base.

Conflict resolutions:
- discoverable_endpoints.py: kept PR's _OAUTH_METADATA_CACHE / prune helpers and PR's inline redirect_uri extraction in /callback; took staging's callback comment that mentions the ops-allowlist code path served by validate_trusted_redirect_uri.
- oauth_utils.py: took staging's version (strict superset: adds MCP_TRUSTED_REDIRECT_ORIGINS ops allowlist, userinfo/backslash netloc rejection, rejection diagnostics).
- server.py: kept both PR's _raise_preemptive_401_for_unauthenticated_servers and staging's _get_forwarded_auth_from_scope / _probe_upstream_auth / _check_passthrough_upstream_auth additions (independent helpers).
- mcp_management_endpoints.py: took staging's verify_exp=False JWT decode option and staging's delegate_auth_to_upstream PKCE bypass block.
- proxy_server.py: took staging's restructured 4-case dynamic_mcp_route, and ported PR's scope['_original_path'] preservation into _mcp_forward_as_path so server.py's OAuth-challenge URL selection still has the public request path.
- types/mcp_server/mcp_server_manager.py: kept both PR's is_oauth_passthrough and staging's has_token_exchange_config properties (additive).
- test_image_edits.py: took staging's _make_test_images() per fix(image_edits).
- test_realtime_guardrails_openai.py: took staging's unicode-punctuation normalization.
- test_fireworks_ai_translation.py: took staging's mocked-payload test_document_inlining_example.
- test_openapi_compliance.py: kept PR's removal of 'steps' per fix(interactions).
- test_discoverable_endpoints.py: took staging's tests (PR's tests assumed the removed get_proxy_base_url import path).
- test_mcp_server.py: took staging's pre-flight upstream-auth tests (cover the helpers we kept).
- test_mcp_management_endpoints.py: took staging's verify_exp=False / delegate_auth_to_upstream tests, and also kept PR's test_mcp_oauth_authorize_token_routes_use_browser_auth_dependency (independent route-wiring assertion).

Co-authored-by: Claude <claude@anthropic.com>
2026-05-20 15:59:22 +00:00
Claude (greptile fixer)
298936f83d
fix(mcp,auth): address greptile review concerns
- handle_sse_mcp now calls _raise_preemptive_401_for_unauthenticated_servers
  so SSE clients to pass-through OAuth MCP servers receive the RFC 9728
  401 + WWW-Authenticate challenge that the streamable-HTTP path already emits.
- get_request_route strips a trailing slash from root_path before length-based
  prefix removal so non-canonical ASGI root_path values like "/litellm/"
  don't strip the leading slash from the returned route.
- _mcp_oauth_user_api_key_auth's cookie JWT decode now passes
  options={"verify_aud": False} so a future revision of the UI session
  JWT containing an aud claim cannot silently downgrade the request to
  unauthenticated.

Co-authored-by: Claude <claude@anthropic.com>
2026-05-20 15:32:09 +00:00
Claude
76c704be34
fix(model_prices): register mistral/ministral-8b-2512
Mistral's API now returns model='ministral-8b-2512' when 'mistral-tiny'
is requested, so test_completion_mistral_api fails with 'This model
isn't mapped yet'. Adding the entry so completion_cost can resolve the
cost for that response.

Author: Claude <noreply@anthropic.com>
2026-05-20 15:26:24 +00:00
yuneng-jiang
37845acb57
fix(tests): migrate realtime + rerank tests off shut-down upstream models (#28191)
* fix(tests): use gpt-realtime in realtime guardrails test

OpenAI shut down gpt-4o-realtime-preview-2024-12-17 on 2026-05-07, so
the live OpenAI realtime guardrails integration test now fails with
model_not_found (session.created never arrives, _wait_for_event times
out). Point OPENAI_REALTIME_URL at the current GA model, gpt-realtime.

Scope limited to this test: the pricing-catalog JSON keeps the retired
entries intentionally (historical cost calc + separate Azure timeline),
and the Azure realtime cost-calc test is unaffected.

* fix(tests): mock nvidia_nim rerank instead of hitting EOL'd endpoint

NVIDIA reached end-of-life for the hosted nvidia/llama-3.2-nv-rerankqa-1b-v2
rerank API on 2026-05-18 with no published replacement, so the live
BaseLLMRerankTest.test_basic_rerank for nvidia_nim now returns HTTP 410
("Gone"). NVIDIA's hosted catalog rotates on a schedule, so swapping in
another live model would only defer the failure.

Override test_basic_rerank in TestNvidiaNim to mock the sync/async HTTP
transport (same pattern as test_nvidia_nim_rerank_ranking_endpoint in this
file) and inject a fake NVIDIA_NIM_API_KEY via monkeypatch. The
request/response transformation and cost calculation stay covered offline.
Scope limited to nvidia_nim; other BaseLLMRerankTest providers untouched.

* fix(tests): migrate remaining realtime tests off shut-down gpt-4o-realtime-preview

OpenAI's 2026-05-07 shutdown removed the entire gpt-4o-realtime-preview
family, including the undated 'gpt-4o-realtime-preview' alias (not just the
dated snapshot fixed earlier). Three live tests still connected with the
dead alias and failed with messages_received=1 (an error event instead of
session.created):

- test_openai_realtime_simple.py: get_model() -> gpt-realtime (drives
  TestOpenAIRealtime.test_realtime_connection / test_realtime_with_query_params)
- test_openai_realtime.py: test_openai_realtime_direct_call_no_intent and
  test_openai_realtime_direct_call_with_intent -> openai/gpt-realtime
  (the with_intent test shares the same dead alias even though it was not
  in the failing set this run)

Mocked unit tests (test_realtime_query_params_construction,
test_realtime_query_params_use_normalized_model_name) are left as-is: they
never hit the network and assert string plumbing only.

Also fixes test_text_message_blocked_by_guardrail_no_ai_response, which now
connects (the earlier URL swap worked) but tripped a model-wording-brittle
assertion. The guardrail flow asks the model to voice the block message
verbatim; gpt-4o-realtime-preview complied (output contained 'blocked'),
gpt-realtime refuses verbatim-repeat instructions ('I'm sorry, but I can't
repeat that message.'). Since the original user message is blocked before
it reaches OpenAI, the refusal is still a safe outcome. Assertion #3 now
accepts both voicing and refusal, and adds a hard check that the blocked
phrase never leaks into AI output.

(cherry picked from commit ce87c411bf)
2026-05-20 15:25:30 +00:00
yuneng-jiang
c65e598402
fix(tests): replace shut-down gpt-4o-audio-preview with gpt-audio-1.5 (#28281)
* fix(tests): replace shut-down gpt-4o-audio-preview with gpt-audio-1.5

OpenAI shut down gpt-4o-audio-preview on 2026-05-07, so the live audio
calls in test_stream_chunk_builder_openai_audio_output_usage and
test_standard_logging_payload_audio now hard-fail with a model-not-found
error on every PR. The error was not "openai-internal", so the except
block swallowed it and execution fell through to an unbound
completion/response (UnboundLocalError).

Switch both tests to gpt-audio-1.5, OpenAI's recommended successor
(GA, not deprecated, already present in the litellm cost map so the
response_cost assertion still resolves). Also broaden the except to
skip with the real error in the reason instead of crashing, so a
transient upstream blip can't reintroduce the UnboundLocalError.

* fix(tests): narrow audio-test skip to model-not-found, re-raise the rest

Address review feedback: an unconditional skip on any exception would
silently mask a litellm-internal regression in the audio path (broken
param transformation, serialization, bad header) instead of failing CI.

Skip only on the upstream-unavailable class (model_not_found / "does not
exist" / openai-internal) and re-raise everything else, so genuine
regressions still fail loudly. The UnboundLocalError is still fixed
because the handler either skips or raises - it never falls through.

* fix(tests): add budget_exceeded to expected Interaction status enum

Staging added budget_exceeded to the Interaction OpenAPI status enum; the staging merge into this branch picked up the spec change but not the matching test update, so test_status_enum_values failed in CI. Align the test's expected list (exact-match by design) with the live spec.

* fix(tests): mock HTTP fetch in test_img_url_token_counter

The test parameterized a live third-party image URL (blog.purpureus.net) which now 404s, causing get_image_dimensions to fall through to its base64 decode path and crash with 'not enough values to unpack' on every PR run. Mock safe_get with a tiny 1x1 PNG so the URL branch is still exercised without any network dependency.

* fix(tests): swap gpt-4o-audio-preview to gpt-audio-1.5 in test_gpt4o_audio

OpenAI shut down gpt-4o-audio-preview on 2026-05-07, so both live tests in test_gpt4o_audio.py (test_audio_output_from_model and test_audio_input_to_model) hard-fail model_not_found on every PR. Swap the hardcoded model to OpenAI's successor gpt-audio-1.5 (same chat-completions audio surface; already in the litellm cost map). Mirror the narrowed-skip pattern from the prior audio fixes: skip on model_not_found / does-not-exist / openai-internal, re-raise everything else so genuine litellm regressions still fail CI loudly.

(cherry picked from commit 92de7423ef)
2026-05-20 15:24:55 +00:00
Yuneng Jiang
76700e7bb3
test(fireworks): mock remaining live smoke tests
test_completion_fireworks_ai and test_completion_cost_fireworks_ai
made real Fireworks calls and broke whenever Fireworks rotated its
serverless catalog (no externally-verifiable model list exists).
They also asserted nothing — just printed.

Mock the HTTP post and assert real behavior instead: the request is
built with the right model/messages and the OpenAI-compatible
response parses back; the cost path yields a non-zero cost against
the local cost map. No network, no model dependency, stronger than
the old smoke checks.

(cherry picked from commit b5db7ed37d)
2026-05-20 15:24:49 +00:00
Yuneng Jiang
a0b61f6dcb
test(fireworks): replace deprecated llama-v3p3-70b-instruct model
Fireworks removed llama-v3p3-70b-instruct from serverless, so every
live test using it now fails with NotFoundError ("Model not found,
inaccessible, and/or not deployed").

Swap the 6 references (3 files) to the currently-served
accounts/fireworks/models/deepseek-v3p1 — the canonical model in
Fireworks' current docs examples and present in LiteLLM's cost map.
test_get_model_params_fireworks_ai is a pure pricing-heuristic test
(no network) asserting the >16b branch, so it uses llama-v3p1-70b-
instruct instead to keep the "fireworks-ai-above-16b" assertion and
branch coverage intact.

(cherry picked from commit 39a1d438f2)
2026-05-20 15:24:45 +00:00
Yuneng Jiang
93d3ed7ee5
fix(tests): drop dall-e-only test classes; route live image tests via gpt-image-1
Second wave of failures from the 2026-05-12 DALL-E shutdown:
- tests/image_gen_tests/test_image_edits.py::TestOpenAIImageEditDallE2
  and tests/image_gen_tests/test_image_generation.py::TestOpenAIDalle3
  are explicitly named for the deprecated models and can't pass; remove.
  gpt-image-1 coverage already exists in sibling classes.
- tests/local_testing/test_router.py image gen tests use dall-e-3 only
  as a routing example; swap to gpt-image-1.
- tests/local_testing/test_custom_callback_input.py image_generation
  success/failure paths swapped to gpt-image-1.

(cherry picked from commit 945b10ded4)
2026-05-20 15:24:41 +00:00
yuneng-jiang
a119324127
test: replace dall-e-3 with gpt-image-1 in health check and router tests (#27813)
OpenAI returns 'The model dall-e-3 does not exist' for the test account,
breaking test_openai_img_gen_health_check and test_image_generation.
Switch to gpt-image-1, matching the existing TestOpenAIGPTImage1 pattern.

(cherry picked from commit aee58db880)
2026-05-20 15:24:38 +00:00