From a205cb7f5e92b21061597acb4271ceae16c33d7b Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Thu, 2 Jul 2026 00:36:57 -0300 Subject: [PATCH] feat(usage): report usage command quotas as percentages + honor observed provider quota resets (#5874) * feat: report usage command quotas as percentages Convert @@om-usage and the HTTP usage endpoint to report personal API key quotas as remaining percentages while keeping USD amounts out of the command output. Scale provider quota remaining percentages by the configured quota cutoff so the protected reserve reads as 0% left. Restore provider USD cost drilldown in the quota dashboard.\n\nAlso sync the 3.8.43 i18n changelog mirrors so the docs-sync pre-commit gate remains green.\n\nTests: DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx/esm --test tests/unit/internal-usage-command.test.ts; DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx/esm --test tests/unit/api-key-usage-limits.test.ts; DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx/esm --test tests/unit/provider-window-costs.test.ts; DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx/esm --test tests/unit/api-manager-usage-command.test.ts tests/unit/apikeys-usage-command.test.ts; npx eslint ; npm run typecheck:core; npm run build; npm run check:migration-numbering; npm run check:docs-sync; docker build --target runner-base (cherry picked from commit f66abd2028a40f2950613da97b8880adfded9db8) * fix: honor observed provider quota resets Detect same-resetAt quota resets when provider usage drops back to the reset floor, and prefer that observed snapshot over stale recorded weekly events for provider USD windows and API-key USD quotas.\n\nTests: npx eslint changed files\nTests: npm run typecheck:core\nTests: DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx/esm --test tests/unit/lib/quota-reset-events.test.ts tests/unit/provider-window-costs.test.ts tests/unit/api-key-usage-limits.test.ts\nTests: npm run build\nTests: docker build --target runner-base --build-arg OMNIROUTE_BUILD_MEMORY_MB=4096 -t omniroute:quota-reset-window-20260702002300 . (cherry picked from commit 39c12a6f17995e3c797456fa1611075050f89aaf) * docs(changelog): credit usage quota percentages extraction from #5863 Co-authored-by: diegosouzapw --------- Co-authored-by: Wital --- CHANGELOG.md | 4 + .../ProviderLimits/ProviderUsdCostModal.tsx | 296 +++++++++ .../components/ProviderLimits/QuotaCard.tsx | 24 +- .../parts/QuotaCardExpanded.tsx | 13 + .../api/usage/provider-window-costs/route.ts | 26 + src/lib/db/quotaResetEvents.ts | 140 ++++- src/lib/usage/apiKeyUsageLimits.ts | 95 ++- src/lib/usage/internalUsageCommand.ts | 181 +++++- src/lib/usage/providerWindowCosts.ts | 571 ++++++++++++++++++ tests/unit/api-key-usage-limits.test.ts | 95 ++- tests/unit/internal-usage-command.test.ts | 187 ++++-- tests/unit/lib/quota-reset-events.test.ts | 131 +++- tests/unit/provider-window-costs.test.ts | 452 ++++++++++++++ 13 files changed, 2082 insertions(+), 133 deletions(-) create mode 100644 src/app/(dashboard)/dashboard/usage/components/ProviderLimits/ProviderUsdCostModal.tsx create mode 100644 src/app/api/usage/provider-window-costs/route.ts create mode 100644 src/lib/usage/providerWindowCosts.ts create mode 100644 tests/unit/provider-window-costs.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 86e8b914b0..c99beeeffb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### ✨ New Features + +- **usage (quota percentages + provider USD drilldown):** `@@om-usage` and the HTTP usage endpoint now report personal API-key quotas as **remaining percentages** (USD amounts stay out of the command output), provider quota remaining is scaled by the configured quota cutoff so the protected reserve reads as 0% left, and the quota dashboard regains a **provider USD cost drilldown** (`/api/usage/provider-window-costs` + `ProviderUsdCostModal`, management-auth gated). Also honors **observed provider quota resets**: a same-`resetAt` reset (usage dropping back to the reset floor) is detected and preferred over stale recorded weekly events for provider USD windows and API-key USD quotas. New `src/lib/usage/providerWindowCosts.ts`. Regression guards: `tests/unit/provider-window-costs.test.ts`, `tests/unit/internal-usage-command.test.ts`, `tests/unit/api-key-usage-limits.test.ts`, `tests/unit/lib/quota-reset-events.test.ts`. Extracted from [#5863](https://github.com/diegosouzapw/OmniRoute/pull/5863) by [@Witroch4](https://github.com/Witroch4). + ### 🔧 Bug Fixes - **fix(kiro):** bound the Claude model-id dash→dot normalization to a 1–2 digit minor so date-suffixed ids (e.g. claude-opus-4-20250514) are no longer corrupted. (thanks @voravitl) diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/ProviderUsdCostModal.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/ProviderUsdCostModal.tsx new file mode 100644 index 0000000000..b5c911a0f0 --- /dev/null +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/ProviderUsdCostModal.tsx @@ -0,0 +1,296 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; + +interface ProviderWindowCostRow { + apiKeyKey: string; + apiKeyId: string | null; + apiKeyName: string; + requests: number; + totalTokens: number; + costUsd: number; + limitUsd: number | null; + limitPeriod: string | null; + limitUsedPercent: number | null; + budgetResetAt: string | null; + lastUsed: string | null; +} + +interface ProviderWindowCostPayload { + provider: string; + connectionId: string | null; + windowStartAt: string; + windowResetAt: string | null; + windowSource: "provider_weekly_reset" | "fallback_rolling_7d"; + windowStartSource: + | "recorded_reset_event" + | "observed_snapshot_reset" + | "inferred_from_reset_at" + | "fallback_rolling_7d"; + quotaName: string | null; + quotaUsedPercent: number | null; + quotaRemainingPercent: number | null; + totalCostUsd: number; + estimatedFullQuotaUsd: number | null; + rows: ProviderWindowCostRow[]; +} + +interface Props { + isOpen: boolean; + onClose: () => void; + connection: any; + providerLabel: string; + accountLabel: string; +} + +function formatUsd(value: number | null | undefined): string { + const numeric = Number(value || 0); + const abs = Math.abs(numeric); + const digits = abs > 0 && abs < 0.01 ? 6 : abs < 1 ? 4 : 2; + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + minimumFractionDigits: digits, + maximumFractionDigits: digits, + }).format(numeric); +} + +function formatDateTime(value: string | null | undefined): string { + if (!value) return "unknown"; + const date = new Date(value); + if (!Number.isFinite(date.getTime())) return "unknown"; + return date.toLocaleString([], { + month: "short", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }); +} + +function formatPercent(value: number | null | undefined): string { + if (value === null || value === undefined || !Number.isFinite(value)) return "n/a"; + return `${value.toFixed(value % 1 === 0 ? 0 : 1)}%`; +} + +export default function ProviderUsdCostModal({ + isOpen, + onClose, + connection, + providerLabel, + accountLabel, +}: Props) { + const [payload, setPayload] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [simulatedPercent, setSimulatedPercent] = useState(25); + + useEffect(() => { + if (!isOpen || !connection?.provider) return; + let alive = true; + + async function load() { + setLoading(true); + setError(null); + try { + const params = new URLSearchParams({ provider: String(connection.provider) }); + if (connection.id) params.set("connectionId", String(connection.id)); + const response = await fetch(`/api/usage/provider-window-costs?${params.toString()}`); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + throw new Error(body.error || `HTTP ${response.status}`); + } + const data = (await response.json()) as ProviderWindowCostPayload; + if (alive) setPayload(data); + } catch (loadError) { + if (alive) { + setError(loadError instanceof Error ? loadError.message : "Failed to load USD costs"); + setPayload(null); + } + } finally { + if (alive) setLoading(false); + } + } + + void load(); + return () => { + alive = false; + }; + }, [isOpen, connection?.id, connection?.provider]); + + const maxCost = useMemo( + () => Math.max(...(payload?.rows || []).map((row) => row.costUsd), 0), + [payload] + ); + const simulatedUsd = + payload?.estimatedFullQuotaUsd !== null && payload?.estimatedFullQuotaUsd !== undefined + ? (payload.estimatedFullQuotaUsd * simulatedPercent) / 100 + : null; + + if (!isOpen) return null; + + return ( +
+
event.stopPropagation()} + > +
+
+

USD Cost

+

+ {providerLabel} · {accountLabel || connection?.id} +

+
+ +
+ +
+ {loading ? ( +
+ + progress_activity + + Loading USD costs +
+ ) : error ? ( +
+ error + {error} +
+ ) : payload ? ( +
+
+
+
Used
+
+ {formatUsd(payload.totalCostUsd)} +
+
+
+
+ Quota used +
+
+ {formatPercent(payload.quotaUsedPercent)} +
+
+
+
+ Est. 100% +
+
+ {payload.estimatedFullQuotaUsd === null + ? "n/a" + : formatUsd(payload.estimatedFullQuotaUsd)} +
+
+
+
Rows
+
+ {payload.rows.length} +
+
+
+ +
+
+ + Window: {formatDateTime(payload.windowStartAt)} →{" "} + {formatDateTime(payload.windowResetAt)} + + + {payload.windowStartSource === "recorded_reset_event" + ? `From recorded ${payload.quotaName || "weekly quota"} reset` + : payload.windowStartSource === "observed_snapshot_reset" + ? `From observed ${payload.quotaName || "weekly quota"} reset` + : payload.windowSource === "provider_weekly_reset" + ? `From ${payload.quotaName || "weekly quota"} reset` + : "Fallback rolling 7d"} + +
+
+
+ Quota estimator + + {simulatedPercent}% ={" "} + {simulatedUsd === null ? "n/a" : formatUsd(simulatedUsd)} + +
+ setSimulatedPercent(Number(event.target.value))} + className="w-full accent-[var(--color-primary,#E54D5E)]" + disabled={payload.estimatedFullQuotaUsd === null} + /> +
+
+ + {payload.rows.length === 0 ? ( +
+ No API key usage in this provider window. +
+ ) : ( +
+ {payload.rows.map((row) => { + const barPercent = maxCost > 0 ? Math.max(4, (row.costUsd / maxCost) * 100) : 0; + return ( +
+
+
+
+ {row.apiKeyName} +
+
+ {row.requests.toLocaleString()} requests ·{" "} + {row.totalTokens.toLocaleString()} tokens +
+
+
+
+ {formatUsd(row.costUsd)} +
+ {row.limitUsd ? ( +
+ {formatPercent(row.limitUsedPercent)} of {formatUsd(row.limitUsd)} + {row.limitPeriod ? ` ${row.limitPeriod}` : ""} +
+ ) : ( +
No USD limit
+ )} +
+
+
+
+
+
+ ); + })} +
+ )} +
+ ) : null} +
+
+
+ ); +} diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCard.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCard.tsx index 9c42bb0647..852cfda219 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCard.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCard.tsx @@ -1,10 +1,12 @@ "use client"; -import { useMemo } from "react"; +import { useMemo, useState } from "react"; import Card from "@/shared/components/Card"; +import { pickDisplayValue } from "@/shared/utils/maskEmail"; import { normalizePlanTier, resolvePlanValue, worstStatus, type CardStatus } from "./utils"; import QuotaCardHeader from "./parts/QuotaCardHeader"; import QuotaCardExpanded from "./parts/QuotaCardExpanded"; +import ProviderUsdCostModal from "./ProviderUsdCostModal"; const STATUS_BORDER: Record = { critical: "#ef4444", @@ -50,6 +52,7 @@ export default function QuotaCard({ togglingActive, }: QuotaCardProps) { const isActive = connection.isActive ?? true; + const [costModalOpen, setCostModalOpen] = useState(false); const quotas = quota?.quotas ?? EMPTY_QUOTAS; const cardStatus = useMemo(() => worstStatus(quotas), [quotas]); const tierMeta = useMemo( @@ -63,6 +66,17 @@ export default function QuotaCard({ () => resolvePlanValue(quota?.plan ?? null, connection.providerSpecificData ?? null), [quota?.plan, connection.providerSpecificData] ); + const accountLabel = useMemo( + () => + pickDisplayValue( + [connection.name, connection.displayName, connection.email], + emailsVisible, + connection.provider + ) || + connection.id || + connection.provider, + [connection, emailsVisible] + ); const overrides = (connection.quotaWindowThresholds as Record | null) || null; const hasOverrides = !!overrides && Object.keys(overrides).length > 0; @@ -96,9 +110,17 @@ export default function QuotaCard({ hasStaleData={hasStaleData} onRefresh={onRefresh} onOpenCutoff={onOpenCutoff} + onOpenCost={() => setCostModalOpen(true)} canEditCutoff={canEditCutoff} hasCutoffOverrides={hasOverrides} /> + setCostModalOpen(false)} + connection={connection} + providerLabel={providerLabel} + accountLabel={accountLabel} + /> ); } diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaCardExpanded.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaCardExpanded.tsx index 47641ec896..58b509f078 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaCardExpanded.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaCardExpanded.tsx @@ -30,6 +30,7 @@ interface Props { hasStaleData: boolean; onRefresh: () => void; onOpenCutoff: () => void; + onOpenCost: () => void; canEditCutoff: boolean; hasCutoffOverrides: boolean; } @@ -115,6 +116,7 @@ export default function QuotaCardExpanded({ hasStaleData, onRefresh, onOpenCutoff, + onOpenCost, canEditCutoff, hasCutoffOverrides, }: Props) { @@ -189,6 +191,17 @@ export default function QuotaCardExpanded({ tune {tr("editCutoffs", "Edit cutoffs")} +