From 5f16a3ad833209e3e74e2cc5cb92e620b2c97e84 Mon Sep 17 00:00:00 2001 From: yucheng Date: Thu, 24 Sep 2026 19:05:34 +0000 Subject: [PATCH] fix(proxy): reschedule cleanup when only the cron or interval changes at runtime Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/proxy/proxy_server.py | 8 +++++-- .../proxy/proxy_server/test_proxy_config.py | 22 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index e3bf5949c4a..af51a05f782 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -7649,10 +7649,14 @@ class ProxyConfig: self.settings.get("maximum_spend_logs_cleanup_cron"), self.settings.get("maximum_spend_logs_retention_interval"), ) - job_missing: Final = wants_job and not has_job and attempt != self._last_cleanup_schedule_attempt - if previous_retention_values != resolved or job_missing or (has_job and not wants_job): + last_attempt: Final = self._last_cleanup_schedule_attempt + schedule_changed: Final = last_attempt is not None and attempt != last_attempt + job_missing: Final = wants_job and not has_job and attempt != last_attempt + if previous_retention_values != resolved or schedule_changed or job_missing or (has_job and not wants_job): self._last_cleanup_schedule_attempt = attempt await self._reschedule_spend_log_cleanup_job() + elif last_attempt is None: + self._last_cleanup_schedule_attempt = attempt async def _apply_ssrf_settings(self, db_values: Mapping[str, SettingsJsonValue]) -> None: _apply_ssrf_general_settings(db_values) diff --git a/tests/test_litellm/proxy/proxy_server/test_proxy_config.py b/tests/test_litellm/proxy/proxy_server/test_proxy_config.py index 33403915635..ab7803d28a6 100644 --- a/tests/test_litellm/proxy/proxy_server/test_proxy_config.py +++ b/tests/test_litellm/proxy/proxy_server/test_proxy_config.py @@ -3940,6 +3940,28 @@ async def test_ProxyConfig__update_general_settings_retries_a_failed_schedule_on assert fake_scheduler.add_job.call_args.kwargs["id"] == "spend_log_cleanup_job" +@pytest.mark.asyncio +async def test_ProxyConfig__update_general_settings_reschedules_when_only_the_cron_changes(monkeypatch): + fake_scheduler = MagicMock() + fake_scheduler.get_job.return_value = MagicMock() + monkeypatch.setattr("litellm.proxy.proxy_server.scheduler", fake_scheduler) + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None) + pc = ProxyConfig() + hourly = {"maximum_daily_tag_spend_retention_period": "90d", "maximum_spend_logs_cleanup_cron": "0 * * * *"} + pc.settings.apply_db_row("general_settings", hourly) + monkeypatch.setattr("litellm.proxy.proxy_server.general_settings", pc.settings) + await pc._update_general_settings(hourly) + await pc._update_general_settings(hourly) + assert fake_scheduler.add_job.call_count == 0 + + daily = {**hourly, "maximum_spend_logs_cleanup_cron": "0 3 * * *"} + pc.settings.apply_db_row("general_settings", daily) + await pc._update_general_settings(daily) + assert fake_scheduler.add_job.call_count == 1 + assert fake_scheduler.add_job.call_args.kwargs["id"] == "spend_log_cleanup_job" + assert "hour='3'" in str(fake_scheduler.add_job.call_args.args[1]) + + # --------------------------------------------------------------------------- # ProxyConfig._update_general_settings # ---------------------------------------------------------------------------