From f8e681bf612d3b5f1e5ffcb22ae8363f4415df2e Mon Sep 17 00:00:00 2001 From: moe-berri Date: Thu, 5 Nov 2026 12:03:14 +0800 Subject: [PATCH] fix: preserve the original evidence for an alert episode --- litellm/repositories/usage_alerts.py | 8 ++- .../integration/database/test_usage_alerts.py | 50 +++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/litellm/repositories/usage_alerts.py b/litellm/repositories/usage_alerts.py index e9dbf977a8a..d2e0c38f7e3 100644 --- a/litellm/repositories/usage_alerts.py +++ b/litellm/repositories/usage_alerts.py @@ -414,11 +414,9 @@ class UsageAlertsRepository: ), updated AS ( UPDATE "LiteLLM_UsageAlertIncident" i SET evaluation_day=p.day, breached=p.breached, - day=CASE WHEN p.breached THEN p.day ELSE i.day END, - record=(CASE WHEN p.breached THEN p.record || jsonb_build_object( - 'id',i.id,'first_seen',i.record->'first_seen', - 'activity_path',replace(p.record->>'activity_path',p.record->>'id',i.id)) ELSE i.record END) - || jsonb_build_object('status',CASE WHEN i.status='open' AND NOT p.breached THEN 'resolved' ELSE i.status END), + record=i.record || jsonb_build_object( + 'last_seen',CASE WHEN p.breached THEN p.record->'last_seen' ELSE i.record->'last_seen' END, + 'status',CASE WHEN i.status='open' AND NOT p.breached THEN 'resolved' ELSE i.status END), status=CASE WHEN i.status='open' AND NOT p.breached THEN 'resolved' ELSE i.status END, updated_at=CURRENT_TIMESTAMP FROM prior p WHERE i.id=p.prior_id AND p.evaluation_day None: + repository: Final = UsageAlertsRepository.from_prisma(db.prisma) + definition: Final = UsageAlertRuleInput( + name="Original episode", scope_type="user", scope_id="episode-example", metric="requests", direction="increase" + ) + rule: Final = await repository.save_rule(definition, "qa-admin") + assert rule is not None + initial: Final = UsageAlertObservation( + entity_id=rule.scope_id, + day=date(2026, 11, 2), + observed=420, + baseline=120, + baseline_sample_days=7, + threshold=360, + breached=True, + ) + try: + original: Final = (await repository.record_observations(rule, (initial,)))[0] + edited: Final = await repository.save_rule( + definition.model_copy(update={"name": "Edited rule", "multiplier": 4}), "qa-admin", rule + ) + assert edited is not None + later: Final = initial.model_copy( + update={ + "day": date(2026, 11, 3), + "observed": 600, + "baseline": 100, + "threshold": 400, + "baseline_sample_days": 4, + } + ) + ongoing: Final = (await repository.record_observations(edited, (later,)))[0] + assert ongoing.last_seen >= original.last_seen + assert ongoing.model_copy(update={"last_seen": original.last_seen}) == original + stored: Final = await repository.get_alert(original.id) + assert stored == ongoing + healthy: Final = later.model_copy(update={"day": date(2026, 11, 4), "breached": False}) + resolved: Final = (await repository.record_observations(edited, (healthy,)))[0] + assert resolved.model_copy(update={"status": "open", "last_seen": original.last_seen}) == original + repeated: Final = later.model_copy(update={"day": date(2026, 11, 5)}) + new_episode: Final = (await repository.record_observations(edited, (repeated,)))[0] + assert new_episode.id != original.id + assert new_episode.day == repeated.day and new_episode.observed_value == 600 + assert new_episode.threshold == 400 and new_episode.rule_name == "Edited rule" + assert new_episode.activity_path != original.activity_path + finally: + await repository.delete_rule(rule.id) + + @pytest.mark.asyncio(loop_scope="session") @pytest.mark.parametrize( "changed_field,new_value",