Skip to content

feat(proxy): add admin toggle to block requests for models without pricing - #35181

Merged
mateo-berri merged 16 commits into
litellm_internal_stagingfrom
litellm_block_unpriced_models
Aug 21, 2026
Merged

feat(proxy): add admin toggle to block requests for models without pricing#35181
mateo-berri merged 16 commits into
litellm_internal_stagingfrom
litellm_block_unpriced_models

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • Requests for models with no cost mapping get no pricing from litellm, so their spend reads $0 unless the provider happens to return a cost of its own
  • Under-billing hides misconfiguration until someone reconciles spend by hand

How it solves it:

  • New admin toggle, off by default, rejects unpriced-model requests with 403
  • It covers every LLM API route, so /chat/completions, /responses, /messages, and /embeddings all refuse the same unpriced model; management routes are untouched
  • The 403 names every unpriced model the request asked for and says pricing is missing
  • A group counts as priced when any deployment prices any billed metric (tokens, characters, seconds, pages, images) or carries tiered pricing, so non-token-billed models stay allowed
  • Explicitly configured prices, including zero, still count as priced
  • Toggle persists to litellm_settings; peer workers apply it on config reload
  • Admins flip "Block Unpriced Models" on the Cost Tracking settings page

User Flow

Before: a request for a model missing from the cost map sails through and litellm prices it at $0, and no setting exists to refuse it

  1. A developer sends POST https://litellm-domain/v1/chat/completions with {"model": "team-onprem-llm", ...} where that model has no entry in the cost map, and gets 200 with a normal completion
  2. The proxy admin opens https://litellm-domain/ui/?page=logs and sees that request logged at $0.00, since litellm has no price to apply and most providers return no cost of their own
  3. The admin opens https://litellm-domain/ui/?page=cost-tracking and finds no way to block unpriced models, so every later request repeats steps 1 and 2

After: once the admin flips the new toggle, the same request is refused with a 403 naming the model

  1. The proxy admin opens https://litellm-domain/ui/?page=cost-tracking, expands "Block Unpriced Models", and turns the switch on; a toast confirms unpriced-model requests will now be blocked
  2. The developer sends the same POST https://litellm-domain/v1/chat/completions with {"model": "team-onprem-llm", ...} and gets 403 with error type model_cost_map_missing and a message naming team-onprem-llm and explaining its pricing is missing
  3. The admin adds pricing for team-onprem-llm, either in the cost map or as an input_cost_per_token/output_cost_per_token override on that deployment, and the developer's next identical request returns 200 again with spend litellm can actually price

Relevant issues

Linear ticket

Resolves LIT-4984

Pre-Submission checklist

  • I have added meaningful tests (auth check: toggle off allows, toggle on blocks with 403 naming the model, priced model allowed, non-LLM route ignored, alias resolving to an unpriced group blocked, a request naming several models blocked when any one of them is unpriced and allowed when all of them are priced, an alias over a group priced through its model_info block allowed, models priced per character, page, image, via litellm_params override or tiered pricing allowed; settings endpoints: GET reflects the flag, PATCH persists and updates it, PATCH requires STORE_MODEL_IN_DB, peer worker applies the persisted value on config reload)
  • 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

Two live proxies, both booted from a real checkout against a real Postgres with STORE_MODEL_IN_DB=True, both calling real provider APIs and spending real money. No mocks, no stubs, no pytest

team-onprem-llm points at openrouter/moonshotai/kimi-k2-0905, which OpenRouter serves today and which has no entry in model_prices_and_context_window.json, so it is genuinely unpriced and genuinely callable. priced-openai-llm points at openai/gpt-4o-mini, which the cost map prices

Before, at the merge base 996693f1eb (proxy on 127.0.0.1:25070)

The setting does not exist and the unpriced model answers normally, so there is nothing an admin can do about it

$ curl -sS -w '\nHTTP_STATUS:%{http_code}\n' -X GET \
  'http://127.0.0.1:25070/config/block_requests_for_models_without_pricing' \
  -H "Authorization: Bearer $LITELLM_KEY"
{"detail":"Not Found"}
HTTP_STATUS:404

$ curl -sS -w '\nHTTP_STATUS:%{http_code}\n' -X PATCH \
  'http://127.0.0.1:25070/config/block_requests_for_models_without_pricing' \
  -H "Authorization: Bearer $LITELLM_KEY" -H 'Content-Type: application/json' \
  -d '{"enabled":true}'
{"detail":"Not Found"}
HTTP_STATUS:404

$ curl -sS -w '\nHTTP_STATUS:%{http_code}\n' -X POST \
  'http://127.0.0.1:25070/v1/chat/completions' \
  -H "Authorization: Bearer $LITELLM_KEY" -H 'Content-Type: application/json' \
  -d '{"model":"team-onprem-llm","messages":[{"role":"user","content":"Reply with exactly: pong"}]}'
{"id":"gen-1787266870-Qg636PUs3nhpBbaGrPZf","model":"team-onprem-llm","object":"chat.completion",
 "choices":[{"finish_reason":"stop","index":0,"message":{"content":"pong","role":"assistant"}}],
 "usage":{"completion_tokens":3,"prompt_tokens":14,"total_tokens":17}}
HTTP_STATUS:200

GET /model/info for that group at the same commit reports "input_cost_per_token": 0, "output_cost_per_token": 0, and every other cost field null, which is exactly the state this PR refuses to bill against

After, at the PR tip c73480c653 (proxy on 127.0.0.1:47831)

Step 0, the toggle exists and defaults to off:

$ curl -isS -X GET "$PROXY/config/block_requests_for_models_without_pricing" \
  -H "Authorization: Bearer $LITELLM_KEY"
HTTP/1.1 200 OK
{"enabled":false}

Step 1, with it off the unpriced model still answers, same as before the PR:

$ curl -isS -X POST "$PROXY/v1/chat/completions" \
  -H "Authorization: Bearer $LITELLM_KEY" -H 'Content-Type: application/json' \
  -d '{"model":"team-onprem-llm","messages":[{"role":"user","content":"Reply with exactly: OK"}],"max_tokens":16}'
HTTP/1.1 200 OK
x-litellm-model-name: openrouter/moonshotai/kimi-k2-0905
{"id":"gen-1787269183-btisPNuevjEUYBjxtLyc","model":"team-onprem-llm",
 "choices":[{"finish_reason":"stop","index":0,"message":{"content":"OK","role":"assistant"}}],
 "usage":{"completion_tokens":2,"prompt_tokens":14,"total_tokens":16}}

Step 2 and 3, the admin turns it on through the endpoint the Cost Tracking page calls, and it reads back on:

$ curl -isS -X PATCH "$PROXY/config/block_requests_for_models_without_pricing" \
  -H "Authorization: Bearer $LITELLM_KEY" -H 'Content-Type: application/json' \
  -d '{"enabled": true}'
HTTP/1.1 200 OK
{"enabled":true}

$ curl -isS -X GET "$PROXY/config/block_requests_for_models_without_pricing" \
  -H "Authorization: Bearer $LITELLM_KEY"
HTTP/1.1 200 OK
{"enabled":true}

Step 4, the identical request from step 1 is now refused, and the message names the model:

$ curl -isS -X POST "$PROXY/v1/chat/completions" \
  -H "Authorization: Bearer $LITELLM_KEY" -H 'Content-Type: application/json' \
  -d '{"model":"team-onprem-llm","messages":[{"role":"user","content":"Reply with exactly: OK"}],"max_tokens":16}'
HTTP/1.1 403 Forbidden
{"error":{"message":"Model 'team-onprem-llm' has no pricing in the cost map, so litellm cannot price the request. Requests for unpriced models are blocked because 'block_requests_for_models_without_pricing' is enabled. Add pricing (input_cost_per_token/output_cost_per_token) to allow the request.","type":"model_cost_map_missing","param":"model","code":"403"}}

Step 5, priced groups keep working with the toggle on, so the block is narrow:

priced-openai-llm       openai/gpt-4o-mini, priced by the cost map        HTTP/1.1 200 OK
team-onprem-llm-priced  same unpriced model, explicit per-token prices    HTTP/1.1 200 OK

Step 6, a request naming several models is blocked when any one of them is unpriced, and books no spend:

$ curl -sS "$PROXY/spend/logs" -H "Authorization: Bearer $LITELLM_KEY" | jq ...
spend_log_rows=3 total_spend=1.58e-05

$ curl -isS -X POST "$PROXY/v1/chat/completions" \
  -H "Authorization: Bearer $LITELLM_KEY" -H 'Content-Type: application/json' \
  -d '{"model":"team-onprem-llm,priced-openai-llm","messages":[{"role":"user","content":"Reply with exactly: OK"}],"max_tokens":16}'
HTTP/1.1 403 Forbidden
{"error":{"message":"Model 'team-onprem-llm' has no pricing in the cost map, ...","type":"model_cost_map_missing","param":"model","code":"403"}}

$ curl -sS "$PROXY/spend/logs" -H "Authorization: Bearer $LITELLM_KEY" | jq ...
spend_log_rows=3 total_spend=1.58e-05

Steps 7 to 10, aliases and groups priced only through their model_info block are still allowed with the toggle on:

priced-alias                      -> priced-openai-llm                HTTP/1.1 200 OK
zero-priced-alias                 -> explicitly zero-priced group     HTTP/1.1 200 OK
team-onprem-llm-modelinfo-priced  priced via model_info only          HTTP/1.1 200 OK
modelinfo-alias                   -> that model_info-priced group     HTTP/1.1 200 OK

The same run against the pre-fix commit, to show the last two fixes were real

Same proxy setup, same database, same config, only the commit differs:

Request, toggle on at df00c334 at c73480c653
team-onprem-llm,priced-openai-llm 200, request billed 403, spend unchanged
modelinfo-alias 403, wrongly blocked 200
team-onprem-llm-modelinfo-priced 200 200

The pre-fix multi-model call reached the provider and billed cost: 1.34e-05, so it was spending money straight through the gate the toggle is supposed to close. The pre-fix modelinfo-alias 403 is the false block, and the group it aliases returned 200 in that same run, which pins the failure to alias resolution rather than to pricing detection

Type

🆕 New Feature

Caveats (if any)

  • UI toggle persistence needs STORE_MODEL_IN_DB=True
  • Peer workers pick the change up on their next config reload
  • A model group with an explicitly configured zero price stays allowed
  • A model the built-in cost map prices at exactly zero, such as codestral/codestral-2405, still counts as unpriced, since a deployment litellm cannot price and one it prices at zero look the same by the time the router has registered it. Declaring input_cost_per_token: 0 in that model's litellm_params keeps it allowed
  • The periodic DB sync now applies every LITELLM_SETTINGS_SAFE_DB_OVERRIDES key the database holds, not only the new toggle. That matches what the startup config load has always done at proxy_server.py:6552, so a worker's live settings no longer drift from a peer's between restarts

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

  • c73480c passes /live-pr-risk

Link to Devin session: https://app.devin.ai/sessions/630f4ee5757e4211b2287850b12c09fa
Requested by: @mateo-berri


Note

Medium Risk
Touches proxy auth and can 403 production LLM traffic when enabled. Also applies DB-backed litellm_settings onto live workers, so a bad or unexpected override could change request behavior cluster-wide.

Overview
Adds an opt-in block_requests_for_models_without_pricing flag (default off) so admins can reject LLM requests whose resolved model has no cost mapping instead of logging them as $0 spend.

When enabled, common_checks returns 403 with model_cost_map_missing. A group is treated as priced if any deployment declares a cost_per* field (including explicit zero), tiered_pricing, or a positive billed metric (tokens, seconds, pages, images, etc.). Non-LLM routes are skipped.

GET/PATCH /config/block_requests_for_models_without_pricing persist the flag in litellm_settings. Peer workers pick it up via a new safe-overrides apply on the periodic DB sync. Cost Tracking UI adds a proxy-admin switch for the same setting.

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

…icing

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

Comment thread litellm/proxy/auth/auth_checks.py
Comment thread litellm/proxy/management_endpoints/cost_tracking_settings.py
Comment thread tests/test_litellm/proxy/auth/test_auth_checks.py
@greptile-apps

greptile-apps Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds an opt-in, DB-persisted setting that blocks requests for model groups without recognized pricing.

  • Detects token, non-token, tiered, explicitly configured zero, alias-resolved, and model-info pricing
  • Returns a structured 403 response when blocking is enabled and an unpriced model is requested
  • Synchronizes the persisted setting across workers during periodic config reconciliation
  • Adds an admin dashboard toggle, generated API types, and regression coverage

Confidence Score: 5/5

The PR appears safe to merge

No blocking failure remains

Important Files Changed

Filename Overview
litellm/proxy/auth/auth_checks.py Adds alias-aware pricing detection and opt-in enforcement in centralized request checks; the previously reported pricing cases are handled
litellm/proxy/proxy_server.py Applies allowlisted persisted LiteLLM settings during every periodic reconciliation, resolving the reported peer-worker inconsistency
litellm/proxy/management_endpoints/cost_tracking_settings.py Adds authenticated GET and PATCH endpoints for reading and persisting the global toggle
tests/test_litellm/proxy/auth/test_auth_checks.py Covers priced and unpriced groups, aliases, non-token metrics, tiered pricing, explicit zero pricing, and route enforcement while retaining project-alias coverage
tests/test_litellm/proxy/management_endpoints/test_cost_tracking_settings.py Covers endpoint persistence, in-memory updates, configuration requirements, and peer-worker reconciliation under both override-gate states
ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/cost_tracking_settings.tsx Adds the proxy-admin-only control for the new setting to Cost Tracking
ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/use_block_unpriced_config.ts Adds dashboard state, API integration, loading behavior, and success or failure notifications for the toggle

Reviews (8): Last reviewed commit: "fix(proxy): block every unpriced model a..." | Re-trigger Greptile

@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 70.83333% with 35 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...oxy/management_endpoints/cost_tracking_settings.py 56.41% 17 Missing ⚠️
litellm/proxy/auth/auth_checks.py 79.71% 14 Missing ⚠️
litellm/proxy/proxy_server.py 50.00% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_block_unpriced_models (c73480c) with litellm_internal_staging (e07a712)1

Open in CodSpeed

Footnotes

  1. No successful run was found on litellm_internal_staging (b2aff8b) during the generation of this report, so e07a712 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

QA: live proxy, real provider APIs, no mocks

Tested against a live LiteLLM proxy on localhost:4000 with LITELLM_LOCAL_MODEL_COST_MAP=True and STORE_MODEL_IN_DB=True, hitting real Fireworks and OpenAI. All scenarios pass

  • Flag OFF (default): request to unpriced unpriced-fw returns 200 with a real completion, no behavior change
  • PATCH /config/block_requests_for_models_without_pricing {"enabled": true} returns {"enabled": true}; the same request now returns 403 model_cost_map_missing naming the model
  • Flag ON: priced gpt-4o-mini still returns 200, so priced traffic is not over-blocked
  • Flag ON: alias unpriced-alias returns 403, confirming resolution through get_model_group_info
  • Admin UI toggle: defaults OFF, flips ON with a success notification, persists across reload (GET returns true), and gates live Playground traffic (403 while ON, 200 while OFF)

Raw 403 body:

{"error":{"message":"Model 'unpriced-fw' has no pricing in the cost map, so its spend would be tracked as $0. Requests for unpriced models are blocked because 'block_requests_for_models_without_pricing' is enabled. Add pricing for this model (input_cost_per_token/output_cost_per_token) to allow it.","type":"model_cost_map_missing","param":"model","code":"403"}}

Admin UI toggle gating live traffic end to end:

Block Unpriced Models toggle gating Playground traffic

Playground request blocked with 403 while the toggle is ON:

Playground 403 while toggle ON

Same request succeeds once the toggle is OFF:

Playground success while toggle OFF

mateo-berri and others added 3 commits July 31, 2026 03:02
…ced-model toggle across workers

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…itellm_block_unpriced_models

# Conflicts:
#	litellm/proxy/auth/auth_checks.py
#	tests/test_litellm/proxy/auth/test_auth_checks.py
#	tests/test_litellm/proxy/management_endpoints/test_cost_tracking_settings.py
#	ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/cost_tracking_settings.tsx
#	ui/litellm-dashboard/src/lib/http/schema.d.ts
@CLAassistant

Copy link
Copy Markdown

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

✅ mateo-berri
❌ web-flow
You have signed the CLA already but the status is still pending? Let us recheck it.

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

Comment thread litellm/proxy/auth/auth_checks.py
mateo-berri and others added 4 commits August 20, 2026 20:59
A deployment that overrides any cost_per field, including at zero, now counts as priced so it is not blocked as unpriced

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…rriAI/litellm into litellm_block_unpriced_models

# Conflicts:
#	litellm/proxy/auth/auth_checks.py
#	tests/test_litellm/proxy/auth/test_auth_checks.py
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

Comment thread litellm/proxy/auth/auth_checks.py
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

mateo-berri and others added 3 commits August 20, 2026 21:55
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…s config reload

Peer workers previously kept their startup value for block_requests_for_models_without_pricing
until a restart, so a toggle from the UI only took effect on the worker that served the request.
The eager f-strings tripped tests/test_litellm/test_logging.py::test_logging_calls_do_not_build_their_message_eagerly.
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

Comment thread litellm/proxy/proxy_server.py Outdated
The hook swallowed errors into the console, so an admin flipping the switch without
STORE_MODEL_IN_DB saw nothing happen and got no reason why. Adds the missing hook tests.
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

…db_objects

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

if self._should_load_db_object(object_type="config_overrides"):
await self._init_hashicorp_vault_config_override(prisma_client=prisma_client)

await self._apply_safe_litellm_settings_overrides_from_db(prisma_client=prisma_client)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Disabled config overrides still apply

When config_overrides is excluded, this unconditional reload applies every allowlisted database value, overriding unrelated YAML settings despite configuration override loading being disabled

Knowledge Base Used: Proxy Server Request Flow

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.

config_overrides gates the vault table, not litellm_settings. Startup already applies these DB values ungated, so the periodic reload just matches it

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.

You're right — the original comment conflated two distinct loading mechanisms. Looking at the diff:

if self._should_load_db_object(object_type="config_overrides"):
    await self._init_hashicorp_vault_config_override(prisma_client=prisma_client)

await self._apply_safe_litellm_settings_overrides_from_db(prisma_client=prisma_client)

The config_overrides gate is specifically for the Hashicorp Vault table (_init_hashicorp_vault_config_override), not for litellm_settings. Since startup already applies litellm_settings values from the DB unconditionally (outside that gate), having the periodic reload do the same is correct and consistent — not a bypass of any intentional guard. I withdraw the comment.

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

Comment thread litellm/proxy/auth/auth_checks.py
Comment thread litellm/proxy/auth/auth_checks.py Outdated
A request can name more than one model, through a comma-separated model or target_model_names on
the batch and fine-tuning routes, and the gate only looked at the string case, so an unpriced model
riding alongside a priced one went through and billed. Check every candidate and name the unpriced
ones in the 403

Aliases had the same problem on the other side: a group that prices itself through its model_info
block lands in the cost map under its deployment id, and the explicit-cost check walked the raw
model list by group name, so an alias pointing at that group read as unpriced. Resolve the group
through the router the way the pricing check already does

Also correct the 403 copy. Providers that return their own usage cost still bill for these models,
so the accurate claim is that litellm has no pricing of its own for them
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

1 similar comment
@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 c73480c. 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 02e67cd into litellm_internal_staging Aug 21, 2026
76 checks passed
@mateo-berri
mateo-berri deleted the litellm_block_unpriced_models branch August 21, 2026 00:32
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.

3 participants