Skip to content

chore(release): backport pending 1.84.x/1.85.x fixes into stable/1.86.x and cut 1.86.3 - #29541

Merged
yuneng-berri merged 10 commits into
stable/1.86.xfrom
litellm_cherrypick_1_86_3
Jun 3, 2026
Merged

chore(release): backport pending 1.84.x/1.85.x fixes into stable/1.86.x and cut 1.86.3#29541
yuneng-berri merged 10 commits into
stable/1.86.xfrom
litellm_cherrypick_1_86_3

Conversation

@yuneng-berri

Copy link
Copy Markdown
Collaborator

Relevant issues

Backports changes that already shipped in the 1.84.x and 1.85.x lines but never made it into 1.86.x. 1.86.2 was branched on May 16 and only ever received the npm builder fix (#28519) and the proxy path helper (#28547), so a handful of fixes that 1.84.4 and 1.85.3 carry were missing on the current stable line. This PR closes that gap and cuts 1.86.3

Linear ticket

N/A

What is included

Every commit here is cherry-picked verbatim from stable/1.85.x (the same changes that shipped in 1.85.3 and 1.85.1), so the set matches what customers on 1.84.x and 1.85.x already run:

The last two commits are the cz bump --increment PATCH (1.86.2 -> 1.86.3) and the matching uv lock

Pre-Submission checklist

  • The cherry-picked PRs each carry their own tests; those tests run green against the 1.86 baseline
  • My PR passes all unit tests on make test-unit
  • Scope is limited to backporting the already-merged fixes plus the release bump
  • Greptile review requested

CI (LiteLLM team)

  • Branch creation CI run
    Link:

  • CI run for the last commit
    Link:

  • Merge / cherry-pick CI run
    Links:

Screenshots / Proof of Fix

These are verbatim backports of changes already merged, released, and validated on the 1.84.x and 1.85.x lines; each linked PR carries its own proof and review. The Gemini pair (#28268 with its regression fix #28324) is included together precisely so 1.86.x does not regress Vertex tool calling

Type

🆕 New Feature
🐛 Bug Fix
🚄 Infrastructure

Changes

See the commit list above. No new code is introduced beyond the cherry-picks, the version bump, and the lockfile refresh

Sameerlite and others added 10 commits June 2, 2026 16:34
* Add day 0 support for gemini 3.5 flash

* Fix pricing

* Fix greptile review

* Fix failing test

* Fix tests

* Fix: revert tool removing logic

* fix greptile and test

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
(cherry picked from commit 3c3d131)
(cherry picked from commit cbf9ffe)
…d 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>
(cherry picked from commit 0fb7104)
(cherry picked from commit c621f58)
#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>
(cherry picked from commit fecf212)
(cherry picked from commit 75c72c5)
…9343)

* refactor(proxy/auth): normalize Bearer prefix in safe-hash helper

UserAPIKeyAuth._safe_hash_litellm_api_key now strips a leading
"Bearer "/"bearer " prefix before its existing sk-/JWT classification, so
the helper produces the same hashed output regardless of whether the
caller stripped the Authorization header prefix or passed the header
value through unchanged.

* refactor(proxy/auth): make Bearer-prefix strip case-insensitive

Per RFC 7235 the HTTP authorization scheme token is case-insensitive.
Replace the two-prefix loop with a single case-insensitive check so the
helper normalizes "Bearer ", "bearer ", "BEARER ", and any mixed-case
variant before classifying the remainder as sk- or JWT. The contract
test gains coverage of "BEARER " and "BeArEr ".

* test(mcp): align auth-handler test expectations with safe-hash helper

The two MCP auth tests asserted that UserAPIKeyAuth(api_key="Bearer ...")
retained the raw header bytes on the api_key field. _safe_hash_litellm_api_key
now normalizes that input — stripping the Bearer prefix and hashing the
resulting sk- key — so the expectations move to the normalized form:
the bare token in the parametrize case, and hash_token("sk-...") in the
backward-compat assertion. This matches what the real auth flow produces
(the builder strips Bearer and the DB stores the hashed token), so the
mocks now line up with production rather than with the un-normalized
validator output.

(cherry picked from commit 87b0e47)
…eroing counter (#29358)

* fix(reset_budget): write only {spend, budget_reset_at} and stop pre-zeroing counter

ResetBudgetJob's batched update_data path shipped the full key/user/team
model on each reset. Prisma rejects object_permission_id and budget_limits
on the update input type, so any row carrying those fields detonated the
entire batch -- spend never reset, budget_reset_at never advanced. After
v1.84.0 started populating object_permission_id on UI-created keys, this
fires routinely.

_reset_budget_common also zeroed the cross-pod spend counter before the
DB write, so failed resets left enforcement reading 0 from the counter
while the DB still held the over-budget spend, admitting requests past
the cap until the counter naturally re-saturated from new reservations.

Switch the write to per-row narrow updates ({spend, budget_reset_at})
via db.batch_, and move the counter invalidation out of
_reset_budget_common so it only fires after the DB write commits. On
DB-write failure the counter is left untouched, enforcement continues
to block, and the next scheduler tick can retry without leaving a
bypass window.

Fixes #27730.

* fix(reset_budget): address Greptile review on #29358

- Strengthen the bypass-half regression test: replace the for-loop over
  call_args_list (vacuously true when empty) with assert_not_called(),
  so the test would actually flag a re-introduction of counter-zeroing
  via any code path.
- Add the same explanatory docstring on _write_user_reset_updates and
  _write_team_reset_updates that _write_key_reset_updates already has,
  so all three helpers point future maintainers at #27730.

* test(reset_budget): update test_proxy_budget_reset for new batch-write path

Same shape as the previous test_reset_budget_job.py update: keys/users/teams
now write through prisma.db.batch_().<table>.update, not update_data, so the
tests need a batcher mock and updated assertions. Adds:

- _wire_batcher_for_test helper that returns a list which accumulates per-row
  batch updates captured from prisma_client.db.batch_().
- _attrify helper that wraps dict fixtures so getattr(item, "token") works
  alongside the dict item-access the fake_reset_* mocks rely on. The new
  narrow-write helpers use getattr to pull out the row's id, and would
  silently skip plain dicts otherwise.
- Updates 3 partial_failure tests to assert against the batch-call list
  (rows by id, payload contains only {spend, budget_reset_at}) instead of
  update_data.assert_awaited_once + data_list inspection.
- Updates test_reset_budget_continues_other_categories_on_failure: only
  budget + enduser still flow through update_data; key/user/team go through
  the batch path now.
- Wires the batcher mock into 3 service_logger_*_success tests so commit()
  is actually awaitable and the success hook fires.

These tests were silently passing locally only because the editable install
in .venv pointed at the main repo, not the worktree — running pytest with
PYTHONPATH overridden to the worktree (matching CI) reproduces the failures.

(cherry picked from commit a06ec43)
…quest body (#29447)

* fix: stop use_chat_completions_api flag from leaking into provider request body

use_chat_completions_api is a LiteLLM control flag that forces the
/responses -> /chat/completions bridge. It was missing from
all_litellm_params, so get_non_default_completion_params treated it as a
model-specific param and forwarded it to the upstream provider. A
model-level "use_chat_completions_api: true" in the proxy config therefore
reached the chat-completions path and was rejected by strict providers
(OpenAI/Anthropic) with HTTP 400 for an unknown body field.

Register it as a known internal param so it is stripped on every path
(completion, the responses bridge that calls litellm.completion, and
filter_out_litellm_params).

Adds a regression test driving litellm.completion() with a mocked OpenAI
client that asserts the flag never reaches the request body.

* test: clarify extra_body assertion in use_chat_completions_api leak test

Replace the misleading 'not in ... or {}' precedence idiom with an explicit
parenthesized guard that also handles extra_body being None.

(cherry picked from commit acbbfe9)
The #29311 cherry-pick onto stable/1.85.x carried the test
test_route_streaming_logging_runs_async_handler_for_sdk_passthrough,
which patches PassThroughStreamingHandler._build_passthrough_logging_result
to verify the SDK-passthrough dispatch contract. The manual conflict
resolution kept the per-endpoint if/elif/elif chain inline in
_route_streaming_logging_to_handler (matching v1.84.4's resolution),
so the patched attribute did not exist and the test errored at
collection with AttributeError.

Extract the chain into the static _build_passthrough_logging_result
helper as #29089 originally designed it. _route_streaming_logging_to_handler
now resolves (standard_logging_response_object, kwargs) through the
helper and dispatches via dispatch_success_handlers; the helper itself
is synchronous and CPU-bound, suitable for the unit test's patch target.

Verified locally: tests/pass_through_unit_tests/test_unit_test_streaming.py
passes (5/5) and tests/test_litellm/litellm_core_utils/test_litellm_logging.py
passes (83/83).

v1.84.4 ships with the same broken test; this strictly improves on that
resolution.

(cherry picked from commit 8824745)
@yuneng-berri
yuneng-berri requested a review from a team June 2, 2026 23:47
@greptile-apps

greptile-apps Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR backports seven fixes from the stable/1.84.x/1.85.x lines into stable/1.86.x and cuts 1.86.3. All commits are verbatim cherry-picks of changes already shipped and validated in prior stable releases.

Confidence Score: 4/5

The changes are cherry-picked from already-released stable lines and carry their own tests; the core correctness of each fix is well-established.

All seven commits are verbatim backports already running in production on 1.84.x/1.85.x. The dispatch_success_handlers dedup guard is sound in asyncio's cooperative model (no await between check and set), the SET NX seed change correctly prevents the concurrent-pod double-seed race, and the targeted Prisma updates sidestep the known DataError. The only non-trivial concerns are a silent behaviour change for sync callbacks on async streaming chunks and the parallel_tool_calls drop bypassing drop_params; both are intentional design decisions rather than bugs.

litellm/litellm_core_utils/litellm_logging.py and litellm/litellm_core_utils/streaming_handler.py carry the most logic surface area due to the logging consolidation; litellm/proxy/common_utils/reset_budget_job.py is worth a second look for the Prisma batch failure semantics.

Important Files Changed

Filename Overview
litellm/litellm_core_utils/litellm_logging.py Adds dispatch_success_handlers as a unified routing method with a has_dispatched_final_stream_success dedup guard; extracts _is_sync_litellm_request helper; modifies async_success_handler guard to allow assembled stream responses to bypass the should_run_logging check
litellm/litellm_core_utils/streaming_handler.py Per-chunk sync success_handler now skipped for async SDK requests; final assembled response switches from paired async_success_handler + executor.submit to dispatch_success_handlers(prefer_async_handlers=True)
litellm/proxy/common_request_processing.py Deferred stream logging paths (orphaned + guardrail) consolidated onto dispatch_success_handlers(prefer_async_handlers=True); sync executor.submit blocks removed
litellm/proxy/common_utils/reset_budget_job.py Adds _write_{key,user,team}_reset_updates helpers that issue targeted Prisma updates for only {spend, budget_reset_at}; spend-counter invalidation moved to after the DB commit via _invalidate_spend_counter
litellm/proxy/db/spend_counter_reseed.py Seeds Redis counter via SET NX instead of INCRBYFLOAT, preventing concurrent pods from multiplying the counter; loser pod reads the winner's value; return type changed from db_spend to current_value
litellm/proxy/_types.py Bearer prefix stripping before token hashing in UserAPIKeyAuth.get_cache_key; normalizes keys arriving with Authorization: Bearer sk-... headers
litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py Adds Gemini 3.5 Flash model support; introduces _forward_gemini_function_call_id gating tool-call id to Google AI Studio only; adds top_k to supported params; replaces parallel_tool_calls validation with silent drop when value=False and multiple tools are present
litellm/types/utils.py Adds use_chat_completions_api to all_litellm_params so it is recognized as an internal flag and stripped before the request body reaches providers

Comments Outside Diff (1)

  1. litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py, line 1249-1265 (link)

    P2 parallel_tool_calls drop no longer respects drop_params

    The old guard raised a validation error when parallel_tool_calls=False was sent without drop_params=True. The new logic silently drops the param whenever value is False and there is more than one tool, regardless of drop_params. Clients that intentionally set parallel_tool_calls=False with multiple tools on Gemini — and expect an error rather than a silent no-op — will now get an unacknowledged drop. This is intentional per the PR description (Responses API clients send this flag by default), but it bypasses the user-controlled drop_params opt-in that the rest of the codebase uses for unsupported-param handling.

Reviews (1): Last reviewed commit: "chore: update uv.lock for 1.86.3" | Re-trigger Greptile

Comment on lines 1808 to +1812
processed_chunk, None, None, cache_hit
)
)
## SYNC LOGGING
self.logging_obj.success_handler(processed_chunk, None, None, cache_hit)
## SYNC LOGGING — only for sync SDK entrypoints; async proxy paths export via async_success_handler
litellm_params = self.logging_obj.model_call_details.get("litellm_params", {})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Sync callbacks silently skipped for async streaming chunks

success_handler is now only called per-chunk for sync SDK entrypoints. Any user who registers a sync callback (via litellm.success_callback) and consumes a streaming response through acompletion will no longer receive per-chunk notifications — only the final assembled response reaches sync callbacks via executor.submit inside dispatch_success_handlers. This is intentional for the duplicate-trace fix, but it's a quiet behaviour change for sync-callback users on async streaming paths.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines 414 to +440
)
return [LiteLLM_EndUserTable(**row.dict()) for row in rows]

async def _write_key_reset_updates(
self, updated_keys: List[LiteLLM_VerificationToken]
) -> None:
"""
Write per-row {spend, budget_reset_at} updates for keys.

Avoids the batched full-model update path, which trips
prisma.errors.DataError on any row carrying object_permission_id or
budget_limits (see #27730). Both fields are rejected by Prisma's
update input type for LiteLLM_VerificationToken, and the failure
aborts the entire batch — silently leaving spend over the cap and
budget_reset_at unchanged forever.
"""
batcher = self.prisma_client.db.batch_()
for k in updated_keys:
token = getattr(k, "token", None)
if token is None:
continue
batcher.litellm_verificationtoken.update(
where={"token": token},
data={"spend": 0, "budget_reset_at": k.budget_reset_at},
)
await batcher.commit()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 spend written as integer 0 instead of float 0.0

All three _write_*_reset_updates helpers pass data={"spend": 0, "budget_reset_at": ...} with an integer zero while the rest of the budget path uses 0.0 (e.g. item.spend = 0.0 in _reset_budget_item). Prisma accepts both, but the type mismatch could surface if any downstream code does a strict float comparison or if the Prisma schema enforces the type more strictly in a future migration.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@yuneng-berri
yuneng-berri merged commit 182fdcd into stable/1.86.x Jun 3, 2026
64 of 75 checks passed
@yuneng-berri
yuneng-berri deleted the litellm_cherrypick_1_86_3 branch June 3, 2026 00:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants