86 lines
2.7 KiB
TypeScript
86 lines
2.7 KiB
TypeScript
import type { CodexAuth, CodexUsageResponse } from "@/types/codex";
|
|
|
|
const USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
|
|
function isNumber(value: unknown): value is number {
|
|
return typeof value === "number" && Number.isFinite(value);
|
|
}
|
|
|
|
export function parseCodexUsageResponse(data: unknown): CodexUsageResponse {
|
|
if (!data || typeof data !== "object") {
|
|
throw new Error("INVALID_CODEX_RESPONSE");
|
|
}
|
|
|
|
const value = data as Record<string, unknown>;
|
|
const rateLimit = value.rate_limit as Record<string, unknown> | undefined;
|
|
const primary = rateLimit?.primary_window as Record<string, unknown> | undefined;
|
|
const secondary = rateLimit?.secondary_window as Record<string, unknown> | undefined;
|
|
const credits = value.credits as Record<string, unknown> | undefined;
|
|
|
|
const validWindow = (window: Record<string, unknown> | undefined) =>
|
|
!!window &&
|
|
isNumber(window.used_percent) &&
|
|
isNumber(window.reset_at) &&
|
|
isNumber(window.limit_window_seconds);
|
|
|
|
if (
|
|
typeof value.plan_type !== "string" ||
|
|
!validWindow(primary) ||
|
|
!validWindow(secondary) ||
|
|
!credits ||
|
|
typeof credits.has_credits !== "boolean" ||
|
|
typeof credits.unlimited !== "boolean" ||
|
|
!isNumber(credits.balance)
|
|
) {
|
|
throw new Error("INVALID_CODEX_RESPONSE");
|
|
}
|
|
|
|
const primaryWindow = primary as Record<string, unknown>;
|
|
const secondaryWindow = secondary as Record<string, unknown>;
|
|
|
|
return {
|
|
plan_type: value.plan_type,
|
|
rate_limit: {
|
|
primary_window: {
|
|
used_percent: primaryWindow.used_percent as number,
|
|
reset_at: primaryWindow.reset_at as number,
|
|
limit_window_seconds: primaryWindow.limit_window_seconds as number,
|
|
},
|
|
secondary_window: {
|
|
used_percent: secondaryWindow.used_percent as number,
|
|
reset_at: secondaryWindow.reset_at as number,
|
|
limit_window_seconds: secondaryWindow.limit_window_seconds as number,
|
|
},
|
|
},
|
|
credits: {
|
|
has_credits: credits.has_credits,
|
|
unlimited: credits.unlimited,
|
|
balance: credits.balance,
|
|
},
|
|
};
|
|
}
|
|
|
|
export async function fetchCodexUsage(auth: CodexAuth): Promise<CodexUsageResponse> {
|
|
const headers: Record<string, string> = {
|
|
Authorization: `Bearer ${auth.accessToken}`,
|
|
"Content-Type": "application/json",
|
|
};
|
|
|
|
if (auth.accountId) {
|
|
headers["ChatGPT-Account-Id"] = auth.accountId;
|
|
}
|
|
|
|
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 data: unknown = await response.json();
|
|
return parseCodexUsageResponse(data);
|
|
}
|