




























@@ -11,6 +11,12 @@ type LimitWindowKey = (typeof LIMIT_WINDOW_KEYS)[number];
1111type RateLimitReset = {
1212resetsAtMs: number;
1313usedPercent?: number;
14+windowDurationMins?: number;
15+};
16+17+type RateLimitWindowEntry = {
18+key: LimitWindowKey;
19+window: RateLimitReset;
1420};
15211622export function formatCodexUsageLimitErrorMessage(params: {
@@ -29,12 +35,27 @@ export function formatCodexUsageLimitErrorMessage(params: {
2935if (nextReset) {
3036parts.push(`Next reset ${formatResetTime(nextReset.resetsAtMs, nowMs)}.`);
3137} else {
32-parts.push("Codex did not return a reset time for this limit.");
38+const codexRetryHint = extractCodexRetryHint(message);
39+if (codexRetryHint) {
40+parts.push(`Codex says to try again ${codexRetryHint}.`);
41+} else {
42+parts.push("Codex did not return a reset time for this limit.");
43+}
3344}
3445parts.push("Run /codex account for current usage details.");
3546return parts.join(" ");
3647}
374849+export function shouldRefreshCodexRateLimitsForUsageLimitMessage(
50+message: string | null | undefined,
51+): boolean {
52+const text = normalizeText(message);
53+return Boolean(
54+text?.includes("You've reached your Codex subscription usage limit.") &&
55+!text.includes("Next reset "),
56+);
57+}
58+3859export function summarizeCodexRateLimits(
3960value: JsonValue | undefined,
4061nowMs = Date.now(),
@@ -49,6 +70,29 @@ export function summarizeCodexRateLimits(
4970.join("; ");
5071}
517273+export function summarizeCodexAccountRateLimits(
74+value: JsonValue | undefined,
75+nowMs = Date.now(),
76+): string[] | undefined {
77+const snapshots = collectCodexRateLimitSnapshots(value);
78+if (snapshots.length === 0) {
79+return undefined;
80+}
81+const blockedSnapshots = snapshots.filter(snapshotHasLimitBlock);
82+const blockingSnapshot =
83+blockedSnapshots.find(isCodexLimitSnapshot) ?? blockedSnapshots[0] ?? undefined;
84+if (!blockingSnapshot) {
85+return ["Codex is available."];
86+}
87+const blockingReset = selectSnapshotBlockingReset(blockingSnapshot, nowMs);
88+return [
89+blockingReset
90+ ? `Codex is paused until ${formatAccountResetTime(blockingReset.resetsAtMs, nowMs)}.`
91+ : "Codex is paused by a usage limit.",
92+formatBlockingLimitReason(blockingReset),
93+];
94+}
95+5296function isCodexUsageLimitError(
5397codexErrorInfo: JsonValue | null | undefined,
5498message: string | undefined,
@@ -90,7 +134,8 @@ function summarizeRateLimitSnapshot(snapshot: JsonObject, nowMs: number): string
90134const window = readRateLimitWindow(snapshot, key);
91135return window ? [formatRateLimitWindow(key, window, nowMs)] : [];
92136});
93-const reachedType = readString(snapshot, "rateLimitReachedType");
137+const reachedType =
138+readString(snapshot, "rateLimitReachedType") ?? readString(snapshot, "rate_limit_reached_type");
94139const suffix = reachedType ? ` (${formatReachedType(reachedType)})` : "";
95140return `${label}: ${windows.join(", ") || "available"}${suffix}`;
96141}
@@ -126,7 +171,14 @@ function collectRateLimitSnapshots(
126171collectRateLimitSnapshots(byLimitId[key], snapshots, seen);
127172}
128173}
174+const snakeByLimitId = value.rate_limits_by_limit_id;
175+if (isJsonObject(snakeByLimitId)) {
176+for (const key of sortedRateLimitKeys(Object.keys(snakeByLimitId))) {
177+collectRateLimitSnapshots(snakeByLimitId[key], snapshots, seen);
178+}
179+}
129180collectRateLimitSnapshots(value.rateLimits, snapshots, seen);
181+collectRateLimitSnapshots(value.rate_limits, snapshots, seen);
130182collectRateLimitSnapshots(value.data, snapshots, seen);
131183collectRateLimitSnapshots(value.items, snapshots, seen);
132184}
@@ -149,8 +201,8 @@ function addRateLimitSnapshot(
149201seen: Set<string>,
150202): void {
151203const signature = [
152-readNullableString(snapshot, "limitId") ?? "",
153-readNullableString(snapshot, "limitName") ?? "",
204+readNullableString(snapshot, "limitId") ?? readNullableString(snapshot, "limit_id") ?? "",
205+readNullableString(snapshot, "limitName") ?? readNullableString(snapshot, "limit_name") ?? "",
154206formatWindowSignature(snapshot.primary),
155207formatWindowSignature(snapshot.secondary),
156208].join("|");
@@ -166,8 +218,11 @@ function isRateLimitSnapshot(value: JsonObject): boolean {
166218isJsonObject(value.primary) ||
167219isJsonObject(value.secondary) ||
168220value.rateLimitReachedType !== undefined ||
221+value.rate_limit_reached_type !== undefined ||
169222value.limitId !== undefined ||
170-value.limitName !== undefined
223+value.limit_id !== undefined ||
224+value.limitName !== undefined ||
225+value.limit_name !== undefined
171226);
172227}
173228@@ -179,31 +234,53 @@ function readRateLimitWindow(
179234if (!isJsonObject(window)) {
180235return undefined;
181236}
182-const resetsAt = readNumber(window, "resetsAt");
237+const resetsAt = readNumber(window, "resetsAt") ?? readNumber(window, "resets_at");
183238return {
184239 ...(typeof resetsAt === "number" && Number.isFinite(resetsAt) && resetsAt > 0
185240 ? { resetsAtMs: resetsAt * 1000 }
186241 : { resetsAtMs: 0 }),
187- ...readOptionalNumberField(window, "usedPercent"),
242+ ...readOptionalNumberField(window, "usedPercent", "used_percent"),
243+ ...readOptionalNumberField(
244+window,
245+"windowDurationMins",
246+"window_duration_mins",
247+"windowMinutes",
248+"window_minutes",
249+),
188250};
189251}
190252191-function readOptionalNumberField(record: JsonObject, key: string): { usedPercent?: number } {
192-const value = readNumber(record, key);
193-return value === undefined ? {} : { usedPercent: value };
253+function readOptionalNumberField(
254+record: JsonObject,
255+ ...keys: string[]
256+): { usedPercent?: number; windowDurationMins?: number } {
257+const value = keys.map((key) => readNumber(record, key)).find((entry) => entry !== undefined);
258+if (value === undefined) {
259+return {};
260+}
261+return keys.some((key) => key.toLowerCase().includes("window"))
262+ ? { windowDurationMins: value }
263+ : { usedPercent: value };
194264}
195265196266function formatRateLimitWindow(key: LimitWindowKey, window: RateLimitReset, nowMs: number): string {
267+return `${key} ${formatRateLimitWindowDetails(window, nowMs)}`;
268+}
269+270+function formatRateLimitWindowDetails(window: RateLimitReset, nowMs: number): string {
197271const usedPercent =
198272window.usedPercent === undefined ? "usage unknown" : `${Math.round(window.usedPercent)}%`;
199273const reset =
200274window.resetsAtMs > nowMs ? `, resets ${formatResetTime(window.resetsAtMs, nowMs)}` : "";
201-return `${key} ${usedPercent}${reset}`;
275+return `${usedPercent}${reset}`;
202276}
203277204278function formatLimitLabel(snapshot: JsonObject): string {
205279const label =
206-readNullableString(snapshot, "limitName") ?? readNullableString(snapshot, "limitId");
280+readNullableString(snapshot, "limitName") ??
281+readNullableString(snapshot, "limit_name") ??
282+readNullableString(snapshot, "limitId") ??
283+readNullableString(snapshot, "limit_id");
207284if (!label || label === CODEX_LIMIT_ID) {
208285return "Codex";
209286}
@@ -215,7 +292,96 @@ function formatReachedType(value: string): string {
215292}
216293217294function formatResetTime(resetsAtMs: number, nowMs: number): string {
218-return `in ${formatRelativeDuration(resetsAtMs - nowMs)} (${new Date(resetsAtMs).toISOString()})`;
295+return `in ${formatRelativeDuration(resetsAtMs - nowMs)}, ${formatCalendarResetTime(
296+ resetsAtMs,
297+ nowMs,
298+ )}`;
299+}
300+301+function formatAccountResetTime(resetsAtMs: number, nowMs: number): string {
302+return `${formatCalendarResetTime(resetsAtMs, nowMs)} (in ${formatRelativeDuration(
303+ resetsAtMs - nowMs,
304+ )})`;
305+}
306+307+function snapshotHasLimitBlock(snapshot: JsonObject): boolean {
308+return Boolean(
309+readString(snapshot, "rateLimitReachedType") ??
310+readString(snapshot, "rate_limit_reached_type") ??
311+readWindowEntries(snapshot).some(
312+(entry) => entry.window.usedPercent !== undefined && entry.window.usedPercent >= 100,
313+),
314+);
315+}
316+317+function isCodexLimitSnapshot(snapshot: JsonObject): boolean {
318+const id = readNullableString(snapshot, "limitId") ?? readNullableString(snapshot, "limit_id");
319+return !id || id === CODEX_LIMIT_ID;
320+}
321+322+function selectSnapshotBlockingReset(
323+snapshot: JsonObject,
324+nowMs: number,
325+): RateLimitReset | undefined {
326+const futureWindows = readWindowEntries(snapshot)
327+.map((entry) => entry.window)
328+.filter((window) => window.resetsAtMs > nowMs);
329+const exhaustedWindows = futureWindows.filter(
330+(window) => window.usedPercent !== undefined && window.usedPercent >= 100,
331+);
332+const candidates = exhaustedWindows.length > 0 ? exhaustedWindows : futureWindows;
333+candidates.sort((left, right) => left.resetsAtMs - right.resetsAtMs);
334+return candidates[0];
335+}
336+337+function readWindowEntries(snapshot: JsonObject): RateLimitWindowEntry[] {
338+return LIMIT_WINDOW_KEYS.flatMap((key) => {
339+const window = readRateLimitWindow(snapshot, key);
340+return window ? [{ key, window }] : [];
341+});
342+}
343+344+function formatBlockingLimitReason(window: RateLimitReset | undefined): string {
345+const period = formatBlockingLimitPeriod(window?.windowDurationMins);
346+return period
347+ ? `Your ${period} Codex usage limit is reached.`
348+ : "Your Codex usage limit is reached.";
349+}
350+351+function formatBlockingLimitPeriod(minutes: number | undefined): string | undefined {
352+if (minutes === 7 * 24 * 60) {
353+return "weekly";
354+}
355+if (minutes === 24 * 60) {
356+return "daily";
357+}
358+if (minutes !== undefined && minutes > 0 && minutes < 24 * 60) {
359+return "short-term";
360+}
361+return undefined;
362+}
363+364+function formatCalendarResetTime(resetsAtMs: number, nowMs: number): string {
365+const resetDate = new Date(resetsAtMs);
366+const resetParts = new Intl.DateTimeFormat("en-US", {
367+month: "short",
368+day: "numeric",
369+ ...(resetDate.getFullYear() === new Date(nowMs).getFullYear() ? {} : { year: "numeric" }),
370+hour: "numeric",
371+minute: "2-digit",
372+timeZoneName: "short",
373+}).formatToParts(resetDate);
374+const part = (type: Intl.DateTimeFormatPartTypes): string | undefined =>
375+resetParts.find((entry) => entry.type === type)?.value;
376+const dateParts = [part("month"), part("day"), part("year")].filter(Boolean);
377+const day =
378+dateParts.length > 1 ? `${dateParts[0]} ${dateParts.slice(1).join(", ")}` : dateParts[0];
379+const time = [part("hour"), part("minute")].filter(Boolean).join(":");
380+const dayPeriod = part("dayPeriod");
381+const timeZone = part("timeZoneName");
382+return [day, "at", [time, dayPeriod, timeZone].filter(Boolean).join(" ")]
383+.filter(Boolean)
384+.join(" ");
219385}
220386221387function formatRelativeDuration(durationMs: number): string {
@@ -239,7 +405,23 @@ function formatWindowSignature(value: JsonValue | undefined): string {
239405if (!isJsonObject(value)) {
240406return "";
241407}
242-return `${readNumber(value, "usedPercent") ?? ""}:${readNumber(value, "resetsAt") ?? ""}`;
408+return `${readNumber(value, "usedPercent") ?? readNumber(value, "used_percent") ?? ""}:${
409+ readNumber(value, "resetsAt") ?? readNumber(value, "resets_at") ?? ""
410+ }`;
411+}
412+413+function extractCodexRetryHint(message: string | undefined): string | undefined {
414+if (!message) {
415+return undefined;
416+}
417+const tryAgainAt = /\btry again\s+(at\s+[^.!?\n]+)(?:[.!?]|$)/iu.exec(message);
418+if (tryAgainAt?.[1]) {
419+return tryAgainAt[1].trim();
420+}
421+const tryAgainRelative = /\btry again\s+((?:tomorrow|in\s+[^.!?\n]+)[^.!?\n]*)(?:[.!?]|$)/iu.exec(
422+message,
423+);
424+return tryAgainRelative?.[1]?.trim();
243425}
244426245427function readString(record: JsonObject, key: string): string | undefined {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。