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