This commit is contained in:
devin-ai-integration[bot] 2026-08-27 20:04:18 +00:00 committed by GitHub
commit 8244f5ef1e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 512 additions and 17 deletions

View file

@ -1534,6 +1534,7 @@ KEY_ROTATION_JOB_NAME: Final = "litellm_key_rotation_job"
EXPIRED_UI_SESSION_KEY_CLEANUP_JOB_NAME: Final = "litellm_expired_ui_session_key_cleanup_job"
WEEKLY_SPEND_REPORT_JOB_ID: Final = "weekly_spend_report_job"
MONTHLY_SPEND_REPORT_JOB_ID: Final = "monthly_spend_report_job"
USER_SPEND_ALERTS_JOB_ID: Final = "user_spend_alerts_job"
PROMETHEUS_FALLBACK_STATS_JOB_ID: Final = "prometheus_fallback_stats_job"
SLACK_DAILY_REPORT_LOCK_ID: Final = "slack_daily_report"
SLACK_MODEL_DEPRECATION_LOCK_ID: Final = "slack_model_deprecation_warning"

View file

@ -61,6 +61,7 @@ from .utils import process_slack_alerting_variables
if TYPE_CHECKING:
from litellm.proxy.db.db_transaction_queue.pod_lock_manager import PodLockManager
from litellm.proxy.utils import PrismaClient
from litellm.router import Router as _Router
Router = _Router
@ -1897,6 +1898,69 @@ Model Info:
except Exception as e:
verbose_proxy_logger.exception("Error sending weekly spend report %s", e)
async def send_user_spend_alerts(self, prisma_client: "PrismaClient | None" = None) -> None:
"""Check per-user daily/monthly spend thresholds and spend anomalies, alerting once per user per period."""
if self.alerting is None or "slack" not in self.alerting:
return
thresholds_enabled: Final = AlertType.user_spend_thresholds in self.alert_types
anomalies_enabled: Final = AlertType.user_spend_anomalies in self.alert_types
if not thresholds_enabled and not anomalies_enabled:
return
if prisma_client is None:
from litellm.proxy.proxy_server import prisma_client as global_prisma_client
prisma_client = global_prisma_client # rebind-ok: fall back to the proxy's global client
if prisma_client is None:
return
from litellm.integrations.SlackAlerting.user_spend_alerts import (
evaluate_user_spend,
fetch_user_spend_rows,
)
try:
today: Final = datetime.datetime.now(datetime.timezone.utc).date()
rows: Final = await fetch_user_spend_rows(
prisma_client=prisma_client,
today=today,
baseline_days=self.alerting_args.spend_anomaly_baseline_days,
)
all_events: Final = tuple(
event
for row in rows
for event in evaluate_user_spend(
row=row,
args=self.alerting_args,
today=today,
thresholds_enabled=thresholds_enabled,
anomalies_enabled=anomalies_enabled,
)
)
cached_flags: Final = await asyncio.gather(
*(self.internal_usage_cache.async_get_cache(key=event.cache_key) for event in all_events)
)
new_events: Final = tuple(event for event, cached in zip(all_events, cached_flags) if not cached)
for alert_type in (AlertType.user_spend_thresholds, AlertType.user_spend_anomalies):
typed_events = tuple(event for event in new_events if event.alert_type == alert_type)
if not typed_events:
continue
await self.send_alert(
message="\n\n".join(event.message for event in typed_events),
level="High",
alert_type=alert_type,
alerting_metadata={}, # mutable-ok: send_alert takes a dict payload
)
for event in typed_events:
await self.internal_usage_cache.async_set_cache(
key=event.cache_key,
value="SENT",
ttl=event.cache_ttl,
)
except Exception as e: # noqa: BLE001 # background job must not crash the scheduler
verbose_proxy_logger.exception("Error sending user spend alerts: %s", e)
async def send_fallback_stats_from_prometheus(self):
"""
Helper to send fallback statistics from prometheus server -> to slack

View file

@ -0,0 +1,139 @@
"""Per-user daily/monthly spend threshold alerts and spend anomaly detection."""
import datetime
from dataclasses import dataclass
from typing import TYPE_CHECKING, Final, Literal
from pydantic import TypeAdapter
from litellm.constants import HOURS_IN_A_DAY
from litellm.types.integrations.slack_alerting import AlertType, SlackAlertingArgs
if TYPE_CHECKING:
from litellm.proxy.utils import PrismaClient
DAY_SECONDS: Final = HOURS_IN_A_DAY * 60 * 60
MONTHLY_ALERT_TTL_SECONDS: Final = 32 * DAY_SECONDS
USER_SPEND_QUERY: Final = """
SELECT
user_id,
COALESCE(SUM(spend) FILTER (WHERE date = $1), 0)::float AS daily_spend,
COALESCE(SUM(spend) FILTER (WHERE date >= $2), 0)::float AS monthly_spend,
COALESCE(SUM(spend) FILTER (WHERE date >= $3 AND date < $1), 0)::float AS baseline_spend
FROM "LiteLLM_DailyUserSpend"
WHERE date >= LEAST($2, $3) AND user_id IS NOT NULL
GROUP BY user_id
HAVING COALESCE(SUM(spend) FILTER (WHERE date >= $2), 0) > 0
"""
@dataclass(frozen=True, slots=True)
class UserSpendRow:
user_id: str
daily_spend: float
monthly_spend: float
baseline_spend: float
@dataclass(frozen=True, slots=True)
class UserSpendAlertEvent:
kind: Literal["daily_threshold", "monthly_threshold", "anomaly"]
alert_type: AlertType
message: str
cache_key: str
cache_ttl: int
USER_SPEND_ROWS_ADAPTER: Final = TypeAdapter(tuple[UserSpendRow, ...])
async def fetch_user_spend_rows(
prisma_client: "PrismaClient",
today: datetime.date,
baseline_days: int,
) -> tuple[UserSpendRow, ...]:
today_str: Final = today.strftime("%Y-%m-%d")
month_start_str: Final = today.replace(day=1).strftime("%Y-%m-%d")
baseline_start_str: Final = (today - datetime.timedelta(days=max(baseline_days, 1))).strftime("%Y-%m-%d")
raw: Final = await prisma_client.db.query_raw(USER_SPEND_QUERY, today_str, month_start_str, baseline_start_str)
return USER_SPEND_ROWS_ADAPTER.validate_python(raw)
def _daily_threshold_event(row: UserSpendRow, args: SlackAlertingArgs, today_str: str) -> UserSpendAlertEvent | None:
threshold: Final = args.daily_spend_per_user_threshold
if threshold is None or row.daily_spend < threshold:
return None
return UserSpendAlertEvent(
kind="daily_threshold",
alert_type=AlertType.user_spend_thresholds,
message=(
f"User Daily Spend Threshold Crossed:\n"
f"User: `{row.user_id}`\n"
f"Spend Today: `${row.daily_spend:.2f}`\n"
f"Daily Threshold: `${threshold:.2f}`"
),
cache_key=f"user_spend_alert_daily_{row.user_id}_{today_str}",
cache_ttl=DAY_SECONDS,
)
def _monthly_threshold_event(row: UserSpendRow, args: SlackAlertingArgs, month_str: str) -> UserSpendAlertEvent | None:
threshold: Final = args.monthly_spend_per_user_threshold
if threshold is None or row.monthly_spend < threshold:
return None
return UserSpendAlertEvent(
kind="monthly_threshold",
alert_type=AlertType.user_spend_thresholds,
message=(
f"User Monthly Spend Threshold Crossed:\n"
f"User: `{row.user_id}`\n"
f"Spend This Month: `${row.monthly_spend:.2f}`\n"
f"Monthly Threshold: `${threshold:.2f}`"
),
cache_key=f"user_spend_alert_monthly_{row.user_id}_{month_str}",
cache_ttl=MONTHLY_ALERT_TTL_SECONDS,
)
def _anomaly_event(row: UserSpendRow, args: SlackAlertingArgs, today_str: str) -> UserSpendAlertEvent | None:
if row.daily_spend < args.spend_anomaly_min_spend:
return None
baseline_daily_avg: Final = row.baseline_spend / args.spend_anomaly_baseline_days
if row.baseline_spend > 0 and row.daily_spend <= args.spend_anomaly_multiplier * baseline_daily_avg:
return None
return UserSpendAlertEvent(
kind="anomaly",
alert_type=AlertType.user_spend_anomalies,
message=(
f"User Spend Anomaly Detected:\n"
f"User: `{row.user_id}`\n"
f"Spend Today: `${row.daily_spend:.2f}`\n"
f"Daily Average (last {args.spend_anomaly_baseline_days} days): `${baseline_daily_avg:.2f}`\n"
f"Trigger: spend above `{args.spend_anomaly_multiplier}x` the daily average "
f"(minimum `${args.spend_anomaly_min_spend:.2f}`)"
),
cache_key=f"user_spend_alert_anomaly_{row.user_id}_{today_str}",
cache_ttl=DAY_SECONDS,
)
def evaluate_user_spend(
row: UserSpendRow,
args: SlackAlertingArgs,
today: datetime.date,
thresholds_enabled: bool,
anomalies_enabled: bool,
) -> tuple[UserSpendAlertEvent, ...]:
today_str: Final = today.strftime("%Y-%m-%d")
month_str: Final = today.strftime("%Y-%m")
threshold_events: Final = (
(
_daily_threshold_event(row=row, args=args, today_str=today_str),
_monthly_threshold_event(row=row, args=args, month_str=month_str),
)
if thresholds_enabled
else ()
)
anomaly_events: Final = (_anomaly_event(row=row, args=args, today_str=today_str),) if anomalies_enabled else ()
return tuple(event for event in (*threshold_events, *anomaly_events) if event is not None)

View file

@ -248,6 +248,7 @@ from litellm.constants import (
PROXY_BUDGET_RESCHEDULER_MAX_TIME,
PROXY_BUDGET_RESCHEDULER_MIN_TIME,
PROXY_CONFIG_RELOAD_INTERVAL_SECONDS,
USER_SPEND_ALERTS_JOB_ID,
WEEKLY_SPEND_REPORT_JOB_ID,
)
from litellm.exceptions import RejectedRequestError
@ -9633,6 +9634,35 @@ class ProxyStartupEvent:
replace_existing=True,
)
slack_alerting_args: Final = proxy_logging_obj.slack_alerting_instance.alerting_args
user_spend_check_interval: Final = (
slack_alerting_args.user_spend_check_interval
if isinstance(slack_alerting_args, SlackAlertingArgs) # pyright: ignore[reportUnnecessaryIsInstance] # tests inject a mock slack_alerting_instance
else SlackAlertingArgs().user_spend_check_interval
)
async def _scheduled_user_spend_alerts() -> None:
if (
await pod_lock_manager.acquire_lock(
cronjob_id=USER_SPEND_ALERTS_JOB_ID,
ttl=max(user_spend_check_interval - 60, 60),
allow_reentrant=False,
)
is False
):
return
await proxy_logging_obj.slack_alerting_instance.send_user_spend_alerts()
scheduler.add_job(
_scheduled_user_spend_alerts,
"interval",
seconds=user_spend_check_interval,
next_run_time=datetime.now() + timedelta(seconds=10 + random.randint(0, 60)),
id=USER_SPEND_ALERTS_JOB_ID,
replace_existing=True,
misfire_grace_time=APSCHEDULER_MISFIRE_GRACE_TIME,
)
if os.getenv("PROMETHEUS_URL"):
from zoneinfo import ZoneInfo
@ -14662,17 +14692,25 @@ async def alerting_settings(
alerting_args_dict = {}
alerting_values = None
allowed_args: Final = {
"slack_alerting": {"type": "Boolean"},
"daily_report_frequency": {"type": "Integer"},
"report_check_interval": {"type": "Integer"},
"budget_alert_ttl": {"type": "Integer"},
"outage_alert_ttl": {"type": "Integer"},
"region_outage_alert_ttl": {"type": "Integer"},
"minor_outage_alert_threshold": {"type": "Integer"},
"major_outage_alert_threshold": {"type": "Integer"},
"max_outage_alert_list_size": {"type": "Integer"},
}
allowed_args: Final = MappingProxyType(
{
"slack_alerting": "Boolean",
"daily_report_frequency": "Integer",
"report_check_interval": "Integer",
"budget_alert_ttl": "Integer",
"outage_alert_ttl": "Integer",
"region_outage_alert_ttl": "Integer",
"minor_outage_alert_threshold": "Integer",
"major_outage_alert_threshold": "Integer",
"max_outage_alert_list_size": "Integer",
"daily_spend_per_user_threshold": "Float",
"monthly_spend_per_user_threshold": "Float",
"spend_anomaly_multiplier": "Float",
"spend_anomaly_baseline_days": "Integer",
"spend_anomaly_min_spend": "Float",
"user_spend_check_interval": "Integer",
}
)
_slack_alerting: Final[SlackAlerting] = proxy_logging_obj.slack_alerting_instance
_slack_alerting_args_dict: Final = _slack_alerting.alerting_args.model_dump()
@ -14687,7 +14725,7 @@ async def alerting_settings(
_response_obj = ConfigList(
field_name="slack_alerting",
field_type=allowed_args["slack_alerting"]["type"],
field_type=allowed_args["slack_alerting"],
field_description="Enable slack alerting for monitoring proxy in production: llm outages, budgets, spend tracking failures.",
field_value=is_slack_enabled,
stored_in_db=True if alerting_values is not None else False,
@ -14706,7 +14744,7 @@ async def alerting_settings(
_response_obj = ConfigList(
field_name=field_name,
field_type=allowed_args[field_name]["type"],
field_type=allowed_args[field_name],
field_description=field_info.description or "",
field_value=_slack_alerting_args_dict.get(field_name, None),
stored_in_db=_stored_in_db,

View file

@ -91,6 +91,40 @@ class SlackAlertingArgs(LiteLLMPydanticObjectBase):
default=False,
description="If true, the alerting payload will be printed to the console.",
)
daily_spend_per_user_threshold: float | None = Field(
default=None,
gt=0,
allow_inf_nan=False,
description="Alert when a user's spend for the current day (UTC) crosses this USD amount. Off by default.",
)
monthly_spend_per_user_threshold: float | None = Field(
default=None,
gt=0,
allow_inf_nan=False,
description="Alert when a user's spend for the current calendar month (UTC) crosses this USD amount. Off by default.",
)
spend_anomaly_multiplier: float = Field(
default=3.0,
gt=0,
allow_inf_nan=False,
description="Flag a user's spend as anomalous when today's spend exceeds this multiple of their trailing daily average.",
)
spend_anomaly_baseline_days: int = Field(
default=7,
ge=1,
description="Number of trailing days used to compute a user's daily average spend for anomaly detection.",
)
spend_anomaly_min_spend: float = Field(
default=10.0,
gt=0,
allow_inf_nan=False,
description="Minimum spend (USD) a user must reach today before an anomaly alert can fire. Reduces false positives.",
)
user_spend_check_interval: int = Field(
default=3600,
ge=60,
description="How often (in seconds) to check per-user spend thresholds and anomalies. Default is hourly.",
)
class DeploymentMetrics(LiteLLMPydanticObjectBase):
@ -138,6 +172,8 @@ class AlertType(str, Enum):
budget_alerts = "budget_alerts"
spend_reports = "spend_reports"
failed_tracking_spend = "failed_tracking_spend"
user_spend_thresholds = "user_spend_thresholds"
user_spend_anomalies = "user_spend_anomalies"
# Database alerts
db_exceptions = "db_exceptions"
@ -182,6 +218,7 @@ DEFAULT_ALERT_TYPES: Final[list[AlertType]] = [
AlertType.budget_alerts,
AlertType.spend_reports,
AlertType.failed_tracking_spend,
AlertType.user_spend_thresholds,
# Database alerts
AlertType.db_exceptions,
# Report alerts

View file

@ -0,0 +1,193 @@
import datetime
from typing import Final
from unittest.mock import AsyncMock, patch
import pytest
from pydantic import ValidationError
from litellm.integrations.SlackAlerting.slack_alerting import SlackAlerting
from litellm.integrations.SlackAlerting.user_spend_alerts import (
UserSpendRow,
evaluate_user_spend,
)
from litellm.types.integrations.slack_alerting import (
DEFAULT_ALERT_TYPES,
AlertType,
SlackAlertingArgs,
)
TODAY: Final = datetime.date(2026, 8, 15)
def _row(
daily_spend: float = 0.0,
monthly_spend: float = 0.0,
baseline_spend: float = 0.0,
) -> UserSpendRow:
return UserSpendRow(
user_id="user-1",
daily_spend=daily_spend,
monthly_spend=monthly_spend,
baseline_spend=baseline_spend,
)
def _evaluate(row: UserSpendRow, args: SlackAlertingArgs, thresholds: bool = True, anomalies: bool = True):
return evaluate_user_spend(
row=row,
args=args,
today=TODAY,
thresholds_enabled=thresholds,
anomalies_enabled=anomalies,
)
def test_daily_threshold_crossed():
args: Final = SlackAlertingArgs(daily_spend_per_user_threshold=50.0, spend_anomaly_min_spend=1000.0)
events: Final = _evaluate(_row(daily_spend=75.0, monthly_spend=75.0), args)
assert [e.kind for e in events] == ["daily_threshold"]
assert "`$75.00`" in events[0].message
assert "`$50.00`" in events[0].message
assert events[0].alert_type == AlertType.user_spend_thresholds
assert events[0].cache_key == "user_spend_alert_daily_user-1_2026-08-15"
def test_daily_threshold_not_crossed():
args: Final = SlackAlertingArgs(daily_spend_per_user_threshold=50.0, spend_anomaly_min_spend=1000.0)
assert _evaluate(_row(daily_spend=49.99, monthly_spend=49.99), args) == ()
def test_thresholds_unset_by_default():
args: Final = SlackAlertingArgs(spend_anomaly_min_spend=1000.0)
assert _evaluate(_row(daily_spend=999.0, monthly_spend=999.0), args) == ()
def test_monthly_threshold_crossed():
args: Final = SlackAlertingArgs(monthly_spend_per_user_threshold=200.0, spend_anomaly_min_spend=1000.0)
events: Final = _evaluate(_row(daily_spend=5.0, monthly_spend=250.0), args)
assert [e.kind for e in events] == ["monthly_threshold"]
assert events[0].cache_key == "user_spend_alert_monthly_user-1_2026-08"
def test_thresholds_disabled_suppresses_threshold_events():
args: Final = SlackAlertingArgs(
daily_spend_per_user_threshold=50.0,
monthly_spend_per_user_threshold=200.0,
spend_anomaly_min_spend=1000.0,
)
assert _evaluate(_row(daily_spend=75.0, monthly_spend=250.0), args, thresholds=False) == ()
def test_anomaly_detected_above_multiple_of_baseline():
args: Final = SlackAlertingArgs(spend_anomaly_multiplier=3.0, spend_anomaly_min_spend=10.0)
events: Final = _evaluate(
_row(daily_spend=70.0, monthly_spend=100.0, baseline_spend=70.0), args
)
assert [e.kind for e in events] == ["anomaly"]
assert events[0].alert_type == AlertType.user_spend_anomalies
assert "`$10.00`" in events[0].message
assert events[0].cache_key == "user_spend_alert_anomaly_user-1_2026-08-15"
def test_no_anomaly_within_baseline_multiple():
args: Final = SlackAlertingArgs(spend_anomaly_multiplier=3.0, spend_anomaly_min_spend=10.0)
assert (
_evaluate(_row(daily_spend=25.0, monthly_spend=100.0, baseline_spend=70.0), args) == ()
)
def test_no_anomaly_below_min_spend_floor():
args: Final = SlackAlertingArgs(spend_anomaly_multiplier=3.0, spend_anomaly_min_spend=10.0)
assert _evaluate(_row(daily_spend=9.0, monthly_spend=9.0, baseline_spend=0.1), args) == ()
def test_anomaly_for_new_user_without_baseline():
args: Final = SlackAlertingArgs(spend_anomaly_multiplier=3.0, spend_anomaly_min_spend=10.0)
events: Final = _evaluate(_row(daily_spend=15.0, monthly_spend=15.0), args)
assert [e.kind for e in events] == ["anomaly"]
def test_sparse_baseline_averages_over_full_window():
args: Final = SlackAlertingArgs(
spend_anomaly_multiplier=3.0, spend_anomaly_min_spend=10.0, spend_anomaly_baseline_days=7
)
events: Final = _evaluate(_row(daily_spend=13.0, monthly_spend=20.0, baseline_spend=7.0), args)
assert [e.kind for e in events] == ["anomaly"]
def test_anomalies_not_in_default_alert_types():
assert AlertType.user_spend_anomalies not in DEFAULT_ALERT_TYPES
assert AlertType.user_spend_thresholds in DEFAULT_ALERT_TYPES
def test_invalid_config_rejected():
with pytest.raises(ValidationError, match="daily_spend_per_user_threshold"):
SlackAlertingArgs(daily_spend_per_user_threshold=0)
with pytest.raises(ValidationError, match="spend_anomaly_baseline_days"):
SlackAlertingArgs(spend_anomaly_baseline_days=0)
with pytest.raises(ValidationError, match="user_spend_check_interval"):
SlackAlertingArgs(user_spend_check_interval=10)
def test_non_finite_config_rejected():
with pytest.raises(ValidationError, match="daily_spend_per_user_threshold"):
SlackAlertingArgs(daily_spend_per_user_threshold=float("inf"))
with pytest.raises(ValidationError, match="spend_anomaly_multiplier"):
SlackAlertingArgs(spend_anomaly_multiplier=float("nan"))
with pytest.raises(ValidationError, match="spend_anomaly_min_spend"):
SlackAlertingArgs(spend_anomaly_min_spend=float("inf"))
with pytest.raises(ValidationError, match="user_spend_check_interval"):
SlackAlertingArgs(user_spend_check_interval=float("inf"))
def test_anomalies_disabled_suppresses_anomaly_events():
args: Final = SlackAlertingArgs(spend_anomaly_multiplier=3.0, spend_anomaly_min_spend=10.0)
assert _evaluate(_row(daily_spend=500.0, monthly_spend=500.0), args, anomalies=False) == ()
@pytest.mark.asyncio
async def test_send_user_spend_alerts_sends_and_dedupes():
slack_alerting: Final = SlackAlerting(
alerting=["slack"],
alerting_args={"daily_spend_per_user_threshold": 50.0, "spend_anomaly_min_spend": 1000.0},
)
mock_prisma: Final = AsyncMock()
mock_prisma.db.query_raw = AsyncMock(
return_value=[
{
"user_id": "user-1",
"daily_spend": 75.0,
"monthly_spend": 75.0,
"baseline_spend": 0.0,
},
{
"user_id": "user-2",
"daily_spend": 60.0,
"monthly_spend": 60.0,
"baseline_spend": 0.0,
},
]
)
with patch.object(slack_alerting, "send_alert", new_callable=AsyncMock) as mock_send_alert:
await slack_alerting.send_user_spend_alerts(prisma_client=mock_prisma)
assert mock_send_alert.call_count == 1
sent_kwargs: Final = mock_send_alert.call_args.kwargs
assert sent_kwargs["alert_type"] == AlertType.user_spend_thresholds
assert "User Daily Spend Threshold Crossed" in sent_kwargs["message"]
assert "`user-1`" in sent_kwargs["message"]
assert "`user-2`" in sent_kwargs["message"]
await slack_alerting.send_user_spend_alerts(prisma_client=mock_prisma)
assert mock_send_alert.call_count == 1
@pytest.mark.asyncio
async def test_send_user_spend_alerts_noop_when_alert_types_disabled():
slack_alerting: Final = SlackAlerting(
alerting=["slack"],
alert_types=[AlertType.budget_alerts],
alerting_args={"daily_spend_per_user_threshold": 50.0},
)
mock_prisma: Final = AsyncMock()
await slack_alerting.send_user_spend_alerts(prisma_client=mock_prisma)
mock_prisma.db.query_raw.assert_not_called()

View file

@ -38,6 +38,14 @@ const SETTINGS: Setting[] = [
stored_in_db: null,
premium_field: false,
},
{
field_name: "daily_spend_per_user_threshold",
field_description: "Daily spend threshold per user",
field_type: "Float",
field_value: 5.5,
stored_in_db: true,
premium_field: false,
},
];
const renderForm = (
@ -170,6 +178,19 @@ describe("DynamicForm change notifications", () => {
expect(handleInputChange).toHaveBeenCalledWith("daily_report_frequency", 128);
});
it("renders a Float field as a decimal-friendly number input and reports changes as numbers", async () => {
const user = userEvent.setup();
const { handleInputChange } = renderForm();
const input = screen.getByDisplayValue("5.5");
expect(input).toHaveAttribute("type", "number");
expect(input).toHaveAttribute("step", "any");
await user.type(input, "1");
expect(handleInputChange).toHaveBeenCalledWith("daily_spend_per_user_threshold", 5.51);
});
it("reports a reset with the field name and its row index", async () => {
const user = userEvent.setup();
const { handleResetField } = renderForm();
@ -216,7 +237,7 @@ describe("DynamicForm presentation", () => {
expect(screen.getByText("daily_report_frequency")).toBeInTheDocument();
expect(screen.getByText("How often the report runs")).toBeInTheDocument();
expect(screen.getByText("In DB")).toBeInTheDocument();
expect(screen.getAllByText("In DB")).toHaveLength(2);
expect(screen.getByText("In Config")).toBeInTheDocument();
expect(screen.getByText("Not Set")).toBeInTheDocument();
});

View file

@ -63,11 +63,11 @@ const DynamicForm: React.FC<DynamicFormProps> = ({
};
const renderControl = (setting: AlertingSetting) => {
if (setting.field_type === "Integer") {
if (setting.field_type === "Integer" || setting.field_type === "Float") {
return (
<Input
type="number"
step={1}
step={setting.field_type === "Integer" ? 1 : "any"}
value={setting.field_value ?? ""}
onChange={(event) => handleNumericChange(setting, event.target.value)}
/>

View file

@ -292,6 +292,8 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
llm_too_slow: "LLM Responses Too Slow",
llm_requests_hanging: "LLM Requests Hanging",
budget_alerts: "Budget Alerts (API Keys, Users)",
user_spend_thresholds: "User Spend Thresholds (Daily/Monthly)",
user_spend_anomalies: "User Spend Anomaly Detection",
db_exceptions: "Database Exceptions (Read/Write)",
daily_reports: "Weekly/Monthly Spend Reports",
outage_alerts: "Outage Alerts",

View file

@ -22533,7 +22533,7 @@ export interface components {
* @description Enum for alert types and management event types
* @enum {string}
*/
AlertType: "llm_exceptions" | "llm_too_slow" | "llm_requests_hanging" | "budget_alerts" | "spend_reports" | "failed_tracking_spend" | "db_exceptions" | "daily_reports" | "cooldown_deployment" | "new_model_added" | "model_deprecation_warnings" | "outage_alerts" | "region_outage_alerts" | "fallback_reports" | "new_virtual_key_created" | "virtual_key_updated" | "virtual_key_deleted" | "new_team_created" | "team_updated" | "team_deleted" | "new_internal_user_created" | "internal_user_updated" | "internal_user_deleted";
AlertType: "llm_exceptions" | "llm_too_slow" | "llm_requests_hanging" | "budget_alerts" | "spend_reports" | "failed_tracking_spend" | "user_spend_thresholds" | "user_spend_anomalies" | "db_exceptions" | "daily_reports" | "cooldown_deployment" | "new_model_added" | "model_deprecation_warnings" | "outage_alerts" | "region_outage_alerts" | "fallback_reports" | "new_virtual_key_created" | "virtual_key_updated" | "virtual_key_deleted" | "new_team_created" | "team_updated" | "team_deleted" | "new_internal_user_created" | "internal_user_updated" | "internal_user_deleted";
/** AllowedVectorStoreIndexItem */
AllowedVectorStoreIndexItem: {
/** Index Name */