feat(codex): show reset credit expiry details (#2290)

Add read-only GET to inspect per-credit reset inventory (status, granted,
expiry, remaining) with a Quota Tracker modal. DRY the route via shared
connection/refresh helpers; keep existing consume POST unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Rafli Ahmad Zulfikar
2026-07-03 15:02:28 +07:00
committed by decolua
parent cd557a2552
commit 5cc4f222f8
6 changed files with 495 additions and 57 deletions

View File

@@ -40,6 +40,7 @@ export default {
},
usage: {
url: "https://chatgpt.com/backend-api/wham/usage",
resetCreditsUrl: "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits",
resetCreditsConsumeUrl: "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume",
},
},

View File

@@ -5,9 +5,9 @@
import { getGitHubUsage } from "./usage/github.js";
import { getGeminiUsage, getAntigravityUsage } from "./usage/google.js";
import { getClaudeUsage } from "./usage/claude.js";
import { getCodexUsage, consumeCodexRateLimitResetCredit } from "./usage/codex.js";
import { getCodexUsage, consumeCodexRateLimitResetCredit, getCodexRateLimitResetCredits } from "./usage/codex.js";
export { consumeCodexRateLimitResetCredit };
export { consumeCodexRateLimitResetCredit, getCodexRateLimitResetCredits };
import { getKiroUsage } from "./usage/kiro.js";
import { getMiniMaxUsage } from "./usage/minimax.js";
import { getCodeBuddyCnUsage } from "./usage/codebuddy-cn.js";

View File

@@ -8,9 +8,23 @@ import { U, parseResetTime, toFiniteNumber } from "./shared.js";
// Codex (OpenAI) API config
const CODEX_CONFIG = {
usageUrl: U("codex").url,
resetCreditsUrl: U("codex").resetCreditsUrl,
resetCreditsConsumeUrl: U("codex").resetCreditsConsumeUrl,
};
function toIsoDate(value) {
if (!value) return null;
const date = value instanceof Date
? value
: new Date(typeof value === "number" && value < 1e12 ? value * 1000 : value);
const time = date.getTime();
return Number.isFinite(time) ? date.toISOString() : null;
}
function getCodexAccountId(providerSpecificData) {
return providerSpecificData?.workspaceId || providerSpecificData?.accountId || providerSpecificData?.chatgptAccountId || null;
}
function getCodexRateLimitBody(snapshot) {
if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) return null;
return snapshot.rate_limit && typeof snapshot.rate_limit === "object"
@@ -101,6 +115,48 @@ export async function getCodexUsage(accessToken, proxyOptions = null) {
}
}
export async function getCodexRateLimitResetCredits(accessToken, proxyOptions = null, providerSpecificData = null) {
if (!accessToken) {
throw new Error("No Codex access token available. Please re-authorize the connection.");
}
const accountId = getCodexAccountId(providerSpecificData);
const headers = {
"Authorization": `Bearer ${accessToken}`,
"Accept": "application/json",
"OpenAI-Beta": "codex-1",
"originator": "codex_cli_rs",
};
if (accountId) headers["ChatGPT-Account-ID"] = accountId;
const response = await proxyAwareFetch(CODEX_CONFIG.resetCreditsUrl, {
method: "GET",
headers,
}, proxyOptions);
let data = null;
try {
data = await response.json();
} catch {
data = null;
}
if (!response.ok) {
const message = data?.message || data?.error || data?.detail || `Codex reset credits API unavailable (${response.status}).`;
throw new Error(message);
}
const credits = Array.isArray(data?.credits) ? data.credits : [];
return {
availableCount: Math.max(0, toFiniteNumber(data?.available_count ?? data?.availableCount, 0)),
credits: credits.map((credit) => ({
status: String(credit?.status || "unknown"),
grantedAt: toIsoDate(credit?.granted_at ?? credit?.grantedAt),
expiresAt: toIsoDate(credit?.expires_at ?? credit?.expiresAt),
})),
};
}
// Consume one Codex rate-limit reset credit (irreversible, spends 1 credit)
export async function consumeCodexRateLimitResetCredit(accessToken, redeemRequestId, proxyOptions = null) {
if (!accessToken) {