Skip to content

chore: sync upstream 2026-08-21 - #208

Merged
shudonglin merged 297 commits into
litellm_internal_stagingfrom
chore/sync-upstream-2026-08-21
Aug 22, 2026
Merged

chore: sync upstream 2026-08-21#208
shudonglin merged 297 commits into
litellm_internal_stagingfrom
chore/sync-upstream-2026-08-21

Conversation

@shudonglin

@shudonglin shudonglin commented Aug 21, 2026

Copy link
Copy Markdown

Full -X theirs sync of BerriAI/litellm litellm_internal_staging (286 commits behind).

Merge conflicts

Resolved 4 modify/delete conflicts by taking upstream's deletion (test files upstream removed, none tracked in fork-patches.txt). Restored two fork-only files (.github/workflows/triage_rollout_heads_up.yml, .github/scripts/triage_rollout_heads_up.py) that git's rename detection spuriously flagged as deleted during this sync's large upstream CI-workflow restructuring; upstream never had these files, so keeping the fork's version is the correct 3-way merge resolution.

Fixes landed on top of the merge

  • Restored UsageTab.test.tsx to upstream (dropped a dead QueryClientProvider wrapper left over from pre-sync fork drift)
  • Restored default_control parameter threading in AnthropicCacheControlHook._apply_message_injections (sync merge dropped it, F821)
  • Narrowed 5 pytest.raises(ValueError) calls in test_pinned_provider_routes.py (pre-existing PT011 debt, fixed while touching the ruff-tests gate)
  • Bumped LIT001/LIT011 (type-discipline) and TQ005 (test-quality) budget ceilings to match this sync's verified upstream growth
  • Removed a duplicate batch-cost claim layer that silently stopped all batch billing: upstream independently added its own _claim_job_for_costing/_release_job_claim this cycle, stacking on top of the fork's pre-existing claim layer with no merge conflict. The outer claim always won the compare-and-swap, so _track_completed_batch_cost always returned None without billing. Fixed by restoring this region to byte-identical with the pre-sync fork state.
  • Fixed genuine upstream breakage (PR fix(proxy): fail the standalone prisma migration entrypoint on migration errors BerriAI/litellm#37692) causing runtime-image/image-scan CI to fail: the standalone entrypoint's redundant prisma generate at container start always PermissionErrors under a non-root uid. Added LITELLM_PRISMA_CLIENT_PREBAKED to skip it since both runtime images already bake the client at build time.
  • Removed a duplicated ui-builder build stage in Dockerfile (merge artifact; silently harmless since Docker uses the later stage, but rebuilt the Admin UI twice per image build)
  • Moved the batch-cost claim to after the results fetch: the fork claimed a row before reading its output file, flipping batch_processed=True while the fetch was still in flight. The managed-files deletion guard only holds files whose batch still has batch_processed=False, so a concurrent delete could remove the very output file the costing run was reading, and a worker killed mid-fetch left the batch marked processed and unbillable by any other pod until the abandoned-claim sweep ran. This is a fork-design fix rather than merge-corruption cleanup, so it is written up separately below.

Verified all documented fork-patches.txt entries with real functional content survived the merge (security pins, provider-pin hardening, path-injection guards, provider display-name UI patch, Dockerfile digest pins). Zero open code-scanning alerts, repo-wide and PR-scoped.

The claim-timing fix

Upstream added its own claim layer this cycle with deliberately different timing: claim after the fetch, immediately before billing. The duplicate-layer removal above deleted upstream's copy because two stacked claims silenced billing entirely, but that left the fork on its older claim-before-fetch timing, which tests/proxy_unit_tests/test_check_batch_cost.py::TestMultiPodBatchCostClaim (upstream's new class) correctly fails.

Rather than adopt upstream's simpler single-flag claim wholesale, this PR moves the fork's claim point and keeps the fork's hardening. The status="pricing" fencing, the _reclaim_abandoned_pricing_claims sweep, the spend-dedup marker and the fenced finalize all stay, so crash-re-billing and disable_spend_logs protection are unchanged. Losing the compare-and-swap now costs only a duplicated results fetch, and is reported to the caller as a CLAIM_LOST sentinel so it stays distinct from an unroutable row. Callers no longer release a claim across the fetch: a failed fetch never took one and leaves the row untouched outright, while a failed spend-log write releases before re-raising.

The 5 previously failing tests now pass with every behavioural assertion upstream makes left intact. Only their journal and claim-call assertions were adapted to the fork's write shape, since the fork's claim carries status="pricing" and its marker and fenced finalize are separate writes. One fork test (test_cost_tracking_failure_leaves_job_unprocessed, LIT-4008) was tightened from "the only extra write is the release" to "no row writes at all", the stronger form of the same guarantee under the new ordering.

Verified by mutation: ignoring a lost claim fails test_a_pod_that_loses_the_claim_after_fetching_does_not_bill, and dropping the release-on-billing-failure fails test_a_failed_spend_log_write_releases_the_claim. The ruff-strict, type-discipline, basedpyright and test-quality budget gates all pass unchanged, with no new ceilings needed

mateo-berri and others added 30 commits August 20, 2026 02:43
…rsation system turns

On models without supports_mid_conversation_system, a system entry between
an assistant tool_use turn and the user tool_result turn became a user turn
in that position and the provider rejected the request ("tool_use ids were
found without tool_result blocks immediately after"). That run of entries
now goes right after the tool_result turn, where consecutive user turns
merge upstream. The converted turn also carries only role and content, as
the hoist did, so an entry with extra keys no longer 400s with "Extra
inputs are not permitted".

The e2e cache priming re-sends the identical first turn until its own cache
entry reads back before the reminder turn goes out, since Vertex can take a
few seconds to serve a freshly written entry.
Binding the bound method at import froze the module-level VertexBase
instance, so callers that swap it no longer reached their replacement.
Taking the secret out of ~/.litellm/token.json stages a replacement and moves it into
place, which needs room for a second file and a directory that will accept a new entry.
A full disk refuses the first and a read-only ~/.litellm the second, and logout gave up
there: it removed the file when it could, dropping the record that the keychain had never
been confirmed clear, so the logout after it reported a clean keychain it never checked

Shortening the file already in place needs neither, so the logout scrub and the legacy
migration now fall back to overwriting it where it lies. On a read-only ~/.litellm the
logout the user asked for now happens, instead of coming back with instructions to delete
the file by hand
`lite whoami` led with "Authenticated" whenever a token file was on disk, even when the
keychain holding the credential would not give it up. The notice about that sat below the
account lines, so the session read as a working one and sent the user looking for the
problem anywhere but the keychain
…arded auto_router marker params

An `auto_router/<alias>` marker entry's litellm_params (for example
`aws_region_name: eu-west-3`) were forwarded onto every routed call with
setdefault and then won the `{**litellm_params, **kwargs}` merge against
the selected tier's own values, so a Bedrock tier pinned to us-east-1 was
called in eu-west-3 and failed with 400.

The hook now records which keys it actually forwarded on the request's
metadata bucket, and `_update_kwargs_with_deployment` drops every
forwarded key the selected deployment defines itself, so marker params
only fill gaps a tier leaves open. Request-supplied values still win over
both. The stamp is stripped from logged metadata like its siblings.

Fixes BerriAI#37613
…t isolation

Five e2e tests over routes a customer drives through the gateway, each one
pinning a fix that currently has no live coverage.

The dedicated /openai_passthrough prefix used to be swallowed by the
provider-scoped /{provider}/v1/files and /{provider}/v1/batches routes, which
bound "openai_passthrough" as a provider name and failed inside the gateway
before ever reaching OpenAI. Two tests now upload a file and list batches
through that prefix and assert OpenAI's own objects come back.

Streamed /openai_passthrough/v1/responses and /openai_passthrough/v1/embeddings
are relayed to OpenAI but still have to be costed, since the customer budgets
against this traffic. Both used to land a row the gateway could not use: the
streamed responses call logged a zero-cost row under a random id, and
embeddings wrote no row at all. Each test now reconciles the logged spend and
token counts against the response the caller was actually served.

GET /v1/files narrowed its data to the caller's own rows but left first_id and
last_id addressing the shared provider account's page, handing any caller raw
provider file ids belonging to other tenants. The new test asserts both cursors
address rows in the page the caller can see.

ResourceManager.defer now accepts any callable rather than one returning None,
so a delete that answers with a response model can be deferred as-is.
…_6_cost_map

feat: add bedrock grok 4.6 to model cost map
…ct credentials

- accept the Live SDK's models/<id> and LiteLLM's vertex_ai/<id> when rewriting the setup model
- keep a dict service account intact instead of stringifying it
- treat same-target deployments holding different credentials as ambiguous
- guard both websocket states before every close so a second close cannot raise
- build the sendable close codes from the public CloseCode enum
…g flag

The final streaming usage frame only carries usage.cost when the proxy runs
with litellm_settings.include_cost_in_streaming_usage: true, and that flag is
readable only off the module-level litellm setting. There is no header, key,
or management route that turns it on per request, so a test cannot ask the
shared e2e proxy for it, and the proxy's config does not live in this repo.

The registry row stays as an uncovered gap with the reason recorded, rather
than being deleted, so the behavior is still on the list of things we want
covered once the gateway config is reachable.

The StreamOptions model, ChatBody.stream_options, Usage.cost, and
AnthropicMessagesResponse.id existed only for that test, so they go with it.
…d keep tier overrides and marker flags

The forwarded-keys record moves off the shared metadata dict onto the
per-request kwargs, where _update_kwargs_with_deployment consumes it, so
sibling requests that share a metadata dict (abatch_completion) can no
longer clear it mid-routing. Per-tier litellm_params from the hook
response are never treated as forwarded marker params, and a deployment
only beats a forwarded value when it sets its own, not when it carries a
LiteLLM_Params default such as merge_reasoning_content_in_choices=False.
When a full disk refuses the replacement file and a read-only token file
refuses the rewrite in place, the only way left to get the secret off disk
is to remove the file carrying it. That file was also the note saying the
keychain went unchecked, so its absence made the next logout read a
keychain that was never confirmed as one already known to be clean.

Removing it is what frees the room the replacement was refused for, so the
note is written again on the way out and the logout after this one still
warns.
…sation-system-cache

fix: preserve prompt cache for mid-conversation system on unflagged Claude models
… extra

`lite up` treats a token record whose key the keychain would not hand over as no
login at all, and that clause had no test: every existing freshness test passed a
record carrying a real key, so deleting the clause left the whole suite green

The base install smoke check now also asserts keyring is absent, which is what
makes the lazy import in cli_keyring meaningful. keyring ships in the cli extra
only, so a plain `pip install litellm` must not be able to reach it
…essages bridge

Both /v1/messages bridges (Responses API adapter for openai/* and the
chat-completions adapter) now derive prompt_cache_key from the first 64
characters of metadata.user_id, next to the existing user mapping. The
chat bridge only sets it when the resolved provider advertises
prompt_cache_key in its supported params, so providers that reject
unknown params are unaffected. A prompt_cache_key sent explicitly by the
client always wins over the derived value.

Fixes BerriAI#37508
The proxy's OAuth authorization server (dynamic registration, PKCE S256,
loopback redirects, single-use codes, refresh rotation) gains a proxy-API
audience: /authorize?resource=<proxy origin> renders a consent page with
team selection and /token mints the same per-user credential lite login
mints, so a native CLI can sign a user in through the system browser and
call /v1/* with user and team attribution. Adds GET /.well-known/litellm-cli-auth
as the versioned discovery contract for non-Python clients, POST /revoke
(RFC 7009) for logout, and lite login --pkce, lite logout, and
lite auth print-token on the CLI side. Proxy-API grants only ever redirect
to a loopback address and the server never picks a team on the user's behalf.

Fixes BerriAI#37332
The two tests that assert on reasoning cost read reasoning_tokens off the
response and required it to be nonzero, without ever asking the model to
reason. Both now send reasoning_effort, so the assertion rests on a
parameter the test sets rather than on the model's default behavior.

The cache-breakdown test sends it on its prime call too: OpenAI's prefix
cache keys on the reasoning setting as well as the tokens, so priming at
a different effort never produces a read.
A pre-flight that times out leaves its write parked inside the keychain, holding
it against every later call, so the next read blocks on the main thread with no
timeout of its own. Anything that resolves the credential more than once in a
process hits it: an SDK Client built a second time never returns.

The vault now remembers the silence and reports the keychain unreachable for the
rest of the process rather than queueing behind the parked call.
…on GPT-5.6+ targets

When the resolved deployment is provider openai and the model is GPT-5.6 or
newer, the cache control hook now writes prompt_cache_breakpoint on the
targeted content block and sets prompt_cache_options to explicit mode unless
the caller already passed one. The /v1/messages bridges carry the marker
through (the Responses bridge moves a marked system prompt into a developer
message, since top-level instructions cannot hold one). Breakpoint counting
and the stand-down check recognise both marker kinds, and client breakpoints
already present in messages are no longer subtracted from the cap twice.

Fixes BerriAI#37509
…bling records

The discovery document is accepted only when its issuer is the --base-url the user
typed and every endpoint and the resource share that origin (RFC 8414 section 3.3),
so a tampered or redirected document can no longer point the code, verifier, or
refresh token at another host. After a failed refresh the re-read token record is
used only when it continues the same credential (same proxy, token endpoint, and
resource) and has not expired, so a concurrent login against a different proxy can
never hand this one its key
A login the keychain accepted whose token file could not be replaced left the
superseded secret on disk, and the next load preferred the file unconditionally,
so it served the old credential and erased the new one from the keychain on the
way past. The keychain entry now carries the timestamp of the sign-in that minted
it, and the two stores are compared on that instead.
The two stores hold different credentials on that path, so the rollback a
migration does would hand the superseded one back out. The login that could not
replace the file already named the state, and logout reports it too.
The stamp in the keychain entry is what decides that secret against one still
sitting in the token file, and it came straight off the wall clock. A clock
that stepped backwards between two logins therefore handed the win to the
older of them: a login the keychain took but the token file could not be
pointed at was resolved back to the credential it replaced, and the fresh one
was erased from the keychain on the way past.

save_cli_token now reads the stamp already on disk and pins the new sign-in
just above it, so the ordering never depends on the clock having moved
forwards. On a clock that did, this changes nothing.
The comment above the extra named cryptography as one of the heavy imports a
thin install leaves out. That stopped being true when keyring joined the
extra: on Linux it reaches the Secret Service through secretstorage, which
depends on cryptography.
A login the keychain took but the token file could not record leaves the
keychain naming a later sign-in than the file does. Reading only the file
then stamps the next login below that keychain entry, and a clock that
went back far enough puts the superseded credential back in use.
devin-ai-integration Bot and others added 27 commits August 20, 2026 19:32
…ting a placeholder (BerriAI#37686)

SCIM group members were matched against litellm user ids only. An identity
provider that lists people by email or by the OIDC subject therefore matched
nothing, and the member fell through to placeholder creation.

Since BerriAI#37688 made a failed member creation fail the group sync rather than drop
the member, that fallthrough is no longer quiet: the placeholder is created with
user_email set to the member value, the duplicate-email check rejects it, and the
whole group push answers 500. So on current staging a group listing anyone by
their email fails outright, every other member in the payload included.

An unmatched member id is now looked up across sso_user_id and user_email in one
query. Searching either field first would hide a value that names one account by
its SSO identity and another by its email, and hand the group to whichever was
searched first. The two are not compared alike: an email is matched the way
new_user matches one before accepting a new account, case-insensitively, because
matching more strictly than the layer that would reject the placeholder is what
turned an id whose casing differed from the stored email into that same 500. An
SSO identity is matched exactly, since OIDC defines sub as case-sensitive and
nothing folds its case on the way in.

An exact user_id hit is checked the same way rather than trusted outright, since a
value can be one account's id and another's SSO identity or email. That is not a
corner case: the placeholders this bug provisioned are keyed by the very id the
provider keeps pushing, so on a tenant that already has them the placeholder wins
the id lookup and the real account can never be matched. Refusing names the
problem instead of silently landing on the placeholder again. Those rows still
have to be deleted before the real account resolves; making the sync heal itself
needs a trustworthy way to tell a placeholder from an account someone created, and
created_via lives in caller-writable metadata, so it is left to a follow-up.

A value that names more than one account is refused with a 400 naming the id
rather than attributed to one of them.

Removals resolve too, since the roster holds canonical user ids and a directory
removes people by the id it added them with. A removal counts the members one
value names: the id as written when the roster holds it verbatim, which is how an
earlier release recorded a member it could not match, together with the members it
resolves to. Counting only the accounts on the roster keeps someone removable
after a second account takes their email, which resolving table-wide would not,
and counting both ways of naming a member together stops one value revoking two
people when it is one member's canonical id and another's email. A value naming
two of the group's own members is undecidable and fails rather than guessing or
reporting a removal it did not perform.

Resolves LIT-5383

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

fix(cost): match streamed Messages usage cost to the recorded spend
…ons (BerriAI#37748)

* test: enforce PT012 so a pytest.raises block cannot hide dead assertions

`with pytest.raises(...)` stops at the first statement that raises. Anything
sequenced after it inside the block never runs, so an assertion written there is
never checked and the test still reports green.

Two sites were doing exactly that, and both assertions turned out to be wrong
once they started running. tests/llm_translation/test_prompt_factory.py asserted
the bedrock rejection names "requires at least one non-system message", which
holds. tests/proxy_unit_tests/test_proxy_server.py asserted the prisma startup
failure mentions "httpx.ConnectError", which never appears: the failure is an
httpx.ConnectError whose message is "All connection attempts failed", so that
test now asserts the type. Its DATABASE_URL override moves to monkeypatch, since
the old restore sat below the assertion and leaked the invalid URL into every
later DB test the moment the assertion started being able to fail.

The remaining 72 sites are rewritten without changing what they exercise: setup
that cannot raise moves above the block, a nested `patch` moves outside it, and
bodies with real control flow (a stream drain, an if/else on sync_mode, a
retry loop) move into a local closure the block calls.

Fixing PT012 unmasked two B017s, since ruff only reports a blind
pytest.raises(Exception) once the block holds a single statement.
tests/proxy_unit_tests/test_auth_checks.py narrows to the ProxyException
can_key_call_model actually raises. tests/local_testing/test_completion_cost.py
was asserting vertex_ai/medlm-medium has no cost entry, which stopped being true
at some point; that dead first half is gone and the rest of the test, which
checks medlm pricing resolves above zero, now runs instead of being skipped.

* chore(ci): ratchet TQ004 to 768 after the prisma test moved to monkeypatch
…ning

The cost map shipped cognition/swe-1.7 at $2.50 in / $12.50 out per million with
$1.00 cache reads. Those are the Lightning numbers. Cognition's own model list at
https://docs.devin.ai/desktop/models has uid swe-1-7 at $0.50 / $2.50 with $0.20
cache reads, and uid swe-1-7-lightning at $2.50 / $12.50 with $1.00 cache reads,
so every swe-1.7 call has been costed at 5x since the entry landed.

swe-1.7 now carries the standard rates and the Lightning tier gets its own entry,
in both cost map copies. The source field on both moves to the desktop models page,
which is the one that lists both tiers.
`members_with_roles` is a denormalized JSON snapshot written at add-time.
`_update_team_members_list` backfilled `user_id` from `user_email` but never
the reverse, so a member added by `user_id` alone was stored with
`user_email=None` permanently - and `/team/info` returns that blob verbatim
with no join to `LiteLLM_UserTable`, so the Admin UI's member table renders
"-" for a user that plainly has an email.

Fix both ends:

- write path: `_resolve_member_identity` resolves identity both ways off the
  user rows the add just touched, so new roster entries stop being born blank.
- read path: `/team/info` fills blank emails from `LiteLLM_UserTable` in one
  indexed `user_id IN (...)` query, repairing rows already in the database.
  Members that already carry an email are passed through untouched and cost
  no query, so this only ever turns a null into the right value.
Add sync and async regression tests for the BaseLLMHTTPHandler streaming
path, which forwards provider response headers for the ~30 providers that
ride the generic handler and had no coverage. Also drop redundant setup
prose from the moonshot invoke test docstring.
…eam_spend_rows

fix(streaming): price partial-stream spend rows at the real model and keep prompt and cache fields
…sponse_headers

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

fix(fal_ai): price gpt-image-2 per size and quality from request params
…_model_name

sagemaker_chat never put X-Amzn-SageMaker-Inference-Component on the request, so any endpoint
backed by inference components answered 400 INFERENCE_COMPONENT_NAME_MISSING and the call never
reached the container. The legacy sagemaker provider has built that header from model_id since
BerriAI#8889, and this brings the chat provider in line. It goes on in validate_environment, which runs
before the request is SigV4-signed, so the signature covers it

The request body also always named the endpoint rather than the served model, which containers
that validate the body's model answer with a 404. hf_model_name now becomes the body's model
when it is set, and endpoints that do not set it keep sending exactly what they send today
…arametrize cases (BerriAI#37769)

`pytest.raises(Exception)` with no `match=` passes on any error that broad. A
TypeError from a refactor, a botched fixture, an import that moved: all of them
read as the rejection the test claims to police, so the test goes green for the
wrong reason and stays green after the behaviour it guards is gone.

PT011 closes that gap for the 317 sites B017 could not reach, because B017 only
fires on a single-statement body with no `as e` binding. Each pattern here is the
message the code actually raised, recorded by running the sites under a plugin
that logged the concrete type and text per call site, so the assertions describe
observed behaviour rather than a guess. Where a site raises more than one message
across its parametrize cases, the pattern is an alternation of what was seen;
where the exception carries an empty `str()` and puts the text on `.message`, the
site keeps a narrow `noqa` with the reason.

PT014 removes four parametrize cases that were listed twice. The duplicate re-runs
an assertion that already passed, and it usually marks a case someone meant to
vary and forgot to edit.
…r_email

fix: populate team member emails missing from the roster snapshot
…_response_headers

fix(bedrock): forward provider response headers on chat completions
…we_1_7_pricing

fix(cognition): price swe-1.7 at the standard tier, add swe-1.7-lightning
…inference_component_header

fix(sagemaker_chat): send the inference component header and honor hf_model_name
# Conflicts:
#	.github/workflows/triage_rollout_heads_up.yml
#	tests/litellm/llms/deepseek/chat/test_deepseek_chat_transformation.py
#	tests/old_proxy_tests/tests/bursty_load_test_completion.py
#	tests/old_proxy_tests/tests/load_test_embedding_100.py
#	tests/old_proxy_tests/tests/test_openai_request_with_traceparent.py
…ider wrapper)

Pre-existing fork drift (present before this sync): the sync merge kept an
orphaned QueryClientProvider import/mock/variable from a previous fork edit
whose actual JSX usage upstream had already dropped when UsageTab stopped
using react-query, leaving an unused-imports lint error. UsageTab.tsx has no
react-query usage, so restoring the test file to match upstream exactly is
correct.
Sync merge corruption (same class as the three prior -X theirs incidents
documented in fork-patches.txt for this file): upstream independently
restructured _apply_message_injections this cycle and the merge took
upstream's simplified signature (no default_control parameter) while
keeping the fork's inner-body reference to it, an F821 undefined-name.

Restores default_control as an optional keyword param, threaded from the
resolved per-request header default at the main pre_call_hook call site
and the literal ephemeral default at the system-block call site, falling
back to ChatCompletionCachedContent(type="ephemeral") when unset so the
third (upstream-only) call site keeps its existing behavior unchanged.
…routes

Pre-existing PT011 debt (file predates this sync unchanged), fixed while
here since it was blocking the ruff-tests gate. Each match= pattern is
verified against the exact ValueError message its call site's guard
raises in litellm/proxy/pinned_provider_routes.py.
LIT001 22714->22942, LIT011 5585->5616: this sync's upstream changes to
litellm/proxy/common_utils/reset_budget_job.py and
litellm/proxy/spend_tracking/budget_reservation.py (byte-identical to
upstream/litellm_internal_staging, verified via git diff) are the source;
make lint-budget-update cannot reconcile a rule that grew, so the ceiling
is bumped by hand to the CI-verified exact totals, same class as every
prior ratchet bump in this file's history.
TQ005 2832->2836: the 5 newly flagged lines are byte-identical to
upstream/litellm_internal_staging (verified via git diff), same class
as the LIT001/LIT011 bump in the prior commit.
Sync merge corruption (same recurring class as this file's 2026-08-09/
2026-08-14/2026-08-15 entries): upstream independently added its own
second job-claim layer (_claim_job_for_costing / _release_job_claim,
called from inside _track_completed_batch_cost right before the spend
log write) this cycle, landing in disjoint hunks from the fork's
pre-existing claim layer (_claim_job / _release_job_claim / _finalize_job,
called earlier in check_batch_cost). The -X theirs merge auto-applied
both with no conflict, stacking two claim layers on the same row: the
outer claim always flips batch_processed=True first, so the inner
claim's own compare-and-swap always found 0 rows and
_track_completed_batch_cost always returned None without ever billing.
Python also silently kept only the last of the two same-named
_release_job_claim definitions, shadowing the fork's fenced one.

Restores this file to byte-identical with the pre-sync commit for the
affected region. A separate, genuine pre-existing gap in the fork's
claim timing (unchanged since before this sync) is documented in
fork-patches.txt and left for deliberate follow-up work rather than a
rushed redesign under this fix.
Genuine upstream breakage from this sync's PR BerriAI#37692 (made the
standalone entrypoint's prisma generate failure fatal by default
instead of log-only), colliding with a real non-root permission
constraint: prisma-python's generate() unconditionally re-copies
schema.prisma into the installed package and chmod's the copy even
when content already matches, and chmod requires owning the
destination file, which no arbitrary runtime uid ever does for a file
baked into the image at build time.

Both runtime images already bake the generated client from this same
schema.prisma at build time, so regenerating it at container start was
always redundant work. Add LITELLM_PRISMA_CLIENT_PREBAKED to skip the
runtime entrypoint's prisma generate call when set, and set it in both
Dockerfile and docker/Dockerfile.non_root.
Tracks the prisma_migration.py/Dockerfile/Dockerfile.non_root fix from
the previous commit so a future -X theirs sync doesn't silently drop
it, consistent with this file's convention for confirmed upstream
breakage fixed fork-side.
Sync merge artifact: two byte-identical FROM ... AS ui-builder blocks
survived non-conflicting since Docker silently uses the later stage
definition for a repeated name, so the build never broke, but it
rebuilt the Admin UI twice per image build. Keep the single stage.
CheckBatchCost claimed a row before fetching its results, flipping
batch_processed=True while the output file was still being read. That
broke two things. The managed-files deletion guard only holds files
whose batch still has batch_processed=False, so a concurrent delete
could remove the very output file an in-flight costing run was reading.
And a worker killed mid-fetch left the batch marked processed, so no
other pod would select it until the abandoned-claim sweep ran.

Move the claim to where upstream's own claim sits, inside
_track_completed_batch_cost immediately before the spend-log write,
without adopting upstream's simpler single-flag claim: the fork's
status="pricing" fencing, reclaim sweep, spend-dedup marker and fenced
finalize are all kept, so the crash-re-billing and disable_spend_logs
protections are unchanged. Losing the compare-and-swap now costs only a
duplicated results fetch, and is reported to the caller as CLAIM_LOST so
it stays distinct from an unroutable row.

Callers no longer release a claim across the fetch: a failed fetch never
took one and leaves the row untouched, while a failed spend-log write
releases before re-raising.

This unblocks tests/proxy_unit_tests/test_check_batch_cost.py::
TestMultiPodBatchCostClaim, upstream's new class this sync cycle. Its
journal and claim-call assertions are adapted to the fork's write shape
while every behavioural assertion upstream makes is preserved.
@shudonglin
shudonglin merged commit dc53c1f into litellm_internal_staging Aug 22, 2026
95 checks passed
@shudonglin
shudonglin deleted the chore/sync-upstream-2026-08-21 branch August 22, 2026 04:52
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.

10 participants