fix(proxy): never block a soft-budget-only user under fail-closed budget enforcement

Stop passing soft_budget to get_current_spend as its repair threshold, since in
fail_closed_budget_enforcement mode an unverifiable read below that threshold
raised a 503 for a user who only has a warning budget. Move formatBudgetValue
into dataUtils so user_info_view.tsx stays under the 800-line lint cap.
This commit is contained in:
Jesus Velarde 2026-11-14 14:45:31 -08:00
parent 7b3c2982a2
commit bc164385b3
4 changed files with 33 additions and 5 deletions

View file

@ -1473,7 +1473,7 @@ async def common_checks(
user_spend: Final = await get_current_spend(
counter_key=user_counter(user_object.user_id),
fallback_spend=user_object.spend or 0.0,
max_budget=user_budget if user_budget is not None else user_object.soft_budget,
max_budget=user_budget,
)
_user_soft_budget_check(user_object=user_object, user_spend=user_spend, proxy_logging_obj=proxy_logging_obj)
if user_budget is None:

View file

@ -35,7 +35,7 @@ import { teamDetailHref } from "@/utils/entityLinks";
import { BadgeLink } from "@/components/shared/BadgeLink";
import { UserEditView } from "../user_edit_view";
import OnboardingModal, { InvitationLink } from "@/components/onboarding_link";
import { formatNumberWithCommas, copyToClipboard as utilCopyToClipboard } from "@/utils/dataUtils";
import { formatBudgetValue, formatNumberWithCommas, copyToClipboard as utilCopyToClipboard } from "@/utils/dataUtils";
import { ArrowLeft, CheckIcon, CopyIcon, Plus, RefreshCw, Trash2 } from "lucide-react";
import { toast } from "@/lib/toast";
import { getBudgetDurationLabel } from "@/components/common_components/budget_duration_dropdown";
@ -51,9 +51,6 @@ import { AutoRouterUsageView } from "@/app/(dashboard)/cost-optimization/_compon
import { useActivityDateRange } from "@/app/(dashboard)/cost-optimization/_components/useDailyActivityRange";
import { BudgetPeriodTooltip } from "@/components/shared/BudgetPeriodTooltip";
const formatBudgetValue = (value: number | null | undefined, fallback: string, precision = 4): string =>
value !== null && value !== undefined ? `$${formatNumberWithCommas(value, precision)}` : fallback;
interface UserInfoViewProps {
userId: string;
onClose: () => void;

View file

@ -48,6 +48,9 @@ export const formatNumberWithCommas = (
return `${sign}${scaled.toLocaleString("en-US", opts)}${suffix}`;
};
export const formatBudgetValue = (value: number | null | undefined, fallback: string, precision = 4): string =>
value !== null && value !== undefined ? `$${formatNumberWithCommas(value, precision)}` : fallback;
export const getSpendString = (value: number | null | undefined, decimals: number = 6): string => {
if (value === null || value === undefined || !Number.isFinite(value) || value === 0) {
return "-";

View file

@ -2571,3 +2571,31 @@ async def test_common_checks_alerts_user_over_soft_budget_without_blocking(max_b
assert soft_alerts[0].user_id == user_id
assert soft_alerts[0].spend == 7.0
assert soft_alerts[0].soft_budget == 5.0
@pytest.mark.asyncio
async def test_soft_budget_only_user_is_not_blocked_when_fail_closed_spend_is_unverifiable(
monkeypatch: pytest.MonkeyPatch,
) -> None:
import litellm.proxy.proxy_server as proxy_server
from litellm.models.user import LiteLLM_UserTable
from litellm.proxy.auth.auth_checks import common_checks
monkeypatch.setitem(proxy_server.general_settings, "fail_closed_budget_enforcement", True)
user_id: Final = f"soft-only-{uuid.uuid4().hex}"
result: Final = await common_checks(
request_body={"model": "gpt-4o-mini"},
team_object=None,
user_object=LiteLLM_UserTable(user_id=user_id, soft_budget=5.0, spend=1.0),
end_user_object=None,
global_proxy_spend=None,
general_settings={},
route="/chat/completions",
llm_router=None,
proxy_logging_obj=MagicMock(spec=ProxyLogging),
valid_token=UserAPIKeyAuth(token=f"token-{user_id}", user_id=user_id),
request=MagicMock(),
)
assert result is True