refactor(ai): deduplicated usage provider coercion helpers

- Deleted usage/shared.ts's duplicate toNumber; every consumer in the directory
  now resolves the single catalog implementation.
- Promoted HOUR_MS, DAY_MS, WEEK_MS, parseIsoTimestamp, parsePositiveTimestamp
  and usageStatus into usage/shared.ts and dropped the local copies.
- Left the epoch coercers, status thresholds and base-url normalizers alone:
  they disagree on the 1e12 boundary, non-positive values and warn levels, so
  merging them would change reported usage.
This commit is contained in:
can1357
2026-08-08 06:32:00 +02:00
parent 5fa578fa3b
commit 2efbdeaab9
16 changed files with 114 additions and 173 deletions
+8 -15
View File
@@ -1,3 +1,4 @@
import { toNumber } from "@oh-my-pi/pi-catalog/utils";
import { parseAlibabaTokenPlanCredential } from "@oh-my-pi/pi-catalog/wire/alibaba-token-plan";
import type {
CredentialRankingStrategy,
@@ -8,7 +9,7 @@ import type {
UsageReport,
} from "../usage";
import { isRecord } from "../utils";
import { toNumber } from "./shared";
import { HOUR_MS, parsePositiveTimestamp, WEEK_MS } from "./shared";
const PROVIDER = "alibaba-token-plan";
const CONSOLE_ORIGIN = "https://home.qwencloud.com";
@@ -19,8 +20,6 @@ const USAGE_API = "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/usage";
const USAGE_URL = `https://cs-data.qwencloud.com/data/api.json?product=sfm_bailian&action=${GATEWAY_ACTION}&api=${encodeURIComponent(USAGE_API)}`;
const BROWSER_USER_AGENT =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36";
const FIVE_HOURS_MS = 5 * 60 * 60 * 1000;
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
const CONSOLE_CORNERSTONE_PARAM = {
domain: "home.qwencloud.com",
consoleSite: "QWENCLOUD",
@@ -55,12 +54,6 @@ function unwrapGatewayData(value: Record<string, unknown>): Record<string, unkno
return current;
}
function parseResetTime(value: unknown): number | undefined {
const parsed = toNumber(value);
if (parsed === undefined || parsed <= 0) return undefined;
return parsed < 1_000_000_000_000 ? parsed * 1000 : parsed;
}
function parseUsedFraction(value: unknown): number | undefined {
const parsed = toNumber(value);
if (parsed === undefined || parsed < 0) return undefined;
@@ -178,17 +171,17 @@ async function fetchAlibabaTokenPlanUsage(
buildLimit(
"5h",
"5 Hour Credits",
FIVE_HOURS_MS,
5 * HOUR_MS,
parseUsedFraction(responseData.per5HourPercentage),
parseResetTime(responseData.per5HourResetTime),
parsePositiveTimestamp(responseData.per5HourResetTime),
accountId,
),
buildLimit(
"7d",
"7 Day Credits",
SEVEN_DAYS_MS,
WEEK_MS,
parseUsedFraction(responseData.per1WeekPercentage),
parseResetTime(responseData.per1WeekResetTime),
parsePositiveTimestamp(responseData.per1WeekResetTime),
accountId,
),
].filter((limit): limit is UsageLimit => limit !== undefined);
@@ -224,7 +217,7 @@ export const alibabaTokenPlanRankingStrategy: CredentialRankingStrategy = {
secondary: report.limits.find(limit => limit.id === "credits:7d"),
}),
windowDefaults: {
primaryMs: FIVE_HOURS_MS,
secondaryMs: SEVEN_DAYS_MS,
primaryMs: 5 * HOUR_MS,
secondaryMs: WEEK_MS,
},
};
+12 -19
View File
@@ -17,10 +17,9 @@ import {
type UsageWindow,
} from "../usage";
import { isRecord } from "../utils";
import { HOUR_MS, parseIsoTimestamp, WEEK_MS } from "./shared";
const DEFAULT_ENDPOINT = "https://api.anthropic.com/api/oauth";
const FIVE_HOURS_MS = 5 * 60 * 60 * 1000;
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
const MAX_ATTEMPTS = 3;
const BASE_RETRY_DELAY_MS = 500;
@@ -123,16 +122,10 @@ interface ParsedApiLimitEntry {
displayName?: string;
}
function parseIsoTime(value: string | undefined): number | undefined {
if (!value) return undefined;
const parsed = Date.parse(value);
return Number.isFinite(parsed) ? parsed : undefined;
}
function parseBucket(bucket: unknown): ParsedUsageBucket | undefined {
if (!isRecord(bucket)) return undefined;
const utilization = toNumber(bucket.utilization);
const resetsAt = parseIsoTime(typeof bucket.resets_at === "string" ? bucket.resets_at : undefined);
const resetsAt = parseIsoTimestamp(typeof bucket.resets_at === "string" ? bucket.resets_at : undefined);
if (utilization === undefined && resetsAt === undefined) {
return undefined;
}
@@ -168,7 +161,7 @@ function parseApiLimitEntries(raw: unknown): ParsedApiLimitEntry[] {
const entry = rawEntry as ClaudeApiLimitEntry;
if (typeof entry.kind !== "string") continue;
const utilization = toNumber(entry.percent);
const resetsAt = parseIsoTime(typeof entry.resets_at === "string" ? entry.resets_at : undefined);
const resetsAt = parseIsoTimestamp(typeof entry.resets_at === "string" ? entry.resets_at : undefined);
if (utilization === undefined && resetsAt === undefined) continue;
const displayName = getApiLimitDisplayName(entry.scope);
entries.push({
@@ -554,7 +547,7 @@ function buildScopedWeeklyUsageLimits(entries: readonly ParsedApiLimitEntry[]):
label: `Claude 7 Day (${entry.displayName})`,
windowId: "7d",
windowLabel: "7 Day",
durationMs: SEVEN_DAYS_MS,
durationMs: WEEK_MS,
bucket: entry.bucket,
provider: "anthropic",
tier: slug,
@@ -574,7 +567,7 @@ export function parseClaudeRateLimitHeaders(headers: Record<string, string>, now
label: "Claude 5 Hour",
windowId: "5h",
windowLabel: "5 Hour",
durationMs: FIVE_HOURS_MS,
durationMs: 5 * HOUR_MS,
bucket: fiveHour,
provider: "anthropic",
shared: true,
@@ -584,7 +577,7 @@ export function parseClaudeRateLimitHeaders(headers: Record<string, string>, now
label: "Claude 7 Day",
windowId: "7d",
windowLabel: "7 Day",
durationMs: SEVEN_DAYS_MS,
durationMs: WEEK_MS,
bucket: sevenDay,
provider: "anthropic",
shared: true,
@@ -594,7 +587,7 @@ export function parseClaudeRateLimitHeaders(headers: Record<string, string>, now
label: "Claude 7 Day (Fable)",
windowId: "7d",
windowLabel: "7 Day",
durationMs: SEVEN_DAYS_MS,
durationMs: WEEK_MS,
bucket: modelScopedSevenDay,
provider: "anthropic",
tier: "fable",
@@ -638,7 +631,7 @@ async function fetchClaudeUsage(params: UsageFetchParams, ctx: UsageFetchContext
label: "Claude 5 Hour",
windowId: "5h",
windowLabel: "5 Hour",
durationMs: FIVE_HOURS_MS,
durationMs: 5 * HOUR_MS,
bucket: fiveHour,
provider: "anthropic",
shared: true,
@@ -648,7 +641,7 @@ async function fetchClaudeUsage(params: UsageFetchParams, ctx: UsageFetchContext
label: "Claude 7 Day",
windowId: "7d",
windowLabel: "7 Day",
durationMs: SEVEN_DAYS_MS,
durationMs: WEEK_MS,
bucket: sevenDay,
provider: "anthropic",
shared: true,
@@ -658,7 +651,7 @@ async function fetchClaudeUsage(params: UsageFetchParams, ctx: UsageFetchContext
label: "Claude 7 Day (Opus)",
windowId: "7d",
windowLabel: "7 Day",
durationMs: SEVEN_DAYS_MS,
durationMs: WEEK_MS,
bucket: sevenDayOpus,
provider: "anthropic",
tier: "opus",
@@ -668,7 +661,7 @@ async function fetchClaudeUsage(params: UsageFetchParams, ctx: UsageFetchContext
label: "Claude 7 Day (Sonnet)",
windowId: "7d",
windowLabel: "7 Day",
durationMs: SEVEN_DAYS_MS,
durationMs: WEEK_MS,
bucket: sevenDaySonnet,
provider: "anthropic",
tier: "sonnet",
@@ -774,7 +767,7 @@ function rankingUsedFraction(limit: UsageLimit): number {
function rankingDrainRate(limit: UsageLimit, nowMs: number): number {
const usedFraction = rankingUsedFraction(limit);
const durationMs = limit.window?.durationMs ?? SEVEN_DAYS_MS;
const durationMs = limit.window?.durationMs ?? WEEK_MS;
if (!Number.isFinite(durationMs) || durationMs <= 0) return usedFraction;
const resetAt = limit.window?.resetsAt;
if (typeof resetAt !== "number" || !Number.isFinite(resetAt)) return usedFraction;
+6 -18
View File
@@ -1,3 +1,4 @@
import { toNumber } from "@oh-my-pi/pi-catalog/utils";
import { extractCursorAccessTokenUserId } from "../registry/oauth/cursor";
import type {
UsageAmount,
@@ -6,21 +7,15 @@ import type {
UsageLimit,
UsageProvider,
UsageReport,
UsageStatus,
UsageWindow,
} from "../usage";
import { toNumber } from "./shared";
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
import { isRecord } from "../utils";
import { parseIsoTimestamp, usageStatus } from "./shared";
function parseTimestamp(value: unknown): number | undefined {
const numeric = toNumber(value);
if (numeric !== undefined) return numeric < 1_000_000_000_000 ? numeric * 1000 : numeric;
if (typeof value !== "string" || !value.trim()) return undefined;
const parsed = Date.parse(value);
return Number.isFinite(parsed) ? parsed : undefined;
return parseIsoTimestamp(value);
}
const DEFAULT_CURSOR_BASE_URL = "https://api2.cursor.sh";
@@ -78,13 +73,6 @@ function deriveResetsAt(payload: Record<string, unknown>): number | undefined {
return undefined;
}
function resolveCursorStatus(usedFraction: number | undefined): UsageStatus {
if (usedFraction === undefined) return "unknown";
if (usedFraction >= 1) return "exhausted";
if (usedFraction >= 0.9) return "warning";
return "ok";
}
export function parseCursorIndividualUsage(payload: unknown, fetchedAt = Date.now()): UsageReport | null {
if (!isRecord(payload) || !isRecord(payload.individualUsage) || !isRecord(payload.individualUsage.overall)) {
return null;
@@ -140,7 +128,7 @@ export function parseCursorIndividualUsage(payload: unknown, fetchedAt = Date.no
},
window,
amount,
...(amount.usedFraction !== undefined ? { status: resolveCursorStatus(amount.usedFraction) } : {}),
...(amount.usedFraction !== undefined ? { status: usageStatus(amount.usedFraction) } : {}),
};
return {
provider: "cursor",
@@ -197,7 +185,7 @@ export function parseCursorUsage(payload: unknown, fetchedAt = Date.now()): Usag
unit,
};
const status = resolveCursorStatus(amount.usedFraction);
const status = usageStatus(amount.usedFraction);
limits.push({
id: limitId,
+3 -2
View File
@@ -8,6 +8,7 @@ import type {
UsageReport,
UsageWindow,
} from "../usage";
import { parseIsoTimestamp } from "./shared";
// (Refresh is the sole responsibility of AuthStorage; no provider-direct refresh here.)
@@ -79,8 +80,8 @@ function parseWindow(resetTime: string | undefined): UsageWindow {
label: "Quota window",
};
}
const resetsAt = Date.parse(resetTime);
if (Number.isNaN(resetsAt)) {
const resetsAt = parseIsoTimestamp(resetTime);
if (resetsAt === undefined) {
return {
id: "quota",
label: "Quota window",
+3 -2
View File
@@ -18,6 +18,7 @@ import type {
UsageWindow,
} from "../usage";
import { isRecord } from "../utils";
import { parseIsoTimestamp } from "./shared";
type CopilotQuotaDetail = {
entitlement: number;
@@ -74,8 +75,8 @@ function resolveGitHubApiBaseUrl(params: UsageFetchParams): string {
function buildWindow(resetDate: string | undefined): UsageWindow | undefined {
if (!resetDate) return undefined;
const resetAt = Date.parse(resetDate);
if (!Number.isFinite(resetAt)) return undefined;
const resetAt = parseIsoTimestamp(resetDate);
if (resetAt === undefined) return undefined;
return {
id: "monthly",
label: "Monthly",
+10 -17
View File
@@ -12,6 +12,7 @@ import type {
UsageStatus,
UsageWindow,
} from "../usage";
import { DAY_MS, parseIsoTimestamp, WEEK_MS } from "./shared";
// (Refresh is the sole responsibility of AuthStorage; no provider-direct refresh here.)
@@ -47,9 +48,6 @@ interface AntigravityUsageResponse {
const DEFAULT_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com";
const FETCH_AVAILABLE_MODELS_PATH = "/v1internal:fetchAvailableModels";
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
const ONE_WEEK_MS = 7 * ONE_DAY_MS;
interface AntigravityWindowDescriptor {
id: string;
label: string;
@@ -59,25 +57,20 @@ interface AntigravityWindowDescriptor {
function classifyWindow(id: string | undefined, label: string | undefined): AntigravityWindowDescriptor | undefined {
const source = `${id ?? ""} ${label ?? ""}`.toLowerCase();
if (source.includes("week") || source.includes("7d") || /7[\s_-]*day/.test(source)) {
return { id: "weekly", label: "Weekly", durationMs: ONE_WEEK_MS };
return { id: "weekly", label: "Weekly", durationMs: WEEK_MS };
}
if (source.includes("day") || source.includes("daily") || source.includes("24h")) {
return { id: "daily", label: "Daily", durationMs: ONE_DAY_MS };
return { id: "daily", label: "Daily", durationMs: DAY_MS };
}
if (id || label) return { id: id ?? label ?? "default", label: label ?? id ?? "Default" };
return undefined;
}
function parseResetTime(info: AntigravityQuotaInfo): number | undefined {
const resetAt = info.resetTime ? Date.parse(info.resetTime) : undefined;
return resetAt !== undefined && Number.isFinite(resetAt) ? resetAt : undefined;
}
function inferWindowFromReset(resetAt: number | undefined, nowMs: number): AntigravityWindowDescriptor {
if (resetAt !== undefined && resetAt - nowMs > ONE_DAY_MS) {
return { id: "weekly", label: "Weekly", durationMs: ONE_WEEK_MS };
if (resetAt !== undefined && resetAt - nowMs > DAY_MS) {
return { id: "weekly", label: "Weekly", durationMs: WEEK_MS };
}
return { id: "daily", label: "Daily", durationMs: ONE_DAY_MS };
return { id: "daily", label: "Daily", durationMs: DAY_MS };
}
function quotaInferenceKey(info: AntigravityQuotaInfo): string {
@@ -98,7 +91,7 @@ function inferWindowDescriptors(
continue;
}
const group = groups.get(quotaInferenceKey(info)) ?? [];
group.push({ info, resetAt: parseResetTime(info) });
group.push({ info, resetAt: parseIsoTimestamp(info.resetTime) });
groups.set(quotaInferenceKey(info), group);
}
@@ -110,7 +103,7 @@ function inferWindowDescriptors(
for (const entry of group) {
const descriptor =
latestReset !== undefined && entry.resetAt === latestReset
? { id: "weekly", label: "Weekly", durationMs: ONE_WEEK_MS }
? { id: "weekly", label: "Weekly", durationMs: WEEK_MS }
: inferWindowFromReset(entry.resetAt, nowMs);
descriptors.set(entry.info, descriptor);
}
@@ -149,7 +142,7 @@ function parseWindow(
info: AntigravityQuotaInfo,
descriptor: AntigravityWindowDescriptor | undefined,
): UsageWindow | undefined {
const resetAt = parseResetTime(info);
const resetAt = parseIsoTimestamp(info.resetTime);
const hasResetAt = resetAt !== undefined;
if (!descriptor && !hasResetAt) return undefined;
return {
@@ -493,5 +486,5 @@ export const antigravityRankingStrategy: CredentialRankingStrategy = {
// Antigravity windows carry `durationMs` when the response identifies them
// as daily/weekly. Fall back to daily for legacy unlabelled quotaInfo
// entries from `daily-cloudcode-pa.googleapis.com`.
windowDefaults: { primaryMs: ONE_DAY_MS, secondaryMs: ONE_DAY_MS },
windowDefaults: { primaryMs: DAY_MS, secondaryMs: DAY_MS },
};
+6 -12
View File
@@ -1,3 +1,4 @@
import { toNumber } from "@oh-my-pi/pi-catalog/utils";
import { $env } from "@oh-my-pi/pi-utils";
import { getKimiCommonHeaders } from "../registry/oauth/kimi";
import type {
@@ -7,12 +8,12 @@ import type {
UsageLimit,
UsageProvider,
UsageReport,
UsageStatus,
UsageWindow,
} from "../usage";
import { isRecord } from "../utils";
import { parseIsoTimestamp, usageStatus } from "./shared";
// (Refresh is the sole responsibility of AuthStorage; no provider-direct refresh here.)
import { toNumber } from "./shared";
const DEFAULT_BASE_URL = "https://api.kimi.com/coding/v1";
const USAGE_PATH = "usages";
@@ -47,8 +48,8 @@ function parseResetTime(data: Record<string, unknown>, nowMs: number): number |
for (const key of timeKeys) {
const value = data[key];
if (typeof value === "string" && value.trim()) {
const parsed = Date.parse(value);
if (Number.isFinite(parsed)) return parsed;
const parsed = parseIsoTimestamp(value);
if (parsed !== undefined) return parsed;
}
if (typeof value === "number" && Number.isFinite(value)) {
return value > 1_000_000_000_000 ? value : value * 1000;
@@ -136,13 +137,6 @@ function buildUsageAmount(row: KimiUsageRow): UsageAmount {
return amount;
}
function buildUsageStatus(amount: UsageAmount): UsageStatus {
if (amount.usedFraction === undefined) return "unknown";
if (amount.usedFraction >= 1) return "exhausted";
if (amount.usedFraction >= 0.9) return "warning";
return "ok";
}
function toUsageLimit(row: KimiUsageRow, provider: string, index: number, accountId?: string): UsageLimit {
// Kimi puts `resetTime` on the limit `detail`, not on `window`, so a
// window built from `duration`/`timeUnit` alone carries no resetsAt.
@@ -172,7 +166,7 @@ function toUsageLimit(row: KimiUsageRow, provider: string, index: number, accoun
},
window,
amount,
status: buildUsageStatus(amount),
status: usageStatus(amount.usedFraction),
};
}
+7 -27
View File
@@ -1,18 +1,11 @@
import type {
UsageFetchContext,
UsageFetchParams,
UsageLimit,
UsageProvider,
UsageReport,
UsageStatus,
} from "../usage";
import { toNumber } from "@oh-my-pi/pi-catalog/utils";
import type { UsageFetchContext, UsageFetchParams, UsageLimit, UsageProvider, UsageReport } from "../usage";
import { isRecord } from "../utils";
import { toNumber } from "./shared";
import { HOUR_MS, parsePositiveTimestamp, usageStatus } from "./shared";
const INTL_PROVIDER = "minimax-code";
const INTL_BASE_URL = "https://api.minimax.io";
const REMAINS_PATH = "/v1/token_plan/remains";
const HOUR_MS = 60 * 60 * 1000;
/** `current_*_status` enum reported per window: 1 normal, 2 exhausted, 3 unlimited. */
const STATUS_EXHAUSTED = 2;
const STATUS_UNLIMITED = 3;
@@ -42,13 +35,6 @@ interface TokenPlanBucket {
weeklyStatus?: number;
}
/** MiniMax reports epoch milliseconds; tolerate seconds in case a deployment differs. */
function parseTimestamp(value: unknown): number | undefined {
const parsed = toNumber(value);
if (parsed === undefined || parsed <= 0) return undefined;
return parsed < 1_000_000_000_000 ? parsed * 1000 : parsed;
}
/** `current_*_remaining_percent` is 0..100 remaining; usage fractions are 0..1 used. */
function usedFractionFromRemainingPercent(value: unknown): number | undefined {
const parsed = toNumber(value);
@@ -57,26 +43,20 @@ function usedFractionFromRemainingPercent(value: unknown): number | undefined {
return Math.min(1, Math.max(0, (100 - parsed) / 100));
}
function usageStatus(usedFraction: number): UsageStatus {
if (usedFraction >= 1) return "exhausted";
if (usedFraction >= 0.9) return "warning";
return "ok";
}
function parseBucket(value: unknown): TokenPlanBucket | null {
if (!isRecord(value)) return null;
const modelName = typeof value.model_name === "string" ? value.model_name.trim() : "";
if (!modelName) return null;
return {
modelName,
intervalStart: parseTimestamp(value.start_time),
intervalEnd: parseTimestamp(value.end_time),
intervalStart: parsePositiveTimestamp(value.start_time),
intervalEnd: parsePositiveTimestamp(value.end_time),
intervalRemainingPercent: toNumber(value.current_interval_remaining_percent),
intervalTotalCount: toNumber(value.current_interval_total_count),
intervalUsageCount: toNumber(value.current_interval_usage_count),
intervalStatus: toNumber(value.current_interval_status),
weeklyStart: parseTimestamp(value.weekly_start_time),
weeklyEnd: parseTimestamp(value.weekly_end_time),
weeklyStart: parsePositiveTimestamp(value.weekly_start_time),
weeklyEnd: parsePositiveTimestamp(value.weekly_end_time),
weeklyRemainingPercent: toNumber(value.current_weekly_remaining_percent),
weeklyTotalCount: toNumber(value.current_weekly_total_count),
weeklyUsageCount: toNumber(value.current_weekly_usage_count),
+1 -1
View File
@@ -18,10 +18,10 @@
* (the `/usage reset` command + auto-redeem) and any out-of-band tooling can
* share one wire contract.
*/
import { toNumber } from "@oh-my-pi/pi-catalog/utils";
import type { FetchImpl } from "../types";
import { isRecord } from "../utils";
import { normalizeCodexBaseUrl } from "./openai-codex-base-url";
import { toNumber } from "./shared";
const RESET_CREDITS_PATH = "wham/rate-limit-reset-credits";
const RESET_CREDITS_CONSUME_PATH = "wham/rate-limit-reset-credits/consume";
+3 -4
View File
@@ -1,4 +1,5 @@
import { Buffer } from "node:buffer";
import { toNumber } from "@oh-my-pi/pi-catalog/utils";
import type {
CredentialRankingContext,
CredentialRankingStrategy,
@@ -14,7 +15,7 @@ import type {
import { isRecord } from "../utils";
import { normalizeCodexBaseUrl } from "./openai-codex-base-url";
import { listCodexResetCredits } from "./openai-codex-reset";
import { toNumber } from "./shared";
import { HOUR_MS } from "./shared";
const CODEX_USAGE_PATH = "wham/usage";
const JWT_AUTH_CLAIM = "https://api.openai.com/auth";
@@ -558,8 +559,6 @@ export const openaiCodexUsageProvider: UsageProvider = {
},
};
const FIVE_HOUR_MS = 5 * 60 * 60 * 1000;
// A Codex request gates only on the chat windows it actually consumes. A
// "-spark" model spends the separate Spark meter; every other Codex model spends
// the 5h/weekly chat windows. Scoping the gating set this way keeps an exhausted
@@ -619,7 +618,7 @@ export const codexRankingStrategy: CredentialRankingStrategy = {
windowId === "5h" ||
(typeof durationMs === "number" &&
Number.isFinite(durationMs) &&
Math.abs(durationMs - FIVE_HOUR_MS) <= 60_000);
Math.abs(durationMs - 5 * HOUR_MS) <= 60_000);
if (!isFiveHourWindow) return false;
const usedFraction = primary.amount.usedFraction;
return typeof usedFraction === "number" && Number.isFinite(usedFraction) && usedFraction === 0;
+1 -2
View File
@@ -1,8 +1,7 @@
import type { UsageCostHistoryEntry, UsageLimit, UsageProvider, UsageWindow } from "../usage";
import { DAY_MS, HOUR_MS } from "./shared";
const OPENCODE_GO_PROVIDER = "opencode-go";
const HOUR_MS = 60 * 60 * 1000;
const DAY_MS = 24 * HOUR_MS;
const OPENCODE_GO_LIMITS = [
{ id: "rolling-5h", label: "5 Hour", durationMs: 5 * HOUR_MS, limitUsd: 12 },
{ id: "weekly", label: "Weekly", durationMs: 7 * DAY_MS, limitUsd: 30 },
+33 -10
View File
@@ -1,10 +1,33 @@
export const toNumber = (value: unknown): number | undefined => {
if (typeof value === "number" && Number.isFinite(value)) return value;
if (typeof value === "string") {
const trimmed = value.trim();
if (!trimmed) return undefined;
const parsed = Number(trimmed);
if (Number.isFinite(parsed)) return parsed;
}
return undefined;
};
import { toNumber } from "@oh-my-pi/pi-catalog/utils";
import type { UsageStatus } from "../usage";
/** Milliseconds in one hour. */
export const HOUR_MS = 60 * 60 * 1000;
/** Milliseconds in one day. */
export const DAY_MS = 24 * HOUR_MS;
/** Milliseconds in one seven-day week. */
export const WEEK_MS = 7 * DAY_MS;
/** Parses a finite positive epoch timestamp, tolerating seconds or milliseconds. */
export function parsePositiveTimestamp(value: unknown): number | undefined {
const parsed = toNumber(value);
if (parsed === undefined || parsed <= 0) return undefined;
return parsed < 1_000_000_000_000 ? parsed * 1000 : parsed;
}
/** Parses an ISO timestamp into epoch milliseconds. */
export function parseIsoTimestamp(value: unknown): number | undefined {
if (typeof value !== "string" || !value) return undefined;
const parsed = Date.parse(value);
return Number.isFinite(parsed) ? parsed : undefined;
}
/** Maps a used fraction to the standard quota status thresholds. */
export function usageStatus(usedFraction: number | undefined): UsageStatus {
if (usedFraction === undefined) return "unknown";
if (usedFraction >= 1) return "exhausted";
if (usedFraction >= 0.9) return "warning";
return "ok";
}
+4 -11
View File
@@ -9,10 +9,9 @@ import type {
UsageStatus,
UsageWindow,
} from "../usage";
import { HOUR_MS, parseIsoTimestamp, WEEK_MS } from "./shared";
const QUOTAS_URL = "https://api.synthetic.new/v2/quotas";
const FIVE_HOUR_MS = 5 * 60 * 60 * 1000;
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
function parseDollarAmount(value: unknown): number | undefined {
if (typeof value !== "string") return undefined;
@@ -21,12 +20,6 @@ function parseDollarAmount(value: unknown): number | undefined {
return Number.isFinite(parsed) ? parsed : undefined;
}
function parseIsoMs(value: unknown): number | undefined {
if (typeof value !== "string" || !value) return undefined;
const ms = Date.parse(value);
return Number.isFinite(ms) ? ms : undefined;
}
function buildUsageAmount(args: {
used: number | undefined;
limit: number | undefined;
@@ -61,7 +54,7 @@ function parseRollingFiveHourLimit(raw: unknown, provider: UsageFetchParams["pro
const remaining = typeof raw.remaining === "number" ? raw.remaining : undefined;
const max = typeof raw.max === "number" ? raw.max : undefined;
const limited = raw.limited === true;
const nextTickAt = parseIsoMs(raw.nextTickAt);
const nextTickAt = parseIsoTimestamp(raw.nextTickAt);
const tickPercent = typeof raw.tickPercent === "number" ? raw.tickPercent : undefined;
if (remaining === undefined && max === undefined) return null;
@@ -71,7 +64,7 @@ function parseRollingFiveHourLimit(raw: unknown, provider: UsageFetchParams["pro
const window: UsageWindow = {
id: "5h",
label: regenPercent !== undefined ? `5h · regen ${regenPercent}%/tick` : "5h",
durationMs: FIVE_HOUR_MS,
durationMs: 5 * HOUR_MS,
...(nextTickAt !== undefined ? { resetsAt: nextTickAt, resetLabel: "tick" } : {}),
};
const amount = buildUsageAmount({
@@ -97,7 +90,7 @@ function parseWeeklyTokenLimit(raw: unknown, provider: UsageFetchParams["provide
const remainingCredits = parseDollarAmount(raw.remainingCredits);
const maxCredits = parseDollarAmount(raw.maxCredits);
const percentRemaining = typeof raw.percentRemaining === "number" ? raw.percentRemaining : undefined;
const nextRegenAt = parseIsoMs(raw.nextRegenAt);
const nextRegenAt = parseIsoTimestamp(raw.nextRegenAt);
if (remainingCredits === undefined && maxCredits === undefined) return null;
+2 -2
View File
@@ -10,11 +10,11 @@ import type {
UsageWindow,
} from "../usage";
import { isRecord } from "../utils";
import { HOUR_MS } from "./shared";
const UMANS_PROVIDER = "umans";
const DEFAULT_ENDPOINT = "https://api.code.umans.ai";
const USAGE_PATH = "/v1/usage";
const FIVE_HOURS_MS = 5 * 60 * 60 * 1000;
/** Umans `GET /v1/usage` response (subset; extras ignored). */
interface UmansUsagePayload {
@@ -89,7 +89,7 @@ function buildRequestsLimit(payload: UmansUsagePayload, provider: string): Usage
const window: UsageWindow = {
id: "5h",
label: "rolling 5h",
durationMs: windowSeconds ? windowSeconds * 1000 : FIVE_HOURS_MS,
durationMs: windowSeconds ? windowSeconds * 1000 : 5 * HOUR_MS,
};
return {
id: "umans:requests",
+13 -26
View File
@@ -9,6 +9,7 @@
* product and must never be sent here.
*/
import { toNumber } from "@oh-my-pi/pi-catalog/utils";
import {
buildXAICliBillingUrl,
extractXAIAccessTokenSubject,
@@ -22,15 +23,12 @@ import type {
UsageLimit,
UsageProvider,
UsageReport,
UsageStatus,
UsageWindow,
} from "../usage";
import { isRecord } from "../utils";
import { toNumber } from "./shared";
import { DAY_MS, parseIsoTimestamp, usageStatus, WEEK_MS } from "./shared";
const PROVIDER_ID = "xai-oauth";
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
const DAY_MS = 24 * 60 * 60 * 1000;
const BILLING_SOURCE = "cli-chat-proxy.grok.com/v1/billing";
interface XaiBillingPeriod {
@@ -71,11 +69,6 @@ interface XaiMonthlyBillingConfig {
type XaiBillingConfig = XaiWeeklyBillingConfig | XaiMonthlyBillingConfig;
function parseIsoMs(value: string): number | undefined {
const parsed = Date.parse(value);
return Number.isFinite(parsed) ? parsed : undefined;
}
function parsePercent(value: unknown): number | undefined {
const percent = toNumber(value);
return percent !== undefined && percent >= 0 && percent <= 100 ? percent : undefined;
@@ -99,12 +92,6 @@ function buildPercentAmount(usagePercent: number): UsageAmount {
};
}
function buildUsageStatus(usedFraction: number): UsageStatus {
if (usedFraction >= 1) return "exhausted";
if (usedFraction >= 0.9) return "warning";
return "ok";
}
function slugifyProduct(product: string): string {
return product
.trim()
@@ -118,13 +105,13 @@ function buildPeriodWindow(period: XaiBillingPeriod): UsageWindow {
id: "1w",
label: "Weekly",
durationMs: WEEK_MS,
resetsAt: parseIsoMs(period.end),
resetsAt: parseIsoTimestamp(period.end),
};
}
function buildMonthlyWindow(periodStart: string, periodEnd: string): UsageWindow | undefined {
const startMs = parseIsoMs(periodStart);
const endMs = parseIsoMs(periodEnd);
const startMs = parseIsoTimestamp(periodStart);
const endMs = parseIsoTimestamp(periodEnd);
if (startMs === undefined || endMs === undefined || endMs <= startMs) return undefined;
// Real calendar months vary; use the observed period length from the API.
const durationMs = endMs - startMs;
@@ -140,8 +127,8 @@ function buildMonthlyWindow(periodStart: string, periodEnd: string): UsageWindow
function parseWeeklyBillingConfig(raw: Record<string, unknown>): XaiWeeklyBillingConfig | null {
if (!isRecord(raw.currentPeriod)) return null;
const start = typeof raw.currentPeriod.start === "string" ? parseIsoMs(raw.currentPeriod.start) : undefined;
const end = typeof raw.currentPeriod.end === "string" ? parseIsoMs(raw.currentPeriod.end) : undefined;
const start = typeof raw.currentPeriod.start === "string" ? parseIsoTimestamp(raw.currentPeriod.start) : undefined;
const end = typeof raw.currentPeriod.end === "string" ? parseIsoTimestamp(raw.currentPeriod.end) : undefined;
const type = typeof raw.currentPeriod.type === "string" ? raw.currentPeriod.type : "";
// Keep recently-ended weekly windows so /usage still renders across period
// rollover while the billing API is mid-refresh. Reject only inverted ranges
@@ -182,8 +169,8 @@ function parseWeeklyBillingConfig(raw: Record<string, unknown>): XaiWeeklyBillin
function parseMonthlyBillingConfig(raw: Record<string, unknown>): XaiMonthlyBillingConfig | null {
const periodStart = typeof raw.billingPeriodStart === "string" ? raw.billingPeriodStart : "";
const periodEnd = typeof raw.billingPeriodEnd === "string" ? raw.billingPeriodEnd : "";
const startMs = parseIsoMs(periodStart);
const endMs = parseIsoMs(periodEnd);
const startMs = parseIsoTimestamp(periodStart);
const endMs = parseIsoTimestamp(periodEnd);
if (!periodStart || !periodEnd || startMs === undefined || endMs === undefined || endMs <= startMs) {
return null;
}
@@ -227,7 +214,7 @@ function buildOnDemandLimit(
remainingFraction: 1 - usedFraction,
unit: "unknown",
},
status: buildUsageStatus(usedFraction),
status: usageStatus(usedFraction),
};
}
@@ -248,7 +235,7 @@ function buildLimits(config: XaiBillingConfig, accountId: string | undefined): U
scope,
window,
amount: overall,
status: buildUsageStatus(overall.usedFraction ?? 0),
status: usageStatus(overall.usedFraction ?? 0),
},
];
@@ -262,7 +249,7 @@ function buildLimits(config: XaiBillingConfig, accountId: string | undefined): U
scope,
window,
amount,
status: buildUsageStatus(amount.usedFraction ?? 0),
status: usageStatus(amount.usedFraction ?? 0),
});
}
const onDemand = buildOnDemandLimit(config.onDemandCap, config.onDemandUsed, accountId);
@@ -293,7 +280,7 @@ function buildLimits(config: XaiBillingConfig, accountId: string | undefined): U
// xAI does not label the unit; amounts match the dashboard quota points.
unit: "unknown",
},
status: buildUsageStatus(usedFraction),
status: usageStatus(usedFraction),
},
];
const onDemand = buildOnDemandLimit(config.onDemandCap, config.onDemandUsed, accountId);
+2 -5
View File
@@ -11,14 +11,11 @@ import type {
UsageWindow,
} from "../usage";
import { isRecord } from "../utils";
import { DAY_MS, HOUR_MS, WEEK_MS } from "./shared";
const DEFAULT_ENDPOINT = "https://api.z.ai";
const QUOTA_PATH = "/api/monitor/usage/quota/limit";
const MODEL_USAGE_PATH = "/api/monitor/usage/model-usage";
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
const HOUR_MS = 60 * 60 * 1000;
const DAY_MS = 24 * HOUR_MS;
const WEEK_MS = 7 * DAY_MS;
const MONTH_MS = 30 * DAY_MS;
interface ZaiUsageDetail {
@@ -188,7 +185,7 @@ function requestQuotaLabel(parsed: ZaiUsageLimitItem): string {
}
function buildModelUsageUrl(baseUrl: string, now: Date): string {
const start = new Date(now.getTime() - SEVEN_DAYS_MS);
const start = new Date(now.getTime() - WEEK_MS);
const startTime = formatDate(start);
const endTime = formatDate(now);
return `${baseUrl}${MODEL_USAGE_PATH}?startTime=${encodeURIComponent(startTime)}&endTime=${encodeURIComponent(endTime)}`;