Repository navigation
refactor(proxy): make the config file win over the database - #41779
Merged
Merged
Conversation
Contributor
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Contributor
|
Contributor
Author
|
@greptileai review |
The precedence used to vary per key: some keys let a stored row win, some let the file win, some merged the two. That meant an operator could not answer "which value is live?" without knowing the key. Now file presence decides ownership. A key the config file declares is config-owned, whatever the database holds, and a key the file omits falls back to the stored row. KeyRule no longer carries a RuleKind, only which row the stored value lives in. Writes to a config-owned key are refused at the two surfaces that reach the database instead of being stored and silently ignored: save_config and /config/field/update both 400 naming the key and the config file path. Both read endpoints now report source and editable off the same SettingsStore, so /config/field/info and /config/list can no longer disagree inside one process. Replaces the 786-case checked-in JSON fixture with cases generated from the rule table, so the matrix tests no longer assert that resolve() agrees with a snapshot of resolve(). BREAKING CHANGE: a dashboard or /config/field/update write to a setting the config file declares now returns 400 instead of being stored. Remove the key from the config file to let the database own it.
… the store Both write paths now go through the same refusal, so /config/field/update and /config/update answer identically instead of each phrasing its own rule. A successful write now applies to the SettingsStore, so the next read sees it. Without this, /config/field/info reported a key the dashboard had just stored as "not set" until the process reloaded from the database. resolve() no longer takes a KeyRule it never reads; the store picks the row. The matrix tests resolve through SettingsStore instead of calling resolve directly, so the section and key in each case actually route a lookup. ConfigFieldInfo and ConfigList type `source` as the FieldSource literal, and the dashboard API types are regenerated for the two new fields.
The two dashboard toggles under litellm_settings wrote through save_config, so the refusal applied, but they mutated the litellm module global first: a refused write still took effect in the running process until the next reload. Both now check before they mutate. /config/field/delete drops the stored key without touching the store, so a deleted key kept reading back from the process. It now refreshes the store like the other write paths. /config/list reported source and editable for the general_settings rows but not for the litellm_settings ones, so the dashboard would have shown a config-declared toggle as editable.
yuneng-berri
had a problem deploying
to
e2e-changed
September 18, 2026 08:56 — with
GitHub Actions
Error
…anges The reload only re-registered pass-through endpoints when the stored row still carried the key, so deleting the row left the deleted routes serving traffic until the process restarted. It now compares the resolved list before and after the row is applied and rebuilds on any difference, including a deletion that resolves back to the config file's list or to nothing. This matches what _apply_retention_settings already does with the retention values, so the two reload effects no longer disagree about what counts as a change. The tests assert the proxy's registry of live pass-through routes, which is what decides whether a request is routed upstream or falls through to the auth error, rather than that the registration helper was called.
yuneng-berri
had a problem deploying
to
e2e-changed
September 18, 2026 09:07 — with
GitHub Actions
Error
…spatcher _apply_general_settings_side_effects grew a fourth argument when the reload started comparing the resolved pass-through list, and this dispatch test calls it positionally, so it failed with a TypeError.
yuneng-berri
had a problem deploying
to
e2e-changed
September 18, 2026 09:19 — with
GitHub Actions
Error
… row /config/field/info used to answer from the LiteLLM_Config row, so "the field 400s" proved the row did not carry it. It now answers from the resolved settings, and the CI stack config declares general_settings.max_parallel_requests, so the endpoint returns that value and the old assertion could never hold. The check that /add/allowed_ip writes only what the caller changed moves to /config/list, which still reports stored_in_db off the row, and the field/info call now asserts the ownership the endpoint reports: the config file owns the key, so it reads back as source=config and editable=false. Verified against a live proxy on an isolated Postgres rather than in CI, where this check has never run: it waits on protected-environment approval.
yuneng-berri
requested a deployment
to
e2e-changed
September 18, 2026 09:55 — with
GitHub Actions
Waiting
Contributor
Author
yuneng-berri
enabled auto-merge
September 18, 2026 16:51
yucheng-berri
approved these changes
Sep 18, 2026
7 tasks done
4 of 6 tasks
6 tasks done
This was referenced Sep 19, 2026
yuneng-berri
added a commit
that referenced
this pull request
Oct 1, 2026
Config-wins (#41779) made general_settings.pass_through_endpoints a config-owned key. The DB reader then got the config list back as if it were DB rows, re-registered each entry without forward_headers on every DB sync, and the stripped copy won the route lookup, so a config pass-through with forward_headers: true stopped forwarding Authorization. UI create, update and delete of pass-throughs were also rejected while the config declared any. This puts pass-throughs back on their pre-#41779 path: the settings store no longer lets the config own the key, the config list is captured env-resolved at load_config, each DB sync merges DB entries with config entries on paths the DB does not declare, and /config/field/info reads the stored rows only. A UI pass-through write re-applies that merge immediately so the config entries stay served until the next sync.
4 of 6 tasks
yuneng-berri
added a commit
that referenced
this pull request
Oct 1, 2026
#43962) * fix(proxy): restore pre-config-wins handling of pass-through endpoints Config-wins (#41779) made general_settings.pass_through_endpoints a config-owned key. The DB reader then got the config list back as if it were DB rows, re-registered each entry without forward_headers on every DB sync, and the stripped copy won the route lookup, so a config pass-through with forward_headers: true stopped forwarding Authorization. UI create, update and delete of pass-throughs were also rejected while the config declared any. This puts pass-throughs back on their pre-#41779 path: the settings store no longer lets the config own the key, the config list is captured env-resolved at load_config, each DB sync merges DB entries with config entries on paths the DB does not declare, and /config/field/info reads the stored rows only. A UI pass-through write re-applies that merge immediately so the config entries stay served until the next sync. * fix(proxy): keep config pass-throughs in every reload of the merged list get_config now returns DB pass-throughs plus config ones on other paths, each DB sync republishes that merged list, and /config/field/info reads pass_through_endpoints from the DB row so a UI write never drops stored entries when models are not stored in the DB * fix(proxy): keep serving pass-throughs while the config file reloads load_yaml cleared the runtime pass-through list, so auth: false routes answered 401 while get_config awaited the database * fix(proxy): read stored pass-throughs from the writer before a UI write A lagging read replica could return an older list, and the UI create and edit flows write the whole field back * fix(proxy): apply config file pass-through auth changes on reload The kept runtime list was merged as if it were DB entries, so an edited config entry on the same path was dropped. Merge the stored DB row with the fresh config instead, and give the field-info test mock a writer * fix(proxy): keep pass-throughs served while a DB sync reads the database get_config resets the stored DB rows before reading them again, which cleared the served pass-through list and made auth: false routes answer 401 for the length of the read * refactor(proxy): move the settings store reload out of the loop basedpyright rejects a Final variable assigned inside a loop
4 of 6 tasks
stvnksslr
pushed a commit
to stvnksslr/litellm
that referenced
this pull request
Oct 1, 2026
BerriAI#43962) * fix(proxy): restore pre-config-wins handling of pass-through endpoints Config-wins (BerriAI#41779) made general_settings.pass_through_endpoints a config-owned key. The DB reader then got the config list back as if it were DB rows, re-registered each entry without forward_headers on every DB sync, and the stripped copy won the route lookup, so a config pass-through with forward_headers: true stopped forwarding Authorization. UI create, update and delete of pass-throughs were also rejected while the config declared any. This puts pass-throughs back on their pre-BerriAI#41779 path: the settings store no longer lets the config own the key, the config list is captured env-resolved at load_config, each DB sync merges DB entries with config entries on paths the DB does not declare, and /config/field/info reads the stored rows only. A UI pass-through write re-applies that merge immediately so the config entries stay served until the next sync. * fix(proxy): keep config pass-throughs in every reload of the merged list get_config now returns DB pass-throughs plus config ones on other paths, each DB sync republishes that merged list, and /config/field/info reads pass_through_endpoints from the DB row so a UI write never drops stored entries when models are not stored in the DB * fix(proxy): keep serving pass-throughs while the config file reloads load_yaml cleared the runtime pass-through list, so auth: false routes answered 401 while get_config awaited the database * fix(proxy): read stored pass-throughs from the writer before a UI write A lagging read replica could return an older list, and the UI create and edit flows write the whole field back * fix(proxy): apply config file pass-through auth changes on reload The kept runtime list was merged as if it were DB entries, so an edited config entry on the same path was dropped. Merge the stored DB row with the fresh config instead, and give the field-info test mock a writer * fix(proxy): keep pass-throughs served while a DB sync reads the database get_config resets the stored DB rows before reading them again, which cleared the served pass-through list and made auth: false routes answer 401 for the length of the read * refactor(proxy): move the settings store reload out of the loop basedpyright rejects a Final variable assigned inside a loop (cherry picked from commit 2eb2bf1)
4 of 6 tasks
yuneng-berri
added a commit
that referenced
this pull request
Oct 1, 2026
#43962) (#44054) * fix(proxy): restore pre-config-wins handling of pass-through endpoints Config-wins (#41779) made general_settings.pass_through_endpoints a config-owned key. The DB reader then got the config list back as if it were DB rows, re-registered each entry without forward_headers on every DB sync, and the stripped copy won the route lookup, so a config pass-through with forward_headers: true stopped forwarding Authorization. UI create, update and delete of pass-throughs were also rejected while the config declared any. This puts pass-throughs back on their pre-#41779 path: the settings store no longer lets the config own the key, the config list is captured env-resolved at load_config, each DB sync merges DB entries with config entries on paths the DB does not declare, and /config/field/info reads the stored rows only. A UI pass-through write re-applies that merge immediately so the config entries stay served until the next sync. * fix(proxy): keep config pass-throughs in every reload of the merged list get_config now returns DB pass-throughs plus config ones on other paths, each DB sync republishes that merged list, and /config/field/info reads pass_through_endpoints from the DB row so a UI write never drops stored entries when models are not stored in the DB * fix(proxy): keep serving pass-throughs while the config file reloads load_yaml cleared the runtime pass-through list, so auth: false routes answered 401 while get_config awaited the database * fix(proxy): read stored pass-throughs from the writer before a UI write A lagging read replica could return an older list, and the UI create and edit flows write the whole field back * fix(proxy): apply config file pass-through auth changes on reload The kept runtime list was merged as if it were DB entries, so an edited config entry on the same path was dropped. Merge the stored DB row with the fresh config instead, and give the field-info test mock a writer * fix(proxy): keep pass-throughs served while a DB sync reads the database get_config resets the stored DB rows before reading them again, which cleared the served pass-through list and made auth: false routes answer 401 for the length of the read * refactor(proxy): move the settings store reload out of the loop basedpyright rejects a Final variable assigned inside a loop (cherry picked from commit 2eb2bf1)
yuneng-berri
added a commit
that referenced
this pull request
Oct 2, 2026
…th (#44267) The failure spend-log row from #42695 is written for pass-through routes that run as LLM API routes, which a config route only does with auth: true. The test omitted auth and passed only while config wins (#41779) registered config entries through the typed model, where auth defaults to true. #43962 restored the pre-config-wins registration, so the route lost that status and the row was never written. Set auth: true on the route so the test covers the logging it was written for without depending on that side effect (cherry picked from commit 1d9cd9b)
yuneng-berri
added a commit
that referenced
this pull request
Oct 2, 2026
…th (#44269) The failure spend-log row from #42695 is written for pass-through routes that run as LLM API routes, which a config route only does with auth: true. The test omitted auth and passed only while config wins (#41779) registered config entries through the typed model, where auth defaults to true. #43962 restored the pre-config-wins registration, so the route lost that status and the row was never written. Set auth: true on the route so the test covers the logging it was written for without depending on that side effect (cherry picked from commit 1d9cd9b)
yuneng-berri
added a commit
that referenced
this pull request
Oct 2, 2026
…th (#44265) The failure spend-log row from #42695 is written for pass-through routes that run as LLM API routes, which a config route only does with auth: true. The test omitted auth and passed only while config wins (#41779) registered config entries through the typed model, where auth defaults to true. #43962 restored the pre-config-wins registration, so the route lost that status and the row was never written. Set auth: true on the route so the test covers the logging it was written for without depending on that side effect
This branch is waiting to be deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TLDR
Problem this solves:
/config/field/infoand/config/listdisagreed inside one processHow it solves it:
sourceUser Flow
Before: an admin edits a setting on the Admin UI settings page, the save succeeds, and nothing changes
general_settings.max_parallel_requests: 111After: the same save is refused up front and says exactly what to do
general_settings.max_parallel_requests: 111max_parallel_requests: 999comes back 400: "general_settings key 'max_parallel_requests' is set in the config file and cannot be changed here", naming the file to edit"source": "config"and"editable": falsemax_request_size_mb, still saves from the UI, comes back 200, and reads back with"source": "db"and"editable": trueRelevant issues
Affected release
Linear ticket
Resolves LIT-7803
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
Both runs use the same isolated Postgres and Redis and the same config file:
The
LiteLLM_Configtable is emptied before each run, so both start from the same state. Case 1 is a real call to OpenAI, billed.Before (8fc9c46)
After (0d9c515)
Type
🧹 Refactoring
Caveats (if any)
Severe
authon a config-declared pathHigh
Medium
/delete/allowed_ipleavesallowed_ips: [], which denies every address, and the cleanup call is already locked out. Reproduced on the merge base, so not from this PR, but it makes the rest oftest_config_misc_endpoints_e2e.pyfail once that test runs/config/field/infoanswers from the resolved settings now, not the stored rowLow
sourceis exposed on both read endpoints so the dashboard can grey out fieldsFinal Attestation