Skip to content

fix(pass-through): clean stale routes from auth allowlist - #26082

Open
silencedoctor wants to merge 11 commits into
BerriAI:litellm_internal_stagingfrom
silencedoctor:fix/pass-through-stale-route-cleanup
Open

fix(pass-through): clean stale routes from auth allowlist#26082
silencedoctor wants to merge 11 commits into
BerriAI:litellm_internal_stagingfrom
silencedoctor:fix/pass-through-stale-route-cleanup

Conversation

@silencedoctor

@silencedoctor silencedoctor commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • Authenticated pass-through reloads can leave stale allowlist paths
  • Removed or unauthenticated endpoints can retain obsolete access paths

How it solves it:

  • Reconcile prior and current authenticated endpoint paths on reload
  • Preserve live, shared, idless, and built-in OpenAI routes

Relevant issues

Fixes #26081

Linear ticket

Pre-Submission checklist

  • I have added meaningful tests
  • My PR passes all CI/CD checks
  • My PR scope is isolated to stale pass-through auth paths
  • I have received a Greptile Confidence Score of at least 4/5

Delays in PR merge?

If review is delayed, please use the LiteLLM Slack #pr-review channel

Screenshots / Proof of Fix

No end-to-end proof was captured for this internal reload registry change

Type

Bug Fix

Changes

The registry bounding fix from the original PR is already present in the current base

This PR now removes dynamic auth allowlist paths that belonged to authenticated endpoints in the previous reload but are absent from the current configuration

Cleanup preserves paths still used by another endpoint, paths retained by idless reloads, and built-in OpenAI routes. It also handles endpoints whose authentication changes from enabled to disabled

Validation

The pass-through and auth test suites completed with 135 passing tests and 1 skipped test

Final Attestation

  • The tests cover removed, shared, idless, auth-disabled, and built-in route cases

@veria-ai veria-ai Bot left a comment

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.

Medium: Stale routes left in authorization allowlist after cleanup

This PR addresses unbounded growth in _registered_pass_through_routes by using dict.pop() instead of remove_endpoint_routes(). However, the old method also cleaned up stale paths from LiteLLMRoutes.openai_routes, which is an authorization allowlist checked during route access control. The new code skips that cleanup, so stale pass-through paths accumulate in the allowlist indefinitely.

In practice, a removed route would 404 at the framework level since the FastAPI handler isn't re-registered, limiting exploitability. But the openai_routes list is consulted for budget enforcement, OAuth2 routing, and admin-viewer access checks, so a growing stale list could have subtle authorization side effects.

Comment thread litellm/proxy/pass_through_endpoints/pass_through_endpoints.py Outdated
@greptile-apps

greptile-apps Bot commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This one-line fix replaces a broken remove_endpoint_routes(endpoint_key) call in initialize_pass_through_endpoints with _registered_pass_through_routes.pop(endpoint_key, None). The root cause is clear: the old helper compared a full composite key (e.g. {uuid}:exact:/path:GET,POST) against value[\"endpoint_id\"] (UUID only), so stale entries for DB-sourced endpoints without an id field were never removed, causing unbounded registry growth and eventual CPU saturation. The fix is correct and well-scoped — the two other call sites of remove_endpoint_routes (explicit delete/update paths) are intentionally left unchanged.

Confidence Score: 5/5

Safe to merge; the fix is minimal, targeted, and does not affect any other code path.

The single changed line correctly resolves the root cause with no side effects. The only finding is P2 (test quality — first two unit tests verify dict built-in behavior rather than the production code path), which does not affect correctness or reliability.

No files require special attention.

Important Files Changed

Filename Overview
litellm/proxy/pass_through_endpoints/pass_through_endpoints.py Replaces the broken remove_endpoint_routes(endpoint_key) call (which never matched because a composite key was compared against UUID-only values) with a direct O(1) dict.pop(). The fix correctly skips LiteLLMRoutes.openai_routes cleanup since those entries are path-keyed (not UUID-keyed) and remain valid across UUID changes.
tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py Adds TestRemoveStaleEndpointRoute with 3 tests; the core regression test (cycle simulation) is valuable, but the other two tests exercise dict built-in behavior rather than production logic.

Reviews (1): Last reviewed commit: "fix(pass-through): pop stale route key d..." | Re-trigger Greptile

Comment thread tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py Outdated
@codspeed-hq

codspeed-hq Bot commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

Congrats! CodSpeed is installed 🎉

🆕 16 new benchmarks were detected.

You will start to see performance impacts in the reports once the benchmarks are run from your default branch.

Detected benchmarks


Open in CodSpeed

@codecov

codecov Bot commented Apr 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@silencedoctor
silencedoctor force-pushed the fix/pass-through-stale-route-cleanup branch from a38aeb5 to 4742643 Compare May 25, 2026 03:45
@silencedoctor
silencedoctor changed the base branch from main to litellm_internal_staging May 25, 2026 03:45
@silencedoctor

Copy link
Copy Markdown
Contributor Author

Rebased onto litellm_internal_staging and retargeted the PR base accordingly, per the Release Cycle doc — external contributions now go to litellm_internal_staging (the active default branch); main only accepts promote-from-staging PRs.

One small conflict in tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py (upstream and this PR both appended a test to the end of the file); resolved by keeping both unrelated tests. The production fix in pass_through_endpoints.py auto-merged cleanly.

@silencedoctor
silencedoctor force-pushed the fix/pass-through-stale-route-cleanup branch from 4742643 to 8fd5b98 Compare May 25, 2026 06:36
@CLAassistant

CLAassistant commented May 25, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
8 out of 9 committers have signed the CLA.

✅ tin-berri
✅ yuneng-berri
✅ yucheng-berri
✅ mubashir1osmani
✅ shivamrawat1
✅ silencedoctor
✅ ryan-crabbe-berri
✅ mateo-berri
❌ devin-ai-integration[bot]
You have signed the CLA already but the status is still pending? Let us recheck it.

@silencedoctor
silencedoctor changed the base branch from litellm_internal_staging to litellm_oss_branch May 25, 2026 06:36
silencedoctor added a commit to silencedoctor/litellm that referenced this pull request May 25, 2026
Veria Medium / Greptile P2 feedback on BerriAI#26082.

The new pop-loop only removed stale keys from the internal registry but
left their paths in LiteLLMRoutes.openai_routes.value — the allowlist
consulted by RouteChecks.is_llm_api_route for authorization decisions.
That allowlist would still grow unboundedly across reload cycles.

Extract _remove_stale_pass_through_routes helper that pops the registry
entry AND removes the path (plus wildcard for subpath routes) from
openai_routes.value, mirroring what remove_endpoint_routes does. Add a
direct test that drives the helper and asserts both data structures
shrink, addressing Greptile's note that the existing tests exercised
dict.pop() semantics rather than the production cleanup logic.
@silencedoctor

Copy link
Copy Markdown
Contributor Author

Addressed Veria Medium + Greptile P2: extracted _remove_stale_pass_through_routes helper that pops the registry entry and removes the route's path (plus wildcard for subpath routes) from LiteLLMRoutes.openai_routes.value — mirroring what the old remove_endpoint_routes did, so the authorization allowlist no longer grows unboundedly across reload cycles. Added a direct test driving the helper that asserts both the registry and allowlist shrink. cc @greptileai

@greptile-apps

greptile-apps Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

The fix looks good. The _remove_stale_pass_through_routes helper correctly addresses both concerns:

  1. Veria Medium (allowlist growth): Properly handled — the helper removes the path and wildcard from LiteLLMRoutes.openai_routes.value before popping the registry entry.

  2. Greptile P2 (test coverage): test_helper_also_clears_openai_routes_allowlist now calls the actual production function and asserts both the registry and allowlist shrink. That's meaningfully better coverage than the first two tests.

One minor nit: the verbose_proxy_logger.debug call sits outside the if route_info is not None: block, so it fires even when route_info is None (key already absent from the registry). That could produce misleading log lines claiming a route was removed when nothing was actually there.

        route_info = _registered_pass_through_routes.pop(endpoint_key, None)
        if route_info is not None:
            path = route_info.get("path")
            if isinstance(path, str):
                openai_routes = LiteLLMRoutes.openai_routes.value
                if path in openai_routes:
                    openai_routes.remove(path)
                if route_info.get("type") == "subpath":
                    wildcard_path = path.rstrip("/") + "/*"
                    if wildcard_path in openai_routes:
                        openai_routes.remove(wildcard_path)
            verbose_proxy_logger.debug(
                "Removed stale pass-through route from registry: %s",
                endpoint_key,
            )

Otherwise the change is correct and the core regression test (test_registry_does_not_grow_across_reload_cycles) combined with the new allowlist test gives solid confidence.

Comment thread litellm/proxy/pass_through_endpoints/pass_through_endpoints.py Outdated
@veria-ai

veria-ai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

PR overview

All previously flagged issues have been addressed. No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

Fixed/addressed: 2 · PR risk: 0/10

yuneng-berri and others added 10 commits July 21, 2026 19:03
chore(ci): promote internal staging to main
chore(ci): promote internal staging to main
chore(ci): promote internal staging to main
chore(ci): promote internal staging to main
_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)
…l_servable_timeout

test(e2e): bound the post-/model/new servable wait at 40s
Remove authenticated pass-through paths from LiteLLMRoutes.openai_routes when no active authenticated endpoint still owns them

Preserve live shared paths, idless reloads, auth transitions, and built-in OpenAI routes while reclaiming departed dynamic paths
@silencedoctor
silencedoctor force-pushed the fix/pass-through-stale-route-cleanup branch from fa8d4e4 to 4cdd05a Compare July 29, 2026 11:56
@silencedoctor
silencedoctor changed the base branch from litellm_oss_branch to litellm_internal_staging July 29, 2026 12:24
@silencedoctor silencedoctor changed the title fix(pass-through): pop stale route key directly to prevent unbounded registry growth fix(pass-through): clean stale routes from auth allowlist Jul 29, 2026
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.

[Bug]: Pass-through endpoint registry grows unbounded causing CPU to reach 100%

4 participants