mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-14 11:08:37 +00:00
Merge pull request #21511 from BerriAI/litellm_store_model_in_db_from_database
[Feature] Allow store_model_in_db to be set via database
This commit is contained in:
@@ -2125,6 +2125,10 @@ class ConfigGeneralSettings(LiteLLMPydanticObjectBase):
|
||||
None,
|
||||
description="CIDR ranges of trusted reverse proxies. When set, X-Forwarded-For headers are only trusted from these IPs.",
|
||||
)
|
||||
store_model_in_db: Optional[bool] = Field(
|
||||
None,
|
||||
description="If True, models and config are stored in and loaded from the database. Default is False.",
|
||||
)
|
||||
|
||||
|
||||
class ConfigYAML(LiteLLMPydanticObjectBase):
|
||||
|
||||
@@ -2808,6 +2808,7 @@ class ProxyConfig:
|
||||
store_model_in_db = general_settings.get("store_model_in_db", False)
|
||||
if store_model_in_db is None:
|
||||
store_model_in_db = False
|
||||
general_settings["store_model_in_db"] = store_model_in_db
|
||||
### CUSTOM API KEY AUTH ###
|
||||
## pass filepath
|
||||
custom_auth = general_settings.get("custom_auth", None)
|
||||
@@ -3845,7 +3846,7 @@ class ProxyConfig:
|
||||
"""
|
||||
Pull from DB, read general settings value
|
||||
"""
|
||||
global general_settings
|
||||
global general_settings, store_model_in_db
|
||||
if db_general_settings is None:
|
||||
return
|
||||
_general_settings = dict(db_general_settings)
|
||||
@@ -3897,6 +3898,19 @@ class ProxyConfig:
|
||||
# For other types, convert to bool
|
||||
general_settings["store_prompts_in_spend_logs"] = bool(value)
|
||||
|
||||
## STORE MODEL IN DB ##
|
||||
if "store_model_in_db" in _general_settings:
|
||||
value = _general_settings["store_model_in_db"]
|
||||
if value is None:
|
||||
pass # Don't change store_model_in_db to None; keep current value
|
||||
elif isinstance(value, bool):
|
||||
store_model_in_db = value
|
||||
elif isinstance(value, str):
|
||||
store_model_in_db = value.lower() == "true"
|
||||
else:
|
||||
store_model_in_db = bool(value)
|
||||
general_settings["store_model_in_db"] = store_model_in_db
|
||||
|
||||
## MAXIMUM SPEND LOGS RETENTION PERIOD ##
|
||||
if "maximum_spend_logs_retention_period" in _general_settings:
|
||||
old_value = general_settings.get("maximum_spend_logs_retention_period")
|
||||
@@ -5429,6 +5443,31 @@ class ProxyStartupEvent:
|
||||
get_secret_bool("STORE_MODEL_IN_DB", store_model_in_db) or store_model_in_db
|
||||
)
|
||||
|
||||
# If store_model_in_db is still False, check DB for override.
|
||||
# This breaks the chicken-and-egg where DB has store_model_in_db=True
|
||||
# but YAML config has False.
|
||||
if store_model_in_db is not True and prisma_client is not None:
|
||||
try:
|
||||
_db_gs_record = await prisma_client.db.litellm_config.find_first(
|
||||
where={"param_name": "general_settings"}
|
||||
)
|
||||
if _db_gs_record is not None and isinstance(
|
||||
_db_gs_record.param_value, dict
|
||||
):
|
||||
_db_val = _db_gs_record.param_value.get("store_model_in_db")
|
||||
if _db_val is True or (
|
||||
isinstance(_db_val, str)
|
||||
and _db_val.lower() == "true"
|
||||
):
|
||||
store_model_in_db = True
|
||||
verbose_proxy_logger.info(
|
||||
"store_model_in_db=True loaded from DB, overriding config/env"
|
||||
)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.debug(
|
||||
"Failed to check DB for store_model_in_db: %s", str(e)
|
||||
)
|
||||
|
||||
if store_model_in_db is True:
|
||||
# MEMORY LEAK FIX: Increase interval from 10s to 30s minimum
|
||||
# Frequent polling was causing excessive memory allocations
|
||||
@@ -11317,6 +11356,7 @@ async def get_config_list(
|
||||
"max_request_size_mb": {"type": "Integer"},
|
||||
"max_response_size_mb": {"type": "Integer"},
|
||||
"pass_through_endpoints": {"type": "PydanticModel"},
|
||||
"store_model_in_db": {"type": "Boolean"},
|
||||
"store_prompts_in_spend_logs": {"type": "Boolean"},
|
||||
"maximum_spend_logs_retention_period": {"type": "String"},
|
||||
"mcp_internal_ip_ranges": {"type": "List"},
|
||||
|
||||
@@ -95,6 +95,7 @@ def test_login_v2_returns_redirect_url_and_sets_cookie(monkeypatch):
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.premium_user", False)
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
|
||||
monkeypatch.setattr("litellm.proxy.utils.get_server_root_path", lambda: "")
|
||||
monkeypatch.setattr("litellm.proxy.utils.get_proxy_base_url", lambda: None)
|
||||
|
||||
client = TestClient(app)
|
||||
response = client.post(
|
||||
@@ -3349,3 +3350,296 @@ class TestInvitationEndpoints:
|
||||
# ProxyException handler returns {"error": {...}}, HTTPException returns {"detail": {...}}
|
||||
error_content = body.get("error", body.get("detail", body))
|
||||
assert "not allowed" in str(error_content).lower()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# store_model_in_db DB Config Override Tests
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def test_store_model_in_db_in_config_general_settings():
|
||||
"""
|
||||
Verify store_model_in_db is a valid field in ConfigGeneralSettings
|
||||
and validates correctly for True/False values.
|
||||
"""
|
||||
from litellm.proxy._types import ConfigGeneralSettings
|
||||
|
||||
assert "store_model_in_db" in ConfigGeneralSettings.model_fields
|
||||
|
||||
# Should validate with True
|
||||
config = ConfigGeneralSettings(store_model_in_db=True)
|
||||
assert config.store_model_in_db is True
|
||||
|
||||
# Should validate with False
|
||||
config = ConfigGeneralSettings(store_model_in_db=False)
|
||||
assert config.store_model_in_db is False
|
||||
|
||||
# Should validate with None (default)
|
||||
config = ConfigGeneralSettings(store_model_in_db=None)
|
||||
assert config.store_model_in_db is None
|
||||
|
||||
# Should validate with no value
|
||||
config = ConfigGeneralSettings()
|
||||
assert config.store_model_in_db is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_general_settings_store_model_in_db_true():
|
||||
"""
|
||||
Verify _update_general_settings sets global store_model_in_db to True
|
||||
when DB general_settings has store_model_in_db=True.
|
||||
"""
|
||||
from litellm.proxy.proxy_server import ProxyConfig
|
||||
|
||||
proxy_config = ProxyConfig()
|
||||
|
||||
with patch(
|
||||
"litellm.proxy.proxy_server.store_model_in_db", False
|
||||
) as mock_store, patch(
|
||||
"litellm.proxy.proxy_server.general_settings", {}
|
||||
) as mock_gs:
|
||||
await proxy_config._update_general_settings(
|
||||
db_general_settings={"store_model_in_db": True}
|
||||
)
|
||||
|
||||
import litellm.proxy.proxy_server as ps
|
||||
|
||||
assert ps.store_model_in_db is True
|
||||
assert ps.general_settings["store_model_in_db"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_general_settings_store_model_in_db_false():
|
||||
"""
|
||||
Verify _update_general_settings sets global store_model_in_db to False
|
||||
when DB general_settings has store_model_in_db=False.
|
||||
"""
|
||||
from litellm.proxy.proxy_server import ProxyConfig
|
||||
|
||||
proxy_config = ProxyConfig()
|
||||
|
||||
with patch(
|
||||
"litellm.proxy.proxy_server.store_model_in_db", True
|
||||
), patch("litellm.proxy.proxy_server.general_settings", {}):
|
||||
await proxy_config._update_general_settings(
|
||||
db_general_settings={"store_model_in_db": False}
|
||||
)
|
||||
|
||||
import litellm.proxy.proxy_server as ps
|
||||
|
||||
assert ps.store_model_in_db is False
|
||||
assert ps.general_settings["store_model_in_db"] is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_general_settings_store_model_in_db_string_normalization():
|
||||
"""
|
||||
Verify _update_general_settings normalizes string values for store_model_in_db.
|
||||
"""
|
||||
from litellm.proxy.proxy_server import ProxyConfig
|
||||
|
||||
proxy_config = ProxyConfig()
|
||||
|
||||
# Test "true" string
|
||||
with patch(
|
||||
"litellm.proxy.proxy_server.store_model_in_db", False
|
||||
), patch("litellm.proxy.proxy_server.general_settings", {}):
|
||||
await proxy_config._update_general_settings(
|
||||
db_general_settings={"store_model_in_db": "true"}
|
||||
)
|
||||
import litellm.proxy.proxy_server as ps
|
||||
|
||||
assert ps.store_model_in_db is True
|
||||
|
||||
# Test "True" string
|
||||
with patch(
|
||||
"litellm.proxy.proxy_server.store_model_in_db", False
|
||||
), patch("litellm.proxy.proxy_server.general_settings", {}):
|
||||
await proxy_config._update_general_settings(
|
||||
db_general_settings={"store_model_in_db": "True"}
|
||||
)
|
||||
import litellm.proxy.proxy_server as ps
|
||||
|
||||
assert ps.store_model_in_db is True
|
||||
|
||||
# Test "false" string
|
||||
with patch(
|
||||
"litellm.proxy.proxy_server.store_model_in_db", True
|
||||
), patch("litellm.proxy.proxy_server.general_settings", {}):
|
||||
await proxy_config._update_general_settings(
|
||||
db_general_settings={"store_model_in_db": "false"}
|
||||
)
|
||||
import litellm.proxy.proxy_server as ps
|
||||
|
||||
assert ps.store_model_in_db is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_general_settings_store_model_in_db_none_keeps_current():
|
||||
"""
|
||||
Verify _update_general_settings does not change store_model_in_db
|
||||
when DB value is None.
|
||||
"""
|
||||
from litellm.proxy.proxy_server import ProxyConfig
|
||||
|
||||
proxy_config = ProxyConfig()
|
||||
|
||||
# When current is True and DB sends None, should stay True
|
||||
with patch(
|
||||
"litellm.proxy.proxy_server.store_model_in_db", True
|
||||
), patch("litellm.proxy.proxy_server.general_settings", {}):
|
||||
await proxy_config._update_general_settings(
|
||||
db_general_settings={"store_model_in_db": None}
|
||||
)
|
||||
import litellm.proxy.proxy_server as ps
|
||||
|
||||
assert ps.store_model_in_db is True
|
||||
|
||||
# When current is False and DB sends None, should stay False
|
||||
with patch(
|
||||
"litellm.proxy.proxy_server.store_model_in_db", False
|
||||
), patch("litellm.proxy.proxy_server.general_settings", {}):
|
||||
await proxy_config._update_general_settings(
|
||||
db_general_settings={"store_model_in_db": None}
|
||||
)
|
||||
import litellm.proxy.proxy_server as ps
|
||||
|
||||
assert ps.store_model_in_db is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_store_model_in_db_db_override_when_config_false():
|
||||
"""
|
||||
Verify the early DB check in initialize_scheduled_background_jobs
|
||||
overrides store_model_in_db=False when DB has True.
|
||||
"""
|
||||
from litellm.proxy.proxy_server import ProxyStartupEvent
|
||||
from litellm.proxy.utils import ProxyLogging
|
||||
|
||||
mock_prisma_client = MagicMock()
|
||||
|
||||
# Mock DB returning store_model_in_db=True in general_settings
|
||||
mock_db_record = MagicMock()
|
||||
mock_db_record.param_value = {"store_model_in_db": True}
|
||||
mock_prisma_client.db.litellm_config.find_first = AsyncMock(
|
||||
return_value=mock_db_record
|
||||
)
|
||||
|
||||
mock_proxy_logging = MagicMock(spec=ProxyLogging)
|
||||
mock_proxy_logging.slack_alerting_instance = MagicMock()
|
||||
mock_proxy_config = AsyncMock()
|
||||
|
||||
with patch(
|
||||
"litellm.proxy.proxy_server.proxy_config", mock_proxy_config
|
||||
), patch(
|
||||
"litellm.proxy.proxy_server.store_model_in_db", False
|
||||
), patch(
|
||||
"litellm.proxy.proxy_server.get_secret_bool", return_value=False
|
||||
):
|
||||
await ProxyStartupEvent.initialize_scheduled_background_jobs(
|
||||
general_settings={},
|
||||
prisma_client=mock_prisma_client,
|
||||
proxy_budget_rescheduler_min_time=1,
|
||||
proxy_budget_rescheduler_max_time=2,
|
||||
proxy_batch_write_at=5,
|
||||
proxy_logging_obj=mock_proxy_logging,
|
||||
)
|
||||
|
||||
import litellm.proxy.proxy_server as ps
|
||||
|
||||
# store_model_in_db should now be True (overridden by DB)
|
||||
assert ps.store_model_in_db is True
|
||||
|
||||
# add_deployment and get_credentials should have been called
|
||||
# since store_model_in_db is now True
|
||||
assert mock_proxy_config.add_deployment.call_count == 1
|
||||
assert mock_proxy_config.get_credentials.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_store_model_in_db_db_check_skipped_when_already_true(monkeypatch):
|
||||
"""
|
||||
Verify the early DB check is skipped when store_model_in_db is already True.
|
||||
The DB query for the early check should not be called.
|
||||
"""
|
||||
monkeypatch.delenv("STORE_MODEL_IN_DB", raising=False)
|
||||
from litellm.proxy.proxy_server import ProxyStartupEvent
|
||||
from litellm.proxy.utils import ProxyLogging
|
||||
|
||||
mock_prisma_client = MagicMock()
|
||||
mock_prisma_client.db.litellm_config.find_first = AsyncMock(return_value=None)
|
||||
|
||||
mock_proxy_logging = MagicMock(spec=ProxyLogging)
|
||||
mock_proxy_logging.slack_alerting_instance = MagicMock()
|
||||
mock_proxy_config = AsyncMock()
|
||||
|
||||
with patch(
|
||||
"litellm.proxy.proxy_server.proxy_config", mock_proxy_config
|
||||
), patch(
|
||||
"litellm.proxy.proxy_server.store_model_in_db", True
|
||||
), patch(
|
||||
"litellm.proxy.proxy_server.get_secret_bool", return_value=True
|
||||
):
|
||||
await ProxyStartupEvent.initialize_scheduled_background_jobs(
|
||||
general_settings={},
|
||||
prisma_client=mock_prisma_client,
|
||||
proxy_budget_rescheduler_min_time=1,
|
||||
proxy_budget_rescheduler_max_time=2,
|
||||
proxy_batch_write_at=5,
|
||||
proxy_logging_obj=mock_proxy_logging,
|
||||
)
|
||||
|
||||
# The early DB check uses find_first with param_name="general_settings".
|
||||
# When store_model_in_db is already True, the early check should be skipped.
|
||||
# However, add_deployment may also call find_first.
|
||||
# We just verify that store_model_in_db stays True and jobs are scheduled.
|
||||
import litellm.proxy.proxy_server as ps
|
||||
|
||||
assert ps.store_model_in_db is True
|
||||
assert mock_proxy_config.add_deployment.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_store_model_in_db_db_failure_graceful(monkeypatch):
|
||||
"""
|
||||
Verify the early DB check handles DB failures gracefully
|
||||
without crashing and keeps store_model_in_db as False.
|
||||
"""
|
||||
monkeypatch.delenv("STORE_MODEL_IN_DB", raising=False)
|
||||
from litellm.proxy.proxy_server import ProxyStartupEvent
|
||||
from litellm.proxy.utils import ProxyLogging
|
||||
|
||||
mock_prisma_client = MagicMock()
|
||||
# Simulate DB failure
|
||||
mock_prisma_client.db.litellm_config.find_first = AsyncMock(
|
||||
side_effect=Exception("DB connection error")
|
||||
)
|
||||
|
||||
mock_proxy_logging = MagicMock(spec=ProxyLogging)
|
||||
mock_proxy_logging.slack_alerting_instance = MagicMock()
|
||||
mock_proxy_config = AsyncMock()
|
||||
|
||||
with patch(
|
||||
"litellm.proxy.proxy_server.proxy_config", mock_proxy_config
|
||||
), patch(
|
||||
"litellm.proxy.proxy_server.store_model_in_db", False
|
||||
), patch(
|
||||
"litellm.proxy.proxy_server.get_secret_bool", return_value=False
|
||||
):
|
||||
# Should not raise an exception
|
||||
await ProxyStartupEvent.initialize_scheduled_background_jobs(
|
||||
general_settings={},
|
||||
prisma_client=mock_prisma_client,
|
||||
proxy_budget_rescheduler_min_time=1,
|
||||
proxy_budget_rescheduler_max_time=2,
|
||||
proxy_batch_write_at=5,
|
||||
proxy_logging_obj=mock_proxy_logging,
|
||||
)
|
||||
|
||||
import litellm.proxy.proxy_server as ps
|
||||
|
||||
# store_model_in_db should remain False
|
||||
assert ps.store_model_in_db is False
|
||||
|
||||
# add_deployment should NOT have been called since store_model_in_db is False
|
||||
mock_proxy_config.add_deployment.assert_not_called()
|
||||
|
||||
Reference in New Issue
Block a user