Repository navigation
fix(register_model): keep bedrock geo deployments priced across price data reloads - #41523
devin-ai-integration[bot] wants to merge 8 commits into
Conversation
… data reloads A request-time pricing registration inserting into litellm.model_cost while a price data reload walked it could abort the reload (dictionary changed size during iteration in add_known_models), or trip the lookup map rebuild inside get_model_info so the deployment replay fell through to the unmapped path and wrote a raw bedrock/au.anthropic.<model> entry with no token pricing, a $0 shadow re-rolled on every later reload. Walk snapshots of the map, bind the case-insensitive lookup map locally, and let the unmapped path inherit the whole built-in entry of a provider get_model_info would accept.
🤖 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:
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
…lt-in pricing An unmapped alias that inherits its built-in entry now takes only the token prices the catalogue really carries, so a synthesized zero never lands next to the caller's custom price. The router regression test reports the writer's failures instead of swallowing them, and the local names carry Final.
|
Second customer report of this race on v1.100.0 (Pylon #8731): custom priced azure_ai deployment logged $0 spend with full token counts for days. Details on LIT-5853 |
…efix when the value names none
…m_fix_model_cost_reload_race # Conflicts: # tests/test_litellm/test_router_model_cost_isolation.py
|
bugbot run |
…efore its region sibling
|
bugbot run |
…t_reload_race # Conflicts: # tests/test_litellm/test_utils.py
|
bugbot run |
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
|
bugbot run |
…m_fix_model_cost_reload_race # Conflicts: # tests/test_litellm/test_utils.py
|
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 4de7bf4. Configure here.
TLDR
Problem this solves:
How it solves it:
User Flow
Before: a proxy admin running bedrock AU geo Claude deployments on several pods sees single models lose their price on single pods after a price data reload, and those pods bill them at $0
bedrock/au.anthropic.claude-sonnet-5and the other AU geo Claude models as deployments on every pod, next to custom-priced deployments that take steady trafficbedrock/moonshotai.kimi-k2.5tooau.anthropic.claude-sonnet-5alone while kimi keeps its price, and GET https://litellm-domain/model/info on that pod returnsinput_cost_per_token: 0for it"model": "claude-sonnet-5", get a 200 with real token usage, and GET https://litellm-domain/spend/logs?request_id= showsspend: 0.0, so the key's budget never movesau.anthropic.claude-opus-5on one refresh andau.anthropic.claude-sonnet-5on another, while kimi stays priced on every podAfter: the same reloads under the same traffic keep every AU geo model priced on every pod
bedrock/au.anthropic.claude-sonnet-5and the other AU geo Claude models as deployments on every pod, next to custom-priced deployments that take steady trafficbedrock/moonshotai.kimi-k2.5tooinput_cost_per_token: 2.2e-06andoutput_cost_per_token: 1.1e-05forau.anthropic.claude-sonnet-5after every reloadspend: 0.0002486, so the key's budget keeps countingRelevant issues
Related: #30044 (the unmapped-key fallback this generalizes) and #30198 (the synthesized zero costs this keeps out of the map)
Affected release
Linear ticket
Resolves LIT-5853
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
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
Verdict: FAIL. The before leg at the merge base 1dd4c13 already passes every step the User Flow predicts to fail, and the after leg at the tip 4de7bf4 (merge base d42f448) passes the same way, so this run does not prove the PR changes anything an end user can see. The mechanism the PR closes only shows in-process (last subsection), which is not end-user proof
Both legs ran the same topology: two proxy processes per leg (
--num_workers 2each, so four workers) on random ports sharing one private Postgres per leg,LITELLM_LOCAL_MODEL_COST_MAP=falsewithLITELLM_MODEL_COST_MAP_URLpointing at a local copy of the remote price map so every reload fetches and parses the full 4112-model catalog, the customer's five AU geo Claude deployments plusbedrock/moonshotai.kimi-k2.5and 150 more AU geo deployments, and four custom-pricedopenai/gpt-5-nanodeployments taking 400 concurrent real clients for the whole run. Every reload, sample, request and spend row hit the live proxies over HTTP with real Bedrock and OpenAI calls. The before leg ran on ports 36211 and 56575 with Postgres on port 60137, the after leg at 4de7bf4 on ports 37941 and 33035 with Postgres on port 30491Each pod boots as
python litellm/proxy/proxy_cli.py --config config.yaml --port <port> --host 127.0.0.1 --num_workers 2from the leg's worktree. The reload storm is eight loops ofcurl -X POST http://127.0.0.1:<port>/reload/model_cost_map -H "Authorization: Bearer $KEY"(four per pod, 200 clicks each, 1,600 in total). The sampler runs four parallelcurl http://127.0.0.1:<port>/model/info -H "Authorization: Bearer $KEY"per pod every second and counts AU rows whoseinput_cost_per_tokenis 0. The traffic is 400 async clients alternatingPOST /v1/chat/completionsacross both pods with"max_tokens": 16. The post-storm probe sends one real request per unified endpoint on each pod and reads backGET /spend/logs?request_id=<id>Before (1dd4c13)
The merge base moved to d42f448 when main was merged into the branch. Its delta since 1dd4c13 leaves the registration and reload code alone (
litellm/utils.pyonly gains thesupports_mid_conversation_systemhelper andlitellm/__init__.pyonly drops theuse_legacy_interactions_schemaflag), so this leg stands as runReload storm under traffic
zsh storm2.sh 36211 56575 $LEG 4 200withpython3 traffic2.py 36211 56575 400 $LEG/stop_trafficrunningModels table sampled on both pods during the storm
zsh sampler2.sh 36211 56575 $LEGrunning for the whole storm, then the Admin UI Models page of pod B after it/v1/chat/completions after the storm
zsh probe.sh 36211 claude-sonnet-5 before-poststorm-podAand the same on 56575 (full output)/v1/responses after the storm
/v1/messages after the storm
After (4de7bf4)
Reload storm under traffic
zsh storm2.sh 37941 33035 $LEG 4 200withpython traffic2.py 37941 33035 400 $LEG/stop_trafficrunningModels table sampled on both pods during the storm
zsh sampler2.sh 37941 33035 $LEGrunning for the whole storm, then the Admin UI Models page of pod B after it/v1/chat/completions after the storm
zsh probe.sh 37941 claude-sonnet-5 poststorm-Aand the same on 33035 (full output)/v1/responses after the storm
/v1/messages after the storm
Earlier live passes, all priced throughout
Where the symptom does reproduce: in-process only, not end-user proof
The same harness against the same code, minus the proxy: a real
Routerwith the customer's six bedrock deployments plus custom-pricedopenai/gpt-5-nanoones, writer threads registering the custom price the way a request does, and reloads swapping in the fetched 4112-model map. Both scripts run withPYTHONPATHat the worktree and the same arguments on both sidesThe same runs at the earlier bases and tips came out the same way: 522 aborted reloads and 3 models at $0 against 0 at 7ffe97a (base, tip), and 4 async aborts against 0 at 659475d (base, tip). In a live proxy the reload runs on the event loop with the fetch and parse ahead of the swap, and 8,153 clicks across the eight logged passes, 6,400 of them under 400 concurrent clients, never landed a registration inside the walk, so the live legs cannot tell the two commits apart
Here's the User Flow that happened instead:
openai/gpt-5-nanodeployments taking 400 concurrent clients"model": "claude-sonnet-5"on each pod return 200, and GET /spend/logs?request_id= showsspend: 0.0002486for 13+20 tokens (0.0001386 for 13+10) on both commits0from /model/info and aspend: 0.0row on the merge base, none appearedObservations from the legs:
azure_ai/claude-sonnet-4-5-20250929alias loses the cache price its shared catalog entry inherited, while its own deployment bills the same on both sides: PR causes, Decision 3, Medium caveatbedrockon the merge base (base); the tip keepsbedrock_converse(tip)openai/ft:...row never reaches pod B's /model/info: pre-existing, both legs alikeType
🐛 Bug Fix
Caveats (if any)
High
Medium
azure_ai/claude-sonnet-4-5-20250929with custom input and output prices inherits Anthropic's 3e-07 cache-read price into the backend-name entry on the merge base via fix(register_model): preserve built-in cache pricing when registering custom overrides under unmapped keys #30044 (1dd4c13815, d42f448e41); the tip registers it withcache_read_input_token_cost: nulland warns instead (4de7bf4ee0)cache_read_input_token_costandcache_creation_input_token_costinmodel_infoLow
test_generic_guardrail_api.py::TestApiKeyHeader::test_x_api_key_header_sent_in_requestnever finishing, the same never-finished test in main's own timed-out runs today (tracked separately); a plain re-run passed it in 5.5 minutesFinal Attestation
Decisions
tuple(d.items())andtuple(d)are atomic snapshots under the GIL._handle_new_key_with_scankeeps its live walk because nothing in production calls itazure/claude-sonnet-4-6no longer picks up Anthropic's cache price and gets the "custom pricing without cache fields" warning instead. A value that names no provider is matched under its key's own provider prefix, soazure/claude-sonnet-4-6registered without a provider never inherits Anthropic's entry (at e5ff6eb it did,litellm_providerincluded, and its own lookup then raised), while anopenai/ft:...key now inherits its base model's entry the way a direct lookup resolves it. The key itself is the first candidate, so a catalog row registered under its own key (au.anthropic.claude-sonnet-5from a configmodel_info) inherits from itself when the lookup trips, instead of from its region-stripped sibling, whose base-rate prices overwrote the geo rate at 7dbd7c0Note
Medium Risk
Changes core model-cost registration, inheritance, and concurrent reload paths that directly drive spend tracking and budget enforcement; behavior shifts for cross-provider custom pricing (cache fields no longer auto-inherited).
Overview
Fixes $0 pricing and aborted Reload Price Data when request threads register into
litellm.model_costwhile the catalog is walked or the case-insensitive lookup map is rebuilt.Concurrent reload safety: Provider-set population, lowercase-map rebuilds, and related walks now iterate
tuple(...)snapshots so mid-walk inserts no longer raise dictionary changed size during iteration. Case-insensitive key resolution binds the lookup map locally so another thread’s invalidation cannot leave a lookup readingNone.Smarter built-in inheritance in
register_model: Whenget_model_infofails or a key is unmapped, registration seeds from the matching catalog row’s full pricing (not only cache fields), with_check_provider_matchso a Bedrock alias cannot pick up OpenAI prices. The registered key is tried first so geo catalog rows keep their own geo rates instead of a region-stripped sibling._without_synthesized_costsstrips helper-synthesized zero token costs so re-registration does not persist “free” models or break budget enforcement (#30198).Regression coverage adds deterministic mid-walk tests and a bounded router reload race for Bedrock geo deployments.
Reviewed by Cursor Bugbot for commit 659475d. Bugbot is set up for automated code reviews on this repo. Configure here.