litellm - 💡(How to fix) Fix [Bug]: Internal-user max_budget blocks zero-cost models — _PROXY_MaxBudgetLimiter ignores skip_budget_checks [2 pull requests]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Error Message

Observed: 429 {"error":{"message":"Max budget limit reached."}} — the zero-cost model is blocked. Expected: the free model is allowed (it adds nothing to spend); only paid-model should return a budget error. {"error":{"message":"Max budget limit reached.","type":"rate_limit_error","code":"429"}} {"error":{"message":"ExceededBudget: User=u@test over budget. Spend=1.0, Budget=0.01","type":"budget_exceeded"}}

Root Cause

Root cause

The personal-budget pre-call hook _PROXY_MaxBudgetLimiter.async_pre_call_hook (litellm/proxy/hooks/max_budget_limiter.py) enforces the user budget for every request and never consults the model's cost or skip_budget_checks. So a zero-cost model is blocked at this hook even though common_checks() would have exempted it.

Fix Action

Fixed

Code Example

services:
    litellm:
      image: ghcr.io/berriai/litellm:main-stable   # reproduces on v1.87+; not on v1.81.12
      ports: ["4000:4000"]
      volumes: ["./config.yaml:/app/config.yaml"]
      command: ["--config=/app/config.yaml", "--port", "4000"]
      environment:
        DATABASE_URL: "postgresql://llmproxy:dbpassword9090@db:5432/litellm"
        STORE_MODEL_IN_DB: "True"
      depends_on: [db]
    db:
      image: postgres:16 
      environment:
        POSTGRES_DB: litellm
        POSTGRES_USER: llmproxy
        POSTGRES_PASSWORD: dbpassword9090
      ports: ["5433:5432"]

  2. config.yaml — one free (cost 0) and one paid model:
  model_list:
    - model_name: free-model
      litellm_params:
        model: openai/x
        api_base: http://127.0.0.1:9/v1   # unreachable on purpose
        api_key: x
        input_cost_per_token: 0
        output_cost_per_token: 0
    - model_name: paid-model
      litellm_params:
        model: openai/y
        api_base: http://127.0.0.1:9/v1
        api_key: y
        input_cost_per_token: 0.00001
        output_cost_per_token: 0.00001
  general_settings:
    master_key: sk-1234
    
  3. Start it
  docker compose up -d

  4. Create an internal user with a small budget, and grab its key
  curl -s -X POST localhost:4000/user/new \
    -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" \
    -d '{"user_id":"u@test","user_role":"internal_user","max_budget":0.01,"budget_duration":"30d"}'
  # -> copy the "key" (sk-...) from the response

  5. Push that user OVER budget, then clear the cache
  docker compose exec db psql -U llmproxy -d litellm \
    -c "UPDATE \"LiteLLM_UserTable\" SET spend=1 WHERE user_id='u@test';"
  docker compose restart litellm   # clears the in-memory user cache
  
  6. Call the FREE model with the user's key
  curl -s localhost:4000/v1/chat/completions \
    -H "Authorization: Bearer <USER_KEY_FROM_STEP_4>" -H "Content-Type: application/json" \
    -d '{"model":"free-model","messages":[{"role":"user","content":"hi"}]}'

  Observed: 429 {"error":{"message":"Max budget limit reached."}} — the zero-cost model is blocked.
  Expected: the free model is allowed (it adds nothing to spend); only paid-model should return a budget error.
RAW_BUFFERClick to expand / collapse

Check for existing issues

  • I have searched the existing issues and checked that my issue is not a duplicate.

What happened?

Problem

When an internal user (the LiteLLM user a virtual key belongs to) exceeds their max_budget, requests to zero-cost models (input + output cost == 0, e.g. self-hosted / on-prem models) are rejected with:

  429 "Max budget limit reached."

even though zero-cost models are supposed to bypass budget enforcement. Every other budget path (key / team / end-user) already exempts zero-cost models via skip_budget_checks / _is_model_cost_zero in common_checks(). The internal-user path does not.

Root cause

The personal-budget pre-call hook _PROXY_MaxBudgetLimiter.async_pre_call_hook (litellm/proxy/hooks/max_budget_limiter.py) enforces the user budget for every request and never consults the model's cost or skip_budget_checks. So a zero-cost model is blocked at this hook even though common_checks() would have exempted it.

It blocks with the hook's message "Max budget limit reached." (429), distinct from the common_checks message "ExceededBudget: User=… over budget" — confirming the block comes from this hook, not from common_checks.

Expected behavior

  • Zero-cost models (input + output cost == 0) remain accessible when the internal user is over budget (they add nothing to spend).
  • Paid models (cost > 0) are still blocked.

Relationship to existing issues / PRs

  • This is the internal-user manifestation of #14004 ("API blocks free (no cost) models when budget is exceeded"), and a duplicate of the symptom in #10973 (closed as stale).
  • #29420 (merged) fixed only the end-user / Customer slice.
  • #29483 (merged) fixed only model-discovery routes (/v1/models) for #27923.
  • No merged/open PR fixes this internal-user hook path.

Note (regression)

This worked on v1.81.12: there the hook read budget from a usually-empty cache key ({user_id}_user_api_key_user_id) and effectively no-op'd, so enforcement fell through to the zero-cost-aware common_checks. The hook was later rewritten to read user_max_budget directly from the auth object, making it reliably fire — but without a zero-cost guard — which re-introduced the block for free models.

Steps to Reproduce

End-to-end reproduction (proxy + Postgres)

1. docker-compose.yml

services:
  litellm:
    image: ghcr.io/berriai/litellm:main-stable   # reproduces on v1.87+; not on v1.81.12
    ports: ["4000:4000"]
    volumes: ["./config.yaml:/app/config.yaml"]
    command: ["--config=/app/config.yaml", "--port", "4000"]
    environment:
      DATABASE_URL: "postgresql://llmproxy:dbpassword9090@db:5432/litellm"
      STORE_MODEL_IN_DB: "True"
    depends_on: [db]
  db:
    image: postgres:16 
    environment:
      POSTGRES_DB: litellm
      POSTGRES_USER: llmproxy
      POSTGRES_PASSWORD: dbpassword9090
    ports: ["5433:5432"]

2. config.yaml — one free (cost 0) and one paid model:
model_list:
  - model_name: free-model
    litellm_params:
      model: openai/x
      api_base: http://127.0.0.1:9/v1   # unreachable on purpose
      api_key: x
      input_cost_per_token: 0
      output_cost_per_token: 0
  - model_name: paid-model
    litellm_params:
      model: openai/y
      api_base: http://127.0.0.1:9/v1
      api_key: y
      input_cost_per_token: 0.00001
      output_cost_per_token: 0.00001
general_settings:
  master_key: sk-1234
  
3. Start it
docker compose up -d

4. Create an internal user with a small budget, and grab its key
curl -s -X POST localhost:4000/user/new \
  -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" \
  -d '{"user_id":"u@test","user_role":"internal_user","max_budget":0.01,"budget_duration":"30d"}'
# -> copy the "key" (sk-...) from the response

5. Push that user OVER budget, then clear the cache
docker compose exec db psql -U llmproxy -d litellm \
  -c "UPDATE \"LiteLLM_UserTable\" SET spend=1 WHERE user_id='u@test';"
docker compose restart litellm   # clears the in-memory user cache

6. Call the FREE model with the user's key
curl -s localhost:4000/v1/chat/completions \
  -H "Authorization: Bearer <USER_KEY_FROM_STEP_4>" -H "Content-Type: application/json" \
  -d '{"model":"free-model","messages":[{"role":"user","content":"hi"}]}'

Observed: 429 {"error":{"message":"Max budget limit reached."}} — the zero-cost model is blocked.
Expected: the free model is allowed (it adds nothing to spend); only paid-model should return a budget error.

Relevant log output

  # FREE (zero-cost) model — over-budget internal user  ->  WRONGLY BLOCKED
  HTTP 429
  {"error":{"message":"Max budget limit reached.","type":"rate_limit_error","code":"429"}}

  # PAID model — over-budget internal user  ->  correctly blocked (different message/path)
  HTTP 429
  {"error":{"message":"ExceededBudget: User=u@test over budget. Spend=1.0, Budget=0.01","type":"budget_exceeded"}}

What part of LiteLLM is this about?

Proxy

What LiteLLM version are you on ?

v1.89.0 (also reproduces on current main; NOT present on v1.81.12)

Twitter / LinkedIn details

No response

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING