mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 23:40:45 +00:00
121 lines
3.4 KiB
TypeScript
121 lines
3.4 KiB
TypeScript
import { buildUsageHttpErrorSnapshot, fetchJson } from "./provider-usage.fetch.shared.js";
|
|
import { clampPercent, PROVIDER_LABELS } from "./provider-usage.shared.js";
|
|
import type { ProviderUsageSnapshot, UsageWindow } from "./provider-usage.types.js";
|
|
|
|
type CodexUsageResponse = {
|
|
rate_limit?: {
|
|
primary_window?: {
|
|
limit_window_seconds?: number;
|
|
used_percent?: number;
|
|
reset_at?: number;
|
|
};
|
|
secondary_window?: {
|
|
limit_window_seconds?: number;
|
|
used_percent?: number;
|
|
reset_at?: number;
|
|
};
|
|
};
|
|
plan_type?: string;
|
|
credits?: { balance?: number | string | null };
|
|
};
|
|
|
|
const WEEKLY_RESET_GAP_SECONDS = 3 * 24 * 60 * 60;
|
|
|
|
function resolveSecondaryWindowLabel(params: {
|
|
windowHours: number;
|
|
secondaryResetAt?: number;
|
|
primaryResetAt?: number;
|
|
}): string {
|
|
if (params.windowHours >= 168) {
|
|
return "Week";
|
|
}
|
|
if (params.windowHours < 24) {
|
|
return `${params.windowHours}h`;
|
|
}
|
|
// Codex occasionally reports a 24h secondary window while exposing a
|
|
// weekly reset cadence in reset timestamps. Prefer cadence in that case.
|
|
if (
|
|
typeof params.secondaryResetAt === "number" &&
|
|
typeof params.primaryResetAt === "number" &&
|
|
params.secondaryResetAt - params.primaryResetAt >= WEEKLY_RESET_GAP_SECONDS
|
|
) {
|
|
return "Week";
|
|
}
|
|
return "Day";
|
|
}
|
|
|
|
export async function fetchCodexUsage(
|
|
token: string,
|
|
accountId: string | undefined,
|
|
timeoutMs: number,
|
|
fetchFn: typeof fetch,
|
|
): Promise<ProviderUsageSnapshot> {
|
|
const headers: Record<string, string> = {
|
|
Authorization: `Bearer ${token}`,
|
|
"User-Agent": "CodexBar",
|
|
Accept: "application/json",
|
|
};
|
|
if (accountId) {
|
|
headers["ChatGPT-Account-Id"] = accountId;
|
|
}
|
|
|
|
const res = await fetchJson(
|
|
"https://chatgpt.com/backend-api/wham/usage",
|
|
{ method: "GET", headers },
|
|
timeoutMs,
|
|
fetchFn,
|
|
);
|
|
|
|
if (!res.ok) {
|
|
return buildUsageHttpErrorSnapshot({
|
|
provider: "openai-codex",
|
|
status: res.status,
|
|
tokenExpiredStatuses: [401, 403],
|
|
});
|
|
}
|
|
|
|
const data = (await res.json()) as CodexUsageResponse;
|
|
const windows: UsageWindow[] = [];
|
|
|
|
if (data.rate_limit?.primary_window) {
|
|
const pw = data.rate_limit.primary_window;
|
|
const windowHours = Math.round((pw.limit_window_seconds || 10800) / 3600);
|
|
windows.push({
|
|
label: `${windowHours}h`,
|
|
usedPercent: clampPercent(pw.used_percent || 0),
|
|
resetAt: pw.reset_at ? pw.reset_at * 1000 : undefined,
|
|
});
|
|
}
|
|
|
|
if (data.rate_limit?.secondary_window) {
|
|
const sw = data.rate_limit.secondary_window;
|
|
const windowHours = Math.round((sw.limit_window_seconds || 86400) / 3600);
|
|
const label = resolveSecondaryWindowLabel({
|
|
windowHours,
|
|
primaryResetAt: data.rate_limit?.primary_window?.reset_at,
|
|
secondaryResetAt: sw.reset_at,
|
|
});
|
|
windows.push({
|
|
label,
|
|
usedPercent: clampPercent(sw.used_percent || 0),
|
|
resetAt: sw.reset_at ? sw.reset_at * 1000 : undefined,
|
|
});
|
|
}
|
|
|
|
let plan = data.plan_type;
|
|
if (data.credits?.balance !== undefined && data.credits.balance !== null) {
|
|
const balance =
|
|
typeof data.credits.balance === "number"
|
|
? data.credits.balance
|
|
: parseFloat(data.credits.balance) || 0;
|
|
plan = plan ? `${plan} ($${balance.toFixed(2)})` : `$${balance.toFixed(2)}`;
|
|
}
|
|
|
|
return {
|
|
provider: "openai-codex",
|
|
displayName: PROVIDER_LABELS["openai-codex"],
|
|
windows,
|
|
plan,
|
|
};
|
|
}
|