Skip to content

fix(least-busy): reserve the picked deployment so a burst of requests spreads out - #47484

Merged
mateo-berri merged 28 commits into
BerriAI:mainfrom
tc3oliver:litellm_least_busy_burst_reservation
Nov 5, 2026
Merged

mateo-berri merged 28 commits into
BerriAI:mainfrom
tc3oliver:litellm_least_busy_burst_reservation

Conversation

@tc3oliver

@tc3oliver tc3oliver commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Under least-busy routing, requests arriving together read the same in-flight counts and pile onto one deployment. This PR reserves each pick at once, so the next request sees it as busy

Details

TLDR

Problem this solves:

  • least-busy counted a request only in log_pre_api_call, so calls picked before then saw the same counts

How it solves it:

  • The router reserves each pick at once, and the call's first attempt turns it into the in-flight count
  • Least-busy reads the deployment from routing metadata, so /v1/decisions, /v1/responses, /v1/messages and /v1/ocr calls now count like chat. Tier-router picks reserve, but their in-flight count stays under the router's name as on main
  • Realtime sessions stay out of the in-flight counts as on main, because each client event would add a count that only the session's close gives back. A realtime pick still holds its reservation until the session closes or 30 s pass
  • When a request ends, the router frees every pick it never started. An unused reservation expires after 30 s

User Flow

Before: four decisions calls sent at once all go to one deployment

  1. The admin puts two decider deployments in one model group with routing_strategy: least-busy
  2. The app sends 4 POST https://litellm-domain/v1/decisions calls to that group at once
  3. All 4 responses carry the same x-litellm-model-id, in every burst
  4. Chat bursts to POST https://litellm-domain/v1/chat/completions often pile up too

After: the same burst is shared across both deployments

  1. The admin puts two decider deployments in one model group with routing_strategy: least-busy
  2. The app sends 4 POST https://litellm-domain/v1/decisions calls to that group at once
  3. The responses come from both deployments, 2/2 or 3/1
  4. Chat bursts spread the same way

Relevant issues

Refs #39322, whose last comment reports this burst case

Pre-Submission checklist

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

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. uv run pytest tests/unit/<your_test_file>.py -v. Leave the suites (make test-unit-*, make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • Every automated review on my PR's latest commit has finished, and I have addressed or replied to each of its findings before requesting a maintainer review

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

Last updated: 5860094. QA ran on 694cb4f, and the realtime case on 5860094. Changes since QA: realtime sessions no longer add in-flight counts, and one unit test waits for its helper thread before its timed join. Proof remains valid because no other case sends realtime traffic, and the realtime case ran again on both sides at 5860094. Verification: tests/unit/router_strategy/test_least_busy.py passes 56 of 56, and its new realtime test fails without the fix

Before is main at the merge base 97aae6c, after is 694cb4f. Every case runs real Typesafe and OpenAI calls on fresh proxies with 2 workers each, its own Redis and no database, and reads each pick from x-litellm-model-id. Full logs, configs and scripts

Case Before (97aae6c) After (694cb4f)
decisions, one proxy 4/0, 4/0, 4/0 2/2, 2/2, 2/2
chat, one proxy 2/2, 2/2, 4/0 2/2, 2/2, 2/2
decisions, two processes 4/0, 4/0, 4/0 3/1, 2/2, 2/2
chat, two processes 3/1, 2/2, 4/0 2/2, 3/1, 4/0
16 calls, 3 of 4 busy 12/1/0/3, 1/7/3/5, 8/5/0/3 4/5/4/3, 4/4/4/4, 4/4/4/4
8 threads, Router.completion 8/0, 0/8, 4/4 4/4, 4/4, 4/4
realtime, 4 sessions a, a, a, a a, a, a, a at 5860094
Cases that answer the same on both sides
Case Before (97aae6c) After (694cb4f)
Router.responses, 8 threads 4/4, 4/4, 4/4 4/4, 4/4, 4/4
/v1/messages beside a long call 0 of 4 on busy one 0 of 4 on busy one
16 calls, two processes 4/3/5/4 5/5/3/3
one at a time after a cache hit 4 of 4 on chat-a 4 of 4 on chat-a
one at a time after 400s 12 of 12 on chat-a 12 of 12 on chat-a
one at a time after input_tokens 6 of 6 on chat-a 6 of 6 on chat-a
complexity router, one at a time 6 of 6 on tier-a 6 of 6 on tier-a
after a rejected fallback 4 of 4 on chat-a 4 of 4 on chat-a

decisions, one proxy with 2 workers

  1. Boot a fresh proxy with python -m litellm.proxy.proxy_cli --config config.yaml --port $PORT --num_workers 2 --timeout_worker_healthcheck 60
  2. Run burst.sh, which sends 3 rounds of 4 parallel /v1/decisions calls and then 4 parallel /v1/chat/completions calls, printing each response's x-litellm-model-id
config.yaml and burst.sh
model_list:
  - model_name: decider-group
    litellm_params:
      model: typesafe/jev-latest
      api_key: os.environ/TYPESAFE_API_KEY
    model_info:
      id: decider-a
  - model_name: decider-group
    litellm_params:
      model: typesafe/jev-latest
      api_key: os.environ/TYPESAFE_API_KEY
    model_info:
      id: decider-b
  - model_name: chat-group
    litellm_params:
      model: openai/gpt-5.4-nano
      api_key: os.environ/OPENAI_API_KEY
    model_info:
      id: chat-a
  - model_name: chat-group
    litellm_params:
      model: openai/gpt-5.4-nano
      api_key: os.environ/OPENAI_API_KEY
    model_info:
      id: chat-b
router_settings:
  routing_strategy: least-busy
  num_retries: 0
general_settings:
  master_key: os.environ/LITELLM_MASTER_KEY
KEY=$LITELLM_MASTER_KEY
DECIDE='{"model":"decider-group","input":"Help! My payouts have been failing for 3 days!","questions":[{"type":"predicate","name":"is_urgent","instructions":"Does this convey urgency?"}]}'
CHAT='{"model":"chat-group","messages":[{"role":"user","content":"Say ok"}]}'
for r in 1 2 3; do
  for kind in decisions chat; do
    if [[ $kind == decisions ]]; then url=/v1/decisions; body=$DECIDE; else url=/v1/chat/completions; body=$CHAT; fi
    for i in 1 2 3 4; do
      curl -s -o /dev/null -D - -X POST "http://127.0.0.1:$PORT$url" -H "Authorization: Bearer $KEY" \
        -H 'content-type: application/json' -d "$body" | tr -d '\r' \
        | awk -v r=$r -v k=$kind -v i=$i '/^HTTP/{s=$2} tolower($1)=="x-litellm-model-id:"{m=$2} END{print "round " r " " k " call " i ": " s " " m}' &
    done
    wait
  done
done
  1. Read where each call went. Before, every decisions call lands on decider-a. After, each burst splits across decider-a and decider-b
Raw output, before and after

Before (97aae6c):

proxy c1 on :26743 with 2 workers, config.yaml
$ PORT=26743 zsh burst.sh
round 1 decisions call 3: 200 decider-a
round 1 decisions call 4: 200 decider-a
round 1 decisions call 2: 200 decider-a
round 1 decisions call 1: 200 decider-a
round 1 chat call 4: 200 chat-b
round 1 chat call 2: 200 chat-a
round 1 chat call 1: 200 chat-a
round 1 chat call 3: 200 chat-b
round 2 decisions call 1: 200 decider-a
round 2 decisions call 3: 200 decider-a
round 2 decisions call 2: 200 decider-a
round 2 decisions call 4: 200 decider-a
round 2 chat call 4: 200 chat-b
round 2 chat call 2: 200 chat-a
round 2 chat call 1: 200 chat-a
round 2 chat call 3: 200 chat-b
round 3 decisions call 2: 200 decider-a
round 3 decisions call 3: 200 decider-a
round 3 decisions call 1: 200 decider-a
round 3 decisions call 4: 200 decider-a
round 3 chat call 4: 200 chat-a
round 3 chat call 2: 200 chat-a
round 3 chat call 3: 200 chat-a
round 3 chat call 1: 200 chat-a

After (694cb4f):

proxy c1 on :32688 with 2 workers, config.yaml
$ PORT=32688 zsh burst.sh
round 1 decisions call 1: 200 decider-a
round 1 decisions call 3: 200 decider-a
round 1 decisions call 4: 200 decider-b
round 1 decisions call 2: 200 decider-b
round 1 chat call 2: 200 chat-a
round 1 chat call 1: 200 chat-a
round 1 chat call 3: 200 chat-b
round 1 chat call 4: 200 chat-b
round 2 decisions call 1: 200 decider-a
round 2 decisions call 4: 200 decider-b
round 2 decisions call 3: 200 decider-b
round 2 decisions call 2: 200 decider-a
round 2 chat call 4: 200 chat-b
round 2 chat call 2: 200 chat-a
round 2 chat call 3: 200 chat-b
round 2 chat call 1: 200 chat-a
round 3 decisions call 3: 200 decider-b
round 3 decisions call 1: 200 decider-b
round 3 decisions call 4: 200 decider-a
round 3 decisions call 2: 200 decider-a
round 3 chat call 4: 200 chat-b
round 3 chat call 1: 200 chat-a
round 3 chat call 2: 200 chat-a
round 3 chat call 3: 200 chat-b
decisions and chat, two processes on one Redis

Two proxies on one Redis with config-redis.yaml, odd calls to the first and even calls to the second

Before (97aae6c):

proxy c2a on :31122 with 2 workers, config-redis.yaml
proxy c2b on :27507 with 2 workers, config-redis.yaml
$ zsh burst2.sh 31122 27507
round 1 decisions call 2 via :27507: 200 decider-a
round 1 decisions call 4 via :27507: 200 decider-a
round 1 decisions call 3 via :31122: 200 decider-a
round 1 decisions call 1 via :31122: 200 decider-a
round 1 chat call 2 via :27507: 200 chat-a
round 1 chat call 4 via :27507: 200 chat-b
round 1 chat call 1 via :31122: 200 chat-b
round 1 chat call 3 via :31122: 200 chat-b
round 2 decisions call 2 via :27507: 200 decider-a
round 2 decisions call 1 via :31122: 200 decider-a
round 2 decisions call 4 via :27507: 200 decider-a
round 2 decisions call 3 via :31122: 200 decider-a
round 2 chat call 3 via :31122: 200 chat-b
round 2 chat call 1 via :31122: 200 chat-a
round 2 chat call 2 via :27507: 200 chat-a
round 2 chat call 4 via :27507: 200 chat-b
round 3 decisions call 3 via :31122: 200 decider-a
round 3 decisions call 1 via :31122: 200 decider-a
round 3 decisions call 2 via :27507: 200 decider-a
round 3 decisions call 4 via :27507: 200 decider-a
round 3 chat call 2 via :27507: 200 chat-a
round 3 chat call 1 via :31122: 200 chat-a
round 3 chat call 3 via :31122: 200 chat-a
round 3 chat call 4 via :27507: 200 chat-a

After (694cb4f):

proxy c2a on :31597 with 2 workers, config-redis.yaml
proxy c2b on :20794 with 2 workers, config-redis.yaml
$ zsh burst2.sh 31597 20794
round 1 decisions call 3 via :31597: 200 decider-a
round 1 decisions call 1 via :31597: 200 decider-b
round 1 decisions call 2 via :20794: 200 decider-a
round 1 decisions call 4 via :20794: 200 decider-a
round 1 chat call 1 via :31597: 200 chat-a
round 1 chat call 2 via :20794: 200 chat-b
round 1 chat call 4 via :20794: 200 chat-a
round 1 chat call 3 via :31597: 200 chat-b
round 2 decisions call 2 via :20794: 200 decider-a
round 2 decisions call 1 via :31597: 200 decider-a
round 2 decisions call 4 via :20794: 200 decider-b
round 2 decisions call 3 via :31597: 200 decider-b
round 2 chat call 3 via :31597: 200 chat-b
round 2 chat call 2 via :20794: 200 chat-a
round 2 chat call 4 via :20794: 200 chat-a
round 2 chat call 1 via :31597: 200 chat-a
round 3 decisions call 1 via :31597: 200 decider-a
round 3 decisions call 4 via :20794: 200 decider-b
round 3 decisions call 2 via :20794: 200 decider-b
round 3 decisions call 3 via :31597: 200 decider-a
round 3 chat call 3 via :31597: 200 chat-a
round 3 chat call 1 via :31597: 200 chat-a
round 3 chat call 2 via :20794: 200 chat-a
round 3 chat call 4 via :20794: 200 chat-a
16 calls in one curl --parallel while 3 long calls hold wide-a, wide-b and wide-c, one proxy

u2.sh starts three long chat calls 3 s apart, then sends 16 short calls at once with curl -Z

Before (97aae6c):

proxy c4 on :22549 with 2 workers, config-u2.yaml
$ zsh u2.sh 22549
round 1 short call 5: 200 wide-a
round 1 short call 9: 200 wide-a
round 1 short call 10: 200 wide-a
round 1 short call 15: 200 wide-d
round 1 short call 6: 200 wide-a
round 1 short call 2: 200 wide-b
round 1 short call 4: 200 wide-a
round 1 short call 1: 200 wide-a
round 1 short call 11: 200 wide-a
round 1 short call 3: 200 wide-d
round 1 short call 16: 200 wide-a
round 1 short call 8: 200 wide-a
round 1 short call 7: 200 wide-a
round 1 short call 12: 200 wide-d
round 1 short call 13: 200 wide-a
round 1 short call 14: 200 wide-a
round 1 long call 1: 200 wide-a
round 1 long call 2: 200 wide-b
round 1 long call 3: 200 wide-c
round 2 short call 3: 200 wide-d
round 2 short call 15: 200 wide-c
round 2 short call 16: 200 wide-c
round 2 short call 14: 200 wide-b
round 2 short call 2: 200 wide-a
round 2 short call 13: 200 wide-b
round 2 short call 1: 200 wide-c
round 2 short call 11: 200 wide-d
round 2 short call 12: 200 wide-d
round 2 short call 5: 200 wide-d
round 2 short call 4: 200 wide-b
round 2 short call 7: 200 wide-d
round 2 short call 10: 200 wide-b
round 2 short call 6: 200 wide-b
round 2 short call 8: 200 wide-b
round 2 short call 9: 200 wide-b
round 2 long call 1: 200 wide-a
round 2 long call 2: 200 wide-b
round 2 long call 3: 200 wide-c
round 3 short call 1: 200 wide-b
round 3 short call 3: 200 wide-a
round 3 short call 4: 200 wide-a
round 3 short call 16: 200 wide-d
round 3 short call 12: 200 wide-b
round 3 short call 9: 200 wide-d
round 3 short call 6: 200 wide-a
round 3 short call 2: 200 wide-a
round 3 short call 11: 200 wide-b
round 3 short call 7: 200 wide-a
round 3 short call 10: 200 wide-a
round 3 short call 8: 200 wide-a
round 3 short call 15: 200 wide-b
round 3 short call 5: 200 wide-a
round 3 short call 13: 200 wide-b
round 3 short call 14: 200 wide-d
round 3 long call 1: 200 wide-a
round 3 long call 2: 200 wide-b
round 3 long call 3: 200 wide-c

After (694cb4f):

proxy c4 on :22177 with 2 workers, config-u2.yaml
$ zsh u2.sh 22177
round 1 short call 4: 200 wide-d
round 1 short call 15: 200 wide-c
round 1 short call 13: 200 wide-b
round 1 short call 1: 200 wide-b
round 1 short call 14: 200 wide-c
round 1 short call 12: 200 wide-a
round 1 short call 6: 200 wide-c
round 1 short call 3: 200 wide-d
round 1 short call 2: 200 wide-a
round 1 short call 11: 200 wide-d
round 1 short call 9: 200 wide-a
round 1 short call 7: 200 wide-a
round 1 short call 10: 200 wide-b
round 1 short call 5: 200 wide-c
round 1 short call 8: 200 wide-b
round 1 short call 16: 200 wide-b
round 1 long call 1: 200 wide-a
round 1 long call 2: 200 wide-b
round 1 long call 3: 200 wide-c
round 2 short call 12: 200 wide-a
round 2 short call 5: 200 wide-c
round 2 short call 6: 200 wide-b
round 2 short call 7: 200 wide-a
round 2 short call 13: 200 wide-d
round 2 short call 8: 200 wide-d
round 2 short call 14: 200 wide-d
round 2 short call 3: 200 wide-b
round 2 short call 10: 200 wide-b
round 2 short call 2: 200 wide-c
round 2 short call 9: 200 wide-c
round 2 short call 16: 200 wide-a
round 2 short call 4: 200 wide-d
round 2 short call 11: 200 wide-b
round 2 short call 1: 200 wide-c
round 2 short call 15: 200 wide-a
round 2 long call 1: 200 wide-a
round 2 long call 2: 200 wide-b
round 2 long call 3: 200 wide-c
round 3 short call 14: 200 wide-d
round 3 short call 1: 200 wide-c
round 3 short call 12: 200 wide-a
round 3 short call 7: 200 wide-c
round 3 short call 9: 200 wide-b
round 3 short call 8: 200 wide-d
round 3 short call 16: 200 wide-a
round 3 short call 4: 200 wide-b
round 3 short call 6: 200 wide-a
round 3 short call 3: 200 wide-b
round 3 short call 11: 200 wide-a
round 3 short call 15: 200 wide-c
round 3 short call 2: 200 wide-c
round 3 short call 5: 200 wide-d
round 3 short call 13: 200 wide-d
round 3 short call 10: 200 wide-b
round 3 long call 1: 200 wide-a
round 3 long call 2: 200 wide-b
round 3 long call 3: 200 wide-c
8 SDK threads on Router.completion

threads_sdk.py builds a least-busy Router over two gpt-5.4-nano deployments and runs 8 threads at once, three times

Before (97aae6c):

$ <head tree>/.venv/bin/python -P threads_sdk.py
round 1: {'chat-a': 8}
round 2: {'chat-b': 8}
round 3: {'chat-a': 4, 'chat-b': 4}

After (694cb4f):

$ <head tree>/.venv/bin/python -P threads_sdk.py
round 1: {'chat-a': 4, 'chat-b': 4}
round 2: {'chat-a': 4, 'chat-b': 4}
round 3: {'chat-a': 4, 'chat-b': 4}
sync Router.responses, streaming and a failed call

responses_sdk.py calls Router.responses one at a time, streams one, fails one before the provider, then runs 8 threads at once

Before (97aae6c):

$ <head tree>/.venv/bin/python -P responses_sdk.py
litellm from <base tree>/litellm/__init__.py
sync responses call 1: nano-a
sync responses call 2: nano-b
sync streamed responses call: streamed 9 events
after the stream, call 1: nano-a
call with an unusable api_base failed before the provider: InternalServerError
after the failed call, call 1: nano-a
after the failed call, call 2: nano-b
8 threads at once, round 1: {'nano-a': 4, 'nano-b': 4}
8 threads at once, round 2: {'nano-a': 4, 'nano-b': 4}
8 threads at once, round 3: {'nano-a': 4, 'nano-b': 4}

After (694cb4f):

$ <head tree>/.venv/bin/python -P responses_sdk.py
litellm from <head tree>/litellm/__init__.py
sync responses call 1: nano-a
sync responses call 2: nano-b
sync streamed responses call: streamed 9 events
after the stream, call 1: nano-a
call with an unusable api_base failed before the provider: InternalServerError
after the failed call, call 1: nano-a
after the failed call, call 2: nano-b
8 threads at once, round 1: {'nano-a': 4, 'nano-b': 4}
8 threads at once, round 2: {'nano-a': 4, 'nano-b': 4}
8 threads at once, round 3: {'nano-a': 4, 'nano-b': 4}
/v1/messages, uneven load and cache hits, two processes on one Redis

legs3.sh sends short /v1/messages calls beside a long one, 16 short chat calls while three long ones run, and calls after a cache hit

Before (97aae6c):

proxy c3a on :27537 with 2 workers, config-r3.yaml
proxy c3b on :20521 with 2 workers, config-r3.yaml
$ zsh legs3.sh 27537 20521
== /v1/messages: one long call in flight, then two short calls
round 1 short call 1 via :20521: 200 chat-b
round 1 short call 2 via :27537: 200 chat-b
round 1 long call via :27537: 200 chat-a
round 2 short call 1 via :20521: 200 chat-b
round 2 short call 2 via :27537: 200 chat-b
round 2 long call via :27537: 200 chat-a
== uneven: three long calls hold three of four deployments, then 16 short calls at once
short call 9 via :27537: 200 wide-a
short call 3 via :27537: 200 wide-d
short call 1 via :27537: 200 wide-a
short call 10 via :20521: 200 wide-c
short call 12 via :20521: 200 wide-b
short call 6 via :20521: 200 wide-b
short call 14 via :20521: 200 wide-c
short call 13 via :27537: 200 wide-a
short call 4 via :20521: 200 wide-c
short call 11 via :27537: 200 wide-d
short call 16 via :20521: 200 wide-c
short call 8 via :20521: 200 wide-c
short call 2 via :20521: 200 wide-b
short call 15 via :27537: 200 wide-a
short call 5 via :27537: 200 wide-d
short call 7 via :27537: 200 wide-d
long call 1 via :27537: 200 wide-a
long call 2 via :20521: 200 wide-b
long call 3 via :27537: 200 wide-c
== cache hit: a cached call, its repeat, then four calls one at a time
cached call via :27537: 200 chat-a
same cached call again via :27537: 200 chat-a (served from cache)
next call 1 via :27537: 200 chat-a
next call 2 via :20521: 200 chat-a
next call 3 via :27537: 200 chat-a
next call 4 via :20521: 200 chat-a

After (694cb4f):

proxy c3a on :21330 with 2 workers, config-r3.yaml
proxy c3b on :31805 with 2 workers, config-r3.yaml
$ zsh legs3.sh 21330 31805
== /v1/messages: one long call in flight, then two short calls
round 1 short call 1 via :31805: 200 chat-b
round 1 short call 2 via :21330: 200 chat-b
round 1 long call via :21330: 200 chat-a
round 2 short call 1 via :31805: 200 chat-b
round 2 short call 2 via :21330: 200 chat-b
round 2 long call via :21330: 200 chat-a
== uneven: three long calls hold three of four deployments, then 16 short calls at once
short call 1 via :21330: 200 wide-d
short call 4 via :31805: 200 wide-b
short call 2 via :31805: 200 wide-d
short call 12 via :31805: 200 wide-a
short call 13 via :21330: 200 wide-b
short call 6 via :31805: 200 wide-a
short call 14 via :31805: 200 wide-a
short call 10 via :31805: 200 wide-c
short call 5 via :21330: 200 wide-b
short call 11 via :21330: 200 wide-a
short call 7 via :21330: 200 wide-c
short call 9 via :21330: 200 wide-b
short call 16 via :31805: 200 wide-b
short call 3 via :21330: 200 wide-c
short call 15 via :21330: 200 wide-a
short call 8 via :31805: 200 wide-d
long call 1 via :21330: 200 wide-a
long call 2 via :31805: 200 wide-b
long call 3 via :21330: 200 wide-c
== cache hit: a cached call, its repeat, then four calls one at a time
cached call via :21330: 200 chat-a
same cached call again via :21330: 200 chat-a (served from cache)
next call 1 via :21330: 200 chat-a
next call 2 via :31805: 200 chat-a
next call 3 via :21330: 200 chat-a
next call 4 via :31805: 200 chat-a
calls that end before the provider, then calls one at a time

early400.sh sends 4 calls the proxy rejects with 400, then 4 calls one at a time. counttok.sh calls /v1/responses/input_tokens, then 2 chat calls

Before (97aae6c):

proxy c7 on :23169 with 2 workers, config.yaml
$ zsh early400.sh 23169
round 1 rejected call 1: 400 chat-a
round 1 rejected call 2: 400 chat-a
round 1 rejected call 3: 400 chat-a
round 1 rejected call 4: 400 chat-a
round 1 next call 1: 200 chat-a
round 1 next call 2: 200 chat-a
round 1 next call 3: 200 chat-a
round 1 next call 4: 200 chat-a
round 2 rejected call 1: 400 chat-a
round 2 rejected call 2: 400 chat-a
round 2 rejected call 3: 400 chat-a
round 2 rejected call 4: 400 chat-a
round 2 next call 1: 200 chat-a
round 2 next call 2: 200 chat-a
round 2 next call 3: 200 chat-a
round 2 next call 4: 200 chat-a
round 3 rejected call 1: 400 chat-a
round 3 rejected call 2: 400 chat-a
round 3 rejected call 3: 400 chat-a
round 3 rejected call 4: 400 chat-a
round 3 next call 1: 200 chat-a
round 3 next call 2: 200 chat-a
round 3 next call 3: 200 chat-a
round 3 next call 4: 200 chat-a
$ zsh counttok.sh 23169
created a response to continue from (id length 377)
round 1 responses/input_tokens: 200 - input_tokens=21
round 1 next chat 1: 200 chat-a
round 1 next chat 2: 200 chat-a
round 2 responses/input_tokens: 200 - input_tokens=21
round 2 next chat 1: 200 chat-a
round 2 next chat 2: 200 chat-a
round 3 responses/input_tokens: 200 - input_tokens=21
round 3 next chat 1: 200 chat-a
round 3 next chat 2: 200 chat-a

After (694cb4f):

proxy c7 on :23371 with 2 workers, config.yaml
$ zsh early400.sh 23371
round 1 rejected call 1: 400 chat-a
round 1 rejected call 2: 400 chat-a
round 1 rejected call 3: 400 chat-a
round 1 rejected call 4: 400 chat-a
round 1 next call 1: 200 chat-a
round 1 next call 2: 200 chat-a
round 1 next call 3: 200 chat-a
round 1 next call 4: 200 chat-a
round 2 rejected call 1: 400 chat-a
round 2 rejected call 2: 400 chat-a
round 2 rejected call 3: 400 chat-a
round 2 rejected call 4: 400 chat-a
round 2 next call 1: 200 chat-a
round 2 next call 2: 200 chat-a
round 2 next call 3: 200 chat-a
round 2 next call 4: 200 chat-a
round 3 rejected call 1: 400 chat-a
round 3 rejected call 2: 400 chat-a
round 3 rejected call 3: 400 chat-a
round 3 rejected call 4: 400 chat-a
round 3 next call 1: 200 chat-a
round 3 next call 2: 200 chat-a
round 3 next call 3: 200 chat-a
round 3 next call 4: 200 chat-a
$ zsh counttok.sh 23371
created a response to continue from (id length 377)
round 1 responses/input_tokens: 200 - input_tokens=21
round 1 next chat 1: 200 chat-a
round 1 next chat 2: 200 chat-a
round 2 responses/input_tokens: 200 - input_tokens=21
round 2 next chat 1: 200 chat-a
round 2 next chat 2: 200 chat-a
round 3 responses/input_tokens: 200 - input_tokens=21
round 3 next chat 1: 200 chat-a
round 3 next chat 2: 200 chat-a
complexity router and a fallback rejected before the provider

r4legs.sh calls a complexity router one at a time, then a dead group whose fallback is rejected, then 4 chat calls one at a time

Before (97aae6c):

proxy c8 on :20179 with 2 workers, config-cx.yaml
$ zsh r4legs.sh 20179
smart-router call 1, one at a time: 200 tier-a
smart-router call 2, one at a time: 200 tier-a
smart-router call 3, one at a time: 200 tier-a
smart-router call 4, one at a time: 200 tier-a
smart-router call 5, one at a time: 200 tier-a
smart-router call 6, one at a time: 200 tier-a
dead-group call that falls back to chat-group and is rejected there: 500 dead-a
next chat-group call 1, one at a time: 200 chat-a
next chat-group call 2, one at a time: 200 chat-a
next chat-group call 3, one at a time: 200 chat-a
next chat-group call 4, one at a time: 200 chat-a

After (694cb4f):

proxy c8 on :31753 with 2 workers, config-cx.yaml
$ zsh r4legs.sh 31753
smart-router call 1, one at a time: 200 tier-a
smart-router call 2, one at a time: 200 tier-a
smart-router call 3, one at a time: 200 tier-a
smart-router call 4, one at a time: 200 tier-a
smart-router call 5, one at a time: 200 tier-a
smart-router call 6, one at a time: 200 tier-a
dead-group call that falls back to chat-group and is rejected there: 500 dead-a
next chat-group call 1, one at a time: 200 chat-a
next chat-group call 2, one at a time: 200 chat-a
next chat-group call 3, one at a time: 200 chat-a
next chat-group call 4, one at a time: 200 chat-a

realtime, two processes, at 5860094

Steps and raw output
  1. The admin puts two openai/gpt-realtime-2.1-mini deployments, rt-a and rt-b, in rt-group with routing_strategy: least-busy. Two proxy processes with 2 workers each share one Redis and one Postgres
  2. A client opens 4 realtime sessions on ws://<proxy>/v1/realtime?model=rt-group one after another, 5 s apart and alternating proxies. The first sends 10 items and the others 3
  3. The admin reads which deployment served each session from GET /spend/logs

Main and 5860094 serve all 4 sessions from rt-a. 6643fc2, the tip before the fix, sends sessions 2 to 4 to rt-b, because the first session's 12 client events stay counted as in flight for up to an hour. Config and scripts are the realtime-* files in the gist

Before (97aae6c)

base tree at 97aae6c65e, two proxy processes with 2 workers each on one Redis and one Postgres, ports 21147 and 23746
$ python -I rt_client.py 21147 rt-group 10
session.created -> response.done, client events sent: 12
$ sleep 5
$ python -I rt_client.py 23746 rt-group 3
session.created -> response.done, client events sent: 5
$ sleep 5
$ python -I rt_client.py 21147 rt-group 3
session.created -> response.done, client events sent: 5
$ sleep 5
$ python -I rt_client.py 23746 rt-group 3
session.created -> response.done, client events sent: 5
$ curl -s 'http://127.0.0.1:23746/spend/logs?start_date=2026-11-05&end_date=2026-12-31&summarize=false' -H "Authorization: Bearer $LITELLM_KEY" | jq -r 'sort_by(.startTime) | .[] | [.startTime, .call_type, .model_id, .status] | @tsv'
2026-11-05T00:16:47.908000Z	_arealtime	rt-a	success
2026-11-05T00:16:54.339000Z	_arealtime	rt-a	success
2026-11-05T00:17:00.595000Z	_arealtime	rt-a	success
2026-11-05T00:17:06.911000Z	_arealtime	rt-a	success

After (5860094)

head tree at 58600945d3, two proxy processes with 2 workers each on one Redis and one Postgres, ports 23058 and 30912
$ python -I rt_client.py 23058 rt-group 10
session.created -> response.done, client events sent: 12
$ sleep 5
$ python -I rt_client.py 30912 rt-group 3
session.created -> response.done, client events sent: 5
$ sleep 5
$ python -I rt_client.py 23058 rt-group 3
session.created -> response.done, client events sent: 5
$ sleep 5
$ python -I rt_client.py 30912 rt-group 3
session.created -> response.done, client events sent: 5
$ curl -s 'http://127.0.0.1:30912/spend/logs?start_date=2026-11-05&end_date=2026-12-31&summarize=false' -H "Authorization: Bearer $LITELLM_KEY" | jq -r 'sort_by(.startTime) | .[] | [.startTime, .call_type, .model_id, .status] | @tsv'
2026-11-05T00:16:50.991000Z	_arealtime	rt-a	success
2026-11-05T00:16:57.434000Z	_arealtime	rt-a	success
2026-11-05T00:17:03.645000Z	_arealtime	rt-a	success
2026-11-05T00:17:09.703000Z	_arealtime	rt-a	success

Before the fix (6643fc2)

prefix tree at 6643fc2e56, two proxy processes with 2 workers each on one Redis and one Postgres, ports 28536 and 21904
$ python -I rt_client.py 28536 rt-group 10
session.created -> response.done, client events sent: 12
$ sleep 5
$ python -I rt_client.py 21904 rt-group 3
session.created -> response.done, client events sent: 5
$ sleep 5
$ python -I rt_client.py 28536 rt-group 3
session.created -> response.done, client events sent: 5
$ sleep 5
$ python -I rt_client.py 21904 rt-group 3
session.created -> response.done, client events sent: 5
$ curl -s 'http://127.0.0.1:21904/spend/logs?start_date=2026-11-05&end_date=2026-12-31&summarize=false' -H "Authorization: Bearer $LITELLM_KEY" | jq -r 'sort_by(.startTime) | .[] | [.startTime, .call_type, .model_id, .status] | @tsv'
2026-11-05T00:19:15.169000Z	_arealtime	rt-a	success
2026-11-05T00:19:21.668000Z	_arealtime	rt-b	success
2026-11-05T00:19:28.392000Z	_arealtime	rt-b	success
2026-11-05T00:19:34.546000Z	_arealtime	rt-b	success

Observations from the run:

  • Two-process chat still hit 4/0 once, not caused here
  • One-at-a-time calls always pick the first deployment, both sides
  • Back-to-back realtime sessions alternate, from this PR's reservations

Type

🐛 Bug Fix

Caveats (if any)

No fresh review has read the fixes from 5cdcb89 on, because the review loop stops at four rounds

Medium

Low

  • Requests that share one x-litellm-call-id can free each other's picks
  • Past 10,000 calls in flight per worker, an evicted start record can keep a call counted up to an hour
  • The reservation lock costs 0.03 ms per pick, 1.1 ms at 100 deployments
Fixed since the fourth review round
  • High. A realtime session added one in-flight count per client event and gave none back, so later sessions avoided its deployment for up to an hour
  • High. A complexity, auto, adaptive or quality router never freed its tier pick, so calls sent one at a time alternated deployments
  • High. get_available_deployment and the deprecated async_get_available_deployment held a 30 s reservation with no call, and a reserve argument made Router.select and batch cancel raise TypeError under a custom routing strategy or a Router subclass
  • Medium. A fallback hop rejected before the provider kept its pick reserved for 30 s, because the proxy logs only the first failure of a request
  • Low. A pinned realtime turn or another router's call could free a reservation it did not make
  • Medium. A vector store create through the router kept its pick for 30 s
  • Low. An SDK call without a call id, such as a fine-tuning listing or a sync text completion, kept its pick 30 s, and another call's start could free it early
  • Medium. A sync SDK call on a generic route such as Router.responses that failed before its provider kept its pick 30 s, so eight-thread bursts split 5/3, and without a call id another call's start could free its pick early
  • Medium. A pick read in-flight counts before taking the reservation lock, so a request starting on another thread at that moment left a busy deployment looking free
  • Medium. A pass-through lookup given request options held its pick 30 s though it made no call, so repeated lookups skewed later routing
/live-pr-risk at 694cb4f, and its realtime fix at 5860094

The full walk ran at 694cb4f, after the merge with main's router refactor, and found one regression. 5860094 fixes it, and the walk of that fix plus its realtime cell ran at 5860094. Nothing else changed in product code since 694cb4f, so every other cell stands

Both sides run two proxy processes with 2 workers each, sharing one Postgres and one Redis, with requests alternating across the processes. Base is the merge base 97aae6c, head runs from a clean detached worktree. Forwarding recorders sit in front of OpenAI, Anthropic, Typesafe and Mistral on both sides, and every deployment is a real provider. Logs, configs and scripts are the lpr-* and realtime-* files in the gist

Breaking

None at 5860094. At 694cb4f, realtime sessions on a least-busy group added one in-flight count per client event and never gave it back, so new sessions avoided that deployment for up to an hour (lpr-c12_realtime.log, lpr-c12b_realtime_scaling.log). Realtime calls logging_obj.pre_call for every client message, and this PR made least-busy read the deployment from litellm_metadata, which realtime fills, while success fires once when the session closes. 5860094 keeps _arealtime out of least-busy counting as main does, the same rule the router's TPM tracking and budget limiter already apply. Its walk found no other producer of that call type. With 4 sessions 5 s apart, main and 5860094 used rt-a for all 4, and 6643fc2 sent sessions 2 to 4 to rt-b (realtime-*.txt)

These also showed up, but main does the same, so they are not counted. The Responses WebSocket leaves one count per session (lpr-c13_responses_ws.log). A fallback group with num_retries: 2 leaves counts behind (lpr-c15_fallback_retries.log). A sync text_completion on a group name fails with "LLM Provider NOT provided" (lpr-sdk_*_noredis_r2.log). A /vllm pass-through with a bad logit_bias answers 500 (lpr-c14_vllm_mod_ocr.log)

Backward incompatible

LeastBusyLoggingHandler.get_available_deployments and async_get_available_deployments take new reserve and call_id keywords. Both default to off, so existing callers keep working. Async picks reserve by default, while sync picks, select, pass-through lookups and the deprecated async_get_available_deployment do not. One-at-a-time SDK picks match main after each of these (lpr-sdk_*_noredis_r2.log)

/v1/responses, /v1/messages and /v1/ocr are now counted too, as the TLDR says. Bursts of 4 on /v1/responses went 2/2 three times on the head and 4/0 three times on main, and /v1/messages went 2/2 three times against 4/0, 2/2, 4/0 (lpr-c3_c6_routes.log). OCR counts go back to 0 after successes and upstream 400s (lpr-c14_vllm_mod_ocr.log). A realtime pick holds its reservation until its session closes or 30 s pass, so a session opened while the last one is still closing goes to the other deployment, where main always picks the first. In a debugging run of 5 sessions on one single-worker proxy, the 2 that started right after the last one ended went to rt-b, and the 2 that started 5 s later went to rt-a (realtime-diag-58600945d3.txt). The recorded upstream requests for c1 to c6 match on method, path, status, header keys and count (lpr-wire_diff_c1_c6.log)

With Redis, a pick uses the larger of the shared count and this worker's count, so a count leaked on one worker outweighs a shared count back at 0. This was traced in the code, not observed alone

Regression risk

These dependents sit outside an owning_picks scope or can bypass it, and only moderation was driven. Prompt-management selection reserves with no owner on a failure before its call, in a group of exactly one deployment. Moderation has the same shape, and its bursts matched main. The Anthropic mid-stream retry picks after its scope has closed. Comma-separated models through abatch_completion, the silent experiment and the auto-router encoder share one owner, so the first call to finish frees its siblings' picks. A Router subclass that overrides the deprecated async_get_available_deployment turns reservations off for its callers. The reservation store holds at most 10,000 keys, and under enough concurrency it drops 30 s reservations before 1 h start records

Dependency graph

Dependent Entry Status
chat, both stream modes /v1/chat/completions verified live
decisions /v1/decisions verified live
Anthropic messages /v1/messages verified live
Responses over HTTP /v1/responses verified live
embeddings /v1/embeddings verified live
rejected before provider chat verified live
fallback, no retries chat verified live
fallback with retries chat same as main
complexity router chat verified live
count_tokens /v1/messages/count_tokens verified live
keys, health, spend key routes verified live
realtime /v1/realtime fixed, verified live
Responses WebSocket ws /v1/responses same as main
router pass-through /vllm/... verified live
moderation /v1/moderations verified live
OCR /v1/ocr verified live
caller-set call id x-litellm-call-id verified live
SDK sync and deprecated picks Router verified live
SDK gathered and threaded Router.acompletion verified live
prompt management Router untested
Anthropic mid-stream retry /v1/messages untested
shared-owner fan-out abatch_completion untested
videos /v1/videos untested
image generation /v1/images/generations untested

On the merged tree at 694cb4f, 7,323 unit tests passed across tests/unit/router_strategy, the router, decisions, pass-through, request processing and realtime streaming files (lpr-unit_merged.log), and the two least-busy integration files passed 4 of 4 (lpr-integ_merged.log). At 5860094, tests/unit/router_strategy/test_least_busy.py passes 56 of 56, and its realtime test fails without the fix

Not verified

Videos were skipped for cost. Image generation, prompt management, the Anthropic mid-stream retry, abatch_completion with comma-separated models, the silent experiment, the auto-router encoder, a routing_strategy_args change at runtime and 10,000-key eviction were not driven, nor were Bedrock and Vertex lazy streams. Realtime sessions open longer than the 30 s reservation were not driven. The dashboard was not opened because the PR touches no ui/ file

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
Bugbot summary

[!NOTE]
Medium Risk
Changes core least-busy selection and in-flight accounting across many router entry points; mis-tuned reserve/release could skew load or leak busy signals for up to 30s per worker.

Overview
Least-busy routing no longer waits until log_pre_api_call to treat a deployment as busy. Each real request can reserve a pick immediately (30s TTL, per-worker store), and selection weighs in-flight counts plus live reservations so bursts do not all read the same snapshot.

The router turns reservations on for actual routed calls (completion, embeddings, fallbacks, generic API paths, etc.) via picks_reserve(True) and owning_picks, and keeps them off for lookups only (select/pick, pass-through picks, batch cancel, deprecated async_get_available_deployment). LeastBusyLoggingHandler gains reserve/call_id, shared reservation storage on the router core, richer routing metadata (including proxy/litellm_metadata), and skips counting long-lived realtime sessions like batch retrieve.

Regression coverage adds broad unit/integration tests (concurrent bursts, cache hits, fallbacks, tier groups, batch/vector-store/text paths) plus a proxy integration test for /v1/decisions.

Reviewed by Cursor Bugbot for commit 5860094. Bugbot is set up for automated code reviews on this repo. Configure here.

… spreads out

The in-flight count only went up in log_pre_api_call, which runs after
selection and the pre-call hooks. Requests selected in that window all saw
the same counts and landed on the same deployment (16 simultaneous
requests over 4 deployments went as lopsided as 12/4/0/0 in the proxy).

When the router selects for a request it is about to send, least-busy now
records a reservation for the chosen deployment in the same synchronous
step that reads the reservations, and log_pre_api_call turns it into the
normal in-flight count. Selections without request kwargs (token counting,
probes) do not reserve. A reservation whose request never reaches the API
call expires after 30 s. The shared and per-worker counts and their
success/failure decrements are unchanged.
@CLAassistant

CLAassistant commented Oct 20, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Comment thread litellm/router_strategy/least_busy.py Resolved
@veria-ai

veria-ai Bot commented Oct 20, 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: 1 · PR risk: 0/10

@codspeed

codspeed Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

1 warning

⚠️ 1 benchmark is dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

✅ 43 untouched benchmarks


Comparing tc3oliver:litellm_least_busy_burst_reservation (5860094) with main (44b85d0)

Open in CodSpeed

@codecov

codecov Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.29932% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/router_strategy/least_busy.py 97.84% 4 Missing ⚠️
litellm/router_python_core.py 98.97% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

The reservation map kept a key per model group forever, so a wildcard
route with caller-chosen model names grew it without bound. Drop expired
and empty entries on every write.

Signed-off-by: Oliver Yu <[email protected]>

@ptimizeroracle ptimizeroracle left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

tested e2b8bc0 with 4 local servers: 64 simultaneous requests now split 16/16/16/16 (main: all 64 on d0), unequal-load burst spreads too

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@greptile-apps

greptile-apps Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The changes appear safe to merge, with no actionable new finding

Summary

Least-busy routing now reserves deployments when real calls select them and frees picks that never start. Lookup-only calls do not reserve deployments

The latest change excludes _arealtime callbacks because each incoming message can log another start, while the session logs one finish. A regression test covers repeated starts followed by session completion

No actionable new issue was established. The earlier tier-count finding was withdrawn after mateo-berri explained that its count keys match existing behavior. The timing-test finding was also withdrawn after mateo-berri explained the event handshake and mutation results

Tests were inspected, not executed

  • Least-busy routing reserves a deployment as soon as it picks one.
  • Least-busy counts routed calls through their selected deployment metadata.
  • Unused picks are released when a request exits routing.

Reviews (20) · Last reviewed commit: "fix(least-busy): keep realtime sessions ..." · Reviewed by Greptile

Comment thread litellm/router_strategy/least_busy.py Resolved
Comment thread litellm/router_strategy/least_busy.py Resolved
Selectors that share a router cache dedupe to one registered callback, so
only that instance saw log_pre_api_call. Reservations held on the other
instances were never released and skewed picks for the full TTL. Storing
them in the router cache's in-memory layer lets any selector release them,
and the per-key TTL replaces the full-map rebuild on every write.

Tests now hold the burst with a barrier instead of a sleep and use a manual
clock object.
@tc3oliver

tc3oliver commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Pushed 250fc4e and d499a03 for the Greptile review. d499a03 also fixes the reservation TTL: InMemoryCache never extends a live key's TTL, so the key expired 30 s after its first write. Could a maintainer approve the workflow runs?

tc3oliver and others added 3 commits October 29, 2026 09:43
InMemoryCache keeps the TTL a live key was first given, so a reservation
key that kept being rewritten still expired 30 s after its first pick and
took the newer reservations with it. Drop the key before each write so the
TTL counts from the latest one, and leave it deleted once it is empty.

Signed-off-by: Oliver Yu <[email protected]>
…s on cache hits

Least-busy now reads the model group from litellm_metadata before metadata, so /v1/decisions calls that carry caller metadata are counted in flight and release their reservation at pre-call. A response cache hit releases its reservation instead of decrementing another request's in-flight count, and Responses WebSocket turn admission selects without reserving. The reservation store is reached through a typed protocol.
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

Comment thread litellm/router_strategy/least_busy.py Resolved
… count requests that start during a Redis read

Least-busy now reads a call's deployment from the routing metadata the router sets, so /v1/decisions logging params stay as on main and failed decisions calls no longer start cooldowns.
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

Comment thread litellm/router_strategy/least_busy.py Resolved
…c calls keep each other's picks

Router.completion from a thread pool and sync success callbacks can read and rewrite a deployment's reservations at the same time, so one thread's pick or release could be lost.
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

…rovider

A call that fails or returns before the provider handoff (a 400 for an unsupported param, token counting, a proxy-gate rejection without a start log, batch create) now drops its reservation instead of decrementing a count it never added. Router.select picks and batch cancel no longer reserve, since neither makes a call that logs.
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

Comment thread litellm/router_strategy/least_busy.py Resolved
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor 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.

Stale Bugbot comment from a previous run.

…rst_reservation

# Conflicts:
#	tests/unit/decisions/test_main.py
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

Comment thread litellm/router.py Resolved
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor 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.

Stale Bugbot comment from a previous run.

…rst_reservation

# Conflicts:
#	litellm/router.py
#	litellm/router_deprecated.py
#	tests/unit/test_router/test_router.py
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

2 similar comments
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor 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.

Stale Bugbot comment from a previous run.

Comment thread litellm/router_python_core.py Resolved
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor 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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 5860094. Configure here.

@mateo-berri mateo-berri 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.

LGTM. Thanks for the contribution!

@mateo-berri
mateo-berri merged commit 733b776 into BerriAI:main Nov 5, 2026
91 checks passed
@tc3oliver
tc3oliver deleted the litellm_least_busy_burst_reservation branch November 7, 2026 06:46
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