@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.
- package/CHANGELOG.md +11 -0
- package/dist/types/auth/sqlite-credential-store.d.ts +117 -0
- package/dist/types/auth-storage.d.ts +1 -108
- package/dist/types/error/flags.d.ts +4 -2
- package/dist/types/error/index.d.ts +1 -0
- package/dist/types/error/rate-limit.d.ts +21 -5
- package/dist/types/error/retryable.d.ts +1 -1
- package/dist/types/providers/anthropic.d.ts +2 -2
- package/dist/types/providers/cursor/exec-modern.d.ts +1 -1
- package/dist/types/providers/cursor-pi-args.d.ts +6 -0
- package/dist/types/providers/mock.d.ts +1 -0
- package/dist/types/providers/openai-codex/request-transformer.d.ts +1 -1
- package/dist/types/providers/openai-codex-responses.d.ts +1 -1
- package/dist/types/providers/pi-native-server.d.ts +1 -1
- package/dist/types/utils/http-inspector.d.ts +5 -4
- package/dist/types/utils/retry.d.ts +3 -2
- package/package.json +5 -5
- package/src/auth/sqlite-credential-store.ts +2049 -0
- package/src/auth-retry.ts +8 -5
- package/src/auth-storage.ts +30 -2023
- package/src/error/auth-classify.ts +5 -4
- package/src/error/flags.ts +74 -18
- package/src/error/index.ts +1 -0
- package/src/error/rate-limit.ts +64 -5
- package/src/error/retryable.ts +1 -1
- package/src/providers/anthropic.ts +15 -3
- package/src/providers/cursor/exec-modern.ts +1 -0
- package/src/providers/cursor-pi-args.ts +30 -0
- package/src/providers/cursor.ts +16 -9
- package/src/providers/mock.ts +1 -1
- package/src/providers/openai-codex/request-transformer.ts +21 -27
- package/src/providers/openai-codex-responses.ts +2 -2
- package/src/providers/pi-native-client.ts +1 -1
- package/src/providers/pi-native-server.ts +1 -1
- package/src/registry/oauth/devin.ts +1 -1
- package/src/stream.ts +3 -3
- package/src/usage/openai-codex.ts +30 -19
- package/src/utils/http-inspector.ts +6 -5
- 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
|
|
267
|
-
if (
|
|
268
|
-
if (usedFraction
|
|
269
|
-
if (usedFraction >=
|
|
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
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
//
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
|
|
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
|
|
99
|
-
*
|
|
100
|
-
* 400 because only
|
|
101
|
-
* in
|
|
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
|
|
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
|
}
|
package/src/utils/retry.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
17
|
-
*
|
|
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
|
-
|
|
42
|
-
|
|
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) {
|