From 0cc6787286dd384d7ca300cfd2edaac749dbd451 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Fri, 1 May 2026 12:39:02 -0700 Subject: [PATCH] address greptile review feedback (greploop iteration 3) - Guard naive expires_at in CREATE path to prevent 500 TypeError - Auto-recompute next_run_at when schedule fields change via PATCH - Reject schedule_kind='once' with fire_once=False to prevent infinite loop Co-Authored-By: Claude Opus 4.6 --- litellm/proxy/scheduled_tasks/endpoints.py | 29 +++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/scheduled_tasks/endpoints.py b/litellm/proxy/scheduled_tasks/endpoints.py index 1c6c0af1268..cab24136fd8 100644 --- a/litellm/proxy/scheduled_tasks/endpoints.py +++ b/litellm/proxy/scheduled_tasks/endpoints.py @@ -128,11 +128,20 @@ async def create_scheduled_task( ) now = datetime.now(timezone.utc) - if data.expires_at <= now: + expires_at = data.expires_at + if expires_at.tzinfo is None: + expires_at = expires_at.replace(tzinfo=timezone.utc) + if expires_at <= now: raise HTTPException(status_code=400, detail="expires_at must be in the future") fire_once = data.fire_once if data.fire_once is not None else (data.schedule_kind == "once") + if data.schedule_kind == "once" and not fire_once: + raise HTTPException( + status_code=400, + detail="fire_once=False is invalid with schedule_kind='once'", + ) + next_run_at = compute_next_run( kind=data.schedule_kind, spec=data.schedule_spec, @@ -296,6 +305,24 @@ async def update_scheduled_task( except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) + if ( + any(k in fields for k in ("schedule_kind", "schedule_spec", "schedule_tz")) + and "next_run_at" not in fields + ): + fields["next_run_at"] = compute_next_run( + kind=merged_kind, + spec=merged_spec, + tz=merged_tz, + from_time=datetime.now(timezone.utc), + ) + + merged_fire_once = fields.get("fire_once", existing.fire_once) + if merged_kind == "once" and not merged_fire_once: + raise HTTPException( + status_code=400, + detail="fire_once=False is invalid with schedule_kind='once'", + ) + merged_action = fields.get("action", existing.action) merged_check = fields.get("check_prompt", existing.check_prompt) if merged_action == "check" and not merged_check: