@oh-my-pi/pi-ai 17.2.7 → 17.2.9

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/types/auth/sqlite-credential-store.d.ts +117 -0
  3. package/dist/types/auth-storage.d.ts +1 -108
  4. package/dist/types/error/flags.d.ts +4 -2
  5. package/dist/types/error/index.d.ts +1 -0
  6. package/dist/types/error/rate-limit.d.ts +21 -5
  7. package/dist/types/error/retryable.d.ts +1 -1
  8. package/dist/types/providers/anthropic.d.ts +2 -2
  9. package/dist/types/providers/cursor/exec-modern.d.ts +1 -1
  10. package/dist/types/providers/cursor-pi-args.d.ts +6 -0
  11. package/dist/types/providers/mock.d.ts +1 -0
  12. package/dist/types/providers/openai-codex/request-transformer.d.ts +1 -1
  13. package/dist/types/providers/openai-codex-responses.d.ts +1 -1
  14. package/dist/types/providers/pi-native-server.d.ts +1 -1
  15. package/dist/types/utils/http-inspector.d.ts +5 -4
  16. package/dist/types/utils/retry.d.ts +3 -2
  17. package/package.json +5 -5
  18. package/src/auth/sqlite-credential-store.ts +2049 -0
  19. package/src/auth-retry.ts +8 -5
  20. package/src/auth-storage.ts +30 -2023
  21. package/src/error/auth-classify.ts +5 -4
  22. package/src/error/flags.ts +74 -18
  23. package/src/error/index.ts +1 -0
  24. package/src/error/rate-limit.ts +64 -5
  25. package/src/error/retryable.ts +1 -1
  26. package/src/providers/anthropic.ts +15 -3
  27. package/src/providers/cursor/exec-modern.ts +1 -0
  28. package/src/providers/cursor-pi-args.ts +30 -0
  29. package/src/providers/cursor.ts +16 -9
  30. package/src/providers/mock.ts +1 -1
  31. package/src/providers/openai-codex/request-transformer.ts +21 -27
  32. package/src/providers/openai-codex-responses.ts +2 -2
  33. package/src/providers/pi-native-client.ts +1 -1
  34. package/src/providers/pi-native-server.ts +1 -1
  35. package/src/registry/oauth/devin.ts +1 -1
  36. package/src/stream.ts +3 -3
  37. package/src/usage/openai-codex.ts +30 -19
  38. package/src/utils/http-inspector.ts +6 -5
  39. package/src/utils/retry.ts +24 -5
@@ -263,11 +263,10 @@ function buildUsageAmount(window: ParsedUsageWindow): UsageAmount {
263
263
  };
264
264
  }
265
265
 
266
- function buildUsageStatus(usedFraction?: number, limitReached?: boolean): UsageLimit["status"] {
267
- if (limitReached) return "exhausted";
268
- if (usedFraction === undefined) return "unknown";
269
- if (usedFraction >= 1) return "exhausted";
270
- if (usedFraction >= 0.9) return "warning";
266
+ function buildUsageStatus(args: { usedFraction?: number; explicitlyAllowed: boolean }): UsageLimit["status"] {
267
+ if (args.usedFraction === undefined) return "unknown";
268
+ if (args.usedFraction >= 1) return args.explicitlyAllowed ? "warning" : "exhausted";
269
+ if (args.usedFraction >= 0.9) return "warning";
271
270
  return "ok";
272
271
  }
273
272
 
@@ -276,6 +275,8 @@ function buildUsageLimit(args: {
276
275
  window: ParsedUsageWindow;
277
276
  accountId?: string;
278
277
  planType?: string;
278
+ allowed?: boolean;
279
+ limitReached?: boolean;
279
280
  nowMs: number;
280
281
  }): UsageLimit {
281
282
  const usageWindow = buildUsageWindow(args.window, args.key, args.nowMs);
@@ -290,16 +291,14 @@ function buildUsageLimit(args: {
290
291
  },
291
292
  window: usageWindow,
292
293
  amount,
293
- // Each chat window's status reflects ONLY its own usage. The account-level
294
- // `rate_limit.limit_reached` flag is intentionally not applied here: Codex
295
- // returns a single shared flag for the whole account, so threading it into
296
- // both the primary (5h) and secondary (weekly) windows marked a window with
297
- // real headroom `exhausted` purely because a different window (or a separate
298
- // metered feature) was at its limit, which over-blocked sibling accounts
299
- // during credential selection. `usedFraction >= 1` already marks a window
300
- // that is genuinely full; a real enforced limit not reflected in
301
- // `used_percent` is caught when the live request returns usage_limit_reached.
302
- status: buildUsageStatus(amount.usedFraction),
294
+ // The shared account-level rejection flag cannot identify which window
295
+ // is binding, but an explicit positive verdict applies to both windows.
296
+ // Preserve 100% as a warning when Codex still allows requests; live
297
+ // usage_limit_reached responses remain authoritative for blocking.
298
+ status: buildUsageStatus({
299
+ usedFraction: amount.usedFraction,
300
+ explicitlyAllowed: args.allowed === true && args.limitReached === false,
301
+ }),
303
302
  };
304
303
  }
305
304
  function additionalLimitSlug(args: { limitName?: string; meteredFeature?: string }): string {
@@ -331,6 +330,8 @@ function buildAdditionalUsageLimit(args: {
331
330
  accountId?: string;
332
331
  limitName?: string;
333
332
  meteredFeature?: string;
333
+ allowed?: boolean;
334
+ limitReached?: boolean;
334
335
  nowMs: number;
335
336
  }): UsageLimit {
336
337
  const usageWindow = buildUsageWindow(args.window, args.key, args.nowMs);
@@ -348,10 +349,12 @@ function buildAdditionalUsageLimit(args: {
348
349
  },
349
350
  window: usageWindow,
350
351
  amount,
351
- // The additional meter exposes one account-level flag for both windows.
352
- // Status must follow this window's own usage or a full weekly meter marks
353
- // the shorter window exhausted and schedules a premature retry.
354
- status: buildUsageStatus(amount.usedFraction),
352
+ // A positive meter verdict is authoritative even when the advisory
353
+ // percentage rounds to 100; negative shared verdicts remain window-local.
354
+ status: buildUsageStatus({
355
+ usedFraction: amount.usedFraction,
356
+ explicitlyAllowed: args.allowed === true && args.limitReached === false,
357
+ }),
355
358
  };
356
359
  }
357
360
 
@@ -449,6 +452,8 @@ export const openaiCodexUsageProvider: UsageProvider = {
449
452
  window: parsed.primary,
450
453
  accountId,
451
454
  planType,
455
+ allowed: parsed.allowed,
456
+ limitReached: parsed.limitReached,
452
457
  nowMs,
453
458
  }),
454
459
  );
@@ -460,6 +465,8 @@ export const openaiCodexUsageProvider: UsageProvider = {
460
465
  window: parsed.secondary,
461
466
  accountId,
462
467
  planType,
468
+ allowed: parsed.allowed,
469
+ limitReached: parsed.limitReached,
463
470
  nowMs,
464
471
  }),
465
472
  );
@@ -478,6 +485,8 @@ export const openaiCodexUsageProvider: UsageProvider = {
478
485
  accountId,
479
486
  limitName: extra.limitName,
480
487
  meteredFeature: extra.meteredFeature,
488
+ allowed: extra.allowed,
489
+ limitReached: extra.limitReached,
481
490
  nowMs,
482
491
  }),
483
492
  );
@@ -492,6 +501,8 @@ export const openaiCodexUsageProvider: UsageProvider = {
492
501
  accountId,
493
502
  limitName: extra.limitName,
494
503
  meteredFeature: extra.meteredFeature,
504
+ allowed: extra.allowed,
505
+ limitReached: extra.limitReached,
495
506
  nowMs,
496
507
  }),
497
508
  );
@@ -95,10 +95,11 @@ export async function finalizeErrorMessage(
95
95
  * Rewrite error message for GitHub Copilot request failures.
96
96
  * Must run AFTER finalizeErrorMessage since it replaces the message entirely.
97
97
  *
98
- * 400 `model_not_supported` = Copilot routing rollout gap for our OAuth client.
99
- * A preview model (gpt-5.3-codex, gpt-5.4*, ...) flaps between 200 and
100
- * 400 because only some of Copilot's backends have the model. After the
101
- * in-request retry exhausts, surface guidance rather than the raw error.
98
+ * 400 model-unavailable = Copilot fleet skew. A model that `/models` advertises
99
+ * (claude-sonnet-4.6, claude-opus-4.6, gpt-5.4, gpt-5.3-codex, ...)
100
+ * flaps between 200 and 400 because only part of Copilot's fleet has it
101
+ * in the integrator allowlist. After the in-request retry exhausts,
102
+ * surface guidance rather than the raw error.
102
103
  * 401 = token invalid/expired → credential removal is safe, prompt re-login.
103
104
  * 403 = token valid but access denied (plan, model policy, org restriction) →
104
105
  * do NOT reuse the auth-failed string (which triggers credential removal).
@@ -113,7 +114,7 @@ export function rewriteCopilotError(errorMessage: string, error: unknown, provid
113
114
  return `GitHub Copilot access denied (HTTP 403). Your account may not have access to this model or feature. Check your Copilot plan or model policy settings.`;
114
115
  }
115
116
  if (isCopilotTransientModelError(error)) {
116
- return `GitHub Copilot rejected this model (HTTP 400 model_not_supported) after retries. This is a known intermittent rollout gap for preview models on OAuth clients other than VS Code. Try again in a few seconds, switch to a GA model (gpt-5-mini, gpt-5.2), or run this model from VS Code.`;
117
+ return `GitHub Copilot rejected this model (HTTP 400) after retries: only part of its fleet currently serves this model id, even though /models advertises it. Try again in a few seconds or switch to a model Copilot serves fleet-wide (claude-opus-4.7, claude-sonnet-4.5, gpt-4.1).`;
117
118
  }
118
119
  return errorMessage;
119
120
  }
@@ -7,14 +7,25 @@ import { getHeadersFromError, getRetryAfterMsFromHeaders } from "./retry-after";
7
7
  // home). Re-exported here so existing `../utils/retry` importers keep working.
8
8
  export { isCopilotTransientModelError };
9
9
 
10
- const COPILOT_MODEL_RETRY_MAX_ATTEMPTS = 3;
10
+ // Copilot's model-availability flap is a per-request coin flip across fleet
11
+ // replicas, not backpressure. Measured per-attempt rejection rates for models
12
+ // mid-rollout reach ~70% (gpt-5.4, 2026-08-04), and a live 10-turn run needed 6
13
+ // attempts on one turn — so a small budget just pushes the failure up to the
14
+ // agent-level retry, which restarts the whole turn. Eight attempts on a flat
15
+ // delay keep the residual near 5% at p=0.7 and under 1% at p=0.5, bounded at
16
+ // ~2.8s of dead time in the pathological case.
17
+ const COPILOT_MODEL_RETRY_MAX_ATTEMPTS = 8;
18
+ // Transport blips and status-bearing failures keep the pre-flap budget: they are
19
+ // not coin flips, so a longer ramp only delays surfacing a persistent fault.
20
+ const COPILOT_GENERIC_RETRY_MAX_ATTEMPTS = 3;
11
21
  const COPILOT_MODEL_RETRY_BASE_DELAY_MS = 400;
12
22
  /** Longest server-requested backoff we are willing to sit out before giving up. */
13
23
  const COPILOT_RETRY_AFTER_MAX_WAIT_MS = 30_000;
14
24
 
15
25
  /**
16
- * Wrap an initial Copilot request so transient `model_not_supported` 400s are
17
- * retried a small number of times. No-op for non-Copilot providers.
26
+ * Wrap an initial Copilot request so transient model-availability 400s
27
+ * (`model_not_supported`, `model_not_available_for_integrator`) are retried a
28
+ * small number of times. No-op for non-Copilot providers.
18
29
  *
19
30
  * The callback **MUST** create a fresh in-flight request each invocation — a
20
31
  * once-consumed AsyncIterable cannot be re-iterated.
@@ -38,8 +49,16 @@ export async function callWithCopilotModelRetry<T>(
38
49
  if (options.signal?.aborted) throw error;
39
50
  const transientModelError = isCopilotTransientModelError(error);
40
51
  if (!transientModelError && !isRetryableError(error)) throw error;
41
- if (attempt === COPILOT_MODEL_RETRY_MAX_ATTEMPTS - 1) break;
42
- let delayMs = retryBaseDelayMs * (attempt + 1);
52
+ // Budget is per failure kind, counted over attempts already spent: the
53
+ // eight-attempt allowance only covers the cheap model-availability reroll.
54
+ const maxAttempts = transientModelError
55
+ ? COPILOT_MODEL_RETRY_MAX_ATTEMPTS
56
+ : COPILOT_GENERIC_RETRY_MAX_ATTEMPTS;
57
+ if (attempt >= maxAttempts - 1) break;
58
+ // Reroll the model flap on a flat delay: a ramp only adds dead time to a
59
+ // coin flip the next attempt is equally likely to win. Generic retryable
60
+ // failures (429/5xx/transport) keep the linear backoff below.
61
+ let delayMs = transientModelError ? retryBaseDelayMs : retryBaseDelayMs * (attempt + 1);
43
62
  if (!transientModelError) {
44
63
  const errorStatus = status(error);
45
64
  if (errorStatus !== undefined) {