import type { CodexAuth, CodexUsageResponse } from "@/types/codex"; const USAGE_URL = "https://chatgpt.com/backend-api/wham/usage"; const RESET_CREDITS_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits"; interface RawUsageWindow { used_percent?: number; limit_window_seconds?: number; reset_at?: number; } interface RawUsageResponse { plan_type?: string | null; rate_limit?: { primary_window?: RawUsageWindow | null; secondary_window?: RawUsageWindow | null; } | null; credits?: { has_credits?: boolean; unlimited?: boolean; balance?: string | number; } | null; rate_limit_reached_type?: string | null; rate_limit_reset_credits?: { available_count?: number | null; } | null; } interface RawResetCredit { status?: string; granted_at?: string | null; expires_at?: string | null; } interface RawResetCreditsResponse { available_count?: number | null; total_earned_count?: number | null; credits?: RawResetCredit[] | null; } function isNumber(value: unknown): value is number { return typeof value === "number" && Number.isFinite(value); } function formatDurationUntil(isoString: string | null | undefined): string | null { if (!isoString) return null; const targetMs = new Date(isoString).getTime(); if (Number.isNaN(targetMs)) return null; let remainingSeconds = Math.max(Math.round((targetMs - Date.now()) / 1000), 0); const days = Math.floor(remainingSeconds / 86400); remainingSeconds -= days * 86400; const hours = Math.floor(remainingSeconds / 3600); remainingSeconds -= hours * 3600; const minutes = Math.floor(remainingSeconds / 60); remainingSeconds -= minutes * 60; const parts: string[] = []; if (days > 0) parts.push(`${days}d`); if (hours > 0) parts.push(`${hours}h`); if (minutes > 0) parts.push(`${minutes}m`); if (remainingSeconds > 0 || parts.length === 0) parts.push(`${remainingSeconds}s`); return parts.join(" "); } function normalizeWindow(window: RawUsageWindow | null | undefined) { if (!window) return null; const windowSeconds = typeof window.limit_window_seconds === "number" ? window.limit_window_seconds : 0; const resetsAt = typeof window.reset_at === "number" ? window.reset_at : 0; return { usedPercent: typeof window.used_percent === "number" ? window.used_percent : 0, resetsAt, windowMinutes: Math.round(windowSeconds / 60), windowSeconds, }; } function buildBaseHeaders(auth: CodexAuth): Record { const headers: Record = { Authorization: `Bearer ${auth.accessToken}`, "Content-Type": "application/json", }; if (auth.accountId) { headers["ChatGPT-Account-Id"] = auth.accountId; } return headers; } async function fetchResetCoupons(auth: CodexAuth, usage: RawUsageResponse) { const headers = { ...buildBaseHeaders(auth), "OpenAI-Beta": "codex-1", originator: "Codex Desktop", }; try { const response = await fetch(RESET_CREDITS_URL, { headers }); if (!response.ok) { throw new Error(`HTTP_ERROR_${response.status}`); } const payload = (await response.json()) as RawResetCreditsResponse; const credits = Array.isArray(payload.credits) ? payload.credits : []; const normalizedCredits = credits .map((credit, index) => ({ index: index + 1, status: credit.status, grantedAt: credit.granted_at ?? null, grantedAtLocal: credit.granted_at ? new Date(credit.granted_at).toLocaleString() : null, expiresAt: credit.expires_at ?? null, expiresAtLocal: credit.expires_at ? new Date(credit.expires_at).toLocaleString() : null, timeUntilExpiry: formatDurationUntil(credit.expires_at), })) .sort((a, b) => { const left = a.expiresAt ? new Date(a.expiresAt).getTime() : Number.MAX_SAFE_INTEGER; const right = b.expiresAt ? new Date(b.expiresAt).getTime() : Number.MAX_SAFE_INTEGER; return left - right; }); const availableCredits = normalizedCredits.filter( (credit) => credit.status === "available" ); return { source: "live_api" as const, sourceDescription: "Live Codex reset-credit endpoint", availableCount: payload.available_count ?? usage.rate_limit_reset_credits?.available_count ?? null, totalEarnedCount: payload.total_earned_count ?? null, credits: normalizedCredits, nextExpiringCredit: availableCredits[0] ?? normalizedCredits[0] ?? null, }; } catch (error) { const message = error instanceof Error ? error.message : "UNKNOWN_ERROR"; return { source: "unavailable" as const, sourceDescription: "Reset-credit endpoint unavailable", availableCount: usage.rate_limit_reset_credits?.available_count ?? null, fallbackReason: message, error: message, }; } } export function parseCodexUsageResponse(data: unknown): CodexUsageResponse { if (!data || typeof data !== "object") { throw new Error("INVALID_CODEX_RESPONSE"); } const value = data as Record; const primary = value.primary as Record | null | undefined; const secondary = value.secondary as Record | null | undefined; const credits = value.credits as Record | undefined; const validWindow = (window: Record | null | undefined) => window === null || window === undefined || (isNumber(window.usedPercent) && isNumber(window.resetsAt) && isNumber(window.windowMinutes) && isNumber(window.windowSeconds)); if ( !validWindow(primary) || !validWindow(secondary) || !credits || typeof credits.hasCredits !== "boolean" || typeof credits.unlimited !== "boolean" || !isNumber(credits.balance) ) { throw new Error("INVALID_CODEX_RESPONSE"); } return value as unknown as CodexUsageResponse; } export async function fetchCodexUsage(auth: CodexAuth): Promise { const headers = buildBaseHeaders(auth); const response = await fetch(USAGE_URL, { headers }); if (response.status === 401 || response.status === 403) { throw new Error("TOKEN_EXPIRED"); } if (!response.ok) { await response.text().catch(() => null); throw new Error(`HTTP_ERROR_${response.status}`); } const payload = (await response.json()) as RawUsageResponse; const resetCoupons = await fetchResetCoupons(auth, payload); return { planType: payload.plan_type ?? null, rateLimitReachedType: payload.rate_limit_reached_type ?? null, primary: normalizeWindow(payload.rate_limit?.primary_window), secondary: normalizeWindow(payload.rate_limit?.secondary_window), credits: { hasCredits: Boolean(payload.credits?.has_credits), unlimited: Boolean(payload.credits?.unlimited), balance: Number(payload.credits?.balance ?? 0), }, resetCoupons, }; }