Skip to content

fix(proxy): share model rate-limit buckets between a model_group_alias and its target - #42516

Merged
mateo-berri merged 2 commits into
mainfrom
litellm_model_group_alias_rate_limit_bucket
Sep 22, 2026
Merged

mateo-berri merged 2 commits into
mainfrom
litellm_model_group_alias_rate_limit_bucket

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • A model_group_alias request counted in its own per-key rate-limit bucket
  • So a key could double default_api_key_rpm_limit / tpm_limit by using the alias
  • Key, team, org, and project model_rpm_limit / model_tpm_limit entries skipped alias requests entirely

How it solves it:

  • The limiter resolves the requested name to its model group before keying any model bucket
  • Alias and model group now count against one <scope>:<model group> bucket
  • A limit keyed by the alias name still applies, on that shared bucket
  • Post-call token charges land on the model group bucket too

User Flow

Before: a developer whose key gets 3 requests a minute on Kimi-K3-lab-claude-compatible gets 3 more by sending to its alias

  1. The proxy admin sets default_api_key_rpm_limit: 3 on the Kimi-K3-lab-claude-compatible deployment and router_settings.model_group_alias: {"Kimi-K3-lab-claude-compatible[1m]": "Kimi-K3-lab-claude-compatible"}
  2. The developer sends POST https://litellm-domain/v1/chat/completions with "model": "Kimi-K3-lab-claude-compatible" three times inside a minute; each returns 200 and x-ratelimit-model_per_key-remaining-requests counts down 2, 1, 0
  3. Their fourth call returns 429 Rate limit exceeded for model_per_key: <key hash>:Kimi-K3-lab-claude-compatible. Limit type: requests. Current limit: 3, Remaining: 0
  4. They resend the same request with "model": "Kimi-K3-lab-claude-compatible[1m]" and get 200 with x-ratelimit-model_per_key-remaining-requests: 2, a fresh bucket of 3 on the same deployment
  5. Any key holder can send twice the configured requests (and tokens) per minute to that deployment by alternating the two names

After: the alias call refuses too, so the key gets 3 requests a minute on that deployment whichever name it uses

  1. The proxy admin sets default_api_key_rpm_limit: 3 on the Kimi-K3-lab-claude-compatible deployment and router_settings.model_group_alias: {"Kimi-K3-lab-claude-compatible[1m]": "Kimi-K3-lab-claude-compatible"}
  2. The developer sends POST https://litellm-domain/v1/chat/completions with "model": "Kimi-K3-lab-claude-compatible" three times inside a minute; each returns 200 and x-ratelimit-model_per_key-remaining-requests counts down 2, 1, 0
  3. Their fourth call returns 429 Rate limit exceeded for model_per_key: <key hash>:Kimi-K3-lab-claude-compatible. Limit type: requests. Current limit: 3, Remaining: 0
  4. They resend the same request with "model": "Kimi-K3-lab-claude-compatible[1m]" and get the same 429 naming <key hash>:Kimi-K3-lab-claude-compatible
  5. No key holder can exceed the deployment's configured requests or tokens per minute by switching between the two names

Design decisions

  • Lookup order is the requested name first, then the model group: an admin who keyed model_rpm_limit by the alias name keeps that limit. The alternatives were model group only (silently drops such entries) and applying both (double-counts one request). Whichever entry supplies the limit, the counter is the model group's
  • The bucket value is <scope>:<model group> for the key, team, org, and project model limits, the project itpm/otpm limits, and the post-call token charge, so pre-call and post-call always meet on one key
  • The 429 text and the x-ratelimit-model_per_key-* headers name the model group, not the alias, so a reader can see which bucket refused. x-litellm-model-group still reports the requested name
  • Resolution happens inside the descriptor builders, so every caller (chat, messages, responses, batch rows, the compaction summary check, websocket frames) gets it without a signature change
  • Dynamic rate limiting's per-model saturation tracking still keys on the requested name; it is a separate feature and out of scope here
  • The request stash keeps the model group resolved at admission (reserved_model is now the resolved pair, not the raw name), and post-call reconciliation charges that group. Re-resolving the alias after the call would move the charge to a new target whenever the alias map is reloaded mid-flight, leaving the reservation stranded on the old one (Greptile's finding on the first push)
  • One counter per model group, every configured entry for either spelling applied to it: a key with model_rpm_limit entries for both the alias and the group is capped at the larger one overall, and the smaller entry refuses requests sent under its name once the shared counter passes it. The alternative, one counter per entry, is the bypass this PR removes. Key-over-team precedence is unchanged and now applies whichever spelling each scope used

Relevant issues

Affected release

Linear ticket

Resolves LIT-8242

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/test_litellm/<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
  • 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)

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

Both legs run the same rig: two proxy processes per leg (instances A and B, each --num_workers 2) on random ports, sharing one Postgres database and one Redis (general_settings.coordination_redis), booted from the same config, with the caller alternating every request between A and B on one fresh virtual key. Real upstreams (Moonshot Kimi K3 and Anthropic Claude Haiku 4.5), no mocks. Redis is flushed between legs

Shared config (qa_config.yaml):

model_list:
  - model_name: Kimi-K3-lab-claude-compatible
    litellm_params:
      model: moonshot/kimi-k3
      api_key: os.environ/MOONSHOT_API_KEY
      default_api_key_rpm_limit: 3
  - model_name: haiku-lab-claude-compatible
    litellm_params:
      model: anthropic/claude-haiku-4-5
      api_key: os.environ/ANTHROPIC_API_KEY
      default_api_key_rpm_limit: 3
  - model_name: haiku-lab-responses
    litellm_params:
      model: anthropic/claude-haiku-4-5
      api_key: os.environ/ANTHROPIC_API_KEY
      default_api_key_rpm_limit: 3
  - model_name: haiku-lab-tpm
    litellm_params:
      model: anthropic/claude-haiku-4-5
      api_key: os.environ/ANTHROPIC_API_KEY
      default_api_key_tpm_limit: 60

router_settings:
  model_group_alias:
    "Kimi-K3-lab-claude-compatible[1m]": Kimi-K3-lab-claude-compatible
    "haiku-lab-claude-compatible[1m]": haiku-lab-claude-compatible
    "haiku-lab-responses[1m]": haiku-lab-responses
    "haiku-lab-tpm[1m]": haiku-lab-tpm

general_settings:
  master_key: os.environ/LITELLM_MASTER_KEY
  coordination_redis:
    host: 127.0.0.1
    port: 39018

Boot, per leg (<root> is the checkout at the leg's commit):

cd <root> && python litellm/proxy/proxy_cli.py --config qa_config.yaml --port <portA> --num_workers 2
cd <root> && python litellm/proxy/proxy_cli.py --config qa_config.yaml --port <portB> --num_workers 2

Caller, per case (qa_leg.sh): one fresh virtual key from instance A, then the first model until it returns 429, alternating A, B, A, then the second model once on the same key. Each call prints the status line, x-litellm-model-group, the x-ratelimit-model_per_key-* headers, and the first 400 characters of the body

Before (88a4cbd)

cases

/v1/chat/completions RPM: Kimi K3 target, then its alias
$ KEY=$(curl -s http://127.0.0.1:55237/key/generate -H 'Authorization: Bearer $LITELLM_MASTER_KEY' -H 'Content-Type: application/json' -d '{}' | jq -r .key)   # instance A
# call 1 to Kimi-K3-lab-claude-compatible via instance A (:55237)
$ curl -s -D - http://127.0.0.1:55237/v1/chat/completions -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"Kimi-K3-lab-claude-compatible","max_tokens":8,"messages":[{"role":"user","content":"Say hi"}]}'
HTTP/1.1 200 OK
x-litellm-model-group: Kimi-K3-lab-claude-compatible
x-ratelimit-model_per_key-remaining-requests: 2
x-ratelimit-model_per_key-limit-requests: 3
{"id":"chatcmpl-6ab2d6f417ee96ce62471a51","created":1790105332,"model":"Kimi-K3-lab-claude-compatible","object":"chat.completion","choices":[{"finish_reason":"length","index":0,"message":{"content":"","role":"assistant","reasoning_content":"The user just said \"","provider_specific_fields":{"refusal":null}},"provider_specific_fields":{}}],"usage":{"completion_tokens":8,"prompt_tokens":87,"total_to
---
# call 2 to Kimi-K3-lab-claude-compatible via instance B (:58609)
$ curl -s -D - http://127.0.0.1:58609/v1/chat/completions -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"Kimi-K3-lab-claude-compatible","max_tokens":8,"messages":[{"role":"user","content":"Say hi"}]}'
HTTP/1.1 200 OK
x-litellm-model-group: Kimi-K3-lab-claude-compatible
x-ratelimit-model_per_key-remaining-requests: 1
x-ratelimit-model_per_key-limit-requests: 3
{"id":"chatcmpl-6ab2d6f69951af35a51cc226","created":1790105334,"model":"Kimi-K3-lab-claude-compatible","object":"chat.completion","choices":[{"finish_reason":"length","index":0,"message":{"content":"","role":"assistant","reasoning_content":"The user is just saying","provider_specific_fields":{"refusal":null}},"provider_specific_fields":{}}],"usage":{"completion_tokens":8,"prompt_tokens":87,"total_
---
# call 3 to Kimi-K3-lab-claude-compatible via instance A (:55237)
$ curl -s -D - http://127.0.0.1:55237/v1/chat/completions -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"Kimi-K3-lab-claude-compatible","max_tokens":8,"messages":[{"role":"user","content":"Say hi"}]}'
HTTP/1.1 200 OK
x-litellm-model-group: Kimi-K3-lab-claude-compatible
x-ratelimit-model_per_key-remaining-requests: 0
x-ratelimit-model_per_key-limit-requests: 3
{"id":"chatcmpl-6ab2d6f834df078910c008b3","created":1790105337,"model":"Kimi-K3-lab-claude-compatible","object":"chat.completion","choices":[{"finish_reason":"length","index":0,"message":{"content":"","role":"assistant","reasoning_content":"The user just said \"","provider_specific_fields":{"refusal":null}},"provider_specific_fields":{}}],"usage":{"completion_tokens":8,"prompt_tokens":87,"total_to
---
# call 4 to Kimi-K3-lab-claude-compatible via instance B (:58609)
$ curl -s -D - http://127.0.0.1:58609/v1/chat/completions -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"Kimi-K3-lab-claude-compatible","max_tokens":8,"messages":[{"role":"user","content":"Say hi"}]}'
HTTP/1.1 429 Too Many Requests
{"error":{"message":"Rate limit exceeded for model_per_key: 2503857b971b7bf3ea40ecae1106ae64324fbaea21502cabad3982e82931385f:Kimi-K3-lab-claude-compatible. Limit type: requests. Current limit: 3, Remaining: 0. Limit resets at: 2026-09-22 19:29:58 UTC","type":"throttling_error","param":null,"code":"429"}}
---
# now the SAME key calls Kimi-K3-lab-claude-compatible[1m]
# call 5 to Kimi-K3-lab-claude-compatible[1m] via instance A (:55237)
$ curl -s -D - http://127.0.0.1:55237/v1/chat/completions -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"Kimi-K3-lab-claude-compatible[1m]","max_tokens":8,"messages":[{"role":"user","content":"Say hi"}]}'
HTTP/1.1 200 OK
x-litellm-model-group: Kimi-K3-lab-claude-compatible[1m]
x-ratelimit-model_per_key-remaining-requests: 2
x-ratelimit-model_per_key-limit-requests: 3
{"id":"chatcmpl-6ab2d6fd80e892c1f8054d3d","created":1790105341,"model":"Kimi-K3-lab-claude-compatible[1m]","object":"chat.completion","choices":[{"finish_reason":"length","index":0,"message":{"content":"","role":"assistant","reasoning_content":"The user just said \"","provider_specific_fields":{"refusal":null}},"provider_specific_fields":{}}],"usage":{"completion_tokens":8,"prompt_tokens":87,"tota
---
/v1/messages RPM: Haiku alias, then its target
$ KEY=$(curl -s http://127.0.0.1:55237/key/generate -H 'Authorization: Bearer $LITELLM_MASTER_KEY' -H 'Content-Type: application/json' -d '{}' | jq -r .key)   # instance A
# call 1 to haiku-lab-claude-compatible[1m] via instance A (:55237)
$ curl -s -D - http://127.0.0.1:55237/v1/messages -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"haiku-lab-claude-compatible[1m]","max_tokens":8,"messages":[{"role":"user","content":"Say hi"}]}'
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible[1m]
x-ratelimit-model_per_key-remaining-requests: 2
x-ratelimit-model_per_key-limit-requests: 3
{"model":"haiku-lab-claude-compatible[1m]","id":"msg_011CfK2kqF9fR3pvjKBqTZnq","type":"message","role":"assistant","content":[{"type":"text","text":"Hi! 👋 How's"}],"container":null,"stop_reason":"max_tokens","stop_sequence":null,"stop_details":null,"usage":{"input_tokens":9,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_inp
---
# call 2 to haiku-lab-claude-compatible[1m] via instance B (:58609)
$ curl -s -D - http://127.0.0.1:58609/v1/messages -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"haiku-lab-claude-compatible[1m]","max_tokens":8,"messages":[{"role":"user","content":"Say hi"}]}'
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible[1m]
x-ratelimit-model_per_key-remaining-requests: 1
x-ratelimit-model_per_key-limit-requests: 3
{"model":"haiku-lab-claude-compatible[1m]","id":"msg_011CfK2kxsHoPuyXFsnRBF1B","type":"message","role":"assistant","content":[{"type":"text","text":"Hi! 👋 How's"}],"container":null,"stop_reason":"max_tokens","stop_sequence":null,"stop_details":null,"usage":{"input_tokens":9,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_inp
---
# call 3 to haiku-lab-claude-compatible[1m] via instance A (:55237)
$ curl -s -D - http://127.0.0.1:55237/v1/messages -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"haiku-lab-claude-compatible[1m]","max_tokens":8,"messages":[{"role":"user","content":"Say hi"}]}'
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible[1m]
x-ratelimit-model_per_key-remaining-requests: 0
x-ratelimit-model_per_key-limit-requests: 3
{"model":"haiku-lab-claude-compatible[1m]","id":"msg_011CfK2m6GY7GrJAMXPkxKtn","type":"message","role":"assistant","content":[{"type":"text","text":"Hi! 👋 How can"}],"container":null,"stop_reason":"max_tokens","stop_sequence":null,"stop_details":null,"usage":{"input_tokens":9,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_i
---
# call 4 to haiku-lab-claude-compatible[1m] via instance B (:58609)
$ curl -s -D - http://127.0.0.1:58609/v1/messages -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"haiku-lab-claude-compatible[1m]","max_tokens":8,"messages":[{"role":"user","content":"Say hi"}]}'
HTTP/1.1 429 Too Many Requests
{"type":"error","error":{"type":"rate_limit_error","message":"Rate limit exceeded for model_per_key: 4f590319c98faff50a5b41950fd99d33b187ea6acbb726cc53006e7e9ad2e2cf:haiku-lab-claude-compatible[1m]. Limit type: requests. Current limit: 3, Remaining: 0. Limit resets at: 2026-09-22 19:30:13 UTC"}}
---
# now the SAME key calls haiku-lab-claude-compatible
# call 5 to haiku-lab-claude-compatible via instance A (:55237)
$ curl -s -D - http://127.0.0.1:55237/v1/messages -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"haiku-lab-claude-compatible","max_tokens":8,"messages":[{"role":"user","content":"Say hi"}]}'
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible
x-ratelimit-model_per_key-remaining-requests: 2
x-ratelimit-model_per_key-limit-requests: 3
{"model":"haiku-lab-claude-compatible","id":"msg_011CfK2mFSCMjBMjuj5deYVV","type":"message","role":"assistant","content":[{"type":"text","text":"Hi! 👋 How can"}],"container":null,"stop_reason":"max_tokens","stop_sequence":null,"stop_details":null,"usage":{"input_tokens":9,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input
---
/v1/responses RPM: Haiku target, then its alias
$ KEY=$(curl -s http://127.0.0.1:55237/key/generate -H 'Authorization: Bearer $LITELLM_MASTER_KEY' -H 'Content-Type: application/json' -d '{}' | jq -r .key)   # instance A
# call 1 to haiku-lab-responses via instance A (:55237)
$ curl -s -D - http://127.0.0.1:55237/v1/responses -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"haiku-lab-responses","max_output_tokens":8,"input":"Say hi"}'
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-responses
x-ratelimit-model_per_key-remaining-requests: 2
x-ratelimit-model_per_key-limit-requests: 3
{"id":"resp_cBtVAG1kZ5SuaQ9xtbi8UJKdk09zCZUCvbffYPuG-4bPGGR4SIkGHk7VnmczfotoLyoojiz6JK0d4TH8dcr80fYVaL5s8dBY8TcKtRWxYtFbaTPiVUyMw2n9T1MVGVM-677P1tMNno6oFLJmYHW15Wz4_B8wJshHqjCw3XS_bo0eaGakgUFcxzX7sgY7hpwjrJLQMVKv6s9FEZ5-zoEtrSBf-bSH_2ke8DBp3MVFO9x0K3vQUmRXk36tYVMsPgb43XcGjXaYQIU4p9K4dnCzyNmkOcZln7ccD9xWPOD8du7oPmh3d8EaXiD1aemrT1ZMAUlsXcL2wPjsCeVAUchixDKw1G97QKGeH_-LzQrqo21lFjgKNjIdkwNFeWEtzYKxdXQr
---
# call 2 to haiku-lab-responses via instance B (:58609)
$ curl -s -D - http://127.0.0.1:58609/v1/responses -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"haiku-lab-responses","max_output_tokens":8,"input":"Say hi"}'
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-responses
x-ratelimit-model_per_key-remaining-requests: 1
x-ratelimit-model_per_key-limit-requests: 3
{"id":"resp_tp3YT8OnyLpPR0YaOBAJjwd7SCk4rkB8ntRoc7Z6SOlHC6gRIOEYzjUdGZsGKkpugL40Y9U4rgUbj1-yFBieDoKkiQRkzHqH1QWiwsUDol_FpT8GjUGjfwHXgGwWNu12_ye9VYmlunJTo55zHj_mLIOvBGZRQTtPHojNt4Hfwy_NmBfriRfzi83Y8EmzgRzwWlOI0vJN4CiABDv_B2xR5NH-btJUd8MjOcXZGnPeZcjuPhKVgfCPNbwbXb1MKkgF_qiqiO0gnMdfMA-91xLY61V9ejRmT8RM_VRaHfXK0WeNqM1NomURqYq1RskQeFgScautlVS-ZjdRt__sSwQ9-KwhoXlj_wY9yKg7bLGZlMrcb-mA30rUoPCbqiemSe7ZAFfI
---
# call 3 to haiku-lab-responses via instance A (:55237)
$ curl -s -D - http://127.0.0.1:55237/v1/responses -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"haiku-lab-responses","max_output_tokens":8,"input":"Say hi"}'
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-responses
x-ratelimit-model_per_key-remaining-requests: 0
x-ratelimit-model_per_key-limit-requests: 3
{"id":"resp_uBv281Bna9xYj0y7q6GOPRdlu1GWzNvWkrT7gxKZwh3ko3FyMKX_VEZ5mOBcDDkepnf4HoeS2xGt8GGMySOh04j8xmXt2iiMfkHIIjiNIIrIHkYbAZcJ6-nkAfR942jS7wuPtRehIIb2cFxrZnSa8etrHBhbdNKRBhHi7Xw2aIqZxXgqLnvsL9peulANd5En8E4ffYbZ8zOlFb4xwThMyHCJ7oRzYhu087dfzdkz2bkuvfV5ulWcsqA18S-yakMeZV1Nnm8zR1k5nWVcq7hdTFxMSqYSJyYsASVProoYo4OYGT_GkJeYiFLkOvkse8cxNZVHGY54ZvRVIJf_l8vO59S2rghl0GyyrzYC5vWctWNmJEqSE6fFrdJW9zRGArcEIW9D
---
# call 4 to haiku-lab-responses via instance B (:58609)
$ curl -s -D - http://127.0.0.1:58609/v1/responses -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"haiku-lab-responses","max_output_tokens":8,"input":"Say hi"}'
HTTP/1.1 429 Too Many Requests
{"error":{"message":"Rate limit exceeded for model_per_key: bda5ca84cfcdb67219ca4e73cc5dbfae54923a46cc4c2c85b55cab639a30e921:haiku-lab-responses. Limit type: requests. Current limit: 3, Remaining: 0. Limit resets at: 2026-09-22 19:30:24 UTC","type":"throttling_error","param":null,"code":"429"}}
---
# now the SAME key calls haiku-lab-responses[1m]
# call 5 to haiku-lab-responses[1m] via instance A (:55237)
$ curl -s -D - http://127.0.0.1:55237/v1/responses -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"haiku-lab-responses[1m]","max_output_tokens":8,"input":"Say hi"}'
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-responses[1m]
x-ratelimit-model_per_key-remaining-requests: 2
x-ratelimit-model_per_key-limit-requests: 3
{"id":"resp_gIxjPXLl6P0I3hTEpL25AZiYK7z87r2QmRCshibBJWNL12XZsNdHIlcSWnhYsU2UZDv4Rn9rykCIUYR6PGuq-8rRzDgeDZwhau42mS025L1fWb0hr9b74_ztQI2t0N9tX9q__d8uBjxucHe2j0BtiUSDQj4k5MD22HkOBC7hCxkj0lV-KPCbvKyTcyx0wscdyQayjtJQi3fIgWTOtMwjHbVA2ixlrtY9xUPQn1QxkhHBcNOWGlNRGvGH-yCmtkfz11pMvq1JsLem_J6BqndbgzXWNT__YInNfz1tqnlDdkvbsxRqMjGwzhRJyd2ROWwpOazJSnQ8HGHxghYkyr4oDbcdy2NDV2qmDE10SRN8L4sVEXgbl7KuPk7_HMYTXoJbSub6
---
/v1/chat/completions TPM: Haiku target, then its alias
$ KEY=$(curl -s http://127.0.0.1:55237/key/generate -H 'Authorization: Bearer $LITELLM_MASTER_KEY' -H 'Content-Type: application/json' -d '{}' | jq -r .key)   # instance A
# call 1 to haiku-lab-tpm via instance A (:55237)
$ curl -s -D - http://127.0.0.1:55237/v1/chat/completions -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"haiku-lab-tpm","max_tokens":8,"messages":[{"role":"user","content":"Say hi"}]}'
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-tpm
x-ratelimit-model_per_key-remaining-tokens: 51
x-ratelimit-model_per_key-limit-tokens: 60
{"id":"chatcmpl-ae261695-5228-4159-99ba-5c04a6db177f","created":1790105383,"model":"haiku-lab-tpm","object":"chat.completion","choices":[{"finish_reason":"length","index":0,"message":{"content":"Hi! 👋 How can","role":"assistant","provider_specific_fields":{"citations":null,"thinking_blocks":null}}}],"usage":{"completion_tokens":8,"prompt_tokens":9,"total_tokens":17,"completion_tokens_details":{"re
---
# call 2 to haiku-lab-tpm via instance B (:58609)
$ curl -s -D - http://127.0.0.1:58609/v1/chat/completions -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"haiku-lab-tpm","max_tokens":8,"messages":[{"role":"user","content":"Say hi"}]}'
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-tpm
x-ratelimit-model_per_key-remaining-tokens: 34
x-ratelimit-model_per_key-limit-tokens: 60
{"id":"chatcmpl-ad76d64e-6da0-42ff-af97-78a29e8dc0d0","created":1790105384,"model":"haiku-lab-tpm","object":"chat.completion","choices":[{"finish_reason":"length","index":0,"message":{"content":"Hi! 👋 How's","role":"assistant","provider_specific_fields":{"citations":null,"thinking_blocks":null}}}],"usage":{"completion_tokens":8,"prompt_tokens":9,"total_tokens":17,"completion_tokens_details":{"reas
---
# call 3 to haiku-lab-tpm via instance A (:55237)
$ curl -s -D - http://127.0.0.1:55237/v1/chat/completions -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"haiku-lab-tpm","max_tokens":8,"messages":[{"role":"user","content":"Say hi"}]}'
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-tpm
x-ratelimit-model_per_key-remaining-tokens: 17
x-ratelimit-model_per_key-limit-tokens: 60
{"id":"chatcmpl-4b111398-d27d-4987-a820-006cd5432360","created":1790105386,"model":"haiku-lab-tpm","object":"chat.completion","choices":[{"finish_reason":"length","index":0,"message":{"content":"Hi! 👋 How's","role":"assistant","provider_specific_fields":{"citations":null,"thinking_blocks":null}}}],"usage":{"completion_tokens":8,"prompt_tokens":9,"total_tokens":17,"completion_tokens_details":{"reas
---
# call 4 to haiku-lab-tpm via instance B (:58609)
$ curl -s -D - http://127.0.0.1:58609/v1/chat/completions -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"haiku-lab-tpm","max_tokens":8,"messages":[{"role":"user","content":"Say hi"}]}'
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-tpm
x-ratelimit-model_per_key-remaining-tokens: 0
x-ratelimit-model_per_key-limit-tokens: 60
{"id":"chatcmpl-397da7da-337a-4bf4-b232-464e6d46d922","created":1790105388,"model":"haiku-lab-tpm","object":"chat.completion","choices":[{"finish_reason":"length","index":0,"message":{"content":"Hi! 👋 How's","role":"assistant","provider_specific_fields":{"citations":null,"thinking_blocks":null}}}],"usage":{"completion_tokens":8,"prompt_tokens":9,"total_tokens":17,"completion_tokens_details":{"reas
---
# call 5 to haiku-lab-tpm via instance A (:55237)
$ curl -s -D - http://127.0.0.1:55237/v1/chat/completions -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"haiku-lab-tpm","max_tokens":8,"messages":[{"role":"user","content":"Say hi"}]}'
HTTP/1.1 429 Too Many Requests
{"error":{"message":"Rate limit exceeded for model_per_key: 8de171ccd824fd6b82223e739c9c2baa73c6dddc50e553385eb443420f0970a9:haiku-lab-tpm. Limit type: tokens. Current limit: 60, Remaining: 0. Limit resets at: 2026-09-22 19:30:49 UTC","type":"throttling_error","param":null,"code":"429"}}
---
# now the SAME key calls haiku-lab-tpm[1m]
# call 6 to haiku-lab-tpm[1m] via instance B (:58609)
$ curl -s -D - http://127.0.0.1:58609/v1/chat/completions -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"haiku-lab-tpm[1m]","max_tokens":8,"messages":[{"role":"user","content":"Say hi"}]}'
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-tpm[1m]
x-ratelimit-model_per_key-remaining-tokens: 51
x-ratelimit-model_per_key-limit-tokens: 60
{"id":"chatcmpl-75129221-f02b-44ca-94fd-e5cf5e68fbd0","created":1790105389,"model":"haiku-lab-tpm[1m]","object":"chat.completion","choices":[{"finish_reason":"length","index":0,"message":{"content":"Hi! 👋 How can","role":"assistant","provider_specific_fields":{"citations":null,"thinking_blocks":null}}}],"usage":{"completion_tokens":8,"prompt_tokens":9,"total_tokens":17,"completion_tokens_details":
---
  • Target hits 429 on the 4th request, both instances agree
  • Alias on the same key answers 200 with a fresh bucket
  • Same on /v1/messages, /v1/responses, and the TPM limit
  • 429 detail names the alias bucket, not the group

After (399c727)

cases

/v1/chat/completions RPM: Kimi K3 target, then its alias
$ KEY=$(curl -s http://127.0.0.1:23219/key/generate -H 'Authorization: Bearer $LITELLM_MASTER_KEY' -H 'Content-Type: application/json' -d '{}' | jq -r .key)   # instance A
# call 1 to Kimi-K3-lab-claude-compatible via instance A (:23219)
$ curl -s -D - http://127.0.0.1:23219/v1/chat/completions -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"Kimi-K3-lab-claude-compatible","max_tokens":8,"messages":[{"role":"user","content":"Say hi"}]}'
HTTP/1.1 200 OK
x-litellm-model-group: Kimi-K3-lab-claude-compatible
x-ratelimit-model_per_key-remaining-requests: 2
x-ratelimit-model_per_key-limit-requests: 3
{"id":"chatcmpl-6ab2d77bce4a287f7c9dabfc","created":1790105467,"model":"Kimi-K3-lab-claude-compatible","object":"chat.completion","choices":[{"finish_reason":"length","index":0,"message":{"content":"","role":"assistant","reasoning_content":"The user just said \"","provider_specific_fields":{"refusal":null}},"provider_specific_fields":{}}],"usage":{"completion_tokens":8,"prompt_tokens":87,"total_to
---
# call 2 to Kimi-K3-lab-claude-compatible via instance B (:42085)
$ curl -s -D - http://127.0.0.1:42085/v1/chat/completions -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"Kimi-K3-lab-claude-compatible","max_tokens":8,"messages":[{"role":"user","content":"Say hi"}]}'
HTTP/1.1 200 OK
x-litellm-model-group: Kimi-K3-lab-claude-compatible
x-ratelimit-model_per_key-remaining-requests: 1
x-ratelimit-model_per_key-limit-requests: 3
{"id":"chatcmpl-6ab2d77e34df078910c00a46","created":1790105470,"model":"Kimi-K3-lab-claude-compatible","object":"chat.completion","choices":[{"finish_reason":"length","index":0,"message":{"content":"","role":"assistant","reasoning_content":"The user just said \"","provider_specific_fields":{"refusal":null}},"provider_specific_fields":{}}],"usage":{"completion_tokens":8,"prompt_tokens":87,"total_to
---
# call 3 to Kimi-K3-lab-claude-compatible via instance A (:23219)
$ curl -s -D - http://127.0.0.1:23219/v1/chat/completions -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"Kimi-K3-lab-claude-compatible","max_tokens":8,"messages":[{"role":"user","content":"Say hi"}]}'
HTTP/1.1 200 OK
x-litellm-model-group: Kimi-K3-lab-claude-compatible
x-ratelimit-model_per_key-remaining-requests: 0
x-ratelimit-model_per_key-limit-requests: 3
{"id":"chatcmpl-6ab2d780d6d2d48d23423e6a","created":1790105472,"model":"Kimi-K3-lab-claude-compatible","object":"chat.completion","choices":[{"finish_reason":"length","index":0,"message":{"content":"","role":"assistant","reasoning_content":"The user has asked me","provider_specific_fields":{"refusal":null}},"provider_specific_fields":{}}],"usage":{"completion_tokens":8,"prompt_tokens":87,"total_to
---
# call 4 to Kimi-K3-lab-claude-compatible via instance B (:42085)
$ curl -s -D - http://127.0.0.1:42085/v1/chat/completions -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"Kimi-K3-lab-claude-compatible","max_tokens":8,"messages":[{"role":"user","content":"Say hi"}]}'
HTTP/1.1 429 Too Many Requests
{"error":{"message":"Rate limit exceeded for model_per_key: 4db35fd5881a10f9d8e12bfe762cf42325d13b8b9d178489b6e9aa4b4fe176d6:Kimi-K3-lab-claude-compatible. Limit type: requests. Current limit: 3, Remaining: 0. Limit resets at: 2026-09-22 19:32:14 UTC","type":"throttling_error","param":null,"code":"429"}}
---
# now the SAME key calls Kimi-K3-lab-claude-compatible[1m]
# call 5 to Kimi-K3-lab-claude-compatible[1m] via instance A (:23219)
$ curl -s -D - http://127.0.0.1:23219/v1/chat/completions -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"Kimi-K3-lab-claude-compatible[1m]","max_tokens":8,"messages":[{"role":"user","content":"Say hi"}]}'
HTTP/1.1 429 Too Many Requests
{"error":{"message":"Rate limit exceeded for model_per_key: 4db35fd5881a10f9d8e12bfe762cf42325d13b8b9d178489b6e9aa4b4fe176d6:Kimi-K3-lab-claude-compatible. Limit type: requests. Current limit: 3, Remaining: 0. Limit resets at: 2026-09-22 19:32:14 UTC","type":"throttling_error","param":null,"code":"429"}}
---
/v1/messages RPM: Haiku alias, then its target
$ KEY=$(curl -s http://127.0.0.1:23219/key/generate -H 'Authorization: Bearer $LITELLM_MASTER_KEY' -H 'Content-Type: application/json' -d '{}' | jq -r .key)   # instance A
# call 1 to haiku-lab-claude-compatible[1m] via instance A (:23219)
$ curl -s -D - http://127.0.0.1:23219/v1/messages -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"haiku-lab-claude-compatible[1m]","max_tokens":8,"messages":[{"role":"user","content":"Say hi"}]}'
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible[1m]
x-ratelimit-model_per_key-remaining-requests: 2
x-ratelimit-model_per_key-limit-requests: 3
{"model":"haiku-lab-claude-compatible[1m]","id":"msg_011CfK2vbXwGAyosZcHXjCTp","type":"message","role":"assistant","content":[{"type":"text","text":"Hi! 👋 How can"}],"container":null,"stop_reason":"max_tokens","stop_sequence":null,"stop_details":null,"usage":{"input_tokens":9,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_i
---
# call 2 to haiku-lab-claude-compatible[1m] via instance B (:42085)
$ curl -s -D - http://127.0.0.1:42085/v1/messages -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"haiku-lab-claude-compatible[1m]","max_tokens":8,"messages":[{"role":"user","content":"Say hi"}]}'
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible[1m]
x-ratelimit-model_per_key-remaining-requests: 1
x-ratelimit-model_per_key-limit-requests: 3
{"model":"haiku-lab-claude-compatible[1m]","id":"msg_011CfK2vkHHcFWDcTSQjjUMB","type":"message","role":"assistant","content":[{"type":"text","text":"Hey! 👋 How's"}],"container":null,"stop_reason":"max_tokens","stop_sequence":null,"stop_details":null,"usage":{"input_tokens":9,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_in
---
# call 3 to haiku-lab-claude-compatible[1m] via instance A (:23219)
$ curl -s -D - http://127.0.0.1:23219/v1/messages -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"haiku-lab-claude-compatible[1m]","max_tokens":8,"messages":[{"role":"user","content":"Say hi"}]}'
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible[1m]
x-ratelimit-model_per_key-remaining-requests: 0
x-ratelimit-model_per_key-limit-requests: 3
{"model":"haiku-lab-claude-compatible[1m]","id":"msg_011CfK2vvxE1pgu48wNZujwL","type":"message","role":"assistant","content":[{"type":"text","text":"Hi! 👋 How can"}],"container":null,"stop_reason":"max_tokens","stop_sequence":null,"stop_details":null,"usage":{"input_tokens":9,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_i
---
# call 4 to haiku-lab-claude-compatible[1m] via instance B (:42085)
$ curl -s -D - http://127.0.0.1:42085/v1/messages -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"haiku-lab-claude-compatible[1m]","max_tokens":8,"messages":[{"role":"user","content":"Say hi"}]}'
HTTP/1.1 429 Too Many Requests
{"type":"error","error":{"type":"rate_limit_error","message":"Rate limit exceeded for model_per_key: a6fe3f41fa25fe229c3a311910e05cd389828b3160f90f51205073b2b6deafa2:haiku-lab-claude-compatible. Limit type: requests. Current limit: 3, Remaining: 0. Limit resets at: 2026-09-22 19:32:27 UTC"}}
---
# now the SAME key calls haiku-lab-claude-compatible
# call 5 to haiku-lab-claude-compatible via instance A (:23219)
$ curl -s -D - http://127.0.0.1:23219/v1/messages -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"haiku-lab-claude-compatible","max_tokens":8,"messages":[{"role":"user","content":"Say hi"}]}'
HTTP/1.1 429 Too Many Requests
{"type":"error","error":{"type":"rate_limit_error","message":"Rate limit exceeded for model_per_key: a6fe3f41fa25fe229c3a311910e05cd389828b3160f90f51205073b2b6deafa2:haiku-lab-claude-compatible. Limit type: requests. Current limit: 3, Remaining: 0. Limit resets at: 2026-09-22 19:32:28 UTC"}}
---
/v1/responses RPM: Haiku target, then its alias
$ KEY=$(curl -s http://127.0.0.1:23219/key/generate -H 'Authorization: Bearer $LITELLM_MASTER_KEY' -H 'Content-Type: application/json' -d '{}' | jq -r .key)   # instance A
# call 1 to haiku-lab-responses via instance A (:23219)
$ curl -s -D - http://127.0.0.1:23219/v1/responses -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"haiku-lab-responses","max_output_tokens":8,"input":"Say hi"}'
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-responses
x-ratelimit-model_per_key-remaining-requests: 2
x-ratelimit-model_per_key-limit-requests: 3
{"id":"resp_GXG3rdQSapz0XtQv5Z8ezG0xjxlN_abMMwgcdQnOsGwOcNtWldJjZTq6KlPE80Qvjgl5bdBj1-o1wT0-_p6V8TwmgXrqG51AXPKTR-U-44kCHmK0LIj857VEnq6f0GdNf9nVFZtdC_nprd2CoLgbQ2SfUqqP289y-g0piuHoUX_-LHTumjTS9FMTmUG8YT27izD0KjPW9mDoIEdlbi9pbzSzSiltu0aRZ0mTtdMlb7p4cEhJNu8Ti2Vq65fPVYxzlWxWDk5bh-o1HF1IdZNp-ud_OL5uAy31fFit12U-GJ2zNqiktyDmDXWpN9j-kbJ6kdm_X44ls3d8OtwBM-kV2kdahxR1Un7KsRznkCAWigisG1iAMc3rq2G-dQJNLOUZPNdS
---
# call 2 to haiku-lab-responses via instance B (:42085)
$ curl -s -D - http://127.0.0.1:42085/v1/responses -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"haiku-lab-responses","max_output_tokens":8,"input":"Say hi"}'
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-responses
x-ratelimit-model_per_key-remaining-requests: 1
x-ratelimit-model_per_key-limit-requests: 3
{"id":"resp_Um7pAVbcAGrIvuiAM-C_shCQft8qGW6cnMZaKEldEvc9khfrUgKEcv4fb_8-Fzlura50spEdtcJf5nYNL2KevR-J4_WYqWsiaO5xd7U9E611bbl9KabmwWPAB96wFIwG6gsrOgC8fkbT0o-J8N9oTRIcKtpzylpT6XviZICEJFt5LJJXRUqHJBLnXrVGbs-fXv3tdcIHvyxOsEPVIxK4sIfYm6dwfEFsidY4FJcsgYntsmd_t7exvnnuTQazRbc2cveot_5HJD5FOJtt8w5f5Vh7oEonSoFn1PpjiVh_ecSisg49nyNVGBIu0zgqEC9IrH--ZDhynECXOyi9eqlDI4WRgCx9fIsNuOdh1il0mUQbBLo9cj4wf_cpKlK6p5OMdiIZ
---
# call 3 to haiku-lab-responses via instance A (:23219)
$ curl -s -D - http://127.0.0.1:23219/v1/responses -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"haiku-lab-responses","max_output_tokens":8,"input":"Say hi"}'
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-responses
x-ratelimit-model_per_key-remaining-requests: 0
x-ratelimit-model_per_key-limit-requests: 3
{"id":"resp_vvTkqfioR1DR4SeivTZCQFpzfU2vM0o3Kf8QNeKMJRoYVkNeLhOgNn2l20Fmhx4RHnkep0x1kc6PwMSc0Y-RGSnPdmBIFEPCyBQYuxSGVYiTDTk7hytEQrVxBvl-a9xuNGVI9aqr6knFDyxtYDGfYRxGI-HHvI6GWSyG5gJFgK16AuhIpW3KzpSeH-Vw7Jzk_5h1ngGuxNTIPrFQTQOEDKJr6Paftq6WdwIIR4cZygHv8GJX_sF2wm-1NkKVGqmzB8k1OZ7FCd8wKO411pCuZQKIhGw59plAYSiqdcZ6Htg959PcLjbiMtmLpTxi9EeaoD2uINQ8lMDlGPX3NEn-WOf2cLvHI7EvvHuZRi3UothQGql7rR-2p0UcrAZV2k6qmwiN
---
# call 4 to haiku-lab-responses via instance B (:42085)
$ curl -s -D - http://127.0.0.1:42085/v1/responses -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"haiku-lab-responses","max_output_tokens":8,"input":"Say hi"}'
HTTP/1.1 429 Too Many Requests
{"error":{"message":"Rate limit exceeded for model_per_key: f72b17065f35339b825f9773022dda83c00dab8cf0c429b1ed64f8914959a576:haiku-lab-responses. Limit type: requests. Current limit: 3, Remaining: 0. Limit resets at: 2026-09-22 19:32:36 UTC","type":"throttling_error","param":null,"code":"429"}}
---
# now the SAME key calls haiku-lab-responses[1m]
# call 5 to haiku-lab-responses[1m] via instance A (:23219)
$ curl -s -D - http://127.0.0.1:23219/v1/responses -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"haiku-lab-responses[1m]","max_output_tokens":8,"input":"Say hi"}'
HTTP/1.1 429 Too Many Requests
{"error":{"message":"Rate limit exceeded for model_per_key: f72b17065f35339b825f9773022dda83c00dab8cf0c429b1ed64f8914959a576:haiku-lab-responses. Limit type: requests. Current limit: 3, Remaining: 0. Limit resets at: 2026-09-22 19:32:36 UTC","type":"throttling_error","param":null,"code":"429"}}
---
/v1/chat/completions TPM: Haiku target, then its alias
$ KEY=$(curl -s http://127.0.0.1:23219/key/generate -H 'Authorization: Bearer $LITELLM_MASTER_KEY' -H 'Content-Type: application/json' -d '{}' | jq -r .key)   # instance A
# call 1 to haiku-lab-tpm via instance A (:23219)
$ curl -s -D - http://127.0.0.1:23219/v1/chat/completions -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"haiku-lab-tpm","max_tokens":8,"messages":[{"role":"user","content":"Say hi"}]}'
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-tpm
x-ratelimit-model_per_key-remaining-tokens: 51
x-ratelimit-model_per_key-limit-tokens: 60
{"id":"chatcmpl-2efd974f-788e-43a0-90c2-6971afd4af12","created":1790105497,"model":"haiku-lab-tpm","object":"chat.completion","choices":[{"finish_reason":"length","index":0,"message":{"content":"Hi! 👋 How can","role":"assistant","provider_specific_fields":{"citations":null,"thinking_blocks":null}}}],"usage":{"completion_tokens":8,"prompt_tokens":9,"total_tokens":17,"completion_tokens_details":{"re
---
# call 2 to haiku-lab-tpm via instance B (:42085)
$ curl -s -D - http://127.0.0.1:42085/v1/chat/completions -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"haiku-lab-tpm","max_tokens":8,"messages":[{"role":"user","content":"Say hi"}]}'
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-tpm
x-ratelimit-model_per_key-remaining-tokens: 34
x-ratelimit-model_per_key-limit-tokens: 60
{"id":"chatcmpl-9e1540ea-61ac-422e-8383-a5059476a2a4","created":1790105499,"model":"haiku-lab-tpm","object":"chat.completion","choices":[{"finish_reason":"length","index":0,"message":{"content":"Hi! 👋 How's","role":"assistant","provider_specific_fields":{"citations":null,"thinking_blocks":null}}}],"usage":{"completion_tokens":8,"prompt_tokens":9,"total_tokens":17,"completion_tokens_details":{"reas
---
# call 3 to haiku-lab-tpm via instance A (:23219)
$ curl -s -D - http://127.0.0.1:23219/v1/chat/completions -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"haiku-lab-tpm","max_tokens":8,"messages":[{"role":"user","content":"Say hi"}]}'
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-tpm
x-ratelimit-model_per_key-remaining-tokens: 17
x-ratelimit-model_per_key-limit-tokens: 60
{"id":"chatcmpl-38b4ce9c-b8c3-4041-9fe7-4f49fc56d356","created":1790105501,"model":"haiku-lab-tpm","object":"chat.completion","choices":[{"finish_reason":"length","index":0,"message":{"content":"Hi! 👋 How can","role":"assistant","provider_specific_fields":{"citations":null,"thinking_blocks":null}}}],"usage":{"completion_tokens":8,"prompt_tokens":9,"total_tokens":17,"completion_tokens_details":{"re
---
# call 4 to haiku-lab-tpm via instance B (:42085)
$ curl -s -D - http://127.0.0.1:42085/v1/chat/completions -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"haiku-lab-tpm","max_tokens":8,"messages":[{"role":"user","content":"Say hi"}]}'
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-tpm
x-ratelimit-model_per_key-remaining-tokens: 0
x-ratelimit-model_per_key-limit-tokens: 60
{"id":"chatcmpl-83f04c7c-4575-48b4-bb57-8b8f99ea36f2","created":1790105503,"model":"haiku-lab-tpm","object":"chat.completion","choices":[{"finish_reason":"length","index":0,"message":{"content":"Hi! 👋 How's","role":"assistant","provider_specific_fields":{"citations":null,"thinking_blocks":null}}}],"usage":{"completion_tokens":8,"prompt_tokens":9,"total_tokens":17,"completion_tokens_details":{"reas
---
# call 5 to haiku-lab-tpm via instance A (:23219)
$ curl -s -D - http://127.0.0.1:23219/v1/chat/completions -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"haiku-lab-tpm","max_tokens":8,"messages":[{"role":"user","content":"Say hi"}]}'
HTTP/1.1 429 Too Many Requests
{"error":{"message":"Rate limit exceeded for model_per_key: 51fe3e5908811356577442ed6b5ddb969324a4768c9c975defb9bf508a9e7846:haiku-lab-tpm. Limit type: tokens. Current limit: 60, Remaining: 0. Limit resets at: 2026-09-22 19:32:44 UTC","type":"throttling_error","param":null,"code":"429"}}
---
# now the SAME key calls haiku-lab-tpm[1m]
# call 6 to haiku-lab-tpm[1m] via instance B (:42085)
$ curl -s -D - http://127.0.0.1:42085/v1/chat/completions -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"haiku-lab-tpm[1m]","max_tokens":8,"messages":[{"role":"user","content":"Say hi"}]}'
HTTP/1.1 429 Too Many Requests
{"error":{"message":"Rate limit exceeded for model_per_key: 51fe3e5908811356577442ed6b5ddb969324a4768c9c975defb9bf508a9e7846:haiku-lab-tpm. Limit type: tokens. Current limit: 60, Remaining: 0. Limit resets at: 2026-09-22 19:32:44 UTC","type":"throttling_error","param":null,"code":"429"}}
---
/v1/chat/completions RPM under the limit: alias, then target, one shared countdown
$ KEY=$(curl -s http://127.0.0.1:23219/key/generate -H 'Authorization: Bearer $LITELLM_MASTER_KEY' -H 'Content-Type: application/json' -d '{}' | jq -r .key)   # instance A
# call 1 to Kimi-K3-lab-claude-compatible[1m] via instance A (:23219)
$ curl -s -D - http://127.0.0.1:23219/v1/chat/completions -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"Kimi-K3-lab-claude-compatible[1m]","max_tokens":8,"messages":[{"role":"user","content":"Say hi"}]}'
HTTP/1.1 200 OK
x-litellm-model-group: Kimi-K3-lab-claude-compatible[1m]
x-ratelimit-model_per_key-remaining-requests: 2
x-ratelimit-model_per_key-limit-requests: 3
{"id":"chatcmpl-6ab2d7b117ee96ce62471cc3","created":1790105521,"model":"Kimi-K3-lab-claude-compatible[1m]","object":"chat.completion","choices":[{"finish_reason":"length","index":0,"message":{"content":"","role":"assistant","reasoning_content":"The user is just saying","provider_specific_fields":{"refusal":null}},"provider_specific_fields":{}}],"usage":{"completion_tokens":8,"prompt_tokens":87,"to
---
# now the SAME key calls Kimi-K3-lab-claude-compatible
# call 2 to Kimi-K3-lab-claude-compatible via instance B (:42085)
$ curl -s -D - http://127.0.0.1:42085/v1/chat/completions -H 'Authorization: Bearer $KEY' -H 'Content-Type: application/json' -d '{"model":"Kimi-K3-lab-claude-compatible","max_tokens":8,"messages":[{"role":"user","content":"Say hi"}]}'
HTTP/1.1 200 OK
x-litellm-model-group: Kimi-K3-lab-claude-compatible
x-ratelimit-model_per_key-remaining-requests: 1
x-ratelimit-model_per_key-limit-requests: 3
{"id":"chatcmpl-6ab2d7b580e892c1f8054f01","created":1790105525,"model":"Kimi-K3-lab-claude-compatible","object":"chat.completion","choices":[{"finish_reason":"length","index":0,"message":{"content":"","role":"assistant","reasoning_content":"The user just said \"","provider_specific_fields":{"refusal":null}},"provider_specific_fields":{}}],"usage":{"completion_tokens":8,"prompt_tokens":87,"total_to
---
  • Alias returns 429 once the target is exhausted
  • 429 detail names the canonical group bucket
  • Holds on /v1/chat/completions, /v1/messages, /v1/responses
  • TPM limit shared the same way
  • Alias and target count down one shared bucket
  • Both proxy instances agree through Redis

PASS: on every endpoint the alias and its target now share one per-key RPM/TPM bucket across both proxy instances

Live risk check: base 88a4cbd vs head 399c727, merge-ref b474e4fbbb

Verdict: no unintended regression found. Every base-vs-head difference below is either the shared bucket this PR exists to introduce or a pre-existing limiter behavior now applied to that bucket, and the merge-ref re-run matches the head

Same rig as the proof above on both sides (two proxies per side, --num_workers 2 each, one Postgres and one Redis shared, caller alternating A, B, A on one fresh key, PYTHONPATH pinned to each worktree), with one config addition for leg L4, a hidden dict-form alias under router_settings.model_group_alias:

    "haiku-hidden[1m]": {model: haiku-lab-claude-compatible, hidden: true}

Breaking

Dependent paths that answered differently before this PR. Each is the change the ticket asks for (its "Suggested fix" names the shared {api_key}:{resolved_model_group} bucket for the deployment default, the TPM path, and the key model_rpm_limit / model_tpm_limit path)

  • Alias requests are refused once the model group's bucket is spent, on every scope that carries a model limit. L2 (team model_rpm_limit keyed on the target, requests sent to the alias): base 200, 200, 200 with no model_per_team header at all; head 200, 200, then 429 naming <key hash>:haiku-lab-claude-compatible, with x-ratelimit-model_per_team-remaining-requests counting 1, 0 on the alias calls. L4 (hidden dict-form alias after the target used its 3): base 200 with remaining-requests: 2, a fresh bucket; head 429 naming the target. Edge: _rate_limited_model inside every descriptor builder (key default, key and team metadata, org, project, project itpm/otpm)
  • The 429 detail and the x-ratelimit-model_per_key-* / model_per_team-* headers on an alias request name the model group (L1 third alias call: base ...:haiku-lab-claude-compatible[1m], head ...:haiku-lab-claude-compatible). A client that parses the bucket name out of the message now reads the target's name; x-litellm-model-group still echoes the requested name. Intended, Design decisions above

Backward incompatible

  • A key with model_rpm_limit entries for both spellings (L1b, alias 2 and target 4): base ran two budgets, 6 x 200 and the 7th 429; head runs one counter and applies each name's own limit to it: target 200 (3 left of 4), 200 (2 left), alias 429 at limit 2 (the counter already reads 2), alias 429, then target 429 at limit 4 because the two refused alias calls advanced the shared counter too (see Regression risk, pre-existing). Net: the group's total is capped by its largest configured entry and a smaller entry refuses requests sent under its name once the counter passes it. Follows from the requested-first lookup in Design decisions
  • Key-over-team precedence now holds across spellings (L2b, team {"alias": 1} and key {"target": 3}, requests to the alias): base applied the team's alias entry (200, then 429 x3 naming model_per_team: <team>:haiku-lab-claude-compatible[1m]); head applies the key's target entry (model_per_key 2, 1, 0 then 429 naming the group) and the team's alias entry never fires. The precedence order itself is unchanged (get_key_model_rpm_limit: key metadata, key model_max_budget, team metadata, deployment default), it is now evaluated on the resolved group
  • Streamed alias usage lands in the group's token bucket (L3, haiku-lab-tpm at 60 tpm, one alias stream of 17 tokens then the target): the target's x-ratelimit-model_per_key-remaining-tokens reads 34 on head and 51 on base. Edge: post-call reconciliation on reconcile_model.group
  • Prometheus litellm_remaining_requests_metric and litellm_remaining_tokens_metric read x-ratelimit-model_per_key-remaining-* from the logging payload (litellm/integrations/prometheus.py), so a gauge labeled with an alias model now reports the shared bucket's countdown instead of an alias-only one. Traced from the header values above, not driven against a live Prometheus (Not verified)
  • The Redis window key of an alias request moves from {model_per_key:<hash>:<alias>} to {model_per_key:<hash>:<group>}, so at upgrade an alias's in-flight window is abandoned once and the group's window takes over; during a rolling deploy old pods keep counting alias calls in the old key for at most one window (LITELLM_RATE_LIMIT_WINDOW_SIZE). Low, one-time

Regression risk

  • Refused requests advance the window counter, on both sides. Probe: one name, model_rpm_limit 4, six calls, then the raw counter: {model_per_key:<hash>:haiku-lab-claude-compatible}:requests = 6 after 4 x 200 and 2 x 429 on base and on head alike. Pre-existing, so not changed here; on the shared bucket it is what makes L1b's target calls 3 to 5 refuse. A follow-up would change the blocked branch of the Lua window script
  • A failed upstream call keeps its request slot on both sides (the failure hook refunds tokens and the parallel slot, never the request count). L5, an alias call answered 400 by the provider and then the target: base 200, 200, 200, 429; head 200, 200, 429, the failed alias call having spent one of the group's 3. Pre-existing accounting applied to the shared bucket
  • resolve_model_group_alias is single-hop, so an alias whose target is itself an alias still gets its own bucket (same as base, not driven)
  • The legacy v1 limiter (LEGACY_MULTI_INSTANCE_RATE_LIMITING=true), dynamic_rate_limiter / dynamic_rate_limiter_v3, and model_max_budget_limiter keep raw-name keying, so the alias bypass persists there. Untouched by this PR, out of scope

Dependency graph

  • _rate_limited_model / RateLimitedModel (new) <- key default descriptor: verified live (L1, L4, L5, proof cases); key metadata model_rpm_limit: live (L1, L1b, L2b) plus test_model_group_alias_shares_metadata_model_rpm_bucket_with_its_target and test_model_rpm_limit_keyed_by_the_alias_name_still_limits_alias_requests_only; team metadata: live (L2, L2b); tpm: live (L3, proof case 4) plus test_model_group_alias_shares_deployment_default_rpm_bucket_with_its_target; org and project descriptors and project itpm/otpm: unit-tested only
  • model_group_resolver (dependency-injected, default _resolve_model_group_alias_via_proxy_router, lazy llm_router import): live on both proxy instances; the resolver keeps the admission-time group across an alias reload, test_success_tpm_accounting_keeps_the_admission_target_after_an_alias_reload
  • Success reconciliation on reconcile_model.group <- chat non-streaming (proof case 4, live), chat streaming (L3, live), /v1/messages and /v1/responses (proof cases, live), test_success_tpm_accounting_charges_the_alias_target_bucket; batch rows, websocket per-frame descriptors, and the compaction summary check reach the same builders: unit-level only
  • Failure hook (async_post_call_failure_hook): live, L5
  • Response headers x-ratelimit-model_per_key-* / model_per_team-* <- Prometheus gauges: traced; dashboard ui/litellm-dashboard: no consumer of these headers or of the bucket name (grep), unaffected
  • tests/e2e/quota_management/ratelimit/test_model_group_alias_rate_limit_e2e.py: added, both directions, green on e2e-changed-tests
  • User docs (litellm-docs): the rate-limit pages describe per-model limits by model name and do not say an alias gets its own bucket, so nothing to update

Not verified

  • Prometheus gauge values for alias-labeled requests on a live scrape (no Prometheus in the rig)
  • Batch rows (/v1/batches), websocket per-frame descriptors, and the compaction summary check with an alias model, driven end to end
  • Org and project model_rpm_limit / model_tpm_limit and project itpm/otpm with an alias, driven end to end
  • The rolling-deploy overlap (old and new pods sharing one Redis inside one window)
  • The legacy v1 limiter and the dynamic rate limiters, by design out of scope

Merge-ref re-run

main moved from 4bcdaf3 to 1373322 while this PR was open; 399c727 merges cleanly onto it (b474e4fbbb) and the proof's cases 1 and 4 re-run there match the head exactly

merge-ref b474e4fbbb: case 1 (Kimi K3 RPM, group x4 then the alias) and case 4 (haiku TPM, group x5 then the alias)
### merge case 1: model group x3, then the model group and the alias
# call 1 to Kimi-K3-lab-claude-compatible via instance A (:31877)
HTTP/1.1 200 OK
x-ratelimit-model_per_key-remaining-requests: 2
# call 2 to Kimi-K3-lab-claude-compatible via instance B (:47193)
HTTP/1.1 200 OK
x-ratelimit-model_per_key-remaining-requests: 1
# call 3 to Kimi-K3-lab-claude-compatible via instance A (:31877)
HTTP/1.1 200 OK
x-ratelimit-model_per_key-remaining-requests: 0
# call 4 to Kimi-K3-lab-claude-compatible via instance B (:47193)
HTTP/1.1 429 Too Many Requests
{"error":{"message":"Rate limit exceeded for model_per_key: dcc27d4e...:Kimi-K3-lab-claude-compatible. Limit type: requests. Current limit: 3, Remaining: 0. ...
# call 5 to Kimi-K3-lab-claude-compatible[1m] via instance A (:31877)
HTTP/1.1 429 Too Many Requests
{"error":{"message":"Rate limit exceeded for model_per_key: dcc27d4e...:Kimi-K3-lab-claude-compatible. Limit type: requests. Current limit: 3, Remaining: 0. ...
### merge case 4: tpm group x4, then the group and the alias
# call 1 to haiku-lab-tpm via instance A (:31877)
HTTP/1.1 200 OK
x-ratelimit-model_per_key-remaining-tokens: 51
# call 2 to haiku-lab-tpm via instance B (:47193)
HTTP/1.1 200 OK
x-ratelimit-model_per_key-remaining-tokens: 34
# call 3 to haiku-lab-tpm via instance A (:31877)
HTTP/1.1 200 OK
x-ratelimit-model_per_key-remaining-tokens: 17
# call 4 to haiku-lab-tpm via instance B (:47193)
HTTP/1.1 200 OK
x-ratelimit-model_per_key-remaining-tokens: 0
# call 5 to haiku-lab-tpm via instance A (:31877)
HTTP/1.1 429 Too Many Requests
{"error":{"message":"Rate limit exceeded for model_per_key: 5bd1fe99...:haiku-lab-tpm. Limit type: tokens. Current limit: 60, Remaining: 0. ...
# call 6 to haiku-lab-tpm[1m] via instance B (:47193)
HTTP/1.1 429 Too Many Requests
{"error":{"message":"Rate limit exceeded for model_per_key: 5bd1fe99...:haiku-lab-tpm. Limit type: tokens. Current limit: 60, Remaining: 0. ...

Leg output, base then head

Same seven legs on both sides (legs.sh), status line, x-litellm-model-group, the x-ratelimit-model_per_(key|team)-* headers, and the start of any 429 detail. 200 bodies elided

base 88a4cbd (instances :55237/58609)
### L1 key model_rpm_limit keyed on the alias only
# call 1 to haiku-lab-claude-compatible[1m] via instance A (:55237)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible[1m]
x-ratelimit-model_per_key-remaining-requests: 1
x-ratelimit-model_per_key-limit-requests: 2
<200 body>
# call 2 to haiku-lab-claude-compatible[1m] via instance B (:58609)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible[1m]
x-ratelimit-model_per_key-remaining-requests: 0
x-ratelimit-model_per_key-limit-requests: 2
<200 body>
# call 3 to haiku-lab-claude-compatible[1m] via instance A (:55237)
HTTP/1.1 429 Too Many Requests
Rate limit exceeded for model_per_key: 3ebf703b8da15709fa863127e9b45c5b500bc06ec57c7eed7226c2aa33a26626:haiku-lab-claude-compatible[1m]. Lim ...
# call 4 to haiku-lab-claude-compatible via instance B (:58609)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible
<200 body>
### L1b key model_rpm_limit keyed on both names (alias 2, target 4)
# call 1 to haiku-lab-claude-compatible via instance A (:55237)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible
x-ratelimit-model_per_key-remaining-requests: 3
x-ratelimit-model_per_key-limit-requests: 4
<200 body>
# call 2 to haiku-lab-claude-compatible via instance B (:58609)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible
x-ratelimit-model_per_key-remaining-requests: 2
x-ratelimit-model_per_key-limit-requests: 4
<200 body>
# call 3 to haiku-lab-claude-compatible[1m] via instance A (:55237)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible[1m]
x-ratelimit-model_per_key-remaining-requests: 1
x-ratelimit-model_per_key-limit-requests: 2
<200 body>
# call 4 to haiku-lab-claude-compatible[1m] via instance B (:58609)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible[1m]
x-ratelimit-model_per_key-remaining-requests: 0
x-ratelimit-model_per_key-limit-requests: 2
<200 body>
# call 5 to haiku-lab-claude-compatible via instance A (:55237)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible
x-ratelimit-model_per_key-remaining-requests: 1
x-ratelimit-model_per_key-limit-requests: 4
<200 body>
# call 6 to haiku-lab-claude-compatible via instance B (:58609)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible
x-ratelimit-model_per_key-remaining-requests: 0
x-ratelimit-model_per_key-limit-requests: 4
<200 body>
# call 7 to haiku-lab-claude-compatible via instance A (:55237)
HTTP/1.1 429 Too Many Requests
Rate limit exceeded for model_per_key: 1eef78009bc245a49df8d867d39b4b277dca76d45ee0cd72e8cbc396e3b0b807:haiku-lab-claude-compatible. Limit t ...
### L2 team model_rpm_limit keyed on the target, alias requests
# call 1 to haiku-lab-claude-compatible[1m] via instance A (:55237)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible[1m]
<200 body>
# call 2 to haiku-lab-claude-compatible[1m] via instance B (:58609)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible[1m]
<200 body>
# call 3 to haiku-lab-claude-compatible[1m] via instance A (:55237)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible[1m]
<200 body>
# call 4 to haiku-lab-claude-compatible via instance B (:58609)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible
x-ratelimit-model_per_key-remaining-requests: 1
x-ratelimit-model_per_key-limit-requests: 2
x-ratelimit-model_per_team-remaining-requests: 1
x-ratelimit-model_per_team-limit-requests: 2
<200 body>
### L2b team limit keyed on the alias (1) vs key limit keyed on the target (3), alias requests
# call 1 to haiku-lab-claude-compatible[1m] via instance A (:55237)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible[1m]
x-ratelimit-model_per_team-remaining-requests: 0
x-ratelimit-model_per_team-limit-requests: 1
<200 body>
# call 2 to haiku-lab-claude-compatible[1m] via instance B (:58609)
HTTP/1.1 429 Too Many Requests
Rate limit exceeded for model_per_team: ee4e77a8-0c18-48fd-b75f-2c4573f7f3aa:haiku-lab-claude-compatible[1m]. Limit type: requests. Current  ...
# call 3 to haiku-lab-claude-compatible[1m] via instance A (:55237)
HTTP/1.1 429 Too Many Requests
Rate limit exceeded for model_per_team: ee4e77a8-0c18-48fd-b75f-2c4573f7f3aa:haiku-lab-claude-compatible[1m]. Limit type: requests. Current  ...
# call 4 to haiku-lab-claude-compatible[1m] via instance B (:58609)
HTTP/1.1 429 Too Many Requests
Rate limit exceeded for model_per_team: ee4e77a8-0c18-48fd-b75f-2c4573f7f3aa:haiku-lab-claude-compatible[1m]. Limit type: requests. Current  ...
### L3 streaming alias call, then the target on the tpm-limited group
# call 1 to haiku-lab-tpm[1m] via instance A (:55237)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-tpm[1m]
"usage":{"completion_tokens":8,"prompt_tokens":9,"total_tokens":17,"completion_tokens_details":{"reasoning_tokens":0,"text_tokens":8}
# call 2 to haiku-lab-tpm via instance B (:58609)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-tpm
x-ratelimit-model_per_key-remaining-tokens: 51
x-ratelimit-model_per_key-limit-tokens: 60
<200 body>
### L4 hidden dict-form alias after the target used its 3 rpm
# call 1 to haiku-lab-claude-compatible via instance A (:55237)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible
x-ratelimit-model_per_key-remaining-requests: 2
x-ratelimit-model_per_key-limit-requests: 3
<200 body>
# call 2 to haiku-lab-claude-compatible via instance B (:58609)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible
x-ratelimit-model_per_key-remaining-requests: 1
x-ratelimit-model_per_key-limit-requests: 3
<200 body>
# call 3 to haiku-lab-claude-compatible via instance A (:55237)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible
x-ratelimit-model_per_key-remaining-requests: 0
x-ratelimit-model_per_key-limit-requests: 3
<200 body>
# call 4 to haiku-hidden[1m] via instance B (:58609)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-hidden[1m]
x-ratelimit-model_per_key-remaining-requests: 2
x-ratelimit-model_per_key-limit-requests: 3
<200 body>
# call 5 to haiku-lab-claude-compatible via instance A (:55237)
HTTP/1.1 429 Too Many Requests
Rate limit exceeded for model_per_key: d0de1bfbb6a8865eac708a6561597c43fcaefb6f1a2cf551b60a6a882b4df0a0:haiku-lab-claude-compatible. Limit t ...
### L5 upstream 400 via the alias, then the target
# call 1 to haiku-lab-claude-compatible[1m] via instance A (:55237)
HTTP/1.1 400 Bad Request
... litellm.BadRequestError from the provider (temperature 5), no fallback ...
# call 2 to haiku-lab-claude-compatible via instance B (:58609)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible
x-ratelimit-model_per_key-remaining-requests: 2
x-ratelimit-model_per_key-limit-requests: 3
<200 body>
# call 3 to haiku-lab-claude-compatible via instance A (:55237)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible
x-ratelimit-model_per_key-remaining-requests: 1
x-ratelimit-model_per_key-limit-requests: 3
<200 body>
# call 4 to haiku-lab-claude-compatible via instance B (:58609)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible
x-ratelimit-model_per_key-remaining-requests: 0
x-ratelimit-model_per_key-limit-requests: 3
<200 body>
# call 5 to haiku-lab-claude-compatible via instance A (:55237)
HTTP/1.1 429 Too Many Requests
Rate limit exceeded for model_per_key: 6b635e27507185e8b53e51b75b9e308016ce9beee37e3deecc15ee466bca9c15:haiku-lab-claude-compatible. Limit t ...
head 399c727 (instances :23219/42085)
### L1 key model_rpm_limit keyed on the alias only
# call 1 to haiku-lab-claude-compatible[1m] via instance A (:23219)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible[1m]
x-ratelimit-model_per_key-remaining-requests: 1
x-ratelimit-model_per_key-limit-requests: 2
<200 body>
# call 2 to haiku-lab-claude-compatible[1m] via instance B (:42085)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible[1m]
x-ratelimit-model_per_key-remaining-requests: 0
x-ratelimit-model_per_key-limit-requests: 2
<200 body>
# call 3 to haiku-lab-claude-compatible[1m] via instance A (:23219)
HTTP/1.1 429 Too Many Requests
Rate limit exceeded for model_per_key: cc631a9b1761015936edeb9b62d283df152261d7f8b47cf5a07ef0ae782f85fa:haiku-lab-claude-compatible. Limit t ...
# call 4 to haiku-lab-claude-compatible via instance B (:42085)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible
<200 body>
### L1b key model_rpm_limit keyed on both names (alias 2, target 4)
# call 1 to haiku-lab-claude-compatible via instance A (:23219)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible
x-ratelimit-model_per_key-remaining-requests: 3
x-ratelimit-model_per_key-limit-requests: 4
<200 body>
# call 2 to haiku-lab-claude-compatible via instance B (:42085)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible
x-ratelimit-model_per_key-remaining-requests: 2
x-ratelimit-model_per_key-limit-requests: 4
<200 body>
# call 3 to haiku-lab-claude-compatible[1m] via instance A (:23219)
HTTP/1.1 429 Too Many Requests
Rate limit exceeded for model_per_key: 41084e38df8648c05573371c7b0131a1a643af70c134b42905920d7182218711:haiku-lab-claude-compatible. Limit t ...
# call 4 to haiku-lab-claude-compatible[1m] via instance B (:42085)
HTTP/1.1 429 Too Many Requests
Rate limit exceeded for model_per_key: 41084e38df8648c05573371c7b0131a1a643af70c134b42905920d7182218711:haiku-lab-claude-compatible. Limit t ...
# call 5 to haiku-lab-claude-compatible via instance A (:23219)
HTTP/1.1 429 Too Many Requests
Rate limit exceeded for model_per_key: 41084e38df8648c05573371c7b0131a1a643af70c134b42905920d7182218711:haiku-lab-claude-compatible. Limit t ...
# call 6 to haiku-lab-claude-compatible via instance B (:42085)
HTTP/1.1 429 Too Many Requests
Rate limit exceeded for model_per_key: 41084e38df8648c05573371c7b0131a1a643af70c134b42905920d7182218711:haiku-lab-claude-compatible. Limit t ...
# call 7 to haiku-lab-claude-compatible via instance A (:23219)
HTTP/1.1 429 Too Many Requests
Rate limit exceeded for model_per_key: 41084e38df8648c05573371c7b0131a1a643af70c134b42905920d7182218711:haiku-lab-claude-compatible. Limit t ...
### L2 team model_rpm_limit keyed on the target, alias requests
# call 1 to haiku-lab-claude-compatible[1m] via instance A (:23219)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible[1m]
x-ratelimit-model_per_key-remaining-requests: 1
x-ratelimit-model_per_key-limit-requests: 2
x-ratelimit-model_per_team-remaining-requests: 1
x-ratelimit-model_per_team-limit-requests: 2
<200 body>
# call 2 to haiku-lab-claude-compatible[1m] via instance B (:42085)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible[1m]
x-ratelimit-model_per_key-remaining-requests: 0
x-ratelimit-model_per_key-limit-requests: 2
x-ratelimit-model_per_team-remaining-requests: 0
x-ratelimit-model_per_team-limit-requests: 2
<200 body>
# call 3 to haiku-lab-claude-compatible[1m] via instance A (:23219)
HTTP/1.1 429 Too Many Requests
Rate limit exceeded for model_per_key: 15ec20ff7f1b260c931d381f7057f5c4b9cfbb0dc79455da9ab523383d8b694e:haiku-lab-claude-compatible. Limit t ...
# call 4 to haiku-lab-claude-compatible via instance B (:42085)
HTTP/1.1 429 Too Many Requests
Rate limit exceeded for model_per_key: 15ec20ff7f1b260c931d381f7057f5c4b9cfbb0dc79455da9ab523383d8b694e:haiku-lab-claude-compatible. Limit t ...
### L2b team limit keyed on the alias (1) vs key limit keyed on the target (3), alias requests
# call 1 to haiku-lab-claude-compatible[1m] via instance A (:23219)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible[1m]
x-ratelimit-model_per_key-remaining-requests: 2
x-ratelimit-model_per_key-limit-requests: 3
<200 body>
# call 2 to haiku-lab-claude-compatible[1m] via instance B (:42085)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible[1m]
x-ratelimit-model_per_key-remaining-requests: 1
x-ratelimit-model_per_key-limit-requests: 3
<200 body>
# call 3 to haiku-lab-claude-compatible[1m] via instance A (:23219)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible[1m]
x-ratelimit-model_per_key-remaining-requests: 0
x-ratelimit-model_per_key-limit-requests: 3
<200 body>
# call 4 to haiku-lab-claude-compatible[1m] via instance B (:42085)
HTTP/1.1 429 Too Many Requests
Rate limit exceeded for model_per_key: a9639d579f425b40a81f0198d8e690ae664825522ae5893ca6f15fa983d6a447:haiku-lab-claude-compatible. Limit t ...
### L3 streaming alias call, then the target on the tpm-limited group
# call 1 to haiku-lab-tpm[1m] via instance A (:23219)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-tpm[1m]
"usage":{"completion_tokens":8,"prompt_tokens":9,"total_tokens":17,"completion_tokens_details":{"reasoning_tokens":0,"text_tokens":8}
# call 2 to haiku-lab-tpm via instance B (:42085)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-tpm
x-ratelimit-model_per_key-remaining-tokens: 34
x-ratelimit-model_per_key-limit-tokens: 60
<200 body>
### L4 hidden dict-form alias after the target used its 3 rpm
# call 1 to haiku-lab-claude-compatible via instance A (:23219)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible
x-ratelimit-model_per_key-remaining-requests: 2
x-ratelimit-model_per_key-limit-requests: 3
<200 body>
# call 2 to haiku-lab-claude-compatible via instance B (:42085)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible
x-ratelimit-model_per_key-remaining-requests: 1
x-ratelimit-model_per_key-limit-requests: 3
<200 body>
# call 3 to haiku-lab-claude-compatible via instance A (:23219)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible
x-ratelimit-model_per_key-remaining-requests: 0
x-ratelimit-model_per_key-limit-requests: 3
<200 body>
# call 4 to haiku-hidden[1m] via instance B (:42085)
HTTP/1.1 429 Too Many Requests
Rate limit exceeded for model_per_key: 6c0a7bc6e7ad28d345008af244d620f03f86c502987a1f713c23c6f00fb35b59:haiku-lab-claude-compatible. Limit t ...
# call 5 to haiku-lab-claude-compatible via instance A (:23219)
HTTP/1.1 429 Too Many Requests
Rate limit exceeded for model_per_key: 6c0a7bc6e7ad28d345008af244d620f03f86c502987a1f713c23c6f00fb35b59:haiku-lab-claude-compatible. Limit t ...
### L5 upstream 400 via the alias, then the target
# call 1 to haiku-lab-claude-compatible[1m] via instance A (:23219)
HTTP/1.1 400 Bad Request
... litellm.BadRequestError from the provider (temperature 5), no fallback ...
# call 2 to haiku-lab-claude-compatible via instance B (:42085)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible
x-ratelimit-model_per_key-remaining-requests: 1
x-ratelimit-model_per_key-limit-requests: 3
<200 body>
# call 3 to haiku-lab-claude-compatible via instance A (:23219)
HTTP/1.1 200 OK
x-litellm-model-group: haiku-lab-claude-compatible
x-ratelimit-model_per_key-remaining-requests: 0
x-ratelimit-model_per_key-limit-requests: 3
<200 body>
# call 4 to haiku-lab-claude-compatible via instance B (:42085)
HTTP/1.1 429 Too Many Requests
Rate limit exceeded for model_per_key: d2de9cc1fe40add8a99c39f47d7d4b1cf9302461c3798384413ef390fa76db93:haiku-lab-claude-compatible. Limit t ...
# call 5 to haiku-lab-claude-compatible via instance A (:23219)
HTTP/1.1 429 Too Many Requests
Rate limit exceeded for model_per_key: d2de9cc1fe40add8a99c39f47d7d4b1cf9302461c3798384413ef390fa76db93:haiku-lab-claude-compatible. Limit t ...

Type

🐛 Bug Fix

Caveats (if any)

  • Observable changes are listed under Breaking and Backward incompatible in the live risk check above: alias requests now refuse on the group's bucket, the 429 detail and rate-limit headers name the model group, limits keyed on both spellings share one counter, key-over-team precedence applies across spellings, alias-labeled Prometheus gauges report the shared bucket, and an alias's in-flight Redis window resets once at upgrade
  • Low: the non-required proxy-infra unit shard is red at this merge sha with four native_compaction tests ('object' object has no attribute 'db', a prisma_client sentinel leaked by an earlier test in the shard order) and, on the second attempt only, test_component_allowlists.py::test_gateway_plus_backend_covers_full_app. All five pass in isolation at base and head, the compaction four fail identically on main's own run 35775627379, and this PR changes nothing that shard covers
  • Low: the opt-in legacy limiter (LEGACY_MULTI_INSTANCE_RATE_LIMITING=true, parallel_request_limiter.py) still keys its per-key model window on the raw requested name ({api_key}::{model}::{minute}), so an alias keeps its own bucket there. It is untouched here because that limiter is a deprecated opt-in the reported deployment does not run, and reworking it would widen a scoped security fix into a second limiter with its own QA rig and review rounds

QA runbook

  • tests/e2e/quota_management/ratelimit/test_model_group_alias_rate_limit_e2e.py::TestModelGroupAliasRateLimit::test_alias_shares_rpm_bucket_with_model_group - a key that used up the model group's 3 requests a minute is refused on the alias inside the same minute
    • Config: a deployment e2e-alias-rl-target (anthropic/claude-haiku-4-5, default_api_key_rpm_limit: 3) and router_settings.model_group_alias: {"e2e-alias-rl-alias": "e2e-alias-rl-target"} (needs ANTHROPIC_API_KEY)
    • Generate a key: curl -X POST http://localhost:4000/key/generate -H "Authorization: Bearer sk-1234" -d '{}'
    • Send three /v1/chat/completions requests with "model": "e2e-alias-rl-target" on that key inside one minute and expect 200 on each
    • Send one /v1/chat/completions request with "model": "e2e-alias-rl-alias" on the same key inside that minute and expect 429 naming model_per_key: <key hash>:e2e-alias-rl-target
    • Sanity check: this test makes sense to add and is not hand-wavey (e.g., assert actual expected spend instead of just spend > 0) or potentially flaky
  • tests/e2e/quota_management/ratelimit/test_model_group_alias_rate_limit_e2e.py::TestModelGroupAliasRateLimit::test_model_group_shares_rpm_bucket_with_alias - the same in the other direction: exhaust the alias, then the model group is refused
    • Same config and a fresh key
    • Send three /v1/chat/completions requests with "model": "e2e-alias-rl-alias" inside one minute and expect 200 on each
    • Send one /v1/chat/completions request with "model": "e2e-alias-rl-target" on the same key inside that minute and expect 429 naming model_per_key: <key hash>:e2e-alias-rl-target
    • Sanity check: this test makes sense to add and is not hand-wavey (e.g., assert actual expected spend instead of just spend > 0) or potentially flaky

Nuance: all four calls of one test must land inside one 60 second window (LITELLM_RATE_LIMIT_WINDOW_SIZE); the test asserts that before the fourth call

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

  • 399c727 passes /live-pr-risk

…s and its target

A request sent under a model_group_alias counted in its own per-key, per-team,
per-org, and per-project model bucket, so a key could double a deployment's
default_api_key_rpm_limit / tpm_limit by alternating the alias and the model
group name, and a metadata model_rpm_limit / model_tpm_limit keyed by the
model group never applied to alias requests. The limiter now resolves the
requested name to its model group before keying any model bucket, looks the
limit up by the requested name first and the model group second, and charges
post-call tokens to the same bucket.
@devin-ai-integration

devin-ai-integration Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@greptile-apps

greptile-apps Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

The PR appears safe to merge; no outstanding correctness or repository-rule issue remains.

Summary

This PR makes model-group aliases and their targets share model-scoped rate-limit buckets across key, team, organization, and project limits.

  • Resolves aliases before constructing rate-limit descriptor keys while preserving alias-specific limit lookup precedence.
  • Keeps post-call token reconciliation on the model group selected at admission, even if the alias mapping changes while a request is in flight.
  • Adds unit and end-to-end coverage for shared RPM buckets, metadata-defined limits, token accounting, and alias reloads.
  • The previously reported in-flight alias reload issue is fully fixed and its thread is resolved.

Reviews (2) · Last reviewed commit: "fix(proxy): charge the model group resol..."

Comment thread litellm/proxy/hooks/parallel_request_limiter_v3.py Outdated
@codecov

codecov Bot commented Sep 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed

codspeed Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_model_group_alias_rate_limit_bucket (399c727) with main (569ccae)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (97a6c27) during the generation of this report, so 569ccae was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

@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 399c727. 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

@mateo-berri
mateo-berri merged commit c6c3881 into main Sep 22, 2026
125 of 128 checks passed
@mateo-berri
mateo-berri deleted the litellm_model_group_alias_rate_limit_bucket branch September 22, 2026 20:32
DanBrima pushed a commit to DanBrima/litellm that referenced this pull request Sep 28, 2026
`_check_summary_model_rate_limit` called
`_add_team_model_rate_limit_descriptor_from_metadata` itself on top of the
copy `_create_rate_limit_descriptors` appends, handing the limiter one
counter as two descriptors.

BerriAI#42516 consolidated the two `model_per_team` assembly sites for the
pre-call path, where the repeat was charged and halved a team's configured
per-model limit. It left this call site reaching for the helper separately,
so the repeat moved here rather than disappearing.

The repeat is harmless today only because this caller passes
`read_only=True`, so the pair is read twice and nothing is incremented --
a property of the call site, not of the code it calls. Every counter
consumer below charges a request once per descriptor it is given.

Dropping the call also removes the `getattr` reaching for a private name
and the guard that depended on it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
DanBrima pushed a commit to DanBrima/litellm that referenced this pull request Oct 1, 2026
`_check_summary_model_rate_limit` called
`_add_team_model_rate_limit_descriptor_from_metadata` itself on top of the
copy `_create_rate_limit_descriptors` appends, handing the limiter one
counter as two descriptors.

BerriAI#42516 consolidated the two `model_per_team` assembly sites for the
pre-call path, where the repeat was charged and halved a team's configured
per-model limit. It left this call site reaching for the helper separately,
so the repeat moved here rather than disappearing.

The repeat is harmless today only because this caller passes
`read_only=True`, so the pair is read twice and nothing is incremented --
a property of the call site, not of the code it calls. Every counter
consumer below charges a request once per descriptor it is given.

Dropping the call also removes the `getattr` reaching for a private name
and the guard that depended on it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant