From 961fe1727c3f289c7f0483ffc35287fd03383605 Mon Sep 17 00:00:00 2001 From: aliabbas-muhammadi <143003167+aliabbas-muhammadi@users.noreply.github.com> Date: Tue, 18 Aug 2026 20:44:43 +1000 Subject: [PATCH] fix: apply DB-stored litellm_settings.max_budget on config reload max_budget was missing from LITELLM_SETTINGS_SAFE_DB_OVERRIDES, so a value written to the DB via POST /config/update was merged into the config dict but never applied to the live litellm.max_budget on reload. After a restart the proxy kept enforcing the config.yaml value and silently ignored the DB override. Add max_budget to the allowlist and apply it through a helper that float-casts (matching the startup load_config path) and leaves the runtime unchanged on a None DB value. A non-numeric persisted value is ignored with a warning rather than aborting the reload of an already-running proxy. Fixes #36533 --- litellm/constants.py | 2 + litellm/proxy/proxy_server.py | 23 ++++++- tests/test_litellm/proxy/test_proxy_server.py | 61 +++++++++++++++++++ 3 files changed, 85 insertions(+), 1 deletion(-) diff --git a/litellm/constants.py b/litellm/constants.py index e73fba1cc9f..964dbf0b333 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -1574,6 +1574,8 @@ LITELLM_SETTINGS_SAFE_DB_OVERRIDES: Final = [ "cost_discount_config", "cost_margin_config", "budget_exceeded_throttle_percentage", + # Proxy-wide budget stored via POST /config/update; applied to litellm.max_budget on reload. + "max_budget", # Every field editable from the Admin UI (proxy_server._GENERAL_SETTINGS_UI_LITELLM_FIELDS) # must be listed here so a DB write from one worker overrides the live litellm attribute on # the others when config reloads; otherwise peer workers stay on their startup value. diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 5d4a306a73e..2602f3597fd 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -3763,6 +3763,27 @@ def _scrub_guardrail_inner(inner: dict[str, JsonValue]) -> None: inner["guardrail"] = None +def _apply_safe_db_litellm_setting(key: str, value: object) -> None: + """Apply a DB-overlaid ``litellm_settings`` key to the live ``litellm`` module. + + ``max_budget`` is coerced to float to match startup config loading + (``ProxyConfig.load_config``). A ``None`` DB value leaves the current + runtime attribute unchanged; a non-numeric value is ignored (with a + warning) rather than aborting the reload of an already-running proxy. + """ + if key == "max_budget": + if value is None: + return + try: + budget = float(value) + except (TypeError, ValueError): + verbose_proxy_logger.warning("Ignoring non-numeric DB litellm_settings.max_budget override: %r", value) + return + litellm.max_budget = budget + return + setattr(litellm, key, value) + + def _scrub_db_overlay_remote_module_loads(section: str, db_value: JsonValue) -> JsonValue: """Strip ``s3://`` / ``gcs://`` entries from the DB-overlay value for fields whose contents reach ``get_instance_fn``. The same scheme is @@ -6405,7 +6426,7 @@ class ProxyConfig: elif param_name == "litellm_settings" and isinstance(db_param_value, dict): for key, value in db_param_value.items(): if key in LITELLM_SETTINGS_SAFE_DB_OVERRIDES: # params that are safe to override with db values - setattr(litellm, key, value) + _apply_safe_db_litellm_setting(key, value) # If param doesn't exist in config, add it if param_name not in current_config: diff --git a/tests/test_litellm/proxy/test_proxy_server.py b/tests/test_litellm/proxy/test_proxy_server.py index 5545ee92e84..7de2a32ae7e 100644 --- a/tests/test_litellm/proxy/test_proxy_server.py +++ b/tests/test_litellm/proxy/test_proxy_server.py @@ -9641,6 +9641,67 @@ def test_prompt_caching_settings_propagate_on_config_reload(monkeypatch, field_n assert getattr(litellm, field_name) == db_value +@pytest.mark.parametrize( + "db_value, expected", + [ + (10, 10.0), + ("10", 10.0), + (0.5, 0.5), + ], +) +def test_max_budget_propagates_on_config_reload(monkeypatch, db_value, expected): + """Regression for #36533: a DB-stored litellm_settings.max_budget must overlay the live + litellm.max_budget when config reloads (STORE_MODEL_IN_DB=True). It was absent from + LITELLM_SETTINGS_SAFE_DB_OVERRIDES, so peer workers kept enforcing the stale config.yaml value.""" + import litellm.proxy.proxy_server as ps + + # peer worker booted with the stale config.yaml value + monkeypatch.setattr(litellm, "max_budget", 0.00001) + + pc = ps.ProxyConfig() + pc._update_config_fields( + current_config={"litellm_settings": {}}, + param_name="litellm_settings", + db_param_value={"max_budget": db_value}, + ) + + assert litellm.max_budget == expected + assert isinstance(litellm.max_budget, float) + + +def test_max_budget_none_db_value_leaves_runtime_unchanged(monkeypatch): + """A null DB max_budget must not clobber the running litellm.max_budget.""" + import litellm.proxy.proxy_server as ps + + monkeypatch.setattr(litellm, "max_budget", 7.5) + + pc = ps.ProxyConfig() + pc._update_config_fields( + current_config={"litellm_settings": {}}, + param_name="litellm_settings", + db_param_value={"max_budget": None}, + ) + + assert litellm.max_budget == 7.5 + + +def test_max_budget_invalid_db_value_is_ignored(monkeypatch): + """A non-numeric DB max_budget must be skipped (logged), not abort the reload of a + running proxy or clobber the live litellm.max_budget.""" + import litellm.proxy.proxy_server as ps + + monkeypatch.setattr(litellm, "max_budget", 7.5) + + pc = ps.ProxyConfig() + pc._update_config_fields( + current_config={"litellm_settings": {}}, + param_name="litellm_settings", + db_param_value={"max_budget": "not-a-number"}, + ) + + assert litellm.max_budget == 7.5 + + def test_get_config_list_marks_untouched_prompt_caching_flag_as_not_set(monkeypatch): """The flag defaults to False rather than None, so a plain 'is not None' check would report the default as 'In Config' and imply an admin had set it."""