@oh-my-pi/pi-ai 16.3.15 → 16.4.0
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 +22 -0
- package/README.md +5 -2
- package/dist/types/auth-broker/remote-store.d.ts +1 -0
- package/dist/types/auth-broker/wire-schemas.d.ts +8 -0
- package/dist/types/auth-storage.d.ts +5 -0
- package/dist/types/providers/azure-openai-responses.d.ts +1 -1
- package/dist/types/providers/ollama.d.ts +1 -1
- package/dist/types/providers/openai-chat-server-schema.d.ts +1 -1
- package/dist/types/providers/openai-chat-wire.d.ts +1 -1
- package/dist/types/providers/openai-codex/request-transformer.d.ts +41 -5
- package/dist/types/providers/openai-codex-responses.d.ts +51 -8
- package/dist/types/providers/openai-completions.d.ts +1 -1
- package/dist/types/providers/openai-responses-wire.d.ts +9 -7
- package/dist/types/providers/openai-responses.d.ts +1 -1
- package/dist/types/providers/openai-shared.d.ts +30 -3
- package/dist/types/registry/novita.d.ts +6 -0
- package/dist/types/registry/registry.d.ts +4 -0
- package/dist/types/types.d.ts +23 -0
- package/package.json +4 -4
- package/src/auth-broker/remote-store.ts +60 -5
- package/src/auth-broker/server.ts +1 -0
- package/src/auth-broker/wire-schemas.ts +1 -0
- package/src/auth-storage.ts +94 -20
- package/src/error/flags.ts +4 -1
- package/src/error/rate-limit.ts +7 -1
- package/src/providers/amazon-bedrock.ts +1 -0
- package/src/providers/anthropic-messages-server.ts +18 -0
- package/src/providers/azure-openai-responses.ts +1 -1
- package/src/providers/ollama.ts +1 -1
- package/src/providers/openai-chat-server-schema.ts +1 -1
- package/src/providers/openai-chat-server.ts +8 -1
- package/src/providers/openai-chat-wire.ts +1 -1
- package/src/providers/openai-codex/request-transformer.ts +86 -41
- package/src/providers/openai-codex-responses.ts +468 -32
- package/src/providers/openai-completions.ts +29 -11
- package/src/providers/openai-responses-server.ts +8 -1
- package/src/providers/openai-responses-wire.ts +9 -7
- package/src/providers/openai-responses.ts +8 -2
- package/src/providers/openai-shared.ts +104 -3
- package/src/registry/novita.ts +22 -0
- package/src/registry/registry.ts +2 -0
- package/src/stream.ts +7 -1
- package/src/types.ts +26 -0
|
@@ -39,6 +39,7 @@ import type {
|
|
|
39
39
|
* one broker call instead of N.
|
|
40
40
|
*/
|
|
41
41
|
const USAGE_CACHE_TTL_MS = 15_000;
|
|
42
|
+
const CREDENTIAL_BLOCK_RECONCILE_DELAY_MS = 5 * 60_000;
|
|
42
43
|
const WAIT_THRESHOLD_MS = 1_000;
|
|
43
44
|
const MAX_WAIT_MS = 5_000;
|
|
44
45
|
const BACKGROUND_WAIT_MS = 30_000;
|
|
@@ -50,7 +51,9 @@ function compareCredentialBlockSnapshots(a: CredentialBlockSnapshot, b: Credenti
|
|
|
50
51
|
if (provider !== 0) return provider;
|
|
51
52
|
const scope = a.blockScope.localeCompare(b.blockScope);
|
|
52
53
|
if (scope !== 0) return scope;
|
|
53
|
-
|
|
54
|
+
const blockedUntil = a.blockedUntilMs - b.blockedUntilMs;
|
|
55
|
+
if (blockedUntil !== 0) return blockedUntil;
|
|
56
|
+
return (a.updatedAtMs ?? 0) - (b.updatedAtMs ?? 0);
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
function toCredentialBlockSnapshot(block: StoredCredentialBlock): CredentialBlockSnapshot {
|
|
@@ -58,6 +61,7 @@ function toCredentialBlockSnapshot(block: StoredCredentialBlock): CredentialBloc
|
|
|
58
61
|
providerKey: block.providerKey,
|
|
59
62
|
blockScope: block.blockScope,
|
|
60
63
|
blockedUntilMs: block.blockedUntilMs,
|
|
64
|
+
...(block.updatedAtMs !== undefined ? { updatedAtMs: block.updatedAtMs } : {}),
|
|
61
65
|
};
|
|
62
66
|
}
|
|
63
67
|
|
|
@@ -74,7 +78,8 @@ function credentialBlockSnapshotsEqual(
|
|
|
74
78
|
if (
|
|
75
79
|
leftBlock.providerKey !== rightBlock.providerKey ||
|
|
76
80
|
leftBlock.blockScope !== rightBlock.blockScope ||
|
|
77
|
-
leftBlock.blockedUntilMs !== rightBlock.blockedUntilMs
|
|
81
|
+
leftBlock.blockedUntilMs !== rightBlock.blockedUntilMs ||
|
|
82
|
+
leftBlock.updatedAtMs !== rightBlock.updatedAtMs
|
|
78
83
|
) {
|
|
79
84
|
return false;
|
|
80
85
|
}
|
|
@@ -208,6 +213,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
208
213
|
#cache: Map<string, CacheEntry> = new Map();
|
|
209
214
|
#usageCache?: UsageCacheEntry;
|
|
210
215
|
#usageInflight?: Promise<UsageReport[] | null>;
|
|
216
|
+
#credentialBlockReconcileAfter: Map<string, number> = new Map();
|
|
211
217
|
#usageCacheEpoch = 0;
|
|
212
218
|
#closed = false;
|
|
213
219
|
/**
|
|
@@ -236,10 +242,12 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
236
242
|
return this.#snapshot;
|
|
237
243
|
}
|
|
238
244
|
|
|
239
|
-
#applySnapshot(snapshot: SnapshotResponse, generation: number): void {
|
|
245
|
+
#applySnapshot(snapshot: SnapshotResponse, generation: number, protectNewBlocks = true): void {
|
|
240
246
|
const nowMs = Date.now();
|
|
247
|
+
const previousCredentials = this.#snapshot.credentials;
|
|
241
248
|
const credentials = snapshot.credentials.map(entry => this.#normalizeSnapshotEntryBlocks(entry, nowMs));
|
|
242
|
-
if (snapshotBlocksChanged(
|
|
249
|
+
if (snapshotBlocksChanged(previousCredentials, credentials)) this.#invalidateUsageCache();
|
|
250
|
+
if (protectNewBlocks) this.#protectNewSnapshotBlocks(previousCredentials, credentials, nowMs);
|
|
243
251
|
this.#snapshot = { ...snapshot, credentials };
|
|
244
252
|
this.#generation = generation;
|
|
245
253
|
this.#snapshotReceivedAt = nowMs;
|
|
@@ -251,6 +259,34 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
251
259
|
logger.debug("auth-broker snapshot callback failed", { error: String(error) });
|
|
252
260
|
}
|
|
253
261
|
}
|
|
262
|
+
#protectNewSnapshotBlocks(previous: readonly SnapshotEntry[], next: readonly SnapshotEntry[], nowMs: number): void {
|
|
263
|
+
const previousBlocksByKey = new Map<string, string>();
|
|
264
|
+
for (const entry of previous) {
|
|
265
|
+
for (const block of entry.blocks ?? []) {
|
|
266
|
+
previousBlocksByKey.set(
|
|
267
|
+
`${entry.id}\0${block.providerKey}\0${block.blockScope}`,
|
|
268
|
+
`${block.blockedUntilMs}\0${block.updatedAtMs ?? ""}`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const activeKeys = new Set<string>();
|
|
273
|
+
for (const entry of next) {
|
|
274
|
+
for (const block of entry.blocks ?? []) {
|
|
275
|
+
const key = `${entry.id}\0${block.providerKey}\0${block.blockScope}`;
|
|
276
|
+
activeKeys.add(key);
|
|
277
|
+
const signature = `${block.blockedUntilMs}\0${block.updatedAtMs ?? ""}`;
|
|
278
|
+
if (previousBlocksByKey.get(key) === signature) continue;
|
|
279
|
+
const updatedAtMs = block.updatedAtMs ?? nowMs;
|
|
280
|
+
this.#credentialBlockReconcileAfter.set(
|
|
281
|
+
key,
|
|
282
|
+
Math.min(block.blockedUntilMs, updatedAtMs + CREDENTIAL_BLOCK_RECONCILE_DELAY_MS),
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
for (const key of this.#credentialBlockReconcileAfter.keys()) {
|
|
287
|
+
if (!activeKeys.has(key)) this.#credentialBlockReconcileAfter.delete(key);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
254
290
|
|
|
255
291
|
async #runBackground(): Promise<void> {
|
|
256
292
|
let backoffMs = BACKGROUND_BACKOFF_INITIAL_MS;
|
|
@@ -340,11 +376,13 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
340
376
|
const incoming = this.#normalizeSnapshotEntryBlocks(entry, Date.now());
|
|
341
377
|
const index = this.#snapshot.credentials.findIndex(candidate => candidate.id === incoming.id);
|
|
342
378
|
const previousBlocks = index === -1 ? undefined : this.#snapshot.credentials[index]?.blocks;
|
|
343
|
-
|
|
379
|
+
const blocksChanged = !credentialBlockSnapshotsEqual(previousBlocks, incoming.blocks);
|
|
380
|
+
if (blocksChanged) this.#invalidateUsageCache();
|
|
344
381
|
const credentials =
|
|
345
382
|
index === -1
|
|
346
383
|
? [...this.#snapshot.credentials, incoming]
|
|
347
384
|
: this.#snapshot.credentials.map((candidate, i) => (i === index ? incoming : candidate));
|
|
385
|
+
if (blocksChanged) this.#protectNewSnapshotBlocks(this.#snapshot.credentials, credentials, Date.now());
|
|
348
386
|
this.#snapshot = { ...this.#snapshot, generation, serverNowMs, refresher, credentials };
|
|
349
387
|
this.#generation = generation;
|
|
350
388
|
this.#snapshotReceivedAt = Date.now();
|
|
@@ -392,6 +430,11 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
392
430
|
return block.blockedUntilMs;
|
|
393
431
|
}
|
|
394
432
|
|
|
433
|
+
getCredentialBlockReconcileAfter(credentialId: number, providerKey: string, blockScope: string): number | undefined {
|
|
434
|
+
if (this.getCredentialBlock(credentialId, providerKey, blockScope) === undefined) return undefined;
|
|
435
|
+
return this.#credentialBlockReconcileAfter.get(`${credentialId}\0${providerKey}\0${blockScope}`);
|
|
436
|
+
}
|
|
437
|
+
|
|
395
438
|
listCredentialBlocks(credentialIds: readonly number[]): StoredCredentialBlock[] {
|
|
396
439
|
const nowMs = Date.now();
|
|
397
440
|
this.cleanExpiredCredentialBlocks(nowMs);
|
|
@@ -406,6 +449,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
406
449
|
providerKey: block.providerKey,
|
|
407
450
|
blockScope: block.blockScope,
|
|
408
451
|
blockedUntilMs: block.blockedUntilMs,
|
|
452
|
+
updatedAtMs: block.updatedAtMs,
|
|
409
453
|
});
|
|
410
454
|
}
|
|
411
455
|
}
|
|
@@ -416,6 +460,10 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
416
460
|
upsertCredentialBlock(block: StoredCredentialBlock): void {
|
|
417
461
|
this.#upsertSnapshotBlock(block);
|
|
418
462
|
this.#invalidateUsageCache();
|
|
463
|
+
this.#credentialBlockReconcileAfter.set(
|
|
464
|
+
`${block.credentialId}\0${block.providerKey}\0${block.blockScope}`,
|
|
465
|
+
Math.min(block.blockedUntilMs, Date.now() + CREDENTIAL_BLOCK_RECONCILE_DELAY_MS),
|
|
466
|
+
);
|
|
419
467
|
const body = toCredentialBlockSnapshot(block);
|
|
420
468
|
void this.#client
|
|
421
469
|
.upsertCredentialBlock(block.credentialId, body)
|
|
@@ -434,6 +482,9 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
434
482
|
|
|
435
483
|
deleteCredentialBlocks(credentialId: number): void {
|
|
436
484
|
this.#deleteSnapshotBlocks(credentialId);
|
|
485
|
+
for (const key of this.#credentialBlockReconcileAfter.keys()) {
|
|
486
|
+
if (key.startsWith(`${credentialId}\0`)) this.#credentialBlockReconcileAfter.delete(key);
|
|
487
|
+
}
|
|
437
488
|
this.#invalidateUsageCache();
|
|
438
489
|
void this.#client
|
|
439
490
|
.deleteCredentialBlocks(credentialId)
|
|
@@ -450,6 +501,9 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
450
501
|
|
|
451
502
|
cleanExpiredCredentialBlocks(nowMs: number): void {
|
|
452
503
|
this.#pruneExpiredCredentialBlocks(nowMs);
|
|
504
|
+
for (const [key, reconcileAfterMs] of this.#credentialBlockReconcileAfter) {
|
|
505
|
+
if (reconcileAfterMs <= nowMs) this.#credentialBlockReconcileAfter.delete(key);
|
|
506
|
+
}
|
|
453
507
|
}
|
|
454
508
|
|
|
455
509
|
/**
|
|
@@ -647,6 +701,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
647
701
|
providerKey: block.providerKey,
|
|
648
702
|
blockScope: block.blockScope,
|
|
649
703
|
blockedUntilMs: block.blockedUntilMs,
|
|
704
|
+
...(block.updatedAtMs !== undefined ? { updatedAtMs: block.updatedAtMs } : {}),
|
|
650
705
|
}))
|
|
651
706
|
.sort(compareCredentialBlockSnapshots);
|
|
652
707
|
if (blocks.length > 0) return { ...entry, blocks };
|
|
@@ -290,6 +290,7 @@ function buildCredentialBlockGroups(
|
|
|
290
290
|
providerKey: block.providerKey,
|
|
291
291
|
blockScope: block.blockScope,
|
|
292
292
|
blockedUntilMs: block.blockedUntilMs,
|
|
293
|
+
updatedAtMs: block.updatedAtMs,
|
|
293
294
|
};
|
|
294
295
|
const existing = byCredentialId.get(block.credentialId);
|
|
295
296
|
if (existing) {
|
package/src/auth-storage.ts
CHANGED
|
@@ -134,6 +134,8 @@ export interface StoredCredentialBlock {
|
|
|
134
134
|
blockScope: string;
|
|
135
135
|
/** Epoch milliseconds. */
|
|
136
136
|
blockedUntilMs: number;
|
|
137
|
+
/** Last row update timestamp in epoch milliseconds, when provided by the backing store. */
|
|
138
|
+
updatedAtMs?: number;
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
/**
|
|
@@ -320,6 +322,8 @@ export interface AuthCredentialStore {
|
|
|
320
322
|
cleanExpiredCache(): void;
|
|
321
323
|
/** Non-expired block for one (credential, providerKey, scope) key, or undefined. */
|
|
322
324
|
getCredentialBlock?(credentialId: number, providerKey: string, blockScope: string): number | undefined;
|
|
325
|
+
/** Earliest time a shared-store block should be eligible for live-usage reconciliation. */
|
|
326
|
+
getCredentialBlockReconcileAfter?(credentialId: number, providerKey: string, blockScope: string): number | undefined;
|
|
323
327
|
/** Upsert with MAX semantics: keep the later blockedUntilMs on conflict. */
|
|
324
328
|
upsertCredentialBlock?(block: StoredCredentialBlock): void;
|
|
325
329
|
/** Drop every block row for a credential (all providerKeys/scopes). */
|
|
@@ -817,6 +821,8 @@ function getUsagePlanType(report: UsageReport | null): string | undefined {
|
|
|
817
821
|
function classifyOpenAICodexPlan(report: UsageReport | null): OpenAICodexPlanClass {
|
|
818
822
|
const planType = getUsagePlanType(report);
|
|
819
823
|
if (!planType) return "unknown";
|
|
824
|
+
// Pro Lite is a paid Codex tier, but does not imply full Pro-only model access.
|
|
825
|
+
if (planType === "prolite" || planType === "pro_lite") return "paid";
|
|
820
826
|
const tokens = planType.split("_");
|
|
821
827
|
if (tokens.some(token => OPENAI_CODEX_PRO_PLAN_TOKENS[token] === true)) return "pro";
|
|
822
828
|
if (tokens.some(token => OPENAI_CODEX_PAID_PLAN_TOKENS[token] === true)) return "paid";
|
|
@@ -1022,6 +1028,8 @@ export class AuthStorage {
|
|
|
1022
1028
|
#sessionLastCredential: Map<string, Map<string, { type: AuthCredential["type"]; index: number }>> = new Map();
|
|
1023
1029
|
/** Maps provider:type -> credentialIndex -> blockedUntilMs for temporary backoff. */
|
|
1024
1030
|
#credentialBackoff: Map<string, Map<number, number>> = new Map();
|
|
1031
|
+
/** Earliest time a freshly-set in-memory block may be cleared by live usage reconciliation. */
|
|
1032
|
+
#credentialBackoffProbeAfter: Map<string, Map<number, number>> = new Map();
|
|
1025
1033
|
#usageProviderResolver?: (provider: Provider) => UsageProvider | undefined;
|
|
1026
1034
|
#rankingStrategyResolver?: (provider: Provider) => CredentialRankingStrategy | undefined;
|
|
1027
1035
|
#usageCache: UsageCache;
|
|
@@ -1402,6 +1410,9 @@ export class AuthStorage {
|
|
|
1402
1410
|
if (backoffMap.size === 0) {
|
|
1403
1411
|
this.#credentialBackoff.delete(backoffKey);
|
|
1404
1412
|
}
|
|
1413
|
+
const probeAfterMap = this.#credentialBackoffProbeAfter.get(backoffKey);
|
|
1414
|
+
probeAfterMap?.delete(credentialIndex);
|
|
1415
|
+
if (probeAfterMap?.size === 0) this.#credentialBackoffProbeAfter.delete(backoffKey);
|
|
1405
1416
|
return undefined;
|
|
1406
1417
|
}
|
|
1407
1418
|
return blockedUntil;
|
|
@@ -1494,6 +1505,9 @@ export class AuthStorage {
|
|
|
1494
1505
|
const nextBlockedUntil = Math.max(existing, blockedUntilMs);
|
|
1495
1506
|
backoffMap.set(credentialIndex, nextBlockedUntil);
|
|
1496
1507
|
this.#credentialBackoff.set(backoffKey, backoffMap);
|
|
1508
|
+
const probeAfterMap = this.#credentialBackoffProbeAfter.get(backoffKey) ?? new Map<number, number>();
|
|
1509
|
+
probeAfterMap.set(credentialIndex, Math.min(nextBlockedUntil, Date.now() + USAGE_REPORT_TTL_MS));
|
|
1510
|
+
this.#credentialBackoffProbeAfter.set(backoffKey, probeAfterMap);
|
|
1497
1511
|
this.#invalidateUsageReportCache(provider);
|
|
1498
1512
|
|
|
1499
1513
|
const upsertCredentialBlock = this.#store.upsertCredentialBlock?.bind(this.#store);
|
|
@@ -3371,18 +3385,36 @@ export class AuthStorage {
|
|
|
3371
3385
|
args.order.map(async idx => {
|
|
3372
3386
|
const selection = args.credentials[idx];
|
|
3373
3387
|
if (!selection) return null;
|
|
3374
|
-
|
|
3388
|
+
let blockedUntil = this.#getCredentialBlockedUntil(
|
|
3375
3389
|
args.provider,
|
|
3376
3390
|
args.providerKey,
|
|
3377
3391
|
selection.index,
|
|
3378
3392
|
args.blockScope,
|
|
3379
3393
|
);
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3394
|
+
let usage: UsageReport | null = null;
|
|
3395
|
+
let usageChecked = false;
|
|
3396
|
+
if (blockedUntil !== undefined && args.provider === "openai-codex") {
|
|
3397
|
+
usage = await this.#getUsageReport(args.provider, selection.credential, {
|
|
3398
|
+
...args.options,
|
|
3399
|
+
timeoutMs: this.#usageRequestTimeoutMs,
|
|
3400
|
+
});
|
|
3401
|
+
usageChecked = true;
|
|
3402
|
+
blockedUntil = this.#getCredentialBlockedUntil(
|
|
3403
|
+
args.provider,
|
|
3404
|
+
args.providerKey,
|
|
3405
|
+
selection.index,
|
|
3406
|
+
args.blockScope,
|
|
3407
|
+
);
|
|
3408
|
+
}
|
|
3409
|
+
if (blockedUntil !== undefined) return { selection, usage, usageChecked, blockedUntil };
|
|
3410
|
+
if (!usageChecked) {
|
|
3411
|
+
usage = await this.#getUsageReport(args.provider, selection.credential, {
|
|
3412
|
+
...args.options,
|
|
3413
|
+
timeoutMs: this.#usageRequestTimeoutMs,
|
|
3414
|
+
});
|
|
3415
|
+
usageChecked = true;
|
|
3416
|
+
}
|
|
3417
|
+
return { selection, usage, usageChecked, blockedUntil: undefined as number | undefined };
|
|
3386
3418
|
}),
|
|
3387
3419
|
);
|
|
3388
3420
|
const timeoutSignal = Promise.withResolvers<null>();
|
|
@@ -4431,23 +4463,24 @@ export class AuthStorage {
|
|
|
4431
4463
|
backoffMap.delete(index);
|
|
4432
4464
|
if (backoffMap.size === 0) this.#credentialBackoff.delete(key);
|
|
4433
4465
|
}
|
|
4466
|
+
for (const [key, probeAfterMap] of this.#credentialBackoffProbeAfter) {
|
|
4467
|
+
if (key !== providerKey && !key.startsWith(scopedPrefix)) continue;
|
|
4468
|
+
probeAfterMap.delete(index);
|
|
4469
|
+
if (probeAfterMap.size === 0) this.#credentialBackoffProbeAfter.delete(key);
|
|
4470
|
+
}
|
|
4434
4471
|
}
|
|
4435
4472
|
|
|
4436
4473
|
/**
|
|
4437
4474
|
* Self-heal a stale Codex usage-limit block: when a fresh live usage report
|
|
4438
|
-
*
|
|
4439
|
-
* in-memory `openai-codex:oauth` blocks so
|
|
4440
|
-
*
|
|
4441
|
-
* scope, so clearing every block for the credential id is exact.
|
|
4475
|
+
* says the account is allowed and below every reported limit, drop the
|
|
4476
|
+
* persisted and in-memory `openai-codex:oauth` blocks so credential selection
|
|
4477
|
+
* can re-include recovered seats before a stale block naturally expires.
|
|
4442
4478
|
*/
|
|
4443
4479
|
#isHealthyCodexUsageReport(report: UsageReport): boolean {
|
|
4480
|
+
if (report.provider !== "openai-codex") return false;
|
|
4444
4481
|
const metadata = report.metadata;
|
|
4445
|
-
return
|
|
4446
|
-
|
|
4447
|
-
metadata?.allowed === true &&
|
|
4448
|
-
metadata.limitReached === false &&
|
|
4449
|
-
!this.#isUsageLimitReached(report.limits)
|
|
4450
|
-
);
|
|
4482
|
+
if (metadata?.allowed !== true || metadata.limitReached !== false) return false;
|
|
4483
|
+
return !this.#isUsageLimitReached(report.limits);
|
|
4451
4484
|
}
|
|
4452
4485
|
|
|
4453
4486
|
#reconcileCodexUsageBlockForCredential(provider: Provider, credentialId: number, report: UsageReport): void {
|
|
@@ -4460,6 +4493,19 @@ export class AuthStorage {
|
|
|
4460
4493
|
const blockScope = this.#rankingStrategyResolver?.(provider)?.blockScope?.({});
|
|
4461
4494
|
const blockedUntilMs = this.#getCredentialBlockedUntil(provider, providerKey, credentialIndex, blockScope);
|
|
4462
4495
|
if (blockedUntilMs === undefined) return;
|
|
4496
|
+
// `/usage` can lag the request path that just returned 429. Fresh local or
|
|
4497
|
+
// broker-sourced blocks get one usage-cache window before healthy reports may
|
|
4498
|
+
// clear them.
|
|
4499
|
+
const nowMs = Date.now();
|
|
4500
|
+
const scopedBackoffKey = this.#toScopedBackoffKey(providerKey, blockScope);
|
|
4501
|
+
const globalProbeAfterMs = this.#credentialBackoffProbeAfter.get(providerKey)?.get(credentialIndex) ?? 0;
|
|
4502
|
+
const scopedProbeAfterMs = this.#credentialBackoffProbeAfter.get(scopedBackoffKey)?.get(credentialIndex) ?? 0;
|
|
4503
|
+
const getStoreReconcileAfter = this.#store.getCredentialBlockReconcileAfter?.bind(this.#store);
|
|
4504
|
+
const storeGlobalProbeAfterMs = getStoreReconcileAfter?.(credentialId, providerKey, "") ?? 0;
|
|
4505
|
+
const storeScopedProbeAfterMs = getStoreReconcileAfter?.(credentialId, providerKey, blockScope ?? "") ?? 0;
|
|
4506
|
+
if (Math.max(globalProbeAfterMs, scopedProbeAfterMs, storeGlobalProbeAfterMs, storeScopedProbeAfterMs) > nowMs) {
|
|
4507
|
+
return;
|
|
4508
|
+
}
|
|
4463
4509
|
this.#clearCredentialBlocks(provider, credentialId);
|
|
4464
4510
|
logger.info("Cleared stale Codex usage-limit block after healthy live usage report", {
|
|
4465
4511
|
credentialId,
|
|
@@ -4963,6 +5009,7 @@ type CredentialBlockRow = {
|
|
|
4963
5009
|
provider_key: string;
|
|
4964
5010
|
block_scope: string;
|
|
4965
5011
|
blocked_until_ms: number;
|
|
5012
|
+
updated_at: number;
|
|
4966
5013
|
};
|
|
4967
5014
|
|
|
4968
5015
|
type SerializedCredentialRecord = {
|
|
@@ -5178,6 +5225,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
5178
5225
|
#upsertCredentialBlockStmt: Statement;
|
|
5179
5226
|
#deleteCredentialBlocksStmt: Statement;
|
|
5180
5227
|
#deleteExpiredCredentialBlocksStmt: Statement;
|
|
5228
|
+
#credentialBlockReconcileAfter: Map<string, number> = new Map();
|
|
5181
5229
|
#insertUsageHistoryStmt: Statement;
|
|
5182
5230
|
#insertUsageCostStmt: Statement;
|
|
5183
5231
|
#listUsageCostsStmt: Statement;
|
|
@@ -5224,10 +5272,10 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
5224
5272
|
);
|
|
5225
5273
|
this.#deleteExpiredCacheStmt = this.#db.prepare(`DELETE FROM cache WHERE expires_at <= ${SQLITE_NOW_EPOCH}`);
|
|
5226
5274
|
this.#getCredentialBlockStmt = this.#db.prepare(
|
|
5227
|
-
"SELECT blocked_until_ms FROM auth_credential_blocks WHERE credential_id = ? AND provider_key = ? AND block_scope = ? AND blocked_until_ms > ?",
|
|
5275
|
+
"SELECT blocked_until_ms, updated_at FROM auth_credential_blocks WHERE credential_id = ? AND provider_key = ? AND block_scope = ? AND blocked_until_ms > ?",
|
|
5228
5276
|
);
|
|
5229
5277
|
this.#listCredentialBlocksByCredentialStmt = this.#db.prepare(
|
|
5230
|
-
"SELECT credential_id, provider_key, block_scope, blocked_until_ms FROM auth_credential_blocks WHERE credential_id = ? AND blocked_until_ms > ? ORDER BY provider_key ASC, block_scope ASC",
|
|
5278
|
+
"SELECT credential_id, provider_key, block_scope, blocked_until_ms, updated_at FROM auth_credential_blocks WHERE credential_id = ? AND blocked_until_ms > ? ORDER BY provider_key ASC, block_scope ASC",
|
|
5231
5279
|
);
|
|
5232
5280
|
this.#upsertCredentialBlockStmt = this.#db.prepare(
|
|
5233
5281
|
`INSERT INTO auth_credential_blocks (credential_id, provider_key, block_scope, blocked_until_ms, updated_at)
|
|
@@ -5841,11 +5889,26 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
5841
5889
|
const nowMs = Date.now();
|
|
5842
5890
|
this.#deleteExpiredCredentialBlocksStmt.run(nowMs);
|
|
5843
5891
|
const row = this.#getCredentialBlockStmt.get(credentialId, providerKey, blockScope, nowMs) as
|
|
5844
|
-
| { blocked_until_ms?: number }
|
|
5892
|
+
| { blocked_until_ms?: number; updated_at?: number }
|
|
5845
5893
|
| undefined;
|
|
5846
5894
|
return typeof row?.blocked_until_ms === "number" ? row.blocked_until_ms : undefined;
|
|
5847
5895
|
}
|
|
5848
5896
|
|
|
5897
|
+
getCredentialBlockReconcileAfter(credentialId: number, providerKey: string, blockScope: string): number | undefined {
|
|
5898
|
+
const nowMs = Date.now();
|
|
5899
|
+
this.#deleteExpiredCredentialBlocksStmt.run(nowMs);
|
|
5900
|
+
const row = this.#getCredentialBlockStmt.get(credentialId, providerKey, blockScope, nowMs) as
|
|
5901
|
+
| { blocked_until_ms?: number; updated_at?: number }
|
|
5902
|
+
| undefined;
|
|
5903
|
+
if (typeof row?.blocked_until_ms !== "number") return undefined;
|
|
5904
|
+
const memoryReconcileAfter =
|
|
5905
|
+
this.#credentialBlockReconcileAfter.get(`${credentialId}\0${providerKey}\0${blockScope}`) ?? 0;
|
|
5906
|
+
const persistedReconcileAfter =
|
|
5907
|
+
typeof row.updated_at === "number" ? row.updated_at * 1000 + USAGE_REPORT_TTL_MS : 0;
|
|
5908
|
+
const reconcileAfter = Math.max(memoryReconcileAfter, persistedReconcileAfter);
|
|
5909
|
+
return reconcileAfter > nowMs ? Math.min(row.blocked_until_ms, reconcileAfter) : undefined;
|
|
5910
|
+
}
|
|
5911
|
+
|
|
5849
5912
|
upsertCredentialBlock(block: StoredCredentialBlock): void {
|
|
5850
5913
|
this.#upsertCredentialBlockStmt.run(
|
|
5851
5914
|
block.credentialId,
|
|
@@ -5853,14 +5916,24 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
5853
5916
|
block.blockScope,
|
|
5854
5917
|
block.blockedUntilMs,
|
|
5855
5918
|
);
|
|
5919
|
+
this.#credentialBlockReconcileAfter.set(
|
|
5920
|
+
`${block.credentialId}\0${block.providerKey}\0${block.blockScope}`,
|
|
5921
|
+
Math.min(block.blockedUntilMs, Date.now() + USAGE_REPORT_TTL_MS),
|
|
5922
|
+
);
|
|
5856
5923
|
}
|
|
5857
5924
|
|
|
5858
5925
|
deleteCredentialBlocks(credentialId: number): void {
|
|
5859
5926
|
this.#deleteCredentialBlocksStmt.run(credentialId);
|
|
5927
|
+
for (const key of this.#credentialBlockReconcileAfter.keys()) {
|
|
5928
|
+
if (key.startsWith(`${credentialId}\0`)) this.#credentialBlockReconcileAfter.delete(key);
|
|
5929
|
+
}
|
|
5860
5930
|
}
|
|
5861
5931
|
|
|
5862
5932
|
cleanExpiredCredentialBlocks(nowMs: number): void {
|
|
5863
5933
|
this.#deleteExpiredCredentialBlocksStmt.run(nowMs);
|
|
5934
|
+
for (const [key, reconcileAfterMs] of this.#credentialBlockReconcileAfter) {
|
|
5935
|
+
if (reconcileAfterMs <= nowMs) this.#credentialBlockReconcileAfter.delete(key);
|
|
5936
|
+
}
|
|
5864
5937
|
}
|
|
5865
5938
|
|
|
5866
5939
|
listCredentialBlocks(credentialIds: readonly number[]): StoredCredentialBlock[] {
|
|
@@ -5879,6 +5952,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
5879
5952
|
providerKey: row.provider_key,
|
|
5880
5953
|
blockScope: row.block_scope,
|
|
5881
5954
|
blockedUntilMs: row.blocked_until_ms,
|
|
5955
|
+
updatedAtMs: row.updated_at * 1000,
|
|
5882
5956
|
});
|
|
5883
5957
|
}
|
|
5884
5958
|
}
|
package/src/error/flags.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isUnexpectedSocketCloseMessage } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import type { Api, AssistantMessage } from "../types";
|
|
3
|
+
import { AwsCredentialsError } from "./aws";
|
|
3
4
|
import {
|
|
4
5
|
AnthropicConnectionError,
|
|
5
6
|
AnthropicConnectionTimeoutError,
|
|
@@ -346,7 +347,9 @@ export function classify(error: unknown, api?: Api): number {
|
|
|
346
347
|
}
|
|
347
348
|
}
|
|
348
349
|
|
|
349
|
-
if (link instanceof
|
|
350
|
+
if (link instanceof AwsCredentialsError) {
|
|
351
|
+
kinds |= Flag.AuthFailed;
|
|
352
|
+
} else if (link instanceof AnthropicConnectionTimeoutError) {
|
|
350
353
|
kinds |= Flag.Timeout | Flag.Transient;
|
|
351
354
|
} else if (link instanceof AnthropicConnectionError) {
|
|
352
355
|
kinds |= Flag.Transient;
|
package/src/error/rate-limit.ts
CHANGED
|
@@ -67,6 +67,12 @@ export function parseRateLimitReason(errorMessage: string): RateLimitReason {
|
|
|
67
67
|
lower.includes("exhausted") ||
|
|
68
68
|
lower.includes("quota") ||
|
|
69
69
|
lower.includes("usage limit") ||
|
|
70
|
+
// xAI SuperGrok: HTTP 403 "run out of credits" / spending-limit is an
|
|
71
|
+
// account-local cap — rotate, don't treat as auth failure.
|
|
72
|
+
lower.includes("run out of credits") ||
|
|
73
|
+
lower.includes("out of credits") ||
|
|
74
|
+
lower.includes("spending-limit") ||
|
|
75
|
+
lower.includes("spending limit") ||
|
|
70
76
|
INSUFFICIENT_BALANCE_PATTERN.test(errorMessage)
|
|
71
77
|
) {
|
|
72
78
|
return "QUOTA_EXHAUSTED";
|
|
@@ -100,7 +106,7 @@ export function calculateRateLimitBackoffMs(reason: RateLimitReason): number {
|
|
|
100
106
|
|
|
101
107
|
/** Detect usage/quota limit errors in error messages (persistent, requires credential switch). */
|
|
102
108
|
const USAGE_LIMIT_PATTERN =
|
|
103
|
-
/usage.?limit|usage_limit_reached|usage_not_included|limit_reached|quota.?(?:exceeded|reached|insufficient)|额度不足|额度耗尽|resource.?exhausted|exhausted your capacity|quota will reset|insufficient.?(?:balance|quota)/i;
|
|
109
|
+
/usage.?limit|usage_limit_reached|usage_not_included|limit_reached|quota.?(?:exceeded|reached|insufficient)|额度不足|额度耗尽|resource.?exhausted|exhausted your capacity|quota will reset|insufficient.?(?:balance|quota)|run out of credits|out of credits|spending[- _]?limit|personal-team-blocked/i;
|
|
104
110
|
|
|
105
111
|
/**
|
|
106
112
|
* HTTP status codes that, absent richer body classification, represent an
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Effort } from "@oh-my-pi/pi-catalog/effort";
|
|
1
2
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
2
3
|
import { type } from "arktype";
|
|
3
4
|
import { captureRequestHeaders, resolvePromptCacheKey } from "../auth-gateway/http";
|
|
@@ -291,6 +292,19 @@ function deriveCacheRetention(data: {
|
|
|
291
292
|
return strongest;
|
|
292
293
|
}
|
|
293
294
|
|
|
295
|
+
/**
|
|
296
|
+
* Inbound `output_config.effort` wire literal → catalog `Effort` (1:1).
|
|
297
|
+
* Values outside this table (none exist in the schema today) are ignored
|
|
298
|
+
* rather than guessed at.
|
|
299
|
+
*/
|
|
300
|
+
const REASONING_EFFORT_BY_WIRE: Partial<Record<string, Effort>> = {
|
|
301
|
+
low: Effort.Low,
|
|
302
|
+
medium: Effort.Medium,
|
|
303
|
+
high: Effort.High,
|
|
304
|
+
xhigh: Effort.XHigh,
|
|
305
|
+
max: Effort.Max,
|
|
306
|
+
};
|
|
307
|
+
|
|
294
308
|
export function parseRequest(body: unknown, headers?: Headers): ParsedRequest {
|
|
295
309
|
const data = anthropicMessagesRequestSchema(body);
|
|
296
310
|
if (data instanceof type.errors) {
|
|
@@ -351,6 +365,10 @@ export function parseRequest(body: unknown, headers?: Headers): ParsedRequest {
|
|
|
351
365
|
if (data.output_config?.task_budget) {
|
|
352
366
|
options.taskBudget = data.output_config.task_budget;
|
|
353
367
|
}
|
|
368
|
+
if (data.output_config?.effort) {
|
|
369
|
+
const mapped = REASONING_EFFORT_BY_WIRE[data.output_config.effort];
|
|
370
|
+
if (mapped !== undefined) options.reasoning = mapped;
|
|
371
|
+
}
|
|
354
372
|
const cacheRetention = deriveCacheRetention(data);
|
|
355
373
|
if (cacheRetention !== undefined) options.cacheRetention = cacheRetention;
|
|
356
374
|
// Anthropic clients commonly send `metadata: { user_id }`; forward verbatim
|
|
@@ -56,7 +56,7 @@ function resolveDeploymentName(model: Model<"azure-openai-responses">, options?:
|
|
|
56
56
|
|
|
57
57
|
// Azure OpenAI Responses-specific options
|
|
58
58
|
export interface AzureOpenAIResponsesOptions extends StreamOptions {
|
|
59
|
-
reasoning?: "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
59
|
+
reasoning?: "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
60
60
|
reasoningSummary?: "auto" | "detailed" | "concise" | null;
|
|
61
61
|
azureApiVersion?: string;
|
|
62
62
|
azureResourceName?: string;
|
package/src/providers/ollama.ts
CHANGED
|
@@ -35,7 +35,7 @@ import { transformMessages } from "./transform-messages";
|
|
|
35
35
|
import { joinTextWithImagePlaceholder, partitionVisionContent } from "./vision-guard";
|
|
36
36
|
|
|
37
37
|
export interface OllamaChatOptions extends StreamOptions {
|
|
38
|
-
reasoning?: "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
38
|
+
reasoning?: "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
39
39
|
disableReasoning?: boolean;
|
|
40
40
|
toolChoice?: ToolChoice;
|
|
41
41
|
}
|
|
@@ -209,7 +209,7 @@ export const openaiChatRequestSchema = type({
|
|
|
209
209
|
"frequency_penalty?": "number",
|
|
210
210
|
"logit_bias?": type({ "[string]": "number" }),
|
|
211
211
|
"user?": "string",
|
|
212
|
-
"reasoning_effort?": "'minimal' | 'low' | 'medium' | 'high' | 'xhigh'",
|
|
212
|
+
"reasoning_effort?": "'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max'",
|
|
213
213
|
"parallel_tool_calls?": "boolean",
|
|
214
214
|
"service_tier?": "'auto' | 'default' | 'flex' | 'scale' | 'priority'",
|
|
215
215
|
"metadata?": type({ "[string]": "unknown" }),
|
|
@@ -35,7 +35,14 @@ export type { ParsedRequest };
|
|
|
35
35
|
type ReasoningEffort = NonNullable<ParsedRequest["options"]["reasoning"]>;
|
|
36
36
|
|
|
37
37
|
function isReasoningEffort(value: unknown): value is ReasoningEffort {
|
|
38
|
-
return
|
|
38
|
+
return (
|
|
39
|
+
value === "minimal" ||
|
|
40
|
+
value === "low" ||
|
|
41
|
+
value === "medium" ||
|
|
42
|
+
value === "high" ||
|
|
43
|
+
value === "xhigh" ||
|
|
44
|
+
value === "max"
|
|
45
|
+
);
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
function isServiceTier(value: unknown): value is ServiceTier {
|
|
@@ -116,7 +116,7 @@ export type Metadata = {
|
|
|
116
116
|
};
|
|
117
117
|
|
|
118
118
|
/** Constrains effort on reasoning for reasoning models. */
|
|
119
|
-
export type ReasoningEffort = "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | null;
|
|
119
|
+
export type ReasoningEffort = "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | null;
|
|
120
120
|
|
|
121
121
|
/** JSON object response format (older JSON mode). */
|
|
122
122
|
export interface ResponseFormatJSONObject {
|