diff --git a/server/plugins/build-in/codex-auth/codex_test.ts b/server/plugins/build-in/codex-auth/codex_test.ts index 8ae3ae0..94f21bc 100644 --- a/server/plugins/build-in/codex-auth/codex_test.ts +++ b/server/plugins/build-in/codex-auth/codex_test.ts @@ -12,8 +12,8 @@ import { buildResponsesBody } from "cursor-byok:protocol/openai-responses"; import { codexProvider, isQuotaError } from "./provider.ts"; import { accountIdentity, - credentialDraft, consumeResetCardAction, + credentialDraft, listResetCardsAction, parseCodexUsage, parseCredentialFiles, @@ -142,9 +142,113 @@ Deno.test("usage maps secondary to weekly and primary to five-hour quota", () => assertEquals(quota.weekly?.remainingPercent, 75); assertEquals(quota.fiveHour?.remainingPercent, 20); assertEquals(quota.weekly?.resetAtMs, 1_900_000_000_000); + assertEquals(quota.resetCreditsAvailable, 2); assertEquals(quotaState(quota, 1_700_000_000_000), { status: "ready" }); }); +Deno.test("reset card action lists safe card metadata and optional expiry", async () => { + const result = await listResetCardsAction.run( + snapshot({ + accessToken: "access-secret", + accountId: "acct-1", + refreshToken: null, + displayName: "person@example.com", + quota: null, + }), + null, + context({ + fetch: (url, init) => { + assertEquals(url, "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits"); + assertEquals(init?.headers?.["ChatGPT-Account-Id"], "acct-1"); + return { + status: 200, + headers: {}, + body: JSON.stringify({ + available_count: 1, + credits: [{ + id: "credit-1", + reset_type: "codex_rate_limits", + status: "available", + granted_at: "2026-06-12T01:33:14Z", + expires_at: "2026-07-12T01:33:14Z", + title: "One free rate limit reset", + }], + }), + }; + }, + }), + ); + assertEquals(result.cards, [{ + id: "credit-1", + title: "One free rate limit reset", + status: "available", + grantedAtMs: Date.parse("2026-06-12T01:33:14Z"), + expiresAtMs: Date.parse("2026-07-12T01:33:14Z"), + fields: [{ + id: "reset-type", + label: { "en-US": "Reset type", "zh-CN": "重置类型" }, + value: "codex_rate_limits", + }], + }]); +}); + +Deno.test("reset card action consumes a selected card and refreshes quota state", async () => { + let requestNumber = 0; + const result = await consumeResetCardAction.run( + snapshot({ + accessToken: "access-secret", + accountId: "acct-1", + refreshToken: null, + displayName: "person@example.com", + quota: null, + }), + { cardId: "credit-1" }, + context({ + fetch: (url, init) => { + requestNumber += 1; + if (requestNumber === 1 || requestNumber === 4) { + assertEquals(url, "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits"); + return { + status: 200, + headers: {}, + body: JSON.stringify( + requestNumber === 1 + ? { available_count: 1, credits: [{ id: "credit-1", status: "available" }] } + : { available_count: 0, credits: [] }, + ), + }; + } + if (requestNumber === 2) { + assertEquals( + url, + "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume", + ); + assertEquals(JSON.parse(init?.body ?? "{}"), { + credit_id: "credit-1", + redeem_request_id: JSON.parse(init?.body ?? "{}").redeem_request_id, + }); + return { status: 200, headers: {}, body: JSON.stringify({ code: "reset" }) }; + } + assertEquals(url, "https://chatgpt.com/backend-api/wham/usage"); + return { + status: 200, + headers: {}, + body: JSON.stringify({ rate_limit_reset_credits: { available_count: 0 } }), + }; + }, + }), + ); + assertEquals(requestNumber, 4); + assertEquals(result.cards, []); + const quota = (result.patch?.privateData as Record).quota as Record< + string, + unknown + >; + assertEquals(quota.resetCreditsAvailable, 0); + assertEquals(quota.weekly, null); + assertEquals(quota.fiveHour, null); +}); + Deno.test("exhausted quota projects a cooling state until the latest reset", () => { const quota = parseCodexUsage({ rate_limit: { diff --git a/server/plugins/build-in/codex-auth/main.ts b/server/plugins/build-in/codex-auth/main.ts index 109332a..6a3d582 100644 --- a/server/plugins/build-in/codex-auth/main.ts +++ b/server/plugins/build-in/codex-auth/main.ts @@ -2,8 +2,8 @@ import { defineProviderPlugin } from "cursor-byok:plugin"; import { codexDeviceOAuth } from "./oauth.ts"; import { codexProvider } from "./provider.ts"; import { - credentialImport, consumeResetCardAction, + credentialImport, listResetCardsAction, presentAccount, refreshAccount, diff --git a/server/plugins/build-in/codex-auth/resources.ts b/server/plugins/build-in/codex-auth/resources.ts index eac0824..94f3df8 100644 --- a/server/plugins/build-in/codex-auth/resources.ts +++ b/server/plugins/build-in/codex-auth/resources.ts @@ -350,7 +350,9 @@ async function fetchResetCredits( const availableCount = number(root.available_count ?? root.availableCount); return { cards, - availableCount: availableCount === null ? cards.filter((card) => card.status === "available").length : Math.max(0, Math.floor(availableCount)), + availableCount: availableCount === null + ? cards.filter((card) => card.status === "available").length + : Math.max(0, Math.floor(availableCount)), }; } @@ -426,7 +428,10 @@ async function consumeResetCard( export const listResetCardsAction: ResourceAction = { id: LIST_RESET_CARDS_ACTION_ID, displayName: { "en-US": "View reset cards", "zh-CN": "查看重置卡" }, - description: { "en-US": "List available Codex reset cards.", "zh-CN": "查看当前账号的 Codex 重置卡。" }, + description: { + "en-US": "List available Codex reset cards.", + "zh-CN": "查看当前账号的 Codex 重置卡。", + }, target: "resource", run: listResetCards, }; @@ -434,7 +439,10 @@ export const listResetCardsAction: ResourceAction = { export const consumeResetCardAction: ResourceAction = { id: CONSUME_RESET_CARD_ACTION_ID, displayName: { "en-US": "Use reset card", "zh-CN": "使用重置卡" }, - description: { "en-US": "Redeem one available Codex reset card.", "zh-CN": "消耗一张可用的 Codex 重置卡。" }, + description: { + "en-US": "Redeem one available Codex reset card.", + "zh-CN": "消耗一张可用的 Codex 重置卡。", + }, target: "card", destructive: true, run: consumeResetCard, diff --git a/server/src/plugin/sdk/worker.ts b/server/src/plugin/sdk/worker.ts index 1e29c58..abd3a0a 100644 --- a/server/src/plugin/sdk/worker.ts +++ b/server/src/plugin/sdk/worker.ts @@ -142,9 +142,7 @@ async function dispatch(message: { id: string; method: string; params?: JsonValu case "oauth.begin": { const support = resourceSupport(params.resourceType); const method = addMethod(support, params.methodId); - result = method.type === "oauth2.authorization-code" - ? await method.begin(params.authorization as never, context) - : await method.begin(context); + result = method.type === "oauth2.authorization-code" ? await method.begin(params.authorization as never, context) : await method.begin(context); break; } case "oauth.poll": {