This commit is contained in:
Delweng 2026-10-20 01:05:55 +00:00 • committed by GitHub
commit 772a0fb8eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 156 additions and 2 deletions

View file

@ -2964,6 +2964,34 @@ disable_spend_logs = False
jwt_handler: Final = JWTHandler()
prompt_injection_detection_obj: Final[OPTIONAL_PromptInjectionDetection | None] = None
store_model_in_db: bool = False
def _publish_store_model_in_db(value: bool, settings_store: SettingsStore | None = None) -> None:
"""Mirror the effective ``store_model_in_db`` flag into the surfaces the Admin UI reads.
``/config/list`` reads the in-memory ``general_settings`` mapping and
``/config/field/info`` reads the settings store — neither sees the
``STORE_MODEL_IN_DB`` env var, so without this an env-enabled deployment
reported the flag as false and the UI toggle rendered OFF while the feature
was active (the runtime global is resolved from YAML OR env OR DB at
startup, and from the DB row on reloads). Callers holding the live store
pass it explicitly; keys owned by the config file are never written to the
store — the file owns the value and shadowed writes are rejected.
"""
try:
if settings_store is None:
settings_store = proxy_config.settings
if not settings_store.owned_by_config("store_model_in_db"):
settings_store["store_model_in_db"] = value
except AttributeError:
# No live settings store (e.g. a mocked ProxyConfig in unit tests).
pass
# ``general_settings`` is usually a plain dict, but some code paths point it
# at the settings store itself, which the branch above already updated.
if general_settings is not settings_store:
general_settings["store_model_in_db"] = value
open_telemetry_logger: OpenTelemetry | None = None
### GATEWAY REQUEST COUNTS (SGR) ###
# Folded in memory by BillableRequestMetricsMiddleware, drained to
@ -8404,8 +8432,7 @@ class ProxyConfig:
return
normalized: Final = coerce_bool(value)
store_model_in_db = normalized if isinstance(normalized, bool) else bool(normalized)
if not self.settings.owned_by_config("store_model_in_db"):
self.settings["store_model_in_db"] = store_model_in_db
_publish_store_model_in_db(store_model_in_db, self.settings)
async def _apply_retention_settings(
self,
@ -11421,6 +11448,10 @@ class ProxyStartupEvent:
except Exception as e:
verbose_proxy_logger.debug("Failed to check DB for store_model_in_db: %s", str(e))
# Keep the reporting surfaces (/config/list, /config/field/info) in sync with the
# effective flag — they only see YAML + DB, not the STORE_MODEL_IN_DB env var.
_publish_store_model_in_db(store_model_in_db)
config_reload_interval_seconds = proxy_config_reload_interval_seconds
if not isinstance(config_reload_interval_seconds, int) or config_reload_interval_seconds <= 0:
verbose_proxy_logger.warning(

View file

@ -1882,3 +1882,126 @@ def test_config_yaml_invalid_method(client):
# Method-not-allowed responses still return a JSON-ish body via the
# FastAPI default handler — assert the body is not the success payload.
assert response.content != b'{"hello":"world"}'
# ---------------------------------------------------------------------------
# store_model_in_db reporting surfaces (/config/list + /config/field/info)
# ---------------------------------------------------------------------------
def test_config_list_reflects_env_resolved_store_model_in_db(client, auth_as, mock_prisma, monkeypatch):
"""store_model_in_db enabled only via the STORE_MODEL_IN_DB env var (absent from
config.yaml and the DB row) must surface as field_value=True in /config/list with
stored_in_db=False — the Admin UI "Store Model in DB" toggle reads this endpoint
and rendered OFF while the feature was active (issue #47238)."""
from litellm.proxy import proxy_server as ps
from litellm.proxy._types import LitellmUserRoles
_seed_settings_store(monkeypatch, db_row={})
table = _install_litellm_config(mock_prisma)
row = MagicMock()
row.param_value = {}
table.find_first = AsyncMock(return_value=row)
monkeypatch.setattr(ps, "prisma_client", mock_prisma)
monkeypatch.setattr(ps, "store_model_in_db", True)
# pre-publish state: the config-load normalization wrote the YAML-resolved false
monkeypatch.setitem(ps.general_settings, "store_model_in_db", False)
ps._publish_store_model_in_db(True, ps.proxy_config.settings) # what startup_event does after resolving the flag
with auth_as(LitellmUserRoles.PROXY_ADMIN):
response = client.get("/config/list", params={"config_type": "general_settings"})
assert response.status_code == 200
entry = {item["field_name"]: item for item in response.json()}["store_model_in_db"]
assert entry["field_value"] is True
assert entry["stored_in_db"] is False
assert entry["editable"] is True
def test_config_field_info_reflects_env_resolved_store_model_in_db(client, auth_as, mock_prisma, monkeypatch):
"""/config/field/info reads the settings store, which does not see the env var either;
after startup publishes the effective flag the endpoint must return it."""
from litellm.proxy import proxy_server as ps
from litellm.proxy._types import LitellmUserRoles
_seed_settings_store(monkeypatch, db_row={})
table = _install_litellm_config(mock_prisma)
row = MagicMock()
row.param_value = {}
table.find_first = AsyncMock(return_value=row)
monkeypatch.setattr(ps, "prisma_client", mock_prisma)
monkeypatch.setattr(ps, "store_model_in_db", True)
ps._publish_store_model_in_db(True, ps.proxy_config.settings)
with auth_as(LitellmUserRoles.PROXY_ADMIN):
response = client.get("/config/field/info", params={"field_name": "store_model_in_db"})
assert response.status_code == 200
body = response.json()
assert body["field_name"] == "store_model_in_db"
assert body["field_value"] is True
assert body["editable"] is True
def test_publish_store_model_in_db_skips_store_write_for_config_owned_key(client, auth_as, mock_prisma, monkeypatch):
"""When general_settings.store_model_in_db is declared in config.yaml the file owns
it: /config/list still reports the effective (env-resolved) value, but the row is
non-editable and the settings store keeps the config-owned value untouched."""
from litellm.proxy import proxy_server as ps
from litellm.proxy._types import LitellmUserRoles
_seed_settings_store(monkeypatch, db_row={}, yaml_values={"store_model_in_db": True})
table = _install_litellm_config(mock_prisma)
row = MagicMock()
row.param_value = {}
table.find_first = AsyncMock(return_value=row)
monkeypatch.setattr(ps, "prisma_client", mock_prisma)
monkeypatch.setattr(ps, "store_model_in_db", True)
monkeypatch.setitem(ps.general_settings, "store_model_in_db", True)
ps._publish_store_model_in_db(True, ps.proxy_config.settings)
with auth_as(LitellmUserRoles.PROXY_ADMIN):
response = client.get("/config/list", params={"config_type": "general_settings"})
assert response.status_code == 200
entry = {item["field_name"]: item for item in response.json()}["store_model_in_db"]
assert entry["field_value"] is True
assert entry["source"] == "config"
assert entry["editable"] is False
def test_store_model_in_db_reload_updates_reporting_surfaces(client, auth_as, mock_prisma, monkeypatch):
"""A DB general_settings row flip applied by the periodic reload must update the
module global AND both reporting surfaces, not just the settings store."""
from litellm.proxy import proxy_server as ps
from litellm.proxy.config_resolvers import SettingsStore
store = SettingsStore("general_settings")
store.load_yaml({})
store.apply_db_row("general_settings", {"store_model_in_db": True})
monkeypatch.setattr(ps.proxy_config, "settings", store)
monkeypatch.setattr(ps, "store_model_in_db", False)
monkeypatch.setitem(ps.general_settings, "store_model_in_db", False)
asyncio.run(ps.proxy_config._apply_store_model_in_db_setting({"store_model_in_db": True}))
assert ps.store_model_in_db is True
assert ps.general_settings["store_model_in_db"] is True
assert store["store_model_in_db"] is True
def test_store_model_in_db_publish_skips_config_owned_store_when_general_settings_aliases_it(monkeypatch):
"""Code paths may point general_settings at the settings store itself; publishing the
effective flag must never raise ConfigOwnedKeyError for a config-owned key, and the
file-owned value must stay untouched."""
from litellm.proxy import proxy_server as ps
from litellm.proxy.config_resolvers import SettingsStore
store = SettingsStore("general_settings")
store.load_yaml({"store_model_in_db": "os.environ/PROOF_FLAG"})
monkeypatch.setattr(ps.proxy_config, "settings", store)
monkeypatch.setattr(ps, "general_settings", store, raising=False)
ps._publish_store_model_in_db(False, store) # the DB row said false; the config file owns the key
assert store["store_model_in_db"] == "os.environ/PROOF_FLAG"