Skip to content

test(e2e): bound the post-/model/new servable wait at 40s - #35020

Merged
yuneng-berri merged 5 commits into
mainfrom
litellm_hotfix_e2e_model_servable_timeout
Jul 29, 2026
Merged

test(e2e): bound the post-/model/new servable wait at 40s#35020
yuneng-berri merged 5 commits into
mainfrom
litellm_hotfix_e2e_model_servable_timeout

Conversation

@yuneng-berri

@yuneng-berri yuneng-berri commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

TLDR

Problem this solves:

  • A broken model reload cost 120s per deployment before failing
  • That 120s is the spend-log read-back budget, not a propagation budget
  • One /v1/models hit can land on an already-hot worker
  • The next /chat then hits a cold worker and 400s

How it solves it:

  • Bound first /v1/models listing at 40s instead of 120s
  • Then require 30s of continuous listing so every worker syncs
  • Cap each readiness poll at 5s, clamped to the remaining deadline
  • Hotfix onto main off litellm_hotfix_*, per the main-branch guard

Relevant issues

Hotfix promotion of #35012 to main. Same change, cherry-picked verbatim off main; #35012 still carries it into litellm_internal_staging

Linear ticket

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

The "meaningful tests" box is unchecked deliberately. #35012's third commit (89204651) removed tests/e2e/test_proxy_client_model_servable.py, so the final diff ships no unit coverage for await_servable. This hotfix is a verbatim promotion of that diff and does not add the file back; the coverage question belongs on #35012, which owns the change

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

Carried over from #35012, captured by its author at commit c082a0e (the first commit of the series) against a live proxy (python litellm/proxy/proxy_cli.py --config litellm/proxy/dev_config.yaml), timing the wait against a model name the data plane never serves

Before, on the old budget (poll_timeout=120s, poll_interval=5s), at c082a0e's parent:

failed after 121.4s
message: model 'model-that-will-never-be-listed' was created but never became servable
on the data plane within 120.0s of /model/new (control/data-plane propagation or
STORE_MODEL_IN_DB reload issue)

After, at c082a0e (model_servable_timeout=40s, model_servable_interval=2s, model_servable_request_timeout=5s):

failed after 38.6s
message: model 'model-that-will-never-be-listed' was created but never became servable
on the data plane within 40.0s of /model/new (control/data-plane propagation or
STORE_MODEL_IN_DB reload issue)

Two caveats a reviewer should carry into the runbook below rather than take from the numbers above. That capture predates 5aa66ea (7d1ee2f on #35012), which added the 30s continuous-listing window, so the happy-path timing quoted on #35012 (2.31s, one poll) no longer describes this diff: a successful create_model now holds for model_servable_db_sync_seconds before returning. And the failure-path capture is from the same earlier commit, so the message text it shows differs from the one this diff emits, which names both the 40s first-listing budget and the 30s sync window

Type

✅ Test

Changes

_await_model_servable polled /v1/models to poll_timeout, which exists for eventually-consistent read-backs like spend rows. Model readiness is a different wait: after /model/new the data plane must list the model before callers can invoke it. Sharing the spend read-back budget meant a broken reload was absorbed as a two-minute stall per model rather than reported

Fixed harness constants: model_servable_timeout (40s) for first listing, model_servable_db_sync_seconds (30s, the product default proxy_config_reload_interval_seconds) of continuous listing after first sight, model_servable_interval (2s), and model_servable_request_timeout (5s, clamped to the remaining budget). None of these are read from live proxy config. A single /v1/models success is not enough on a multi-worker gateway: only the writer reloads immediately, and peers sync on the add_deployment job

The poll loop is extracted as await_servable, a pure function over an injected clock and sleep plus a list_models callback taking the per-poll request timeout, returning a Servable | NotServable union. Transport.get gains an optional per-call timeout so the poller can pass a deadline shorter than the transport-wide one

Scope note: this changes only how long the harness waits before reporting. It does not make a model propagate faster, and it does not fix any test that fails after the wait succeeds. Read it as latency-of-failure, not as a fix for model-propagation failures

Why this is on main and not only on staging: .github/workflows/guard-main-branch.yml accepts PRs to main from litellm_internal_staging or a litellm_hotfix_* branch, so this branch is cut from main and carries #35012's five commits with cherry-pick -x. tests/e2e/proxy_client.py and tests/e2e/transport.py were identical on main and litellm_internal_staging at 9ead580, so the picks applied with no conflict and both blobs match #35012's head exactly

QA runbook

  • ProxyClient._await_model_servable - a model the data plane never lists fails in ~40s instead of ~120s, with the diagnostic preserved

    • Start a proxy: python litellm/proxy/proxy_cli.py --config litellm/proxy/dev_config.yaml (needs STORE_MODEL_IN_DB=True and a reachable DATABASE_URL)
    • POST /model/new with the master key and a real deployment, then immediately POST /chat/completions against that model name; expect 200, proving the happy path still resolves
    • Time that create_model; expect roughly 30s, not the sub-second first-poll return, because the DB-sync window now holds before returning
    • Call ProxyClient._await_model_servable("model-that-will-never-be-listed") and time it; expect an AssertionError in ~40s naming the model, the 40.0s first-listing budget, and the 30.0s sync window
    • Sanity check: 40s and 30s are deliberate harness constants (not proxy general_settings) and are not hand-wavey or potentially flaky
  • Transport.get(..., timeout=...) - the new per-call override, so one slow /v1/models cannot outlast the remaining budget

    • Point the harness at a proxy that stalls /v1/models, or drop the first-listing budget below 5s, and confirm total wall time still respects the budget rather than the transport-wide request_timeout
    • Sanity check: mutating await_servable to pass the full 5s cap while ignoring the remaining budget should make the wait overrun its deadline

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

Left unchecked for the same reason as the tests box: the diff this promotes ships no unit coverage for await_servable, so there is nothing here that would fail if the deadline arithmetic or the request-timeout clamp regressed

_await_model_servable used poll_timeout (120s), the spend/log read-back
budget. A stuck model reload therefore stalled every suite that creates a
deployment for two minutes before failing

Give create_model a fixed harness middle ground: model_servable_timeout=40s,
polled every 2s, with each /v1/models call capped at 5s and clamped to the
remaining deadline so one slow GET cannot overrun the wait. Happy path still
returns on the first listing. Not derived from proxy general_settings or env

Transport.get accepts an optional per-call timeout for that clamp. Unit tests
cover the deadline arithmetic and clamp without a live proxy

(cherry picked from commit c082a0e)
create_model returned after the first /v1/models hit that listed the model,
so chat could still land on a cold gateway worker (numWorkers>1 / peer pod)
and 400 Invalid model name. Require continuous listing for the product
default add_deployment interval (30s) after first sight so every worker has
synced from the DB; first listing still bounded at 40s

(cherry picked from commit 7d1ee2f)
Keep the create_model DB-sync wait in the harness; the pure-function unit
file is not needed for this PR

(cherry picked from commit 8920465)
When less than one full poll interval remained in the first-listing budget,
the pre-sleep check returned NotServable without another /v1/models call.
Sleep only min(interval, time left) so a model that becomes listable in the
last seconds of the timeout still gets a clamped final poll

(cherry picked from commit 8439195)
A poll may start with remaining budget and still return after started+timeout
if the transport overruns its clamp. Recheck the first-listing deadline after
the response so a late listing does not open the continuous DB-sync phase

(cherry picked from commit 7ff2bcb)
Comment thread tests/e2e/proxy_client.py
)
t = now()
if not listed:
first_seen_at = None

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.

P1 Reset revives expired deadline

When a model is listed within 40 seconds but a later poll lands on a worker that has not reloaded, clearing first_seen_at restores the already-expired first-listing deadline. The next iteration immediately returns NotServable, causing create_model to fail instead of restarting the continuous synchronization window.

Comment thread tests/e2e/proxy_client.py
Comment on lines +186 to +189
f"model {model_name!r} was created but never became servable on the data "
f"plane within {timeout}s of first listing (plus {db_sync_seconds}s continuous "
f"DB sync) after /model/new (control/data-plane propagation or "
f"STORE_MODEL_IN_DB reload issue){last_error}"

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 Diagnostic reverses timeout phase

The message describes timeout as running “of first listing,” although this value is the budget before first listing. This misidentifies the failed phase in E2E output, especially when the model was never listed or failed during the continuous synchronization window.

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!

@greptile-apps

greptile-apps Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR bounds the E2E model-readiness wait and adds per-request timeout overrides.

  • Extracts model readiness into a two-phase poller with a 40-second first-listing budget and 30-second continuous-listing window.
  • Adds configurable model-readiness timing fields to ProxyClient.
  • Adds an optional timeout override to transport GET requests.

Confidence Score: 3/5

This PR should not merge until transient listing misses restart the continuous synchronization window without reviving an expired first-listing deadline.

A realistic multi-worker response sequence can list the model once and later omit it; after 40 seconds, that miss makes the poller fail immediately even though the model can recover and satisfy a new continuous-listing window. The resulting diagnostic also labels the timeout phase incorrectly.

Files Needing Attention: tests/e2e/proxy_client.py

Important Files Changed

Filename Overview
tests/e2e/proxy_client.py Adds bounded two-phase model readiness polling, but a transient miss after the initial deadline prematurely terminates synchronization and the failure message reverses the timeout phase.
tests/e2e/transport.py Consistently adds and forwards an optional per-call GET timeout across the protocol and concrete transports.

Reviews (1): Last reviewed commit: "fix(e2e): reject first listing that retu..." | Re-trigger Greptile

@yuneng-berri
yuneng-berri merged commit 2cd62cf into main Jul 29, 2026
63 checks passed
@yuneng-berri
yuneng-berri deleted the litellm_hotfix_e2e_model_servable_timeout branch July 29, 2026 00:58
@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

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.

4 participants