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:
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 },
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,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 },
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)}`;
|
||||
|
||||
Reference in New Issue
Block a user