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) {

View File

@@ -97,6 +97,30 @@ function getCodexResetCreditCount(quota) {
return Number.isFinite(count) ? Math.max(0, count) : 0;
}
function formatCreditDate(value) {
if (!value) return "N/A";
const date = new Date(value);
if (!Number.isFinite(date.getTime())) return "N/A";
return date.toLocaleString(undefined, {
month: "short",
day: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
});
}
function formatTimeRemaining(value) {
if (!value) return "N/A";
const diffMs = new Date(value).getTime() - Date.now();
if (!Number.isFinite(diffMs)) return "N/A";
if (diffMs <= 0) return "Expired";
const totalHours = Math.ceil(diffMs / (60 * 60 * 1000));
const days = Math.floor(totalHours / 24);
const hours = totalHours % 24;
return days > 0 ? `${days}d ${hours}h` : `${hours}h`;
}
export default function ProviderLimits() {
const { copied, copy } = useCopyToClipboard();
const [connections, setConnections] = useState([]);
@@ -114,6 +138,7 @@ export default function ProviderLimits() {
const [togglingId, setTogglingId] = useState(null);
const [resettingLimitId, setResettingLimitId] = useState(null);
const [resetConfirmState, setResetConfirmState] = useState(null);
const [resetCreditsState, setResetCreditsState] = useState(null);
const [showEditModal, setShowEditModal] = useState(false);
const [selectedConnection, setSelectedConnection] = useState(null);
const [proxyPools, setProxyPools] = useState([]);
@@ -298,6 +323,26 @@ export default function ProviderLimits() {
[fetchQuota, resettingLimitId],
);
const handleViewCodexResetCredits = useCallback(async (connection) => {
setResetCreditsState({ connection, loading: true, error: null, data: null });
try {
const response = await fetch(`/api/usage/${connection.id}/codex-reset-credits`, { cache: "no-store" });
const result = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(result.error || result.message || "Failed to load Codex reset credits");
}
const credits = Array.isArray(result.credits) ? [...result.credits] : [];
credits.sort((a, b) => {
const aTime = a.expiresAt ? new Date(a.expiresAt).getTime() : Number.POSITIVE_INFINITY;
const bTime = b.expiresAt ? new Date(b.expiresAt).getTime() : Number.POSITIVE_INFINITY;
return aTime - bTime;
});
setResetCreditsState({ connection, loading: false, error: null, data: { ...result, credits } });
} catch (error) {
setResetCreditsState({ connection, loading: false, error: error.message || "Failed to load Codex reset credits", data: null });
}
}, []);
const handleDeleteConnection = useCallback(
async (id) => {
if (!confirm("Delete this connection?")) return;
@@ -1008,34 +1053,47 @@ export default function ProviderLimits() {
<div className="flex items-center gap-1 shrink-0">
{isCodex && (
<Tooltip
text={
resetCreditCount > 0
? `Use one Codex reset credit. Available: ${resetCreditCount}`
: "No Codex reset credits available"
}
>
<button
type="button"
onClick={() => setResetConfirmState({ connection: conn, resetCreditCount })}
disabled={resetCreditCount <= 0 || isLoading || rowBusy}
aria-label={
<>
<Tooltip
text={
resetCreditCount > 0
? `Use one Codex reset credit. ${resetCreditCount} available.`
? `Use one Codex reset credit. Available: ${resetCreditCount}`
: "No Codex reset credits available"
}
className={`flex h-8 min-w-10 items-center justify-center gap-1 rounded-lg border px-2 text-[11px] font-medium tabular-nums transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary/60 disabled:cursor-not-allowed disabled:opacity-60 ${
resetCreditCount > 0
? "border-primary/30 bg-primary/5 text-primary hover:bg-primary/10"
: "border-black/10 bg-black/[0.02] text-text-muted dark:border-white/10 dark:bg-white/[0.03]"
}`}
>
<span className={`material-symbols-outlined text-[15px] ${isResettingLimit ? "animate-spin" : ""}`}>
{isResettingLimit ? "progress_activity" : "restart_alt"}
</span>
<span>{resetCreditCount}</span>
</button>
</Tooltip>
<button
type="button"
onClick={() => setResetConfirmState({ connection: conn, resetCreditCount })}
disabled={resetCreditCount <= 0 || isLoading || rowBusy}
aria-label={
resetCreditCount > 0
? `Use one Codex reset credit. ${resetCreditCount} available.`
: "No Codex reset credits available"
}
className={`flex h-8 min-w-10 items-center justify-center gap-1 rounded-lg border px-2 text-[11px] font-medium tabular-nums transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary/60 disabled:cursor-not-allowed disabled:opacity-60 ${
resetCreditCount > 0
? "border-primary/30 bg-primary/5 text-primary hover:bg-primary/10"
: "border-black/10 bg-black/[0.02] text-text-muted dark:border-white/10 dark:bg-white/[0.03]"
}`}
>
<span className={`material-symbols-outlined text-[15px] ${isResettingLimit ? "animate-spin" : ""}`}>
{isResettingLimit ? "progress_activity" : "restart_alt"}
</span>
<span>{resetCreditCount}</span>
</button>
</Tooltip>
<Tooltip text="View Codex reset credit expiry">
<button
type="button"
onClick={() => handleViewCodexResetCredits(conn)}
disabled={isLoading || rowBusy}
aria-label="View Codex reset credit expiry"
className="flex h-8 w-8 items-center justify-center rounded-lg border border-black/10 text-text-muted transition-colors hover:bg-black/5 hover:text-primary disabled:cursor-not-allowed disabled:opacity-50 dark:border-white/10 dark:hover:bg-white/5"
>
<span className="material-symbols-outlined text-[17px]">schedule</span>
</button>
</Tooltip>
</>
)}
{AUTO_PING_SETTINGS_KEYS[conn.provider] && conn.authType === "oauth" && (
<Tooltip text={AUTO_PING_TOOLTIPS[conn.provider]}>
@@ -1292,6 +1350,79 @@ export default function ProviderLimits() {
loading={Boolean(resettingLimitId)}
/>
{resetCreditsState && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 px-4 backdrop-blur-sm">
<div className="w-full max-w-2xl overflow-hidden rounded-2xl border border-black/15 bg-white shadow-2xl ring-1 ring-black/10 dark:border-white/15 dark:bg-neutral-950 dark:ring-white/10">
<div className="flex items-start justify-between gap-3 border-b border-black/10 bg-black/[0.03] px-4 py-3 dark:border-white/10 dark:bg-white/[0.04]">
<div className="min-w-0">
<h3 className="text-base font-semibold text-text-primary">Codex Reset Credit Expiry</h3>
<p className="mt-0.5 truncate text-xs text-text-muted">
{getConnectionLabel(resetCreditsState.connection) || "Codex account"}
</p>
</div>
<button
type="button"
onClick={() => setResetCreditsState(null)}
className="flex h-8 w-8 items-center justify-center rounded-lg text-text-muted transition-colors hover:bg-black/5 hover:text-text-primary dark:hover:bg-white/5"
aria-label="Close reset credit expiry modal"
>
<span className="material-symbols-outlined text-[18px]">close</span>
</button>
</div>
<div className="max-h-[70vh] overflow-auto bg-white p-4 dark:bg-neutral-950">
{resetCreditsState.loading ? (
<div className="flex items-center justify-center gap-2 py-10 text-sm text-text-muted">
<span className="material-symbols-outlined animate-spin text-[20px]">progress_activity</span>
Loading reset credits...
</div>
) : resetCreditsState.error ? (
<div className="rounded-xl border border-red-500/20 bg-red-500/10 px-3 py-2 text-sm text-red-600 dark:text-red-300">
{resetCreditsState.error}
</div>
) : resetCreditsState.data?.credits?.length ? (
<div className="space-y-3">
<div className="flex items-center justify-between rounded-xl border border-black/10 bg-black/[0.02] px-3 py-2 text-xs text-text-muted dark:border-white/10 dark:bg-white/[0.03]">
<span>{resetCreditsState.data.credits.length} reset credit{resetCreditsState.data.credits.length === 1 ? "" : "s"}</span>
<span>{resetCreditsState.data.availableCount ?? 0} available</span>
</div>
<div className="overflow-x-auto rounded-xl border border-black/10 dark:border-white/10">
<table className="w-full min-w-[560px] text-left text-sm">
<thead className="bg-black/[0.03] text-xs uppercase tracking-wide text-text-muted dark:bg-white/[0.04]">
<tr>
<th className="px-3 py-2 font-medium">Status</th>
<th className="px-3 py-2 font-medium">Granted At</th>
<th className="px-3 py-2 font-medium">Expires At</th>
<th className="px-3 py-2 font-medium">Remaining</th>
</tr>
</thead>
<tbody>
{resetCreditsState.data.credits.map((credit, index) => (
<tr key={`${credit.status}-${credit.expiresAt || index}`} className="border-t border-black/5 dark:border-white/5">
<td className="px-3 py-2">
<span className="rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
{credit.status || "unknown"}
</span>
</td>
<td className="px-3 py-2 text-text-muted">{formatCreditDate(credit.grantedAt)}</td>
<td className="px-3 py-2 text-text-primary">{formatCreditDate(credit.expiresAt)}</td>
<td className="px-3 py-2 font-medium text-text-primary">{formatTimeRemaining(credit.expiresAt)}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
) : (
<div className="rounded-xl border border-black/10 bg-black/[0.02] px-3 py-8 text-center text-sm text-text-muted dark:border-white/10 dark:bg-white/[0.03]">
No reset credit details returned for this account.
</div>
)}
</div>
</div>
</div>
)}
<EditConnectionModal
isOpen={showEditModal}
connection={selectedConnection}

View File

@@ -2,7 +2,7 @@
import "open-sse/index.js";
import { getProviderConnectionById } from "@/lib/localDb";
import { consumeCodexRateLimitResetCredit } from "open-sse/services/usage.js";
import { consumeCodexRateLimitResetCredit, getCodexRateLimitResetCredits } from "open-sse/services/usage.js";
import { resolveConnectionProxyConfig } from "@/lib/network/connectionProxy";
import { refreshAndUpdateCredentials } from "../route.js";
@@ -15,6 +15,10 @@ function isAuthExpiredResult(result) {
return values.some((value) => AUTH_EXPIRED_PATTERNS.some((pattern) => value.includes(pattern)));
}
function isAuthExpiredError(error) {
return isAuthExpiredResult({ message: error?.message });
}
function getResponseForConsumeResult(result, redeemRequestId) {
if (result.ok) {
return Response.json({
@@ -43,42 +47,90 @@ function getResponseForConsumeResult(result, redeemRequestId) {
}, { status: result.status >= 400 && result.status < 500 ? result.status : 502 });
}
async function getCodexConnection(connectionId) {
const connection = await getProviderConnectionById(connectionId);
if (!connection) {
return { response: Response.json({ error: "Connection not found" }, { status: 404 }) };
}
if (connection.provider !== "codex") {
return { response: Response.json({ error: "Codex reset credits are only available for Codex connections." }, { status: 400 }) };
}
const isOAuth = connection.authType === "oauth";
const isAccessToken = connection.authType === "access_token";
if (!isOAuth && !isAccessToken) {
return { response: Response.json({ error: "Codex reset credits require an OAuth or access-token connection." }, { status: 400 }) };
}
const proxyConfig = await resolveConnectionProxyConfig(connection.providerSpecificData);
const proxyOptions = {
connectionProxyEnabled: proxyConfig.connectionProxyEnabled === true,
connectionProxyUrl: proxyConfig.connectionProxyUrl || "",
connectionNoProxy: proxyConfig.connectionNoProxy || "",
vercelRelayUrl: proxyConfig.vercelRelayUrl || "",
strictProxy: false,
};
return { connection, isOAuth, proxyOptions };
}
async function refreshCodexConnection(connection, proxyOptions) {
try {
const result = await refreshAndUpdateCredentials(connection, false, proxyOptions);
return { connection: result.connection };
} catch (refreshError) {
console.error("[Codex Reset Credits API] Credential refresh failed:", refreshError);
return { response: Response.json({ error: `Credential refresh failed: ${refreshError.message}` }, { status: 401 }) };
}
}
export async function GET(_request, { params }) {
let connection;
try {
const { connectionId } = await params;
const resolved = await getCodexConnection(connectionId);
if (resolved.response) return resolved.response;
({ connection } = resolved);
const { isOAuth, proxyOptions } = resolved;
if (isOAuth) {
const refreshed = await refreshCodexConnection(connection, proxyOptions);
if (refreshed.response) return refreshed.response;
connection = refreshed.connection;
}
let result;
try {
result = await getCodexRateLimitResetCredits(connection.accessToken, proxyOptions, connection.providerSpecificData);
} catch (fetchError) {
if (!isOAuth || !connection.refreshToken || !isAuthExpiredError(fetchError)) throw fetchError;
const retryResult = await refreshAndUpdateCredentials(connection, true, proxyOptions);
connection = retryResult.connection;
result = await getCodexRateLimitResetCredits(connection.accessToken, proxyOptions, connection.providerSpecificData);
}
return Response.json(result);
} catch (error) {
const provider = connection?.provider ?? "unknown";
console.warn(`[Codex Reset Credits] ${provider}: ${error.message}`);
return Response.json({ error: error.message }, { status: 500 });
}
}
export async function POST(request, { params }) {
let connection;
try {
const { connectionId } = await params;
connection = await getProviderConnectionById(connectionId);
if (!connection) {
return Response.json({ error: "Connection not found" }, { status: 404 });
}
if (connection.provider !== "codex") {
return Response.json({ error: "Codex reset credits are only available for Codex connections." }, { status: 400 });
}
const isOAuth = connection.authType === "oauth";
const isAccessToken = connection.authType === "access_token";
if (!isOAuth && !isAccessToken) {
return Response.json({ error: "Codex reset credits require an OAuth or access-token connection." }, { status: 400 });
}
const proxyConfig = await resolveConnectionProxyConfig(connection.providerSpecificData);
const proxyOptions = {
connectionProxyEnabled: proxyConfig.connectionProxyEnabled === true,
connectionProxyUrl: proxyConfig.connectionProxyUrl || "",
connectionNoProxy: proxyConfig.connectionNoProxy || "",
vercelRelayUrl: proxyConfig.vercelRelayUrl || "",
strictProxy: false,
};
const resolved = await getCodexConnection(connectionId);
if (resolved.response) return resolved.response;
({ connection } = resolved);
const { isOAuth, proxyOptions } = resolved;
if (isOAuth) {
try {
const result = await refreshAndUpdateCredentials(connection, false, proxyOptions);
connection = result.connection;
} catch (refreshError) {
console.error("[Codex Reset Credits API] Credential refresh failed:", refreshError);
return Response.json({ error: `Credential refresh failed: ${refreshError.message}` }, { status: 401 });
}
const refreshed = await refreshCodexConnection(connection, proxyOptions);
if (refreshed.response) return refreshed.response;
connection = refreshed.connection;
}
// Server-generated redeem id prevents client-controlled replay

View File

@@ -0,0 +1,198 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
const mocks = vi.hoisted(() => ({
proxyAwareFetch: vi.fn(),
getProviderConnectionById: vi.fn(),
resolveConnectionProxyConfig: vi.fn(),
refreshAndUpdateCredentials: vi.fn(),
getCodexRateLimitResetCredits: vi.fn(),
consumeCodexRateLimitResetCredit: vi.fn(),
}));
vi.mock("../../open-sse/utils/proxyFetch.js", () => ({
proxyAwareFetch: mocks.proxyAwareFetch,
}));
vi.mock("open-sse/index.js", () => ({}));
vi.mock("@/lib/localDb", () => ({
getProviderConnectionById: mocks.getProviderConnectionById,
}));
vi.mock("@/lib/network/connectionProxy", () => ({
resolveConnectionProxyConfig: mocks.resolveConnectionProxyConfig,
}));
vi.mock("@/app/api/usage/[connectionId]/route.js", () => ({
refreshAndUpdateCredentials: mocks.refreshAndUpdateCredentials,
}));
vi.mock("open-sse/services/usage.js", () => ({
getCodexRateLimitResetCredits: mocks.getCodexRateLimitResetCredits,
consumeCodexRateLimitResetCredit: mocks.consumeCodexRateLimitResetCredit,
}));
describe("Codex reset credits", () => {
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
mocks.resolveConnectionProxyConfig.mockResolvedValue({});
});
it("returns normalized reset credit expiry details", async () => {
mocks.proxyAwareFetch.mockResolvedValue({
ok: true,
status: 200,
json: async () => ({
available_count: 2,
credits: [
{
status: "available",
granted_at: "2026-06-18T00:25:18Z",
expires_at: "2026-07-18T00:25:18Z",
},
{
status: "redeemed",
granted_at: "bad-date",
expires_at: null,
},
],
}),
});
const { getCodexRateLimitResetCredits } = await import("../../open-sse/services/usage/codex.js");
const result = await getCodexRateLimitResetCredits("token", { strictProxy: false }, { workspaceId: "acct_123" });
expect(mocks.proxyAwareFetch).toHaveBeenCalledWith(
expect.stringContaining("/rate-limit-reset-credits"),
expect.objectContaining({
method: "GET",
headers: expect.objectContaining({
Authorization: "Bearer token",
"ChatGPT-Account-ID": "acct_123",
}),
}),
{ strictProxy: false },
);
expect(result).toEqual({
availableCount: 2,
credits: [
{
status: "available",
grantedAt: "2026-06-18T00:25:18.000Z",
expiresAt: "2026-07-18T00:25:18.000Z",
},
{
status: "redeemed",
grantedAt: null,
expiresAt: null,
},
],
});
});
it("GET refreshes OAuth credentials before returning reset credit details", async () => {
const connection = {
id: "conn_1",
provider: "codex",
authType: "oauth",
accessToken: "old-token",
refreshToken: "refresh-token",
providerSpecificData: { workspaceId: "acct_123" },
};
const refreshedConnection = { ...connection, accessToken: "new-token" };
const resetCredits = {
availableCount: 1,
credits: [{ status: "available", grantedAt: "2026-06-18T00:25:18.000Z", expiresAt: "2026-07-18T00:25:18.000Z" }],
};
mocks.getProviderConnectionById.mockResolvedValue(connection);
mocks.resolveConnectionProxyConfig.mockResolvedValue({ connectionProxyEnabled: true, connectionProxyUrl: "http://proxy.local" });
mocks.refreshAndUpdateCredentials.mockResolvedValue({ connection: refreshedConnection });
mocks.getCodexRateLimitResetCredits.mockResolvedValue(resetCredits);
const { GET } = await import("../../src/app/api/usage/[connectionId]/codex-reset-credits/route.js");
const response = await GET(new Request("http://localhost/api/usage/conn_1/codex-reset-credits"), {
params: Promise.resolve({ connectionId: "conn_1" }),
});
expect(response.status).toBe(200);
expect(await response.json()).toEqual(resetCredits);
expect(mocks.refreshAndUpdateCredentials).toHaveBeenCalledWith(
connection,
false,
expect.objectContaining({ connectionProxyEnabled: true, connectionProxyUrl: "http://proxy.local", strictProxy: false }),
);
expect(mocks.getCodexRateLimitResetCredits).toHaveBeenCalledWith(
"new-token",
expect.objectContaining({ connectionProxyEnabled: true, connectionProxyUrl: "http://proxy.local", strictProxy: false }),
{ workspaceId: "acct_123" },
);
});
it("GET force-refreshes OAuth credentials when reset credit fetch reports expired auth", async () => {
const connection = {
id: "conn_1",
provider: "codex",
authType: "oauth",
accessToken: "old-token",
refreshToken: "refresh-token",
providerSpecificData: {},
};
const refreshedConnection = { ...connection, accessToken: "new-token" };
const forcedConnection = { ...connection, accessToken: "forced-token" };
const resetCredits = { availableCount: 0, credits: [] };
mocks.getProviderConnectionById.mockResolvedValue(connection);
mocks.refreshAndUpdateCredentials
.mockResolvedValueOnce({ connection: refreshedConnection })
.mockResolvedValueOnce({ connection: forcedConnection });
mocks.getCodexRateLimitResetCredits
.mockRejectedValueOnce(new Error("Unauthorized 401"))
.mockResolvedValueOnce(resetCredits);
const { GET } = await import("../../src/app/api/usage/[connectionId]/codex-reset-credits/route.js");
const response = await GET(new Request("http://localhost/api/usage/conn_1/codex-reset-credits"), {
params: Promise.resolve({ connectionId: "conn_1" }),
});
expect(response.status).toBe(200);
expect(await response.json()).toEqual(resetCredits);
expect(mocks.refreshAndUpdateCredentials).toHaveBeenNthCalledWith(1, connection, false, expect.any(Object));
expect(mocks.refreshAndUpdateCredentials).toHaveBeenNthCalledWith(2, refreshedConnection, true, expect.any(Object));
expect(mocks.getCodexRateLimitResetCredits).toHaveBeenNthCalledWith(2, "forced-token", expect.any(Object), {});
});
it("POST returns 409 when there are no reset credits to consume", async () => {
mocks.getProviderConnectionById.mockResolvedValue({
id: "conn_1",
provider: "codex",
authType: "access_token",
accessToken: "token",
providerSpecificData: {},
});
mocks.consumeCodexRateLimitResetCredit.mockResolvedValue({
ok: false,
noCredit: true,
status: 200,
code: "no_credit",
windowsReset: 0,
});
const { POST } = await import("../../src/app/api/usage/[connectionId]/codex-reset-credits/route.js");
const response = await POST(new Request("http://localhost/api/usage/conn_1/codex-reset-credits", { method: "POST" }), {
params: Promise.resolve({ connectionId: "conn_1" }),
});
expect(response.status).toBe(409);
expect(await response.json()).toMatchObject({
code: "no_credit",
reset: false,
windows_reset: 0,
message: "No Codex reset credits available.",
});
expect(mocks.consumeCodexRateLimitResetCredit).toHaveBeenCalledWith(
"token",
expect.any(String),
expect.objectContaining({ strictProxy: false }),
);
});
});