@oh-my-pi/pi-ai 18.13.0 → 18.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.13.1] - 2026-10-27
6
+
7
+ ### Fixed
8
+
9
+ - Codex accounts whose spend cap is used up are no longer picked through credit overage or from usage cached before the upgrade ([#16973](https://github.com/can1357/oh-my-pi/pull/16973) by [@DusKing1](https://github.com/DusKing1))
10
+ - Fixed Codex capacity 429s (`turn_rate_limit_exceeded`, "Codex is at capacity") reporting "Try again in ~N min" from the plan's usage-window reset, which made retries wait hours instead of the response's Retry-After ([#17087](https://github.com/can1357/oh-my-pi/pull/17087) by [@H4vC](https://github.com/H4vC))
11
+
5
12
  ## [18.13.0] - 2026-10-26
6
13
 
7
14
  ### Breaking Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-ai",
3
- "version": "18.13.0",
3
+ "version": "18.13.1",
4
4
  "description": "Unified LLM API with automatic model discovery and provider configuration",
5
5
  "keywords": [
6
6
  "ai",
@@ -155,11 +155,11 @@
155
155
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
156
156
  },
157
157
  "dependencies": {
158
- "@oh-my-pi/omptype": "18.13.0",
159
- "@oh-my-pi/pi-catalog": "18.13.0",
160
- "@oh-my-pi/pi-natives": "18.13.0",
161
- "@oh-my-pi/pi-utils": "18.13.0",
162
- "@oh-my-pi/pi-wire": "18.13.0"
158
+ "@oh-my-pi/omptype": "18.13.1",
159
+ "@oh-my-pi/pi-catalog": "18.13.1",
160
+ "@oh-my-pi/pi-natives": "18.13.1",
161
+ "@oh-my-pi/pi-utils": "18.13.1",
162
+ "@oh-my-pi/pi-wire": "18.13.1"
163
163
  },
164
164
  "devDependencies": {
165
165
  "@types/bun": "^1.3.14"
@@ -66,15 +66,28 @@ export async function parseCodexError(response: Response): Promise<CodexErrorInf
66
66
 
67
67
  const code = String((err as { code?: string; type?: string }).code ?? (err as { type?: string }).type ?? "");
68
68
  errorCode = code || undefined;
69
- const resetsAt = (err as { resets_at?: number }).resets_at ?? primary.resets_at ?? secondary.resets_at;
70
- const mins = resetsAt ? Math.max(0, Math.round((resetsAt * 1000 - Date.now()) / 60000)) : undefined;
69
+ const errResetsAt = typeof err.resets_at === "number" ? err.resets_at : undefined;
70
+ const minutesUntil = (resetsAt: number | undefined): number | undefined =>
71
+ resetsAt ? Math.max(0, Math.round((resetsAt * 1000 - Date.now()) / 60000)) : undefined;
71
72
 
72
73
  if (/usage_limit_reached|usage_not_included/i.test(code)) {
73
74
  const planType = (err as { plan_type?: string }).plan_type;
74
75
  const plan = planType ? ` (${String(planType).toLowerCase()} plan)` : "";
76
+ const mins = minutesUntil(errResetsAt ?? primary.resets_at ?? secondary.resets_at);
75
77
  const when = mins !== undefined ? ` Try again in ~${mins} min.` : "";
76
78
  friendlyMessage = `You have hit your ChatGPT usage limit${plan}.${when}`.trim();
77
79
  } else if (/rate_limit_exceeded/i.test(code) || response.status === 429) {
80
+ // A rate limit (e.g. `turn_rate_limit_exceeded`, "Codex is at capacity")
81
+ // is not the plan window: a window with headroom would claim an
82
+ // hours-long wait that outranks the response's real Retry-After. Only
83
+ // the error's own `resets_at` or an exhausted window's reset applies;
84
+ // with both windows spent, the later reset is the one that unblocks.
85
+ let exhaustedResetsAt: number | undefined;
86
+ for (const window of [primary, secondary]) {
87
+ if (window.used_percent === undefined || window.used_percent < 100 || !window.resets_at) continue;
88
+ exhaustedResetsAt = Math.max(exhaustedResetsAt ?? 0, window.resets_at);
89
+ }
90
+ const mins = minutesUntil(errResetsAt ?? exhaustedResetsAt);
78
91
  const when = mins !== undefined ? ` Try again in ~${mins} min.` : "";
79
92
  friendlyMessage = `ChatGPT rate limit exceeded.${when}`.trim();
80
93
  }
@@ -252,14 +252,22 @@ function parseAdditionalRateLimit(payload: unknown): ParsedAdditionalUsage | nul
252
252
  * not plan exhaustion is left alone for the same reason: credits answer
253
253
  * "allowance spent", not "request refused".
254
254
  */
255
- function hasPlanCreditOverage(payload: Record<string, unknown>, planLimitReached: boolean | undefined): boolean {
255
+ function hasPlanCreditOverage(
256
+ payload: Record<string, unknown>,
257
+ planLimitReached: boolean | undefined,
258
+ spendCap: ParsedSpendCap | undefined,
259
+ ): boolean {
256
260
  if (planLimitReached !== true) return false;
257
261
  const credits = isRecord(payload.credits) ? payload.credits : undefined;
258
262
  if (!credits) return false;
259
263
  if (credits.unlimited !== true && credits.has_credits !== true) return false;
260
264
  if (credits.overage_limit_reached === true) return false;
261
265
  const spendControl = isRecord(payload.spend_control) ? payload.spend_control : undefined;
262
- return spendControl?.reached !== true;
266
+ if (spendControl?.reached === true) return false;
267
+ // Without an explicit verdict, a spend cap whose counters show no headroom
268
+ // cannot fund overage either.
269
+ if (spendCap && spendCap.reached === undefined && (spendCap.usedFraction ?? 0) >= 1) return false;
270
+ return true;
263
271
  }
264
272
 
265
273
  function parseSpendCap(payload: Record<string, unknown>): ParsedSpendCap | undefined {
@@ -272,13 +280,16 @@ function parseSpendCap(payload: Record<string, unknown>): ParsedSpendCap | undef
272
280
  const remaining = toNumber(individual.remaining);
273
281
  const usedPercent = toNumber(individual.used_percent);
274
282
  // The backend rounds `used_percent` to an integer; the absolute amounts
275
- // keep sub-percent precision when both are present.
283
+ // keep sub-percent precision when both are present. A bare `remaining`
284
+ // of zero still proves the cap has no headroom.
276
285
  const rawFraction =
277
286
  used !== undefined && limit !== undefined && limit > 0
278
287
  ? used / limit
279
288
  : usedPercent !== undefined
280
289
  ? usedPercent / 100
281
- : undefined;
290
+ : remaining !== undefined && remaining <= 0
291
+ ? 1
292
+ : undefined;
282
293
  if (used === undefined && limit === undefined && remaining === undefined && rawFraction === undefined) {
283
294
  return undefined;
284
295
  }
@@ -316,7 +327,7 @@ function parseUsagePayload(payload: unknown): ParsedUsage | null {
316
327
  secondary: rateLimit ? parseUsageWindow(rateLimit.secondary_window) : undefined,
317
328
  additional,
318
329
  ...(spendCap ? { spendCap } : {}),
319
- creditOverage: hasPlanCreditOverage(payload, planLimitReached),
330
+ creditOverage: hasPlanCreditOverage(payload, planLimitReached, spendCap),
320
331
  raw: payload as CodexUsagePayload,
321
332
  };
322
333
  if (
@@ -602,6 +613,8 @@ function buildPlanMeterState(
602
613
 
603
614
  export const openaiCodexUsageProvider: UsageProvider = {
604
615
  id: "openai-codex",
616
+ // v1: credit-billed seats gained the spend-cap limit; retire their limitless reports.
617
+ cacheVersion: 1,
605
618
  supports(params: UsageFetchParams): boolean {
606
619
  return params.provider === "openai-codex" && params.credential.type === "oauth";
607
620
  },