@oh-my-pi/pi-ai 18.2.1 → 18.2.2

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.
@@ -43,10 +43,23 @@ interface CodexUsageAdditionalRateLimitPayload {
43
43
  rate_limit?: CodexUsageRateLimitPayload | null;
44
44
  }
45
45
 
46
+ interface CodexUsageCreditsPayload {
47
+ has_credits?: boolean;
48
+ unlimited?: boolean;
49
+ overage_limit_reached?: boolean;
50
+ balance?: string | number;
51
+ }
52
+
53
+ interface CodexUsageSpendControlPayload {
54
+ reached?: boolean;
55
+ }
56
+
46
57
  interface CodexUsagePayload {
47
58
  plan_type?: string;
48
59
  rate_limit?: CodexUsageRateLimitPayload | null;
49
60
  additional_rate_limits?: CodexUsageAdditionalRateLimitPayload[] | null;
61
+ credits?: CodexUsageCreditsPayload | null;
62
+ spend_control?: CodexUsageSpendControlPayload | null;
50
63
  }
51
64
 
52
65
  interface ParsedUsageWindow {
@@ -72,6 +85,13 @@ interface ParsedUsage {
72
85
  primary?: ParsedUsageWindow;
73
86
  secondary?: ParsedUsageWindow;
74
87
  additional: ParsedAdditionalUsage[];
88
+ /**
89
+ * True when the account can still serve requests on credits after its plan
90
+ * windows report `limit_reached`. `/wham/usage` only describes the *plan*
91
+ * allowance, so without this a credit-funded account looks permanently
92
+ * exhausted until the weekly reset while `/responses` keeps accepting it.
93
+ */
94
+ creditOverage: boolean;
75
95
  raw: CodexUsagePayload;
76
96
  }
77
97
 
@@ -161,6 +181,28 @@ function parseAdditionalRateLimit(payload: unknown): ParsedAdditionalUsage | nul
161
181
  return { limitName, meteredFeature, allowed, limitReached, primary, secondary };
162
182
  }
163
183
 
184
+ /**
185
+ * True when paid credits can still fund plan-window overage. Codex CLI never
186
+ * gates on `/wham/usage`, so once the plan allowance is spent it keeps working
187
+ * off this balance; omp must mirror that or it parks a perfectly usable account
188
+ * until the weekly reset.
189
+ *
190
+ * Scoped to the plan verdict on purpose. `credits` describes the account's
191
+ * overage funding for the plan windows, and nothing in the payload says a
192
+ * balance covers a separate metered feature (Spark, reserve). A denial that is
193
+ * not plan exhaustion is left alone for the same reason: credits answer
194
+ * "allowance spent", not "request refused".
195
+ */
196
+ function hasPlanCreditOverage(payload: Record<string, unknown>, planLimitReached: boolean | undefined): boolean {
197
+ if (planLimitReached !== true) return false;
198
+ const credits = isRecord(payload.credits) ? payload.credits : undefined;
199
+ if (!credits) return false;
200
+ if (credits.unlimited !== true && credits.has_credits !== true) return false;
201
+ if (credits.overage_limit_reached === true) return false;
202
+ const spendControl = isRecord(payload.spend_control) ? payload.spend_control : undefined;
203
+ return spendControl?.reached !== true;
204
+ }
205
+
164
206
  function parseUsagePayload(payload: unknown): ParsedUsage | null {
165
207
  if (!isRecord(payload)) return null;
166
208
  const planType = typeof payload.plan_type === "string" ? payload.plan_type : undefined;
@@ -170,13 +212,15 @@ function parseUsagePayload(payload: unknown): ParsedUsage | null {
170
212
  .map(parseAdditionalRateLimit)
171
213
  .filter((value): value is ParsedAdditionalUsage => value !== null);
172
214
  if (!rateLimit && additional.length === 0) return null;
215
+ const planLimitReached = rateLimit ? toBoolean(rateLimit.limit_reached) : undefined;
173
216
  const parsed: ParsedUsage = {
174
217
  planType,
175
218
  allowed: rateLimit ? toBoolean(rateLimit.allowed) : undefined,
176
- limitReached: rateLimit ? toBoolean(rateLimit.limit_reached) : undefined,
219
+ limitReached: planLimitReached,
177
220
  primary: rateLimit ? parseUsageWindow(rateLimit.primary_window) : undefined,
178
221
  secondary: rateLimit ? parseUsageWindow(rateLimit.secondary_window) : undefined,
179
222
  additional,
223
+ creditOverage: hasPlanCreditOverage(payload, planLimitReached),
180
224
  raw: payload as CodexUsagePayload,
181
225
  };
182
226
  if (
@@ -273,6 +317,17 @@ function buildUsageStatus(args: { usedFraction?: number; explicitlyAllowed: bool
273
317
  return "ok";
274
318
  }
275
319
 
320
+ /**
321
+ * Whether Codex will still serve this meter: an explicit positive verdict, or
322
+ * credits covering overage of a spent plan window. The credit override needs
323
+ * `limitReached === true`; a refusal for any other reason is not something a
324
+ * balance can pay for.
325
+ */
326
+ function isCodexRequestAllowed(args: { allowed?: boolean; limitReached?: boolean; creditOverage?: boolean }): boolean {
327
+ if (args.creditOverage === true && args.limitReached === true) return true;
328
+ return args.allowed === true && args.limitReached === false;
329
+ }
330
+
276
331
  function buildUsageLimit(args: {
277
332
  key: "primary" | "secondary";
278
333
  window: ParsedUsageWindow;
@@ -280,6 +335,7 @@ function buildUsageLimit(args: {
280
335
  planType?: string;
281
336
  allowed?: boolean;
282
337
  limitReached?: boolean;
338
+ creditOverage?: boolean;
283
339
  nowMs: number;
284
340
  }): UsageLimit {
285
341
  const usageWindow = buildUsageWindow(args.window, args.key, args.nowMs);
@@ -296,11 +352,12 @@ function buildUsageLimit(args: {
296
352
  amount,
297
353
  // The shared account-level rejection flag cannot identify which window
298
354
  // is binding, but an explicit positive verdict applies to both windows.
299
- // Preserve 100% as a warning when Codex still allows requests; live
300
- // usage_limit_reached responses remain authoritative for blocking.
355
+ // Preserve 100% as a warning when Codex still allows requests — either
356
+ // explicitly, or because credits fund overage past the plan window.
357
+ // Live usage_limit_reached responses remain authoritative for blocking.
301
358
  status: buildUsageStatus({
302
359
  usedFraction: amount.usedFraction,
303
- explicitlyAllowed: args.allowed === true && args.limitReached === false,
360
+ explicitlyAllowed: isCodexRequestAllowed(args),
304
361
  }),
305
362
  };
306
363
  }
@@ -354,9 +411,11 @@ function buildAdditionalUsageLimit(args: {
354
411
  amount,
355
412
  // A positive meter verdict is authoritative even when the advisory
356
413
  // percentage rounds to 100; negative shared verdicts remain window-local.
414
+ // Plan credits are deliberately not passed here: this meter is a separate
415
+ // allowance, and nothing in the payload says a balance funds its overage.
357
416
  status: buildUsageStatus({
358
417
  usedFraction: amount.usedFraction,
359
- explicitlyAllowed: args.allowed === true && args.limitReached === false,
418
+ explicitlyAllowed: isCodexRequestAllowed(args),
360
419
  }),
361
420
  };
362
421
  }
@@ -367,7 +426,11 @@ function buildAdditionalUsageLimit(args: {
367
426
  * ingesting them lets credential selection block an exhausted account before
368
427
  * the next request burns a wire 429.
369
428
  */
370
- export function parseCodexRateLimitHeaders(headers: Record<string, string>, now = Date.now()): UsageReport | null {
429
+ export function parseCodexRateLimitHeaders(
430
+ headers: Record<string, string>,
431
+ now = Date.now(),
432
+ context?: { responseStatus?: number },
433
+ ): UsageReport | null {
371
434
  const parseWindow = (key: "primary" | "secondary"): ParsedUsageWindow | undefined => {
372
435
  const usedPercent = toNumber(headers[`x-codex-${key}-used-percent`]);
373
436
  if (usedPercent === undefined) return undefined;
@@ -383,8 +446,11 @@ export function parseCodexRateLimitHeaders(headers: Record<string, string>, now
383
446
  const secondary = parseWindow("secondary");
384
447
  if (!primary && !secondary) return null;
385
448
  const limits: UsageLimit[] = [];
386
- if (primary) limits.push(buildUsageLimit({ key: "primary", window: primary, nowMs: now }));
387
- if (secondary) limits.push(buildUsageLimit({ key: "secondary", window: secondary, nowMs: now }));
449
+ const requestSucceeded =
450
+ context?.responseStatus !== undefined && context.responseStatus >= 200 && context.responseStatus < 300;
451
+ const verdict = requestSucceeded ? { allowed: true, limitReached: false } : {};
452
+ if (primary) limits.push(buildUsageLimit({ key: "primary", window: primary, ...verdict, nowMs: now }));
453
+ if (secondary) limits.push(buildUsageLimit({ key: "secondary", window: secondary, ...verdict, nowMs: now }));
388
454
  return {
389
455
  provider: "openai-codex",
390
456
  fetchedAt: now,
@@ -393,6 +459,21 @@ export function parseCodexRateLimitHeaders(headers: Record<string, string>, now
393
459
  };
394
460
  }
395
461
 
462
+ /**
463
+ * Plan meter verdict as credential selection should see it. Credits funding
464
+ * overage flip a plan-level rejection back to serving, which is what lets a
465
+ * stale usage-limit block self-heal instead of parking the account until reset.
466
+ * Only the plan meter takes this override — {@link hasPlanCreditOverage}.
467
+ */
468
+ function buildPlanMeterState(
469
+ allowed: boolean | undefined,
470
+ limitReached: boolean | undefined,
471
+ creditOverage: boolean,
472
+ ): { allowed?: boolean; limitReached?: boolean } {
473
+ if (!creditOverage) return { allowed, limitReached };
474
+ return { allowed: true, limitReached: false };
475
+ }
476
+
396
477
  export const openaiCodexUsageProvider: UsageProvider = {
397
478
  id: "openai-codex",
398
479
  supports(params: UsageFetchParams): boolean {
@@ -444,9 +525,10 @@ export const openaiCodexUsageProvider: UsageProvider = {
444
525
  parsed?.planType ??
445
526
  (isRecord(payload) && typeof payload.plan_type === "string" ? payload.plan_type : undefined);
446
527
 
528
+ const creditOverage = parsed?.creditOverage === true;
447
529
  const limits: UsageLimit[] = [];
448
530
  const meterStates: Record<string, { allowed?: boolean; limitReached?: boolean }> = {
449
- chat: { allowed: parsed?.allowed, limitReached: parsed?.limitReached },
531
+ chat: buildPlanMeterState(parsed?.allowed, parsed?.limitReached, creditOverage),
450
532
  };
451
533
  if (parsed?.primary) {
452
534
  limits.push(
@@ -457,6 +539,7 @@ export const openaiCodexUsageProvider: UsageProvider = {
457
539
  planType,
458
540
  allowed: parsed.allowed,
459
541
  limitReached: parsed.limitReached,
542
+ creditOverage,
460
543
  nowMs,
461
544
  }),
462
545
  );
@@ -470,6 +553,7 @@ export const openaiCodexUsageProvider: UsageProvider = {
470
553
  planType,
471
554
  allowed: parsed.allowed,
472
555
  limitReached: parsed.limitReached,
556
+ creditOverage,
473
557
  nowMs,
474
558
  }),
475
559
  );
@@ -548,8 +632,7 @@ export const openaiCodexUsageProvider: UsageProvider = {
548
632
  ...(resetCredits ? { resetCredits } : {}),
549
633
  metadata: {
550
634
  planType,
551
- allowed: parsed?.allowed,
552
- limitReached: parsed?.limitReached,
635
+ ...buildPlanMeterState(parsed?.allowed, parsed?.limitReached, creditOverage),
553
636
  email,
554
637
  accountId,
555
638
  meterStates,
package/src/usage.ts CHANGED
@@ -356,7 +356,11 @@ export interface UsageProvider {
356
356
  id: Provider;
357
357
  fetchUsage(params: UsageFetchParams, ctx: UsageFetchContext): Promise<UsageReport | null>;
358
358
  /** Parse provider rate-limit response headers (lowercased keys) into a usage report, if supported. */
359
- parseRateLimitHeaders?(headers: Record<string, string>, now?: number): UsageReport | null;
359
+ parseRateLimitHeaders?(
360
+ headers: Record<string, string>,
361
+ now?: number,
362
+ context?: { responseStatus?: number },
363
+ ): UsageReport | null;
360
364
  supports?(params: UsageFetchParams): boolean;
361
365
  /** True when fetchUsage contacts upstream and can authenticate the credential for health checks. */
362
366
  validatesCredentials?: boolean;
@@ -165,10 +165,30 @@ export function rewriteClinePassError(errorMessage: string, provider: string): s
165
165
  function sanitizeDump(dump: RawHttpRequestDump): RawHttpRequestDump {
166
166
  return {
167
167
  ...dump,
168
+ url: redactUrlQuery(dump.url),
168
169
  headers: redactHeaders(dump.headers),
169
170
  };
170
171
  }
171
172
 
173
+ /**
174
+ * Strips a persisted dump's query string entirely rather than picking sensitive
175
+ * params by name: a configurable `baseUrl` (e.g. Bedrock's gateway routing) can
176
+ * carry an arbitrary query-based credential the way `SENSITIVE_HEADER_PATTERN`
177
+ * matches arbitrary header names, and dumps exist to diagnose the request body,
178
+ * not the query.
179
+ */
180
+ function redactUrlQuery(url: string | undefined): string | undefined {
181
+ if (!url) return url;
182
+ try {
183
+ const parsed = new URL(url);
184
+ if (!parsed.search) return url;
185
+ parsed.search = "";
186
+ return `${parsed.toString()}[redacted-query]`;
187
+ } catch {
188
+ return url;
189
+ }
190
+ }
191
+
172
192
  function redactHeaders(headers: Record<string, string> | undefined): Record<string, string> | undefined {
173
193
  if (!headers) {
174
194
  return undefined;