Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 1 addition & 73 deletions open-sse/services/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import {
toDisplayLabel,
pickFirstNonEmptyString,
} from "./usage/scalars.ts";
import { type UsageQuota, parseResetTime, createQuotaFromUsage } from "./usage/quota.ts";

// Quota / usage upstream URLs (overridable for testing or relays).
const CROF_USAGE_URL = process.env.OMNIROUTE_CROF_USAGE_URL ?? "https://crof.ai/usage_api/";
Expand Down Expand Up @@ -133,29 +134,6 @@ const MINIMAX_USAGE_CONFIG = {
} as const;

type JsonRecord = Record<string, unknown>;
type UsageQuota = {
used: number;
total: number;
remaining?: number;
remainingPercentage?: number;
resetAt: string | null;
unlimited: boolean;
/**
* True when the upstream provider reported the remaining fraction. False
* means the API didn't include the field and the 0 value here is a sentinel,
* NOT a confirmed-exhausted state. Antigravity-specific.
*/
fractionReported?: boolean;
quotaSource?: "retrieveUserQuota" | "fetchAvailableModels" | "localUsageHistory";
displayName?: string;
details?: Array<{
name: string;
used: number;
}>;
currency?: string;
grantedBalance?: number;
toppedUpBalance?: number;
};
type UsageProviderConnection = JsonRecord & {
id?: string;
provider?: string;
Expand Down Expand Up @@ -277,25 +255,6 @@ function getClaudePlanLabel(...candidates: Array<string | null | undefined>): st
return null;
}

function createQuotaFromUsage(
usedValue: unknown,
totalValue: unknown,
resetValue: unknown
): UsageQuota {
const total = Math.max(0, toNumber(totalValue, 0));
const used = total > 0 ? Math.min(Math.max(0, toNumber(usedValue, 0)), total) : 0;
const remaining = total > 0 ? Math.max(total - used, 0) : 0;

return {
used,
total,
remaining,
remainingPercentage: total > 0 ? clampPercentage((remaining / total) * 100) : 0,
resetAt: parseResetTime(resetValue),
unlimited: false,
};
}

function getMiniMaxQuotaResetAt(
model: JsonRecord,
capturedAtMs: number,
Expand Down Expand Up @@ -1371,37 +1330,6 @@ export async function getUsageForProvider(
* Parse reset date/time to ISO string
* Handles multiple formats: Unix timestamp (ms), ISO date string, etc.
*/
function parseResetTime(resetValue: unknown): string | null {
if (!resetValue) return null;

try {
let date: Date;
if (resetValue instanceof Date) {
date = resetValue;
} else if (typeof resetValue === "number") {
date = new Date(resetValue < 1e12 ? resetValue * 1000 : resetValue);
} else if (typeof resetValue === "string") {
// Numeric strings are Unix timestamps too (seconds or milliseconds).
// `new Date("1700000000")` otherwise returns Invalid Date.
if (/^\d+$/.test(resetValue)) {
const ts = Number(resetValue);
date = new Date(ts < 1e12 ? ts * 1000 : ts);
} else {
date = new Date(resetValue);
}
} else {
return null;
}

// Epoch-zero (1970-01-01) means no scheduled reset — treat as null
if (date.getTime() <= 0) return null;

return date.toISOString();
} catch (error) {
return null;
}
}

/**
* GitHub Copilot Usage
* Uses GitHub accessToken (not copilotToken) to call copilot_internal/user API
Expand Down
85 changes: 85 additions & 0 deletions open-sse/services/usage/quota.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* usage/quota.ts — shared usage-quota shape + builders for the usage fetchers.
*
* Extracted from services/usage.ts (god-file decomposition): the `UsageQuota` value
* type every per-provider fetcher returns, plus the two pure builders that normalize
* upstream reset timestamps and assemble a quota from used/total counts. Depends only on
* the sibling scalar leaf — no network, no DB, no module state — so usage.ts and the
* per-provider fetcher leaves (MiniMax, GLM, …) import it without a cycle.
*/

import { toNumber, clampPercentage } from "./scalars.ts";

export type UsageQuota = {
used: number;
total: number;
remaining?: number;
remainingPercentage?: number;
resetAt: string | null;
unlimited: boolean;
/**
* True when the upstream provider reported the remaining fraction. False
* means the API didn't include the field and the 0 value here is a sentinel,
* NOT a confirmed-exhausted state. Antigravity-specific.
*/
fractionReported?: boolean;
quotaSource?: "retrieveUserQuota" | "fetchAvailableModels" | "localUsageHistory";
displayName?: string;
details?: Array<{
name: string;
used: number;
}>;
currency?: string;
grantedBalance?: number;
toppedUpBalance?: number;
};

export function parseResetTime(resetValue: unknown): string | null {
if (!resetValue) return null;

try {
let date: Date;
if (resetValue instanceof Date) {
date = resetValue;
} else if (typeof resetValue === "number") {
date = new Date(resetValue < 1e12 ? resetValue * 1000 : resetValue);
} else if (typeof resetValue === "string") {
// Numeric strings are Unix timestamps too (seconds or milliseconds).
// `new Date("1700000000")` otherwise returns Invalid Date.
if (/^\d+$/.test(resetValue)) {
const ts = Number(resetValue);
date = new Date(ts < 1e12 ? ts * 1000 : ts);
} else {
date = new Date(resetValue);
}
} else {
return null;
}

// Epoch-zero (1970-01-01) means no scheduled reset — treat as null
if (date.getTime() <= 0) return null;

return date.toISOString();
Comment on lines +59 to +62

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When resetValue is an invalid date string (e.g., 'not a date'), new Date(resetValue) produces an Invalid Date object whose getTime() returns NaN. Since NaN <= 0 evaluates to false, the code proceeds to call date.toISOString(), which throws a RangeError: Invalid time value.

While the try...catch block handles this and returns null, relying on exceptions for expected control flow is inefficient and can disrupt debugging (e.g., when 'Pause on caught exceptions' is enabled). Explicitly checking for NaN avoids throwing the exception.

    // Epoch-zero (1970-01-01) means no scheduled reset — treat as null
    const time = date.getTime();
    if (Number.isNaN(time) || time <= 0) return null;

    return date.toISOString();

} catch {
return null;
}
}

export function createQuotaFromUsage(
usedValue: unknown,
totalValue: unknown,
resetValue: unknown
): UsageQuota {
const total = Math.max(0, toNumber(totalValue, 0));
const used = total > 0 ? Math.min(Math.max(0, toNumber(usedValue, 0)), total) : 0;
const remaining = total > 0 ? Math.max(total - used, 0) : 0;

return {
used,
total,
remaining,
remainingPercentage: total > 0 ? clampPercentage((remaining / total) * 100) : 0,
resetAt: parseResetTime(resetValue),
unlimited: false,
};
}
52 changes: 52 additions & 0 deletions tests/unit/usage-quota-core-split.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Characterization of the services/usage.ts quota-core split (god-file decomposition): the shared
// UsageQuota shape + the two pure builders (parseResetTime, createQuotaFromUsage) moved into
// services/usage/quota.ts so the per-provider fetcher leaves can share them without a cycle.
// Behavior-preserving move — these locks pin the timestamp normalization (sec vs ms, epoch-zero →
// null, numeric strings) and the used/total → quota assembly (clamping, remaining %).
import { test } from "node:test";
import assert from "node:assert/strict";

const Q = await import("../../open-sse/services/usage/quota.ts");

test("parseResetTime normalizes seconds, millis, numeric strings; epoch-zero → null", () => {
// seconds (< 1e12) get *1000
assert.equal(Q.parseResetTime(1_700_000_000), new Date(1_700_000_000_000).toISOString());
// millis pass through
assert.equal(Q.parseResetTime(1_700_000_000_000), new Date(1_700_000_000_000).toISOString());
// numeric string as seconds
assert.equal(Q.parseResetTime("1700000000"), new Date(1_700_000_000_000).toISOString());
// ISO string passes through
assert.equal(Q.parseResetTime("2026-01-02T03:04:05.000Z"), "2026-01-02T03:04:05.000Z");
// epoch-zero / falsy / junk → null
assert.equal(Q.parseResetTime(0), null);
assert.equal(Q.parseResetTime(null), null);
assert.equal(Q.parseResetTime("not a date"), null);
});

test("createQuotaFromUsage clamps used to total and computes remaining %", () => {
const q = Q.createQuotaFromUsage(30, 100, null);
assert.equal(q.total, 100);
assert.equal(q.used, 30);
assert.equal(q.remaining, 70);
assert.equal(q.remainingPercentage, 70);
assert.equal(q.unlimited, false);
assert.equal(q.resetAt, null);

// used above total is clamped
const over = Q.createQuotaFromUsage(250, 100, null);
assert.equal(over.used, 100);
assert.equal(over.remaining, 0);
assert.equal(over.remainingPercentage, 0);

// total 0 → everything zeroed
const zero = Q.createQuotaFromUsage(5, 0, null);
assert.equal(zero.total, 0);
assert.equal(zero.used, 0);
assert.equal(zero.remaining, 0);
assert.equal(zero.remainingPercentage, 0);
});

test("createQuotaFromUsage threads the reset timestamp through parseResetTime", () => {
const q = Q.createQuotaFromUsage(1, 10, 1_700_000_000);
assert.equal(q.resetAt, new Date(1_700_000_000_000).toISOString());
});
Loading