mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-28 01:32:17 +00:00
fix(proxy): ignore team member alert thresholds outside 1 to 100 on both the backend and the dashboard
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
2ce37c0b4e
commit
d1022dab91
4 changed files with 42 additions and 4 deletions
|
|
@ -5539,14 +5539,17 @@ def _team_member_max_budget_alert_check(
|
|||
max_budget: float,
|
||||
) -> None:
|
||||
raw_config: Final = (team_metadata or {}).get(TEAM_MEMBER_MAX_BUDGET_ALERT_EMAILS_KEY)
|
||||
alert_email_config: Final = _merge_budget_alert_email_configs(
|
||||
merged_config: Final = _merge_budget_alert_email_configs(
|
||||
global_cfg=None,
|
||||
per_key_cfg=raw_config if isinstance(raw_config, Mapping) else None,
|
||||
)
|
||||
alert_email_config: Final = {
|
||||
pct: emails for pct, emails in (merged_config or {}).items() if pct.isdigit() and 1 <= int(pct) <= 100
|
||||
}
|
||||
if not alert_email_config or spend <= 0:
|
||||
return
|
||||
min_pct: Final = min((int(k) for k in alert_email_config if k.isdigit()), default=None)
|
||||
if min_pct is None or spend < max_budget * (min_pct / 100.0):
|
||||
min_pct: Final = min(int(pct) for pct in alert_email_config)
|
||||
if spend < max_budget * (min_pct / 100.0):
|
||||
return
|
||||
call_info: Final = CallInfo(
|
||||
spend=spend,
|
||||
|
|
|
|||
|
|
@ -3345,6 +3345,8 @@ async def test_virtual_key_max_budget_alert_check_without_user_obj():
|
|||
(0.049, {"team_member_max_budget_alert_emails": {"50": [], "100": ["finance@co.com"]}}, False),
|
||||
(0.0, {"team_member_max_budget_alert_emails": {"50": []}}, False),
|
||||
(0.10, {"team_member_max_budget_alert_emails": {"abc": []}}, False),
|
||||
(0.05, {"team_member_max_budget_alert_emails": {"0": ["finance@co.com"], "100": []}}, False),
|
||||
(0.10, {"team_member_max_budget_alert_emails": {"101": ["finance@co.com"]}}, False),
|
||||
(0.10, {"team_member_max_budget_alert_emails": "50"}, False),
|
||||
(0.10, {"soft_budget_alerting_emails": ["finance@co.com"]}, False),
|
||||
(0.10, None, False),
|
||||
|
|
@ -3386,6 +3388,30 @@ async def test_team_member_max_budget_alert_check_dispatches_only_at_configured_
|
|||
assert call_info.token is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_team_member_max_budget_alert_check_drops_thresholds_outside_1_to_100():
|
||||
captured: list[CallInfo] = []
|
||||
|
||||
class RecordingProxyLogging:
|
||||
async def budget_alerts(self, type, user_info):
|
||||
captured.append(user_info)
|
||||
|
||||
_team_member_max_budget_alert_check(
|
||||
team_id="team-1",
|
||||
team_alias="platform",
|
||||
team_metadata={"team_member_max_budget_alert_emails": {"0": ["a@co.com"], "50": [], "150": ["b@co.com"]}},
|
||||
organization_id="org-1",
|
||||
user_id="user-1",
|
||||
user_email="member@co.com",
|
||||
proxy_logging_obj=RecordingProxyLogging(),
|
||||
spend=0.05,
|
||||
max_budget=0.10,
|
||||
)
|
||||
await asyncio.sleep(0)
|
||||
|
||||
assert [call_info.max_budget_alert_emails for call_info in captured] == [{"50": []}], captured
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_team_member_budget_dispatches_the_configured_alert_before_the_hard_cap():
|
||||
from litellm.proxy._types import LiteLLM_BudgetTable, LiteLLM_TeamMembership
|
||||
|
|
|
|||
|
|
@ -24,6 +24,13 @@ describe("teamMemberBudgetAlertRowsFromMetadata", () => {
|
|||
expect(teamMemberBudgetAlertRowsFromMetadata(metadata)).toEqual([{ threshold: 100, emails: "a@b.c" }]);
|
||||
});
|
||||
|
||||
it("drops API-stored thresholds outside 1 to 100 so they never block the form", () => {
|
||||
const metadata = {
|
||||
team_member_max_budget_alert_emails: { "0": ["a@b.c"], "50": [], "101": ["a@b.c"] },
|
||||
};
|
||||
expect(teamMemberBudgetAlertRowsFromMetadata(metadata)).toEqual([{ threshold: 50, emails: "" }]);
|
||||
});
|
||||
|
||||
it.each([undefined, null, "50", { team_member_max_budget_alert_emails: "50" }, { soft_budget_alerting_emails: [] }])(
|
||||
"returns no rows for unrelated or malformed metadata %j",
|
||||
(metadata) => {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,9 @@ export const teamMemberBudgetAlertRowsFromMetadata = (metadata: unknown): readon
|
|||
return Object.entries(config as Record<string, unknown>)
|
||||
.flatMap(([key, emails]) => {
|
||||
const threshold = Number(key);
|
||||
return /^\d+$/.test(key) && isEmailList(emails) ? [{ threshold, emails: emails.join(", ") }] : [];
|
||||
return /^\d+$/.test(key) && isValidThreshold(threshold) && isEmailList(emails)
|
||||
? [{ threshold, emails: emails.join(", ") }]
|
||||
: [];
|
||||
})
|
||||
.sort((a, b) => (a.threshold ?? 0) - (b.threshold ?? 0));
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue