mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-15 09:12:18 +00:00
feat(alerts): support whole-gateway alert scope
This commit is contained in:
parent
3609b9e494
commit
59f5bc42d4
4 changed files with 46 additions and 4 deletions
|
|
@ -6,7 +6,7 @@ from typing_extensions import Self
|
|||
|
||||
AlertType: TypeAlias = Literal["spend", "traffic", "latency", "reliability", "model_deprecation"]
|
||||
AlertStatus: TypeAlias = Literal["open", "resolved", "dismissed"]
|
||||
AlertResourceType: TypeAlias = Literal["key", "team", "user", "model"]
|
||||
AlertResourceType: TypeAlias = Literal["gateway", "key", "team", "user", "model"]
|
||||
MAX_ALERT_ID: Final = 2**63 - 1
|
||||
|
||||
|
||||
|
|
@ -45,7 +45,9 @@ class AlertDetection(BaseModel):
|
|||
type: AlertType = Field(description="Built-in alert category.")
|
||||
resource_type: AlertResourceType = Field(description="Kind of affected resource.")
|
||||
resource_id: str = Field(
|
||||
min_length=1, max_length=1024, description="Resource ID. Keys use their SHA-256 hash, never a secret."
|
||||
min_length=1,
|
||||
max_length=1024,
|
||||
description="Resource ID. Whole-gateway alerts use 'gateway'. Keys use their SHA-256 hash, never a secret.",
|
||||
)
|
||||
window_start: AwareDatetime = Field(
|
||||
description="Inclusive start of the observed window, normalized to UTC milliseconds."
|
||||
|
|
@ -64,6 +66,8 @@ class AlertDetection(BaseModel):
|
|||
def validate_detection(self) -> Self:
|
||||
if self.window_end <= self.window_start:
|
||||
raise ValueError("window_end must be after window_start")
|
||||
if self.resource_type == "gateway" and self.resource_id != "gateway":
|
||||
raise ValueError("Whole-gateway alerts must use resource_id 'gateway'")
|
||||
if self.resource_type == "key" and (
|
||||
len(self.resource_id) != 64 or any(c not in "0123456789abcdef" for c in self.resource_id)
|
||||
):
|
||||
|
|
|
|||
|
|
@ -86,6 +86,37 @@ async def test_each_alert_type_round_trips_through_api_and_status_changes(
|
|||
assert await alerts.repository.get(int(saved.id)) == changed
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("kind", ("spend", "traffic", "latency", "reliability"))
|
||||
async def test_gateway_alert_round_trips_without_colliding_with_resource_alerts(
|
||||
alerts: Alerts, gateway: Gateway, kind: AlertType
|
||||
) -> None:
|
||||
detection: Final = AlertDetection.model_validate(
|
||||
{**alerts.detection(kind).model_dump(), "resource_type": "gateway", "resource_id": "gateway"}
|
||||
)
|
||||
try:
|
||||
saved: Final = await alerts.repository.record(detection)
|
||||
scoped: Final = await alerts.repository.record(detection.model_copy(update={"resource_type": "model"}))
|
||||
assert scoped.id != saved.id
|
||||
detail: Final = gateway.request("GET", f"/alerts/{saved.id}")
|
||||
assert detail.status_code == 200, detail.text
|
||||
assert Alert.model_validate_json(detail.content) == saved
|
||||
listed: Final = gateway.request("GET", "/alerts", params={"limit": "2"})
|
||||
assert listed.status_code == 200, listed.text
|
||||
assert AlertPage.model_validate_json(listed.content).data == (scoped, saved)
|
||||
update: Final = gateway.request("PATCH", f"/alerts/{saved.id}", {"status": "dismissed"})
|
||||
assert update.status_code == 200, update.text
|
||||
dismissed: Final = Alert.model_validate_json(update.content)
|
||||
assert dismissed.status == "dismissed"
|
||||
assert await alerts.repository.record(detection) == dismissed
|
||||
assert await alerts.repository.get(int(scoped.id)) == scoped
|
||||
finally:
|
||||
write_rows(
|
||||
'DELETE FROM "LiteLLM_Alert" WHERE resource_id=%s AND type=%s AND window_start=%s AND window_end=%s',
|
||||
("gateway", kind, detection.window_start, detection.window_end),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_concurrent_detection_retries_preserve_dismissal_and_original_evidence(alerts: Alerts) -> None:
|
||||
detection: Final = alerts.detection()
|
||||
|
|
|
|||
|
|
@ -23,6 +23,13 @@ DETECTION: Final = {
|
|||
{"resource_id": "x" * 1025},
|
||||
{"resource_id": ""},
|
||||
{"resource_type": "key", "resource_id": "sk-example-not-a-real-key"},
|
||||
{"resource_type": "gateway", "resource_id": "another-gateway"},
|
||||
{
|
||||
"resource_type": "gateway",
|
||||
"resource_id": "gateway",
|
||||
"type": "model_deprecation",
|
||||
"evidence": {"deprecation_date": "2026-02-01"},
|
||||
},
|
||||
{"window_start": "2026-01-08T00:00:00"},
|
||||
{"window_start": "2026-01-08T00:00:00.123456Z", "window_end": "2026-01-08T00:00:00.123457Z"},
|
||||
{"window_end": "2026-01-08T00:00:00Z"},
|
||||
|
|
|
|||
4
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
4
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
|
|
@ -27090,7 +27090,7 @@ export interface components {
|
|||
id: string;
|
||||
/**
|
||||
* Resource Id
|
||||
* @description Resource ID. Keys use their SHA-256 hash, never a secret.
|
||||
* @description Resource ID. Whole-gateway alerts use 'gateway'. Keys use their SHA-256 hash, never a secret.
|
||||
*/
|
||||
resource_id: string;
|
||||
/**
|
||||
|
|
@ -27098,7 +27098,7 @@ export interface components {
|
|||
* @description Kind of affected resource.
|
||||
* @enum {string}
|
||||
*/
|
||||
resource_type: "key" | "team" | "user" | "model";
|
||||
resource_type: "gateway" | "key" | "team" | "user" | "model";
|
||||
/**
|
||||
* Status
|
||||
* @description Current review status.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue