diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/ProviderUsdCostModal.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/ProviderUsdCostModal.tsx index 9f044ece1b..b5c911a0f0 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/ProviderUsdCostModal.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/ProviderUsdCostModal.tsx @@ -22,7 +22,11 @@ interface ProviderWindowCostPayload { windowStartAt: string; windowResetAt: string | null; windowSource: "provider_weekly_reset" | "fallback_rolling_7d"; - windowStartSource: "recorded_reset_event" | "inferred_from_reset_at" | "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; @@ -207,9 +211,11 @@ export default function ProviderUsdCostModal({ {payload.windowStartSource === "recorded_reset_event" ? `From recorded ${payload.quotaName || "weekly quota"} reset` - : payload.windowSource === "provider_weekly_reset" - ? `From ${payload.quotaName || "weekly quota"} reset` - : "Fallback rolling 7d"} + : 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"}
diff --git a/src/lib/db/quotaResetEvents.ts b/src/lib/db/quotaResetEvents.ts index 86701e65c8..7c94337aea 100644 --- a/src/lib/db/quotaResetEvents.ts +++ b/src/lib/db/quotaResetEvents.ts @@ -36,6 +36,17 @@ interface QuotaSnapshotObservationRow { remainingPercentage: number | null; } +interface QuotaSnapshotWindowRow { + nextResetAt: string | null; + remainingPercentage: number | null; + createdAt: string | null; +} + +export interface ProviderQuotaWindowStart { + windowStartIso: string; + source: "recorded_reset_event" | "observed_snapshot_reset"; +} + function toNumberOrNull(value: unknown): number | null { if (typeof value === "number" && Number.isFinite(value)) return value; if (typeof value === "string" && value.trim()) { @@ -55,6 +66,17 @@ function usedPercent(remainingPercentage: number | null): number | null { return remaining === null ? null : Math.max(0, Math.min(100, 100 - remaining)); } +function isResetDrop( + previousUsedPercentage: number | null, + currentUsedPercentage: number +): boolean { + if (previousUsedPercentage === null) return false; + const droppedToResetFloor = + currentUsedPercentage <= 1 && previousUsedPercentage > currentUsedPercentage; + const significantDrop = previousUsedPercentage - currentUsedPercentage >= 5; + return droppedToResetFloor || significantDrop; +} + function parseResetIso(value: string | null): string | null { if (!value) return null; const parsed = Date.parse(value); @@ -127,12 +149,22 @@ export function recordProviderQuotaResetEventIfChanged(input: ResetEventInput): const previousResetMs = Date.parse(previousResetIso); const currentResetMs = Date.parse(currentResetIso); if (!Number.isFinite(previousResetMs) || !Number.isFinite(currentResetMs)) return; - if (currentResetMs <= previousResetMs) return; - if (resetDay(previousResetIso) === resetDay(currentResetIso)) return; const previousRemaining = clampPercent(toNumberOrNull(previous?.remainingPercentage)); const currentRemaining = clampPercent(toNumberOrNull(input.currentRemainingPercentage)); const observedAt = parseResetIso(input.observedAt ?? null) ?? new Date().toISOString(); + const previousUsed = usedPercent(previousRemaining); + const currentUsed = usedPercent(currentRemaining); + const resetMovedForward = + currentResetMs > previousResetMs && resetDay(previousResetIso) !== resetDay(currentResetIso); + const resetObservedWithinSameResetAt = + resetDay(previousResetIso) === resetDay(currentResetIso) && + currentUsed !== null && + isResetDrop(previousUsed, currentUsed); + + if (!resetMovedForward && !resetObservedWithinSameResetAt) return; + + const windowStartedAt = resetMovedForward ? previousResetIso : observedAt; try { const db = getDbInstance() as unknown as DbLike; @@ -148,13 +180,13 @@ export function recordProviderQuotaResetEventIfChanged(input: ResetEventInput): input.provider, input.connectionId, input.windowKey, - previousResetIso, + windowStartedAt, currentResetIso, observedAt, previousRemaining, currentRemaining, - usedPercent(previousRemaining), - usedPercent(currentRemaining), + previousUsed, + currentUsed, null ); } catch (error: unknown) { @@ -163,7 +195,7 @@ export function recordProviderQuotaResetEventIfChanged(input: ResetEventInput): } } -export function getProviderQuotaWindowStartIso( +function getRecordedQuotaWindowStartIso( connectionId: string, targetResetAtIso: string, nowMs = Date.now() @@ -204,3 +236,99 @@ export function getProviderQuotaWindowStartIso( throw error; } } + +function getObservedQuotaWindowStartIso( + connectionId: string, + targetResetAtIso: string, + nowMs = Date.now() +): { windowStartIso: string; resetDrop: boolean } | null { + if (!connectionId || !targetResetAtIso) return null; + const targetDay = resetDay(targetResetAtIso); + if (!targetDay) return null; + + const db = getDbInstance() as unknown as DbLike; + const nowIso = new Date(nowMs).toISOString(); + + try { + const rows = db + .prepare( + ` + SELECT + next_reset_at as nextResetAt, + remaining_percentage as remainingPercentage, + created_at as createdAt + FROM quota_snapshots + WHERE connection_id = @connectionId + AND LOWER(window_key) LIKE '%weekly%' + AND LOWER(window_key) NOT LIKE '%sonnet%' + AND created_at <= @nowIso + ORDER BY created_at ASC, id ASC + ` + ) + .all({ connectionId, nowIso }); + + let firstObservedIso: string | null = null; + let resetDropIso: string | null = null; + let previousUsedPercentage: number | null = null; + + for (const row of rows) { + const createdIso = parseResetIso(row.createdAt); + if (!createdIso || resetDay(row.nextResetAt) !== targetDay) continue; + + if (!firstObservedIso) firstObservedIso = createdIso; + + const currentUsedPercentage = usedPercent(clampPercent(row.remainingPercentage)); + if (currentUsedPercentage !== null) { + if (isResetDrop(previousUsedPercentage, currentUsedPercentage)) { + resetDropIso = createdIso; + } + previousUsedPercentage = currentUsedPercentage; + } + } + + if (resetDropIso) return { windowStartIso: resetDropIso, resetDrop: true }; + if (firstObservedIso) return { windowStartIso: firstObservedIso, resetDrop: false }; + return null; + } catch (error: unknown) { + if (error instanceof Error && error.message.includes("no such table")) return null; + throw error; + } +} + +export function getProviderQuotaWindowStart( + connectionId: string, + targetResetAtIso: string, + nowMs = Date.now() +): ProviderQuotaWindowStart | null { + const recordedIso = getRecordedQuotaWindowStartIso(connectionId, targetResetAtIso, nowMs); + const observed = getObservedQuotaWindowStartIso(connectionId, targetResetAtIso, nowMs); + + if (!recordedIso && !observed) return null; + if (!recordedIso && observed) { + return { windowStartIso: observed.windowStartIso, source: "observed_snapshot_reset" }; + } + if (recordedIso && !observed) { + return { windowStartIso: recordedIso, source: "recorded_reset_event" }; + } + + const recordedMs = Date.parse(recordedIso!); + const observedMs = Date.parse(observed!.windowStartIso); + if ( + observed!.resetDrop && + Number.isFinite(recordedMs) && + Number.isFinite(observedMs) && + observedMs > recordedMs + ) { + return { windowStartIso: observed!.windowStartIso, source: "observed_snapshot_reset" }; + } + + return { windowStartIso: recordedIso!, source: "recorded_reset_event" }; +} + +export function getProviderQuotaWindowStartIso( + connectionId: string, + targetResetAtIso: string, + nowMs = Date.now() +): string | null { + return getProviderQuotaWindowStart(connectionId, targetResetAtIso, nowMs)?.windowStartIso ?? null; +} diff --git a/src/lib/usage/providerWindowCosts.ts b/src/lib/usage/providerWindowCosts.ts index 0e391d804b..c92f5dcead 100644 --- a/src/lib/usage/providerWindowCosts.ts +++ b/src/lib/usage/providerWindowCosts.ts @@ -2,7 +2,7 @@ import { getCostSummary } from "@/domain/costRules"; import { getApiKeys } from "@/lib/db/apiKeys"; import { getDbInstance } from "@/lib/db/core"; import { getAllProviderLimitsCache, getProviderLimitsCache } from "@/lib/db/providerLimits"; -import { getProviderQuotaWindowStartIso } from "@/lib/db/quotaResetEvents"; +import { getProviderQuotaWindowStart } from "@/lib/db/quotaResetEvents"; import { calculateCost } from "@/lib/usage/costCalculator"; const WEEK_MS = 7 * 24 * 60 * 60 * 1000; @@ -69,7 +69,11 @@ export interface ProviderWindowCostBreakdown { windowStartAt: string; windowResetAt: string | null; windowSource: "provider_weekly_reset" | "fallback_rolling_7d"; - windowStartSource: "recorded_reset_event" | "inferred_from_reset_at" | "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; @@ -111,19 +115,19 @@ function parseResetAt(value: unknown, nowMs: number): number | null { return parsed; } -function getRecordedWindowStartMs( +function getProviderWindowStart( connectionId: string | null, resetMs: number, nowMs: number -): number | null { +): { startMs: number; source: ProviderWindowCostBreakdown["windowStartSource"] } | null { if (!connectionId) return null; const resetIso = new Date(resetMs).toISOString(); - const startIso = getProviderQuotaWindowStartIso(connectionId, resetIso, nowMs); - if (!startIso) return null; - const startMs = Date.parse(startIso); + const start = getProviderQuotaWindowStart(connectionId, resetIso, nowMs); + if (!start) return null; + const startMs = Date.parse(start.windowStartIso); if (!Number.isFinite(startMs)) return null; if (startMs > nowMs || startMs >= resetMs) return null; - return startMs; + return { startMs, source: start.source }; } function getRemainingPercent(quota: JsonRecord): number | null { @@ -212,16 +216,16 @@ function selectWeeklyWindow( } if (selected) { - const recordedStartMs = getRecordedWindowStartMs( + const providerWindowStart = getProviderWindowStart( selected.connectionId, selected.resetMs, nowMs ); return { - startMs: recordedStartMs ?? selected.resetMs - WEEK_MS, + startMs: providerWindowStart?.startMs ?? selected.resetMs - WEEK_MS, resetMs: selected.resetMs, source: "provider_weekly_reset", - windowStartSource: recordedStartMs ? "recorded_reset_event" : "inferred_from_reset_at", + windowStartSource: providerWindowStart?.source ?? "inferred_from_reset_at", quotaName: selected.quotaName, quotaUsedPercent: selected.quotaUsedPercent, quotaRemainingPercent: selected.quotaRemainingPercent, diff --git a/tests/unit/api-key-usage-limits.test.ts b/tests/unit/api-key-usage-limits.test.ts index 605d0f02b6..5a3c277887 100644 --- a/tests/unit/api-key-usage-limits.test.ts +++ b/tests/unit/api-key-usage-limits.test.ts @@ -231,6 +231,27 @@ test("getApiKeyUsageLimitStatus cuts weekly USD spend at observed provider quota null, "2026-06-20T02:10:00.000Z" ); + db.prepare( + ` + INSERT INTO provider_quota_reset_events + (provider, connection_id, window_key, window_started_at, window_resets_at, + observed_at, previous_remaining_percentage, new_remaining_percentage, + previous_used_percentage, new_used_percentage, raw_data) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ` + ).run( + "claude", + "conn-claude", + "weekly (7d)", + "2026-06-18T23:00:00.000Z", + "2026-06-25T23:00:00.000Z", + "2026-06-18T23:04:00.000Z", + 0, + 100, + 100, + 0, + null + ); const metadata = await apiKeysDb.getApiKeyMetadata(created.key); assert.ok(metadata); diff --git a/tests/unit/lib/quota-reset-events.test.ts b/tests/unit/lib/quota-reset-events.test.ts index cb91316fb7..155d515b50 100644 --- a/tests/unit/lib/quota-reset-events.test.ts +++ b/tests/unit/lib/quota-reset-events.test.ts @@ -8,9 +8,11 @@ const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-quota-res process.env.DATA_DIR = TEST_DATA_DIR; const core = await import("../../../src/lib/db/core.ts"); -const { recordProviderQuotaResetEventIfChanged, getProviderQuotaWindowStartIso } = await import( - "../../../src/lib/db/quotaResetEvents.ts" -); +const { + recordProviderQuotaResetEventIfChanged, + getProviderQuotaWindowStart, + getProviderQuotaWindowStartIso, +} = await import("../../../src/lib/db/quotaResetEvents.ts"); // Force migrations (incl. 108_provider_quota_reset_events) to run. core.getDbInstance(); @@ -49,13 +51,132 @@ test("getWindowStart returns null for a reset day with no recorded event", () => ); }); -test("does not record when previous and current reset fall on the same day (no transition)", () => { +test("observed same-resetAt quota drop overrides an older recorded weekly window", () => { + const connectionId = "conn-early-reset-snapshot"; + const targetResetAt = "2026-07-02T23:00:00.000Z"; + const db = core.getDbInstance(); + + db.prepare( + ` + INSERT INTO provider_quota_reset_events + (provider, connection_id, window_key, window_started_at, window_resets_at, + observed_at, previous_remaining_percentage, new_remaining_percentage, + previous_used_percentage, new_used_percentage, raw_data) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ` + ).run( + "claude", + connectionId, + "weekly (7d)", + "2026-06-25T23:00:00.000Z", + targetResetAt, + "2026-06-25T23:04:00.000Z", + 0, + 100, + 100, + 0, + null + ); + + const insertSnapshot = db.prepare(` + INSERT INTO quota_snapshots ( + provider, + connection_id, + window_key, + remaining_percentage, + is_exhausted, + next_reset_at, + window_duration_ms, + raw_data, + created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + insertSnapshot.run( + "claude", + connectionId, + "weekly (7d)", + 100, + 0, + targetResetAt, + null, + null, + "2026-06-25T23:04:00.000Z" + ); + insertSnapshot.run( + "claude", + connectionId, + "weekly (7d)", + 4, + 0, + targetResetAt, + null, + null, + "2026-07-01T00:05:00.000Z" + ); + insertSnapshot.run( + "claude", + connectionId, + "weekly (7d)", + 100, + 0, + targetResetAt, + null, + null, + "2026-07-01T21:41:13.293Z" + ); + + const start = getProviderQuotaWindowStart( + connectionId, + targetResetAt, + Date.parse("2026-07-02T00:00:00.000Z") + ); + + assert.deepEqual(start, { + windowStartIso: "2026-07-01T21:41:13.293Z", + source: "observed_snapshot_reset", + }); + assert.equal( + getProviderQuotaWindowStartIso( + connectionId, + targetResetAt, + Date.parse("2026-07-02T00:00:00.000Z") + ), + "2026-07-01T21:41:13.293Z" + ); +}); + +test("records same-resetAt weekly resets when usage drops back to the reset floor", () => { + const connectionId = "conn-early-reset-record"; + const targetResetAt = "2026-07-02T23:00:00.000Z"; + const observedAt = "2026-07-01T21:41:13.293Z"; + + recordProviderQuotaResetEventIfChanged({ + provider: "claude", + connectionId, + windowKey: "weekly (7d)", + currentResetAt: targetResetAt, + currentRemainingPercentage: 100, + previousObservation: { resetAt: targetResetAt, remainingPercentage: 4 }, + observedAt, + }); + + assert.equal( + getProviderQuotaWindowStartIso( + connectionId, + targetResetAt, + Date.parse("2026-07-02T00:00:00.000Z") + ), + observedAt + ); +}); + +test("does not record when previous and current reset fall on the same day without a reset drop", () => { recordProviderQuotaResetEventIfChanged({ provider: PROVIDER, connectionId: "conn-sameday", windowKey: "weekly", currentResetAt: "2026-03-10T23:00:00.000Z", - currentRemainingPercentage: 80, + currentRemainingPercentage: 69, previousObservation: { resetAt: "2026-03-10T01:00:00.000Z", remainingPercentage: 70 }, observedAt: "2026-03-10T23:30:00.000Z", }); diff --git a/tests/unit/provider-window-costs.test.ts b/tests/unit/provider-window-costs.test.ts index 869bbd4e36..bca518644d 100644 --- a/tests/unit/provider-window-costs.test.ts +++ b/tests/unit/provider-window-costs.test.ts @@ -267,6 +267,126 @@ test("provider window costs use the recorded reset event as the cost cutoff", as assert.equal(result.rows[0].requests, 1); }); +test("provider window costs cut at an observed same-resetAt quota reset", async () => { + await localDb.updatePricing({ + claude: { + "claude-opus-4-8": { input: 1, output: 1, cached: 1, cache_creation: 1, reasoning: 1 }, + }, + }); + + const targetResetAt = "2026-07-02T23:00:00.000Z"; + providerLimits.setProviderLimitsCache("claude-early-reset", { + quotas: { + "weekly (7d)": { + used: 7, + total: 100, + remainingPercentage: 93, + resetAt: targetResetAt, + }, + }, + plan: "default_claude_max_20x", + message: null, + fetchedAt: "2026-07-02T00:04:00.000Z", + }); + + const db = core.getDbInstance(); + db.prepare( + ` + INSERT INTO provider_quota_reset_events + (provider, connection_id, window_key, window_started_at, window_resets_at, + observed_at, previous_remaining_percentage, new_remaining_percentage, + previous_used_percentage, new_used_percentage, raw_data) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ` + ).run( + "claude", + "claude-early-reset", + "weekly (7d)", + "2026-06-25T23:00:00.000Z", + targetResetAt, + "2026-06-25T23:04:00.000Z", + 0, + 100, + 100, + 0, + null + ); + + const insertSnapshot = db.prepare(` + INSERT INTO quota_snapshots ( + provider, + connection_id, + window_key, + remaining_percentage, + is_exhausted, + next_reset_at, + window_duration_ms, + raw_data, + created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + insertSnapshot.run( + "claude", + "claude-early-reset", + "weekly (7d)", + 4, + 0, + targetResetAt, + null, + null, + "2026-07-01T00:05:00.000Z" + ); + insertSnapshot.run( + "claude", + "claude-early-reset", + "weekly (7d)", + 100, + 0, + targetResetAt, + null, + null, + "2026-07-01T21:41:13.293Z" + ); + insertSnapshot.run( + "claude", + "claude-early-reset", + "weekly (7d)", + 93, + 0, + targetResetAt, + null, + null, + "2026-07-02T00:04:00.000Z" + ); + + await usageHistory.saveRequestUsage({ + provider: "claude", + model: "claude-opus-4-8", + connectionId: "claude-early-reset", + tokens: { input: 8_000_000, output: 0 }, + timestamp: "2026-07-01T20:00:00.000Z", + }); + await usageHistory.saveRequestUsage({ + provider: "claude", + model: "claude-opus-4-8", + connectionId: "claude-early-reset", + tokens: { input: 2_000_000, output: 0 }, + timestamp: "2026-07-01T22:00:00.000Z", + }); + + const result = await getProviderWindowCostBreakdown({ + provider: "claude", + connectionId: "claude-early-reset", + now: Date.parse("2026-07-02T00:10:00.000Z"), + }); + + assert.equal(result.windowStartAt, "2026-07-01T21:41:13.293Z"); + assert.equal(result.windowStartSource, "observed_snapshot_reset"); + assert.equal(result.totalCostUsd, 2); + assert.equal(result.rows.length, 1); + assert.equal(result.rows[0].requests, 1); +}); + test("provider window costs prefer recorded USD history over repricing usage tokens", async () => { await localDb.updatePricing({ claude: {