feat(proxy): add admin toggle to block requests for models without pricing - #35181
Conversation
…icing Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Greptile SummaryAdds an opt-in, DB-persisted setting that blocks requests for model groups without recognized pricing.
Confidence Score: 5/5The PR appears safe to merge No blocking failure remains
|
| 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 Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
|
QA: live proxy, real provider APIs, no mocks Tested against a live LiteLLM proxy on
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: Playground request blocked with 403 while the toggle is ON: Same request succeeds once the toggle is OFF: |
…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
|
|
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>
…evin.ai/proxy/github.com/BerriAI/litellm into litellm_block_unpriced_models
…rriAI/litellm into litellm_block_unpriced_models # Conflicts: # litellm/proxy/auth/auth_checks.py # tests/test_litellm/proxy/auth/test_auth_checks.py
…itellm_block_unpriced_models
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.
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.
…db_objects Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
| 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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
config_overrides gates the vault table, not litellm_settings. Startup already applies these DB values ungated, so the periodic reload just matches it
There was a problem hiding this comment.
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.
|
bugbot run |
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
1 similar comment
|
bugbot run |
There was a problem hiding this comment.
✅ 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.
TLDR
Problem this solves:
How it solves it:
/chat/completions,/responses,/messages, and/embeddingsall refuse the same unpriced model; management routes are untouchedUser 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
{"model": "team-onprem-llm", ...}where that model has no entry in the cost map, and gets 200 with a normal completionAfter: once the admin flips the new toggle, the same request is refused with a 403 naming the model
{"model": "team-onprem-llm", ...}and gets 403 with error typemodel_cost_map_missingand a message namingteam-onprem-llmand explaining its pricing is missingteam-onprem-llm, either in the cost map or as aninput_cost_per_token/output_cost_per_tokenoverride on that deployment, and the developer's next identical request returns 200 again with spend litellm can actually priceRelevant issues
Linear ticket
Resolves LIT-4984
Pre-Submission checklist
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@greptileaito 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 pytestteam-onprem-llmpoints atopenrouter/moonshotai/kimi-k2-0905, which OpenRouter serves today and which has no entry inmodel_prices_and_context_window.json, so it is genuinely unpriced and genuinely callable.priced-openai-llmpoints atopenai/gpt-4o-mini, which the cost map pricesBefore, 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
GET /model/infofor that group at the same commit reports"input_cost_per_token": 0,"output_cost_per_token": 0, and every other cost fieldnull, which is exactly the state this PR refuses to bill againstAfter, at the PR tip
c73480c653(proxy on 127.0.0.1:47831)Step 0, the toggle exists and defaults to off:
Step 1, with it off the unpriced model still answers, same as before the PR:
Step 2 and 3, the admin turns it on through the endpoint the Cost Tracking page calls, and it reads back on:
Step 4, the identical request from step 1 is now refused, and the message names the model:
Step 5, priced groups keep working with the toggle on, so the block is narrow:
Step 6, a request naming several models is blocked when any one of them is unpriced, and books no spend:
Steps 7 to 10, aliases and groups priced only through their
model_infoblock are still allowed with the toggle on: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:
df00c334c73480c653team-onprem-llm,priced-openai-llmmodelinfo-aliasteam-onprem-llm-modelinfo-pricedThe 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-fixmodelinfo-alias403 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 detectionType
🆕 New Feature
Caveats (if any)
STORE_MODEL_IN_DB=Truecodestral/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. Declaringinput_cost_per_token: 0in that model's litellm_params keeps it allowedLITELLM_SETTINGS_SAFE_DB_OVERRIDESkey 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 restartsFinal 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_pricingflag (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_checksreturns 403 withmodel_cost_map_missing. A group is treated as priced if any deployment declares acost_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_pricingpersist the flag inlitellm_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.