@oh-my-pi/pi-ai 16.3.5 → 16.3.6
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-broker/client.d.ts +3 -1
- package/dist/types/auth-broker/remote-store.d.ts +6 -1
- package/dist/types/auth-broker/types.d.ts +13 -1
- package/dist/types/auth-broker/wire-schemas.d.ts +46 -0
- package/dist/types/auth-storage.d.ts +38 -0
- package/dist/types/types.d.ts +16 -0
- package/dist/types/utils/schema/normalize.d.ts +5 -0
- package/package.json +4 -4
- package/src/auth-broker/client.ts +41 -12
- package/src/auth-broker/remote-store.ts +189 -9
- package/src/auth-broker/server.ts +109 -7
- package/src/auth-broker/types.ts +22 -1
- package/src/auth-broker/wire-schemas.ts +22 -0
- package/src/auth-storage.ts +292 -22
- package/src/providers/ollama.ts +2 -2
- package/src/types.ts +18 -0
- package/src/usage/claude.ts +28 -8
- package/src/utils/schema/normalize.ts +131 -0
package/src/auth-storage.ts
CHANGED
|
@@ -123,6 +123,18 @@ export interface StoredAuthCredential {
|
|
|
123
123
|
disabledCause: string | null;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/** One persisted rate-limit block: credential row id + provider-type key + optional scope. */
|
|
127
|
+
export interface StoredCredentialBlock {
|
|
128
|
+
/** SQLite row id of the credential (auth_credentials.id). */
|
|
129
|
+
credentialId: number;
|
|
130
|
+
/** `${provider}:${credentialType}` — same value as AuthStorage's in-memory providerKey. */
|
|
131
|
+
providerKey: string;
|
|
132
|
+
/** Block scope (e.g. "tier:fable"); empty string = unscoped. Never NUL-delimited. */
|
|
133
|
+
blockScope: string;
|
|
134
|
+
/** Epoch milliseconds. */
|
|
135
|
+
blockedUntilMs: number;
|
|
136
|
+
}
|
|
137
|
+
|
|
126
138
|
/**
|
|
127
139
|
* Per-credential health record returned by {@link AuthStorage.checkCredentials}.
|
|
128
140
|
*
|
|
@@ -305,6 +317,16 @@ export interface AuthCredentialStore {
|
|
|
305
317
|
getCache(key: string, options?: { includeExpired?: boolean }): string | null;
|
|
306
318
|
setCache(key: string, value: string, expiresAtSec: number): void;
|
|
307
319
|
cleanExpiredCache(): void;
|
|
320
|
+
/** Non-expired block for one (credential, providerKey, scope) key, or undefined. */
|
|
321
|
+
getCredentialBlock?(credentialId: number, providerKey: string, blockScope: string): number | undefined;
|
|
322
|
+
/** Upsert with MAX semantics: keep the later blockedUntilMs on conflict. */
|
|
323
|
+
upsertCredentialBlock?(block: StoredCredentialBlock): void;
|
|
324
|
+
/** Drop every block row for a credential (all providerKeys/scopes). */
|
|
325
|
+
deleteCredentialBlocks?(credentialId: number): void;
|
|
326
|
+
/** Prune rows with blocked_until_ms <= nowMs. */
|
|
327
|
+
cleanExpiredCredentialBlocks?(nowMs: number): void;
|
|
328
|
+
/** List non-expired blocks for broker snapshots. */
|
|
329
|
+
listCredentialBlocks?(credentialIds: readonly number[]): StoredCredentialBlock[];
|
|
308
330
|
/**
|
|
309
331
|
* Append usage-limit snapshots for trend history. Optional: stores without
|
|
310
332
|
* durable storage (e.g. the broker remote store) omit it and recording is
|
|
@@ -987,6 +1009,11 @@ export class AuthStorage {
|
|
|
987
1009
|
} catch {
|
|
988
1010
|
// Best-effort.
|
|
989
1011
|
}
|
|
1012
|
+
try {
|
|
1013
|
+
this.#store.cleanExpiredCredentialBlocks?.(Date.now());
|
|
1014
|
+
} catch {
|
|
1015
|
+
// Best-effort.
|
|
1016
|
+
}
|
|
990
1017
|
this.#usageFetch = options.usageFetch ?? fetch;
|
|
991
1018
|
this.#usageRequestTimeoutMs = options.usageRequestTimeoutMs ?? DEFAULT_USAGE_REQUEST_TIMEOUT_MS;
|
|
992
1019
|
this.#refreshOAuthCredentialOverride = options.refreshOAuthCredential;
|
|
@@ -1305,13 +1332,13 @@ export class AuthStorage {
|
|
|
1305
1332
|
return blockScope ? `${providerKey}\0${blockScope}` : providerKey;
|
|
1306
1333
|
}
|
|
1307
1334
|
|
|
1308
|
-
/** Returns block expiry timestamp for a credential/key pair, cleaning up expired entries. */
|
|
1309
|
-
#getCredentialBlockedUntilForKey(backoffKey: string, credentialIndex: number): number | undefined {
|
|
1335
|
+
/** Returns in-memory block expiry timestamp for a credential/key pair, cleaning up expired entries. */
|
|
1336
|
+
#getCredentialBlockedUntilForKey(backoffKey: string, credentialIndex: number, nowMs: number): number | undefined {
|
|
1310
1337
|
const backoffMap = this.#credentialBackoff.get(backoffKey);
|
|
1311
1338
|
if (!backoffMap) return undefined;
|
|
1312
1339
|
const blockedUntil = backoffMap.get(credentialIndex);
|
|
1313
1340
|
if (!blockedUntil) return undefined;
|
|
1314
|
-
if (blockedUntil <=
|
|
1341
|
+
if (blockedUntil <= nowMs) {
|
|
1315
1342
|
backoffMap.delete(credentialIndex);
|
|
1316
1343
|
if (backoffMap.size === 0) {
|
|
1317
1344
|
this.#credentialBackoff.delete(backoffKey);
|
|
@@ -1321,28 +1348,80 @@ export class AuthStorage {
|
|
|
1321
1348
|
return blockedUntil;
|
|
1322
1349
|
}
|
|
1323
1350
|
|
|
1324
|
-
|
|
1351
|
+
#readPersistedCredentialBlock(
|
|
1352
|
+
credentialId: number,
|
|
1353
|
+
providerKey: string,
|
|
1354
|
+
blockScope: string | undefined,
|
|
1355
|
+
): number | undefined {
|
|
1356
|
+
const getCredentialBlock = this.#store.getCredentialBlock?.bind(this.#store);
|
|
1357
|
+
if (!getCredentialBlock) return undefined;
|
|
1358
|
+
try {
|
|
1359
|
+
return getCredentialBlock(credentialId, providerKey, blockScope ?? "");
|
|
1360
|
+
} catch (err) {
|
|
1361
|
+
logger.debug("Failed to read credential block from persistent store", {
|
|
1362
|
+
err,
|
|
1363
|
+
credentialId,
|
|
1364
|
+
providerKey,
|
|
1365
|
+
blockScope,
|
|
1366
|
+
});
|
|
1367
|
+
return undefined;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
/** Returns block expiry timestamp for a credential, checking unscoped and scoped blocks. */
|
|
1325
1372
|
#getCredentialBlockedUntil(
|
|
1373
|
+
provider: string,
|
|
1326
1374
|
providerKey: string,
|
|
1327
1375
|
credentialIndex: number,
|
|
1328
1376
|
blockScope: string | undefined = undefined,
|
|
1329
1377
|
): number | undefined {
|
|
1330
|
-
const
|
|
1331
|
-
|
|
1332
|
-
|
|
1378
|
+
const nowMs = Date.now();
|
|
1379
|
+
let blockedUntil = this.#getCredentialBlockedUntilForKey(providerKey, credentialIndex, nowMs);
|
|
1380
|
+
if (blockScope) {
|
|
1381
|
+
const scopedBlockedUntil = this.#getCredentialBlockedUntilForKey(
|
|
1382
|
+
this.#toScopedBackoffKey(providerKey, blockScope),
|
|
1383
|
+
credentialIndex,
|
|
1384
|
+
nowMs,
|
|
1385
|
+
);
|
|
1386
|
+
if (scopedBlockedUntil !== undefined && (blockedUntil === undefined || scopedBlockedUntil > blockedUntil)) {
|
|
1387
|
+
blockedUntil = scopedBlockedUntil;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
const credentialId = this.#getStoredCredentials(provider)[credentialIndex]?.id;
|
|
1392
|
+
if (credentialId === undefined) return blockedUntil;
|
|
1393
|
+
const persistedGlobalBlockedUntil = this.#readPersistedCredentialBlock(credentialId, providerKey, "");
|
|
1394
|
+
if (
|
|
1395
|
+
persistedGlobalBlockedUntil !== undefined &&
|
|
1396
|
+
(blockedUntil === undefined || persistedGlobalBlockedUntil > blockedUntil)
|
|
1397
|
+
) {
|
|
1398
|
+
blockedUntil = persistedGlobalBlockedUntil;
|
|
1399
|
+
}
|
|
1400
|
+
if (blockScope) {
|
|
1401
|
+
const persistedScopedBlockedUntil = this.#readPersistedCredentialBlock(credentialId, providerKey, blockScope);
|
|
1402
|
+
if (
|
|
1403
|
+
persistedScopedBlockedUntil !== undefined &&
|
|
1404
|
+
(blockedUntil === undefined || persistedScopedBlockedUntil > blockedUntil)
|
|
1405
|
+
) {
|
|
1406
|
+
blockedUntil = persistedScopedBlockedUntil;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
return blockedUntil;
|
|
1333
1410
|
}
|
|
1334
1411
|
|
|
1335
1412
|
/** Checks if a credential is temporarily blocked due to usage limits. */
|
|
1336
1413
|
#isCredentialBlocked(
|
|
1414
|
+
provider: string,
|
|
1337
1415
|
providerKey: string,
|
|
1338
1416
|
credentialIndex: number,
|
|
1339
1417
|
blockScope: string | undefined = undefined,
|
|
1340
1418
|
): boolean {
|
|
1341
|
-
return this.#getCredentialBlockedUntil(providerKey, credentialIndex, blockScope) !== undefined;
|
|
1419
|
+
return this.#getCredentialBlockedUntil(provider, providerKey, credentialIndex, blockScope) !== undefined;
|
|
1342
1420
|
}
|
|
1343
1421
|
|
|
1344
1422
|
/** Marks a credential as blocked until the specified time. */
|
|
1345
1423
|
#markCredentialBlocked(
|
|
1424
|
+
provider: string,
|
|
1346
1425
|
providerKey: string,
|
|
1347
1426
|
credentialIndex: number,
|
|
1348
1427
|
blockedUntilMs: number,
|
|
@@ -1351,8 +1430,31 @@ export class AuthStorage {
|
|
|
1351
1430
|
const backoffKey = this.#toScopedBackoffKey(providerKey, blockScope);
|
|
1352
1431
|
const backoffMap = this.#credentialBackoff.get(backoffKey) ?? new Map<number, number>();
|
|
1353
1432
|
const existing = backoffMap.get(credentialIndex) ?? 0;
|
|
1354
|
-
|
|
1433
|
+
const nextBlockedUntil = Math.max(existing, blockedUntilMs);
|
|
1434
|
+
backoffMap.set(credentialIndex, nextBlockedUntil);
|
|
1355
1435
|
this.#credentialBackoff.set(backoffKey, backoffMap);
|
|
1436
|
+
|
|
1437
|
+
const upsertCredentialBlock = this.#store.upsertCredentialBlock?.bind(this.#store);
|
|
1438
|
+
if (!upsertCredentialBlock) return;
|
|
1439
|
+
const credentialId = this.#getStoredCredentials(provider)[credentialIndex]?.id;
|
|
1440
|
+
if (credentialId === undefined) return;
|
|
1441
|
+
try {
|
|
1442
|
+
upsertCredentialBlock({
|
|
1443
|
+
credentialId,
|
|
1444
|
+
providerKey,
|
|
1445
|
+
blockScope: blockScope ?? "",
|
|
1446
|
+
blockedUntilMs: nextBlockedUntil,
|
|
1447
|
+
});
|
|
1448
|
+
} catch (err) {
|
|
1449
|
+
logger.debug("Failed to persist credential block", {
|
|
1450
|
+
err,
|
|
1451
|
+
credentialId,
|
|
1452
|
+
provider,
|
|
1453
|
+
providerKey,
|
|
1454
|
+
blockScope,
|
|
1455
|
+
blockedUntilMs: nextBlockedUntil,
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1356
1458
|
}
|
|
1357
1459
|
|
|
1358
1460
|
/** Records which credential was used for a session (for rate-limit switching). */
|
|
@@ -1469,7 +1571,7 @@ export class AuthStorage {
|
|
|
1469
1571
|
|
|
1470
1572
|
for (const idx of order) {
|
|
1471
1573
|
const candidate = credentials[idx];
|
|
1472
|
-
if (!this.#isCredentialBlocked(providerKey, candidate.index)) {
|
|
1574
|
+
if (!this.#isCredentialBlocked(provider, providerKey, candidate.index)) {
|
|
1473
1575
|
return candidate;
|
|
1474
1576
|
}
|
|
1475
1577
|
}
|
|
@@ -2983,7 +3085,7 @@ export class AuthStorage {
|
|
|
2983
3085
|
}
|
|
2984
3086
|
}
|
|
2985
3087
|
|
|
2986
|
-
this.#markCredentialBlocked(providerKey, sessionCredential.index, blockedUntil, blockScope);
|
|
3088
|
+
this.#markCredentialBlocked(provider, providerKey, sessionCredential.index, blockedUntil, blockScope);
|
|
2987
3089
|
|
|
2988
3090
|
const remainingCredentials = this.#getCredentialsForProvider(provider)
|
|
2989
3091
|
.map((credential, index) => ({ credential, index }))
|
|
@@ -2994,7 +3096,12 @@ export class AuthStorage {
|
|
|
2994
3096
|
|
|
2995
3097
|
let retryAtMs: number | undefined;
|
|
2996
3098
|
for (const candidate of remainingCredentials) {
|
|
2997
|
-
const candidateBlockedUntil = this.#getCredentialBlockedUntil(
|
|
3099
|
+
const candidateBlockedUntil = this.#getCredentialBlockedUntil(
|
|
3100
|
+
provider,
|
|
3101
|
+
providerKey,
|
|
3102
|
+
candidate.index,
|
|
3103
|
+
blockScope,
|
|
3104
|
+
);
|
|
2998
3105
|
if (candidateBlockedUntil === undefined) return { switched: true };
|
|
2999
3106
|
if (retryAtMs === undefined || candidateBlockedUntil < retryAtMs) retryAtMs = candidateBlockedUntil;
|
|
3000
3107
|
}
|
|
@@ -3174,7 +3281,12 @@ export class AuthStorage {
|
|
|
3174
3281
|
args.order.map(async idx => {
|
|
3175
3282
|
const selection = args.credentials[idx];
|
|
3176
3283
|
if (!selection) return null;
|
|
3177
|
-
const blockedUntil = this.#getCredentialBlockedUntil(
|
|
3284
|
+
const blockedUntil = this.#getCredentialBlockedUntil(
|
|
3285
|
+
args.provider,
|
|
3286
|
+
args.providerKey,
|
|
3287
|
+
selection.index,
|
|
3288
|
+
args.blockScope,
|
|
3289
|
+
);
|
|
3178
3290
|
if (blockedUntil !== undefined) return { selection, usage: null, usageChecked: false, blockedUntil };
|
|
3179
3291
|
const usage = await this.#getUsageReport(args.provider, selection.credential, {
|
|
3180
3292
|
...args.options,
|
|
@@ -3211,7 +3323,13 @@ export class AuthStorage {
|
|
|
3211
3323
|
if (!blocked && scopedLimits && this.#isUsageLimitReached(scopedLimits)) {
|
|
3212
3324
|
const resetAtMs = this.#getUsageResetAtMs(scopedLimits, nowMs);
|
|
3213
3325
|
blockedUntil = resetAtMs ?? Date.now() + AuthStorage.#defaultBackoffMs;
|
|
3214
|
-
this.#markCredentialBlocked(
|
|
3326
|
+
this.#markCredentialBlocked(
|
|
3327
|
+
args.provider,
|
|
3328
|
+
args.providerKey,
|
|
3329
|
+
selection.index,
|
|
3330
|
+
blockedUntil,
|
|
3331
|
+
args.blockScope,
|
|
3332
|
+
);
|
|
3215
3333
|
blocked = true;
|
|
3216
3334
|
}
|
|
3217
3335
|
const windows = usage ? strategy.findWindowLimits(usage, args.rankingContext) : undefined;
|
|
@@ -3275,7 +3393,7 @@ export class AuthStorage {
|
|
|
3275
3393
|
// with the most headroom proactively and fall back intelligently when rate-limited.
|
|
3276
3394
|
const sessionPreferredIsAvailable =
|
|
3277
3395
|
sessionPreferredIndex !== undefined &&
|
|
3278
|
-
!this.#isCredentialBlocked(providerKey, sessionPreferredIndex, blockScope);
|
|
3396
|
+
!this.#isCredentialBlocked(provider, providerKey, sessionPreferredIndex, blockScope);
|
|
3279
3397
|
const shouldRank = checkUsage && (!sessionPreferredIsAvailable || requiresProModel);
|
|
3280
3398
|
const rankingOrder = shouldRank && sessionId ? credentials.map((_credential, index) => index) : order;
|
|
3281
3399
|
const candidates = shouldRank
|
|
@@ -3298,7 +3416,7 @@ export class AuthStorage {
|
|
|
3298
3416
|
if (sessionPreferredIndex !== undefined && !requiresProModel) {
|
|
3299
3417
|
const sessionPreferredCandidate = candidates.findIndex(
|
|
3300
3418
|
candidate =>
|
|
3301
|
-
!this.#isCredentialBlocked(providerKey, candidate.selection.index, blockScope) &&
|
|
3419
|
+
!this.#isCredentialBlocked(provider, providerKey, candidate.selection.index, blockScope) &&
|
|
3302
3420
|
candidate.selection.index === sessionPreferredIndex,
|
|
3303
3421
|
);
|
|
3304
3422
|
if (sessionPreferredCandidate > 0) {
|
|
@@ -3403,7 +3521,7 @@ export class AuthStorage {
|
|
|
3403
3521
|
if (resolved) return resolved;
|
|
3404
3522
|
}
|
|
3405
3523
|
|
|
3406
|
-
if (fallback && this.#isCredentialBlocked(providerKey, fallback.selection.index, blockScope)) {
|
|
3524
|
+
if (fallback && this.#isCredentialBlocked(provider, providerKey, fallback.selection.index, blockScope)) {
|
|
3407
3525
|
return this.#tryOAuthCredential(provider, fallback.selection, providerKey, sessionId, options, {
|
|
3408
3526
|
checkUsage,
|
|
3409
3527
|
allowBlocked: true,
|
|
@@ -3566,7 +3684,7 @@ export class AuthStorage {
|
|
|
3566
3684
|
blockScope,
|
|
3567
3685
|
allowFallback = true,
|
|
3568
3686
|
} = usageOptions;
|
|
3569
|
-
if (!allowBlocked && this.#isCredentialBlocked(providerKey, selection.index, blockScope)) {
|
|
3687
|
+
if (!allowBlocked && this.#isCredentialBlocked(provider, providerKey, selection.index, blockScope)) {
|
|
3570
3688
|
return undefined;
|
|
3571
3689
|
}
|
|
3572
3690
|
|
|
@@ -3603,6 +3721,7 @@ export class AuthStorage {
|
|
|
3603
3721
|
if (this.#isUsageLimitReached(scopedLimits)) {
|
|
3604
3722
|
const resetAtMs = this.#getUsageResetAtMs(scopedLimits, Date.now());
|
|
3605
3723
|
this.#markCredentialBlocked(
|
|
3724
|
+
provider,
|
|
3606
3725
|
providerKey,
|
|
3607
3726
|
selection.index,
|
|
3608
3727
|
resetAtMs ?? Date.now() + AuthStorage.#defaultBackoffMs,
|
|
@@ -3679,6 +3798,7 @@ export class AuthStorage {
|
|
|
3679
3798
|
if (this.#isUsageLimitReached(scopedLimits)) {
|
|
3680
3799
|
const resetAtMs = this.#getUsageResetAtMs(scopedLimits, Date.now());
|
|
3681
3800
|
this.#markCredentialBlocked(
|
|
3801
|
+
provider,
|
|
3682
3802
|
providerKey,
|
|
3683
3803
|
selection.index,
|
|
3684
3804
|
resetAtMs ?? Date.now() + AuthStorage.#defaultBackoffMs,
|
|
@@ -3755,7 +3875,7 @@ export class AuthStorage {
|
|
|
3755
3875
|
}
|
|
3756
3876
|
} else {
|
|
3757
3877
|
// Block temporarily for transient failures (5 minutes)
|
|
3758
|
-
this.#markCredentialBlocked(providerKey, selection.index, Date.now() + 5 * 60 * 1000);
|
|
3878
|
+
this.#markCredentialBlocked(provider, providerKey, selection.index, Date.now() + 5 * 60 * 1000);
|
|
3759
3879
|
}
|
|
3760
3880
|
}
|
|
3761
3881
|
|
|
@@ -4154,6 +4274,15 @@ export class AuthStorage {
|
|
|
4154
4274
|
* that `markUsageLimitReached` set for the now-obsolete reset time.
|
|
4155
4275
|
*/
|
|
4156
4276
|
#clearCredentialBlocks(provider: string, credentialId: number): void {
|
|
4277
|
+
const deleteCredentialBlocks = this.#store.deleteCredentialBlocks?.bind(this.#store);
|
|
4278
|
+
if (deleteCredentialBlocks) {
|
|
4279
|
+
try {
|
|
4280
|
+
deleteCredentialBlocks(credentialId);
|
|
4281
|
+
} catch (err) {
|
|
4282
|
+
logger.debug("Failed to clear persisted credential blocks", { err, provider, credentialId });
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
|
|
4157
4286
|
const index = this.#getStoredCredentials(provider).findIndex(entry => entry.id === credentialId);
|
|
4158
4287
|
if (index < 0) return;
|
|
4159
4288
|
const providerKey = this.#getProviderTypeKey(provider, "oauth");
|
|
@@ -4213,6 +4342,7 @@ export class AuthStorage {
|
|
|
4213
4342
|
|
|
4214
4343
|
this.#clearSessionCredential(provider, sessionId);
|
|
4215
4344
|
this.#markCredentialBlocked(
|
|
4345
|
+
provider,
|
|
4216
4346
|
this.#getProviderTypeKey(provider, matched.type),
|
|
4217
4347
|
matched.index,
|
|
4218
4348
|
Date.now() + AuthStorage.#defaultBackoffMs,
|
|
@@ -4275,11 +4405,16 @@ export class AuthStorage {
|
|
|
4275
4405
|
(credential, index) =>
|
|
4276
4406
|
credential.type === sessionCredential.type &&
|
|
4277
4407
|
index !== sessionCredential.index &&
|
|
4278
|
-
!this.#isCredentialBlocked(providerKey, index),
|
|
4408
|
+
!this.#isCredentialBlocked(provider, providerKey, index),
|
|
4279
4409
|
);
|
|
4280
4410
|
const target = this.#getStoredCredentials(provider)[sessionCredential.index];
|
|
4281
4411
|
this.#clearSessionCredential(provider, sessionId);
|
|
4282
|
-
this.#markCredentialBlocked(
|
|
4412
|
+
this.#markCredentialBlocked(
|
|
4413
|
+
provider,
|
|
4414
|
+
providerKey,
|
|
4415
|
+
sessionCredential.index,
|
|
4416
|
+
Date.now() + AuthStorage.#defaultBackoffMs,
|
|
4417
|
+
);
|
|
4283
4418
|
|
|
4284
4419
|
if (target) {
|
|
4285
4420
|
const markSuspect = this.#store.markCredentialSuspect?.bind(this.#store);
|
|
@@ -4505,6 +4640,33 @@ export class AuthStorage {
|
|
|
4505
4640
|
});
|
|
4506
4641
|
}
|
|
4507
4642
|
|
|
4643
|
+
/**
|
|
4644
|
+
* Broker-server seam: list non-expired persisted blocks for snapshot entries.
|
|
4645
|
+
*/
|
|
4646
|
+
listCredentialBlocks(credentialIds: readonly number[]): StoredCredentialBlock[] {
|
|
4647
|
+
return this.#store.listCredentialBlocks?.(credentialIds) ?? [];
|
|
4648
|
+
}
|
|
4649
|
+
|
|
4650
|
+
/**
|
|
4651
|
+
* Broker-server seam: persist one credential block and notify snapshot waiters.
|
|
4652
|
+
*/
|
|
4653
|
+
upsertCredentialBlock(block: StoredCredentialBlock): void {
|
|
4654
|
+
const upsertCredentialBlock = this.#store.upsertCredentialBlock?.bind(this.#store);
|
|
4655
|
+
if (!upsertCredentialBlock) return;
|
|
4656
|
+
upsertCredentialBlock(block);
|
|
4657
|
+
this.#bumpGeneration("credential-block");
|
|
4658
|
+
}
|
|
4659
|
+
|
|
4660
|
+
/**
|
|
4661
|
+
* Broker-server seam: clear all persisted blocks for one credential and notify snapshot waiters.
|
|
4662
|
+
*/
|
|
4663
|
+
deleteCredentialBlocks(credentialId: number): void {
|
|
4664
|
+
const deleteCredentialBlocks = this.#store.deleteCredentialBlocks?.bind(this.#store);
|
|
4665
|
+
if (!deleteCredentialBlocks) return;
|
|
4666
|
+
deleteCredentialBlocks(credentialId);
|
|
4667
|
+
this.#bumpGeneration("credential-block");
|
|
4668
|
+
}
|
|
4669
|
+
|
|
4508
4670
|
/**
|
|
4509
4671
|
* Describe where the active credential for a provider came from.
|
|
4510
4672
|
*
|
|
@@ -4570,13 +4732,20 @@ type AuthRow = {
|
|
|
4570
4732
|
identity_key: string | null;
|
|
4571
4733
|
};
|
|
4572
4734
|
|
|
4735
|
+
type CredentialBlockRow = {
|
|
4736
|
+
credential_id: number;
|
|
4737
|
+
provider_key: string;
|
|
4738
|
+
block_scope: string;
|
|
4739
|
+
blocked_until_ms: number;
|
|
4740
|
+
};
|
|
4741
|
+
|
|
4573
4742
|
type SerializedCredentialRecord = {
|
|
4574
4743
|
credentialType: AuthCredential["type"];
|
|
4575
4744
|
data: string;
|
|
4576
4745
|
identityKey: string | null;
|
|
4577
4746
|
};
|
|
4578
4747
|
|
|
4579
|
-
const AUTH_SCHEMA_VERSION =
|
|
4748
|
+
const AUTH_SCHEMA_VERSION = 5;
|
|
4580
4749
|
const SQLITE_NOW_EPOCH = "CAST(strftime('%s','now') AS INTEGER)";
|
|
4581
4750
|
|
|
4582
4751
|
/**
|
|
@@ -4776,6 +4945,11 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
4776
4945
|
#getCacheIncludingExpiredStmt: Statement;
|
|
4777
4946
|
#upsertCacheStmt: Statement;
|
|
4778
4947
|
#deleteExpiredCacheStmt: Statement;
|
|
4948
|
+
#getCredentialBlockStmt: Statement;
|
|
4949
|
+
#listCredentialBlocksByCredentialStmt: Statement;
|
|
4950
|
+
#upsertCredentialBlockStmt: Statement;
|
|
4951
|
+
#deleteCredentialBlocksStmt: Statement;
|
|
4952
|
+
#deleteExpiredCredentialBlocksStmt: Statement;
|
|
4779
4953
|
#insertUsageHistoryStmt: Statement;
|
|
4780
4954
|
#insertUsageCostStmt: Statement;
|
|
4781
4955
|
#listUsageCostsStmt: Statement;
|
|
@@ -4821,6 +4995,23 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
4821
4995
|
"INSERT INTO cache (key, value, expires_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, expires_at = excluded.expires_at",
|
|
4822
4996
|
);
|
|
4823
4997
|
this.#deleteExpiredCacheStmt = this.#db.prepare(`DELETE FROM cache WHERE expires_at <= ${SQLITE_NOW_EPOCH}`);
|
|
4998
|
+
this.#getCredentialBlockStmt = this.#db.prepare(
|
|
4999
|
+
"SELECT blocked_until_ms FROM auth_credential_blocks WHERE credential_id = ? AND provider_key = ? AND block_scope = ? AND blocked_until_ms > ?",
|
|
5000
|
+
);
|
|
5001
|
+
this.#listCredentialBlocksByCredentialStmt = this.#db.prepare(
|
|
5002
|
+
"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",
|
|
5003
|
+
);
|
|
5004
|
+
this.#upsertCredentialBlockStmt = this.#db.prepare(
|
|
5005
|
+
`INSERT INTO auth_credential_blocks (credential_id, provider_key, block_scope, blocked_until_ms, updated_at)
|
|
5006
|
+
VALUES (?, ?, ?, ?, ${SQLITE_NOW_EPOCH})
|
|
5007
|
+
ON CONFLICT(credential_id, provider_key, block_scope) DO UPDATE SET
|
|
5008
|
+
blocked_until_ms = MAX(blocked_until_ms, excluded.blocked_until_ms),
|
|
5009
|
+
updated_at = excluded.updated_at`,
|
|
5010
|
+
);
|
|
5011
|
+
this.#deleteCredentialBlocksStmt = this.#db.prepare("DELETE FROM auth_credential_blocks WHERE credential_id = ?");
|
|
5012
|
+
this.#deleteExpiredCredentialBlocksStmt = this.#db.prepare(
|
|
5013
|
+
"DELETE FROM auth_credential_blocks WHERE blocked_until_ms <= ?",
|
|
5014
|
+
);
|
|
4824
5015
|
this.#insertUsageHistoryStmt = this.#db.prepare(
|
|
4825
5016
|
"INSERT INTO usage_history (recorded_at, provider, account_key, email, account_id, limit_id, label, window_label, used_fraction, status, resets_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
4826
5017
|
);
|
|
@@ -4932,6 +5123,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
4932
5123
|
|
|
4933
5124
|
if (!this.#authCredentialsTableExists()) {
|
|
4934
5125
|
this.#createAuthCredentialsTable();
|
|
5126
|
+
this.#createAuthCredentialBlocksTable();
|
|
4935
5127
|
this.#writeAuthSchemaVersion(AUTH_SCHEMA_VERSION);
|
|
4936
5128
|
return;
|
|
4937
5129
|
}
|
|
@@ -4948,6 +5140,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
4948
5140
|
}
|
|
4949
5141
|
|
|
4950
5142
|
this.#createAuthCredentialIndexes();
|
|
5143
|
+
this.#createAuthCredentialBlocksTable();
|
|
4951
5144
|
this.#backfillCredentialIdentityKeys();
|
|
4952
5145
|
// Rewriting an already-current version row is a no-op write transaction
|
|
4953
5146
|
// on every boot; only persist when the recorded version actually changes.
|
|
@@ -5031,6 +5224,20 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
5031
5224
|
`);
|
|
5032
5225
|
}
|
|
5033
5226
|
|
|
5227
|
+
#createAuthCredentialBlocksTable(): void {
|
|
5228
|
+
this.#db.run(`
|
|
5229
|
+
CREATE TABLE IF NOT EXISTS auth_credential_blocks (
|
|
5230
|
+
credential_id INTEGER NOT NULL,
|
|
5231
|
+
provider_key TEXT NOT NULL,
|
|
5232
|
+
block_scope TEXT NOT NULL DEFAULT '',
|
|
5233
|
+
blocked_until_ms INTEGER NOT NULL,
|
|
5234
|
+
updated_at INTEGER NOT NULL,
|
|
5235
|
+
PRIMARY KEY (credential_id, provider_key, block_scope)
|
|
5236
|
+
);
|
|
5237
|
+
CREATE INDEX IF NOT EXISTS idx_auth_credential_blocks_expires ON auth_credential_blocks(blocked_until_ms);
|
|
5238
|
+
`);
|
|
5239
|
+
}
|
|
5240
|
+
|
|
5034
5241
|
#migrateAuthSchema(fromVersion: number): void {
|
|
5035
5242
|
if (fromVersion < 1) {
|
|
5036
5243
|
this.#migrateAuthSchemaV0ToV1();
|
|
@@ -5041,6 +5248,9 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
5041
5248
|
if (fromVersion < 4) {
|
|
5042
5249
|
this.#migrateAuthSchemaV3ToV4();
|
|
5043
5250
|
}
|
|
5251
|
+
if (fromVersion < 5) {
|
|
5252
|
+
this.#migrateAuthSchemaV4ToV5();
|
|
5253
|
+
}
|
|
5044
5254
|
}
|
|
5045
5255
|
|
|
5046
5256
|
#migrateAuthSchemaV0ToV1(): void {
|
|
@@ -5127,6 +5337,13 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
5127
5337
|
migrate();
|
|
5128
5338
|
}
|
|
5129
5339
|
|
|
5340
|
+
#migrateAuthSchemaV4ToV5(): void {
|
|
5341
|
+
const migrate = this.#db.transaction(() => {
|
|
5342
|
+
this.#createAuthCredentialBlocksTable();
|
|
5343
|
+
});
|
|
5344
|
+
migrate();
|
|
5345
|
+
}
|
|
5346
|
+
|
|
5130
5347
|
#backfillCredentialIdentityKeys(): void {
|
|
5131
5348
|
const selectRowsStmt = this.#db.prepare(
|
|
5132
5349
|
"SELECT id, provider, credential_type, data, disabled_cause, identity_key FROM auth_credentials WHERE identity_key IS NULL ORDER BY id ASC",
|
|
@@ -5392,6 +5609,54 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
5392
5609
|
}
|
|
5393
5610
|
}
|
|
5394
5611
|
|
|
5612
|
+
getCredentialBlock(credentialId: number, providerKey: string, blockScope: string): number | undefined {
|
|
5613
|
+
const nowMs = Date.now();
|
|
5614
|
+
this.#deleteExpiredCredentialBlocksStmt.run(nowMs);
|
|
5615
|
+
const row = this.#getCredentialBlockStmt.get(credentialId, providerKey, blockScope, nowMs) as
|
|
5616
|
+
| { blocked_until_ms?: number }
|
|
5617
|
+
| undefined;
|
|
5618
|
+
return typeof row?.blocked_until_ms === "number" ? row.blocked_until_ms : undefined;
|
|
5619
|
+
}
|
|
5620
|
+
|
|
5621
|
+
upsertCredentialBlock(block: StoredCredentialBlock): void {
|
|
5622
|
+
this.#upsertCredentialBlockStmt.run(
|
|
5623
|
+
block.credentialId,
|
|
5624
|
+
block.providerKey,
|
|
5625
|
+
block.blockScope,
|
|
5626
|
+
block.blockedUntilMs,
|
|
5627
|
+
);
|
|
5628
|
+
}
|
|
5629
|
+
|
|
5630
|
+
deleteCredentialBlocks(credentialId: number): void {
|
|
5631
|
+
this.#deleteCredentialBlocksStmt.run(credentialId);
|
|
5632
|
+
}
|
|
5633
|
+
|
|
5634
|
+
cleanExpiredCredentialBlocks(nowMs: number): void {
|
|
5635
|
+
this.#deleteExpiredCredentialBlocksStmt.run(nowMs);
|
|
5636
|
+
}
|
|
5637
|
+
|
|
5638
|
+
listCredentialBlocks(credentialIds: readonly number[]): StoredCredentialBlock[] {
|
|
5639
|
+
if (credentialIds.length === 0) return [];
|
|
5640
|
+
const nowMs = Date.now();
|
|
5641
|
+
this.cleanExpiredCredentialBlocks(nowMs);
|
|
5642
|
+
const seenCredentialIds = new Set<number>();
|
|
5643
|
+
const blocks: StoredCredentialBlock[] = [];
|
|
5644
|
+
for (const credentialId of credentialIds) {
|
|
5645
|
+
if (seenCredentialIds.has(credentialId)) continue;
|
|
5646
|
+
seenCredentialIds.add(credentialId);
|
|
5647
|
+
const rows = this.#listCredentialBlocksByCredentialStmt.all(credentialId, nowMs) as CredentialBlockRow[];
|
|
5648
|
+
for (const row of rows) {
|
|
5649
|
+
blocks.push({
|
|
5650
|
+
credentialId: row.credential_id,
|
|
5651
|
+
providerKey: row.provider_key,
|
|
5652
|
+
blockScope: row.block_scope,
|
|
5653
|
+
blockedUntilMs: row.blocked_until_ms,
|
|
5654
|
+
});
|
|
5655
|
+
}
|
|
5656
|
+
}
|
|
5657
|
+
return blocks;
|
|
5658
|
+
}
|
|
5659
|
+
|
|
5395
5660
|
recordUsageSnapshots(entries: UsageHistoryEntry[]): void {
|
|
5396
5661
|
try {
|
|
5397
5662
|
for (const entry of entries) {
|
|
@@ -5585,6 +5850,11 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
5585
5850
|
this.#getCacheIncludingExpiredStmt.finalize();
|
|
5586
5851
|
this.#upsertCacheStmt.finalize();
|
|
5587
5852
|
this.#deleteExpiredCacheStmt.finalize();
|
|
5853
|
+
this.#getCredentialBlockStmt.finalize();
|
|
5854
|
+
this.#listCredentialBlocksByCredentialStmt.finalize();
|
|
5855
|
+
this.#upsertCredentialBlockStmt.finalize();
|
|
5856
|
+
this.#deleteCredentialBlocksStmt.finalize();
|
|
5857
|
+
this.#deleteExpiredCredentialBlocksStmt.finalize();
|
|
5588
5858
|
this.#insertUsageHistoryStmt.finalize();
|
|
5589
5859
|
this.#lastUsageHistoryStmt.finalize();
|
|
5590
5860
|
this.#listUsageHistoryStmt.finalize();
|
package/src/providers/ollama.ts
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
getOpenAIStreamFirstEventTimeoutMs,
|
|
24
24
|
getOpenAIStreamIdleTimeoutMs,
|
|
25
25
|
} from "../utils/idle-iterator";
|
|
26
|
-
import { toolWireSchema } from "../utils/schema
|
|
26
|
+
import { sanitizeSchemaForOllama, toolWireSchema } from "../utils/schema";
|
|
27
27
|
import {
|
|
28
28
|
getStreamMarkupHealingPattern,
|
|
29
29
|
type HealedToolCall,
|
|
@@ -280,7 +280,7 @@ function convertTools(tools: Tool[] | undefined): OllamaFunctionTool[] | undefin
|
|
|
280
280
|
function: {
|
|
281
281
|
name: tool.name,
|
|
282
282
|
description: tool.description,
|
|
283
|
-
parameters: toolWireSchema(tool),
|
|
283
|
+
parameters: sanitizeSchemaForOllama(toolWireSchema(tool)),
|
|
284
284
|
},
|
|
285
285
|
}));
|
|
286
286
|
}
|
package/src/types.ts
CHANGED
|
@@ -647,6 +647,23 @@ export interface DeveloperMessage {
|
|
|
647
647
|
timestamp: number; // Unix timestamp in milliseconds
|
|
648
648
|
}
|
|
649
649
|
|
|
650
|
+
export type AssistantRetryRecoveryKind = "credential" | "model" | "wait" | "plain";
|
|
651
|
+
|
|
652
|
+
export interface AssistantRetryRecovery {
|
|
653
|
+
kind: "auto-retry";
|
|
654
|
+
status: "recovered";
|
|
655
|
+
attempt: number;
|
|
656
|
+
recoveredAt: string;
|
|
657
|
+
recovery: AssistantRetryRecoveryKind;
|
|
658
|
+
note: string;
|
|
659
|
+
supersededBy?: {
|
|
660
|
+
timestamp: number;
|
|
661
|
+
responseId?: string;
|
|
662
|
+
provider: string;
|
|
663
|
+
model: string;
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
650
667
|
export interface ContextSnapshot {
|
|
651
668
|
promptTokens: number; // authoritative provider prompt/input tokens
|
|
652
669
|
nonMessageTokens: number; // estimated non-message total at send time
|
|
@@ -660,6 +677,7 @@ export interface AssistantMessage {
|
|
|
660
677
|
provider: Provider;
|
|
661
678
|
model: string;
|
|
662
679
|
contextSnapshot?: ContextSnapshot;
|
|
680
|
+
retryRecovery?: AssistantRetryRecovery;
|
|
663
681
|
responseId?: string; // Provider-specific response/message identifier when the upstream API exposes one
|
|
664
682
|
/**
|
|
665
683
|
* Name of the upstream provider an aggregator routed this request to, as
|
package/src/usage/claude.ts
CHANGED
|
@@ -615,20 +615,40 @@ function scopeClaudeLimitsForModel(report: UsageReport, context: CredentialRanki
|
|
|
615
615
|
}
|
|
616
616
|
|
|
617
617
|
/**
|
|
618
|
-
*
|
|
619
|
-
* (
|
|
620
|
-
*
|
|
621
|
-
*
|
|
618
|
+
* A Fable/Mythos weekly row is trusted for gating only at full exhaustion
|
|
619
|
+
* (server `exhausted` status or used fraction >= 1) with a live reset
|
|
620
|
+
* timestamp. Anything below that stays untrusted: the counters are
|
|
621
|
+
* notoriously unreliable short of the cap (they report high utilization
|
|
622
|
+
* while the account can still serve requests).
|
|
623
|
+
*/
|
|
624
|
+
function isConfirmedExhaustedTierRow(limit: UsageLimit, nowMs: number): boolean {
|
|
625
|
+
const resetsAt = limit.window?.resetsAt;
|
|
626
|
+
if (typeof resetsAt !== "number" || !Number.isFinite(resetsAt) || resetsAt <= nowMs) return false;
|
|
627
|
+
if (limit.status === "exhausted") return true;
|
|
628
|
+
const fraction = resolveUsedFraction(limit);
|
|
629
|
+
return typeof fraction === "number" && fraction >= 1;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Scope limits for proactive hard-blocking (gating). Fable and Mythos tier
|
|
634
|
+
* weekly caps participate only when {@link isConfirmedExhaustedTierRow}
|
|
635
|
+
* confirms them, so a confirmed-dead account is skipped up front and a
|
|
636
|
+
* reactive 429 block extends to the tier reset in markUsageLimitReached,
|
|
637
|
+
* while unconfirmed rows remain ranking pressure only via
|
|
638
|
+
* scopeClaudeLimitsForModel.
|
|
622
639
|
*/
|
|
623
640
|
function scopeClaudeLimitsForModelHardBlock(
|
|
624
641
|
report: UsageReport,
|
|
625
642
|
context: CredentialRankingContext | undefined,
|
|
626
643
|
): UsageLimit[] {
|
|
627
644
|
const kind = getClaudeModelKind(context);
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
645
|
+
const requireConfirmedTierRow = kind === "fable" || kind === "mythos";
|
|
646
|
+
const nowMs = Date.now();
|
|
647
|
+
return report.limits.filter(limit => {
|
|
648
|
+
if (limit.scope.shared === true) return true;
|
|
649
|
+
if (kind === undefined || limit.scope.tier !== kind) return false;
|
|
650
|
+
return !requireConfirmedTierRow || isConfirmedExhaustedTierRow(limit, nowMs);
|
|
651
|
+
});
|
|
632
652
|
}
|
|
633
653
|
|
|
634
654
|
function rankingUsedFraction(limit: UsageLimit): number {
|