mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-09 22:31:41 +00:00
fix(proxy): load db credentials in the model reconcile so a worker never serves a model before its credential (#39876)
* fix(proxy): load db credentials inside the model reconcile so a worker never serves a model before its credential * fix(proxy): load db credentials in the model read-through so a request miss never adds a model before its credential * fix(proxy): read credentials from the writer db before the router update and look a credential up once * test(proxy): assert the credential is loaded when db models reach the router instead of the call order
This commit is contained in:
parent
99824533ff
commit
a85c3152ca
10 changed files with 278 additions and 76 deletions
|
|
@ -7,16 +7,19 @@ from litellm.types.utils import CredentialItem
|
|||
|
||||
|
||||
class CredentialAccessor:
|
||||
@staticmethod
|
||||
def find_credential(credential_name: str) -> CredentialItem | None:
|
||||
return next(
|
||||
(credential for credential in litellm.credential_list if credential.credential_name == credential_name),
|
||||
None,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_credential_values(credential_name: str) -> dict:
|
||||
"""Safe accessor for credentials."""
|
||||
|
||||
if not litellm.credential_list:
|
||||
return {}
|
||||
for credential in litellm.credential_list:
|
||||
if credential.credential_name == credential_name:
|
||||
return credential.credential_values.copy()
|
||||
return {}
|
||||
credential: Final = CredentialAccessor.find_credential(credential_name)
|
||||
return {} if credential is None else credential.credential_values.copy()
|
||||
|
||||
@staticmethod
|
||||
def upsert_credentials(credentials: list[CredentialItem]):
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@ async def _resync_model_deployments(model_name: str) -> bool:
|
|||
)
|
||||
return proxy_server.llm_router is not None
|
||||
async with proxy_server.MODEL_RECONCILE_LOCK:
|
||||
await proxy_server.proxy_config.get_credentials(prisma_client=prisma_client)
|
||||
proxy_server.proxy_config._add_deployment(db_models=rows)
|
||||
proxy_server.llm_model_list = router.get_model_list()
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -7109,11 +7109,10 @@ class ProxyConfig:
|
|||
],
|
||||
)
|
||||
|
||||
# Only load models from DB if "models" is in supported_db_objects (or if supported_db_objects is not set)
|
||||
if self._should_load_db_object(object_type="models"):
|
||||
new_models: Final = await self._get_models_from_db(prisma_client=prisma_client)
|
||||
|
||||
# update llm router
|
||||
load_models: Final = self._should_load_db_object(object_type="models")
|
||||
new_models: Final = await self._get_models_from_db(prisma_client=prisma_client) if load_models else None
|
||||
await self.get_credentials(prisma_client=prisma_client)
|
||||
if load_models:
|
||||
still_desired_ids = await self._update_llm_router(
|
||||
new_models=new_models, proxy_logging_obj=proxy_logging_obj
|
||||
)
|
||||
|
|
@ -7153,12 +7152,9 @@ class ProxyConfig:
|
|||
async def _resync_config_from_db() -> None:
|
||||
await self.add_deployment(prisma_client=prisma_client, proxy_logging_obj=proxy_logging_obj)
|
||||
|
||||
async def _resync_credentials_from_db() -> None:
|
||||
await self.get_credentials(prisma_client=prisma_client)
|
||||
|
||||
subscriber: Final = ConfigSyncSubscriber(
|
||||
redis_cache=redis_cache,
|
||||
resync_callbacks=(_resync_config_from_db, _resync_credentials_from_db),
|
||||
resync_callbacks=(_resync_config_from_db,),
|
||||
)
|
||||
self.config_sync_subscriber = subscriber
|
||||
subscriber.start()
|
||||
|
|
@ -8013,7 +8009,7 @@ class ProxyConfig:
|
|||
|
||||
async def get_credentials(self, prisma_client: PrismaClient):
|
||||
try:
|
||||
credentials = await CredentialsRepository(prisma_client).find_all()
|
||||
credentials = await CredentialsRepository(WriterPinnedClient(prisma_client.db)).find_all()
|
||||
credentials = [self.decrypt_credentials(cred) for cred in credentials]
|
||||
await self.delete_credentials(credentials) # delete credentials that are not in the all-up list
|
||||
CredentialAccessor.upsert_credentials(credentials) # upsert credentials that are in the all-up list
|
||||
|
|
@ -9597,19 +9593,6 @@ class ProxyStartupEvent:
|
|||
)
|
||||
|
||||
if store_model_in_db is True:
|
||||
### GET STORED CREDENTIALS ###
|
||||
scheduler.add_job(
|
||||
proxy_config.get_credentials,
|
||||
"interval",
|
||||
seconds=config_reload_interval_seconds,
|
||||
# REMOVED jitter parameter - major cause of memory leak
|
||||
args=[prisma_client],
|
||||
id="get_credentials_job",
|
||||
replace_existing=True,
|
||||
misfire_grace_time=APSCHEDULER_MISFIRE_GRACE_TIME,
|
||||
)
|
||||
await proxy_config.get_credentials(prisma_client=prisma_client)
|
||||
|
||||
# MEMORY LEAK FIX: Increase interval from 10s to 30s minimum
|
||||
# Frequent polling was causing excessive memory allocations
|
||||
scheduler.add_job(
|
||||
|
|
@ -9623,7 +9606,7 @@ class ProxyStartupEvent:
|
|||
misfire_grace_time=APSCHEDULER_MISFIRE_GRACE_TIME,
|
||||
)
|
||||
|
||||
# this will load all existing models on proxy startup
|
||||
# this will load all existing credentials and models on proxy startup
|
||||
await proxy_config.add_deployment(prisma_client=prisma_client, proxy_logging_obj=proxy_logging_obj)
|
||||
|
||||
proxy_config.start_config_sync_subscriber(
|
||||
|
|
|
|||
|
|
@ -672,11 +672,19 @@ def load_credentials_from_list(kwargs: dict):
|
|||
CredentialAccessor: Final = getattr(sys.modules[__name__], "CredentialAccessor")
|
||||
|
||||
credential_name: Final = kwargs.get("litellm_credential_name")
|
||||
if credential_name and litellm.credential_list:
|
||||
credential_accessor: Final[Mapping[str, object]] = CredentialAccessor.get_credential_values(credential_name)
|
||||
for key, value in credential_accessor.items():
|
||||
if key not in kwargs:
|
||||
kwargs[key] = value
|
||||
if not credential_name:
|
||||
return
|
||||
credential: Final = CredentialAccessor.find_credential(credential_name)
|
||||
if credential is None:
|
||||
verbose_logger.warning(
|
||||
"litellm_credential_name=%s matched none of the %d loaded credentials; the request runs without it",
|
||||
credential_name,
|
||||
len(litellm.credential_list),
|
||||
)
|
||||
return
|
||||
for key, value in credential.credential_values.items():
|
||||
if key not in kwargs:
|
||||
kwargs[key] = value
|
||||
|
||||
|
||||
def get_dynamic_callbacks(
|
||||
|
|
|
|||
|
|
@ -824,7 +824,7 @@ class _StopFailingSubscriber(ConfigSyncSubscriber):
|
|||
raise RuntimeError("stop failed")
|
||||
|
||||
|
||||
async def test_proxy_config_subscriber_resyncs_deployments_and_credentials() -> None:
|
||||
async def test_proxy_config_subscriber_resyncs_deployments_only() -> None:
|
||||
from litellm.proxy.proxy_server import ProxyConfig
|
||||
|
||||
cache = _FakeRedisCache(_ScriptedPubSubRedisClient([_QueuePubSub()]))
|
||||
|
|
@ -852,10 +852,7 @@ async def test_proxy_config_subscriber_resyncs_deployments_and_credentials() ->
|
|||
await callback()
|
||||
await config.stop_config_sync_subscriber()
|
||||
|
||||
assert calls == [
|
||||
("add_deployment", prisma_client, proxy_logging_obj),
|
||||
("get_credentials", prisma_client, None),
|
||||
]
|
||||
assert calls == [("add_deployment", prisma_client, proxy_logging_obj)]
|
||||
assert config.config_sync_subscriber is None
|
||||
assert subscriber._task is None
|
||||
|
||||
|
|
|
|||
|
|
@ -393,6 +393,51 @@ async def test_resync_model_deployments_mutates_router_under_model_reconcile_loc
|
|||
assert not proxy_server.MODEL_RECONCILE_LOCK.locked()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resync_model_deployments_loads_db_credentials_before_reconciling_models(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import litellm
|
||||
import litellm.proxy.proxy_server as proxy_server
|
||||
from litellm.litellm_core_utils.credential_accessor import CredentialAccessor
|
||||
from litellm.proxy.common_utils.registry_read_through import _resync_model_deployments
|
||||
from litellm.types.utils import CredentialItem
|
||||
|
||||
rows: Final = [MagicMock()]
|
||||
prisma_client: Final = MagicMock()
|
||||
prisma_client.db.litellm_proxymodeltable.find_many = AsyncMock(return_value=rows)
|
||||
router: Final = MagicMock()
|
||||
router.get_model_list.return_value = []
|
||||
installed: Final = MagicMock()
|
||||
|
||||
async def load_credentials_from_db(prisma_client: object) -> None:
|
||||
CredentialAccessor.upsert_credentials(
|
||||
[
|
||||
CredentialItem(
|
||||
credential_name="openai-cred",
|
||||
credential_values={"api_key": "sk-from-db"},
|
||||
credential_info={},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
def install_models(db_models: object) -> None:
|
||||
installed(db_models=db_models, credential=CredentialAccessor.get_credential_values("openai-cred"))
|
||||
|
||||
monkeypatch.setattr(litellm, "credential_list", [])
|
||||
monkeypatch.setattr(proxy_server, "prisma_client", prisma_client)
|
||||
monkeypatch.setattr(proxy_server, "store_model_in_db", True)
|
||||
monkeypatch.setattr(proxy_server, "llm_router", router)
|
||||
monkeypatch.setattr(proxy_server, "llm_model_list", None)
|
||||
monkeypatch.setattr(proxy_server.proxy_config, "get_credentials", load_credentials_from_db)
|
||||
monkeypatch.setattr(proxy_server.proxy_config, "_add_deployment", install_models)
|
||||
|
||||
assert await _resync_model_deployments("model-created-on-a-sibling-replica") is True
|
||||
installed.assert_called_once_with(db_models=rows, credential={"api_key": "sk-from-db"})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resync_model_deployments_respects_supported_db_objects(monkeypatch):
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ from litellm.proxy.common_utils.scheduled_job_stagger import (
|
|||
)
|
||||
|
||||
OPERATOR_CRON_JOB_ID = "spend_log_cleanup_job"
|
||||
SHARED_INTERVAL_JOB_IDS = ("periodic_reload_job", "get_credentials_job", "add_deployment_job")
|
||||
SHARED_INTERVAL_JOB_IDS = ("periodic_reload_job", "add_deployment_job")
|
||||
|
||||
|
||||
async def _noop() -> None: ...
|
||||
|
|
|
|||
|
|
@ -2939,6 +2939,129 @@ async def test_ProxyConfig_add_deployment_applies_db_router_settings(monkeypatch
|
|||
fake_router.update_settings.assert_called_once_with(routing_strategy="latency-based-routing")
|
||||
|
||||
|
||||
def _stub_add_deployment_collaborators(
|
||||
monkeypatch: pytest.MonkeyPatch, pc: ProxyConfig, fake_prisma: MagicMock
|
||||
) -> None:
|
||||
from litellm.proxy import proxy_server
|
||||
|
||||
fake_router = MagicMock()
|
||||
fake_router.get_model_list = MagicMock(return_value=[])
|
||||
|
||||
async def fake_get_config(*args: object, **kwargs: object) -> dict[str, object]:
|
||||
return {}
|
||||
|
||||
monkeypatch.setattr(litellm, "credential_list", [])
|
||||
monkeypatch.setattr(pc, "get_config", fake_get_config)
|
||||
monkeypatch.setattr(pc, "_init_non_llm_objects_in_db", AsyncMock())
|
||||
monkeypatch.setattr(proxy_server, "prefetch_config_params", AsyncMock())
|
||||
monkeypatch.setattr(proxy_server, "get_config_param", AsyncMock(return_value=None))
|
||||
monkeypatch.setattr(proxy_server, "llm_router", fake_router)
|
||||
monkeypatch.setattr(proxy_server, "master_key", "sk-master")
|
||||
monkeypatch.setattr(proxy_server, "prisma_client", fake_prisma)
|
||||
monkeypatch.setattr(proxy_server, "proxy_config", pc)
|
||||
monkeypatch.delenv("LITELLM_SALT_KEY", raising=False)
|
||||
|
||||
|
||||
def _encrypted_credential_row(credential_name: str, api_key: str) -> dict[str, object]:
|
||||
from litellm.proxy.common_utils.encrypt_decrypt_utils import encrypt_value_helper
|
||||
|
||||
return {
|
||||
"credential_name": credential_name,
|
||||
"credential_values": {"api_key": encrypt_value_helper(api_key, new_encryption_key="sk-master")},
|
||||
"credential_info": {"custom_llm_provider": "openai"},
|
||||
}
|
||||
|
||||
|
||||
def _fake_prisma_with_encrypted_credential(credential_name: str, api_key: str) -> MagicMock:
|
||||
fake_prisma = MagicMock()
|
||||
fake_prisma.db.litellm_credentialstable.find_many = AsyncMock(
|
||||
return_value=[_encrypted_credential_row(credential_name, api_key)]
|
||||
)
|
||||
return fake_prisma
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ProxyConfig_add_deployment_loads_db_credentials_before_reconciling_models(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
from litellm.litellm_core_utils.credential_accessor import CredentialAccessor
|
||||
from litellm.proxy import proxy_server
|
||||
from litellm.utils import load_credentials_from_list
|
||||
|
||||
pc = ProxyConfig()
|
||||
fake_prisma = MagicMock()
|
||||
fake_prisma.db.litellm_credentialstable.find_many = AsyncMock(return_value=[])
|
||||
_stub_add_deployment_collaborators(monkeypatch, pc, fake_prisma)
|
||||
monkeypatch.setattr(proxy_server, "general_settings", {})
|
||||
installed = MagicMock()
|
||||
|
||||
async def read_models_while_a_credential_lands(prisma_client: object) -> list[MagicMock]:
|
||||
fake_prisma.db.litellm_credentialstable.find_many.return_value = [
|
||||
_encrypted_credential_row("openai-cred", "sk-from-db")
|
||||
]
|
||||
return [MagicMock()]
|
||||
|
||||
async def install_models(new_models: object, proxy_logging_obj: object) -> None:
|
||||
installed(credential=CredentialAccessor.get_credential_values("openai-cred"))
|
||||
|
||||
monkeypatch.setattr(pc, "_get_models_from_db", read_models_while_a_credential_lands)
|
||||
monkeypatch.setattr(pc, "_update_llm_router", install_models)
|
||||
|
||||
await pc.add_deployment(prisma_client=fake_prisma, proxy_logging_obj=MagicMock())
|
||||
|
||||
installed.assert_called_once_with(credential={"api_key": "sk-from-db"})
|
||||
assert CredentialAccessor.get_credential_values("openai-cred") == {"api_key": "sk-from-db"}
|
||||
request_kwargs = {"litellm_credential_name": "openai-cred"}
|
||||
load_credentials_from_list(request_kwargs)
|
||||
assert request_kwargs == {"litellm_credential_name": "openai-cred", "api_key": "sk-from-db"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ProxyConfig_add_deployment_loads_db_credentials_even_when_models_are_not_db_objects(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
from litellm.litellm_core_utils.credential_accessor import CredentialAccessor
|
||||
from litellm.proxy import proxy_server
|
||||
|
||||
pc = ProxyConfig()
|
||||
fake_prisma = _fake_prisma_with_encrypted_credential("openai-cred", "sk-from-db")
|
||||
_stub_add_deployment_collaborators(monkeypatch, pc, fake_prisma)
|
||||
monkeypatch.setattr(proxy_server, "general_settings", {"supported_db_objects": ["mcp"]})
|
||||
models_fetch = AsyncMock(return_value=[])
|
||||
monkeypatch.setattr(pc, "_get_models_from_db", models_fetch)
|
||||
|
||||
await pc.add_deployment(prisma_client=fake_prisma, proxy_logging_obj=MagicMock())
|
||||
|
||||
models_fetch.assert_not_awaited()
|
||||
assert CredentialAccessor.get_credential_values("openai-cred") == {"api_key": "sk-from-db"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ProxyConfig_get_credentials_reads_from_writer_not_replica(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
from litellm.litellm_core_utils.credential_accessor import CredentialAccessor
|
||||
from litellm.proxy.db.prisma_client import PrismaWrapper
|
||||
from litellm.proxy.db.routing_prisma_wrapper import RoutingPrismaWrapper
|
||||
|
||||
pc = ProxyConfig()
|
||||
writer_inner = MagicMock(name="writer_prisma")
|
||||
reader_inner = MagicMock(name="reader_prisma")
|
||||
writer_inner.litellm_credentialstable.find_many = AsyncMock(
|
||||
return_value=[_encrypted_credential_row("openai-cred", "sk-from-writer")]
|
||||
)
|
||||
reader_inner.litellm_credentialstable.find_many = AsyncMock(return_value=[])
|
||||
fake_prisma = MagicMock()
|
||||
fake_prisma.db = RoutingPrismaWrapper(
|
||||
writer=PrismaWrapper(original_prisma=writer_inner, iam_token_db_auth=False),
|
||||
reader=PrismaWrapper(original_prisma=reader_inner, iam_token_db_auth=False),
|
||||
)
|
||||
_stub_add_deployment_collaborators(monkeypatch, pc, fake_prisma)
|
||||
|
||||
await pc.get_credentials(prisma_client=fake_prisma)
|
||||
|
||||
assert CredentialAccessor.get_credential_values("openai-cred") == {"api_key": "sk-from-writer"}
|
||||
reader_inner.litellm_credentialstable.find_many.assert_not_awaited()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ProxyConfig._add_general_settings_from_db_config
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -822,16 +822,16 @@ def _mock_scheduled_proxy_config() -> MagicMock:
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_initialize_scheduled_jobs_credentials(monkeypatch):
|
||||
"""
|
||||
Test that get_credentials is only called when store_model_in_db is True
|
||||
"""
|
||||
async def test_initialize_scheduled_jobs_loads_credentials_only_through_add_deployment(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.delenv("DISABLE_PRISMA_SCHEMA_UPDATE", raising=False)
|
||||
monkeypatch.delenv("STORE_MODEL_IN_DB", raising=False)
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
|
||||
from litellm.proxy.proxy_server import ProxyStartupEvent
|
||||
from litellm.proxy.utils import ProxyLogging
|
||||
|
||||
# Mock dependencies
|
||||
mock_prisma_client = MagicMock()
|
||||
mock_proxy_logging = MagicMock(spec=ProxyLogging)
|
||||
mock_proxy_logging.slack_alerting_instance = MagicMock()
|
||||
|
|
@ -841,25 +841,6 @@ async def test_initialize_scheduled_jobs_credentials(monkeypatch):
|
|||
with (
|
||||
patch("litellm.proxy.proxy_server.proxy_config", mock_proxy_config),
|
||||
patch("litellm.proxy.proxy_server.store_model_in_db", False),
|
||||
): # set store_model_in_db to False
|
||||
# Test when store_model_in_db is 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,
|
||||
)
|
||||
|
||||
# Verify get_credentials was not called
|
||||
mock_proxy_config.get_credentials.assert_not_called()
|
||||
|
||||
# Now test with store_model_in_db = True
|
||||
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={},
|
||||
|
|
@ -870,12 +851,31 @@ async def test_initialize_scheduled_jobs_credentials(monkeypatch):
|
|||
proxy_logging_obj=mock_proxy_logging,
|
||||
)
|
||||
|
||||
# Verify get_credentials was called both directly and scheduled
|
||||
assert mock_proxy_config.get_credentials.call_count == 1 # Direct call
|
||||
mock_proxy_config.get_credentials.assert_not_called()
|
||||
mock_proxy_config.add_deployment.assert_not_called()
|
||||
|
||||
# Verify a scheduled job was added for get_credentials
|
||||
mock_scheduler_calls = [call[0] for call in mock_proxy_config.get_credentials.mock_calls]
|
||||
assert len(mock_scheduler_calls) > 0
|
||||
scheduler = AsyncIOScheduler()
|
||||
try:
|
||||
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.AsyncIOScheduler", return_value=scheduler),
|
||||
):
|
||||
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,
|
||||
)
|
||||
|
||||
assert scheduler.get_job("get_credentials_job") is None
|
||||
assert scheduler.get_job("add_deployment_job") is not None
|
||||
mock_proxy_config.get_credentials.assert_not_called()
|
||||
assert mock_proxy_config.add_deployment.call_count == 1
|
||||
finally:
|
||||
scheduler.shutdown(wait=False)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -924,7 +924,7 @@ async def test_periodic_reload_job_scheduled_without_store_model_in_db(monkeypat
|
|||
@pytest.mark.asyncio
|
||||
async def test_initialize_scheduled_jobs_uses_configured_config_reload_interval(monkeypatch):
|
||||
"""
|
||||
The DB config-reload jobs (add_deployment, get_credentials) that keep multi-pod
|
||||
The DB config-reload job (add_deployment) that keeps multi-pod
|
||||
deployments in sync must be scheduled at the configured
|
||||
proxy_config_reload_interval_seconds, not a hardcoded value.
|
||||
"""
|
||||
|
|
@ -967,7 +967,7 @@ async def test_initialize_scheduled_jobs_uses_configured_config_reload_interval(
|
|||
if "id" in job_call.kwargs
|
||||
}
|
||||
assert scheduled_seconds["add_deployment_job"] == configured_interval
|
||||
assert scheduled_seconds["get_credentials_job"] == configured_interval
|
||||
assert "get_credentials_job" not in scheduled_seconds
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -1011,7 +1011,7 @@ async def test_initialize_scheduled_jobs_rejects_non_positive_config_reload_inte
|
|||
if "id" in job_call.kwargs
|
||||
}
|
||||
assert scheduled_seconds["add_deployment_job"] == 30
|
||||
assert scheduled_seconds["get_credentials_job"] == 30
|
||||
assert "get_credentials_job" not in scheduled_seconds
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -7446,10 +7446,8 @@ async def test_store_model_in_db_db_override_when_config_false():
|
|||
# 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
|
||||
mock_proxy_config.get_credentials.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
|
|
@ -6092,3 +6092,47 @@ class TestFinalOptionalParamsLineRedaction:
|
|||
|
||||
assert "'max_tokens': 17" in printed
|
||||
assert "'temperature': 0.25" in printed
|
||||
|
||||
|
||||
def _credential_warnings(caplog: pytest.LogCaptureFixture) -> list[str]:
|
||||
return [record.getMessage() for record in caplog.records if "litellm_credential_name=" in record.getMessage()]
|
||||
|
||||
|
||||
def test_load_credentials_from_list_warns_when_the_named_credential_is_not_loaded(
|
||||
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
from litellm.utils import load_credentials_from_list
|
||||
|
||||
monkeypatch.setattr(litellm, "credential_list", [])
|
||||
request_kwargs = {"litellm_credential_name": "openai-cred", "model": "openai/gpt-5.4-mini"}
|
||||
with caplog.at_level(logging.WARNING, logger=verbose_logger.name):
|
||||
load_credentials_from_list(request_kwargs)
|
||||
|
||||
assert request_kwargs == {"litellm_credential_name": "openai-cred", "model": "openai/gpt-5.4-mini"}
|
||||
assert _credential_warnings(caplog) == [
|
||||
"litellm_credential_name=openai-cred matched none of the 0 loaded credentials; the request runs without it"
|
||||
]
|
||||
|
||||
|
||||
def test_load_credentials_from_list_fills_kwargs_from_the_loaded_credential_without_warning(
|
||||
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
from litellm.types.utils import CredentialItem
|
||||
from litellm.utils import load_credentials_from_list
|
||||
|
||||
loaded = CredentialItem(
|
||||
credential_name="openai-cred",
|
||||
credential_values={"api_key": "sk-from-db", "api_base": "https://credential.example"},
|
||||
credential_info={},
|
||||
)
|
||||
monkeypatch.setattr(litellm, "credential_list", [loaded])
|
||||
request_kwargs = {"litellm_credential_name": "openai-cred", "api_base": "https://request.example"}
|
||||
with caplog.at_level(logging.WARNING, logger=verbose_logger.name):
|
||||
load_credentials_from_list(request_kwargs)
|
||||
|
||||
assert request_kwargs == {
|
||||
"litellm_credential_name": "openai-cred",
|
||||
"api_base": "https://request.example",
|
||||
"api_key": "sk-from-db",
|
||||
}
|
||||
assert _credential_warnings(caplog) == []
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue