@oh-my-pi/pi-ai 16.3.4 → 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 +22 -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 +40 -5
- package/dist/types/registry/oauth/types.d.ts +14 -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 +300 -24
- package/src/providers/ollama.ts +2 -2
- package/src/providers/openai-codex-responses.ts +6 -1
- package/src/providers/openai-completions.ts +12 -1
- package/src/providers/openai-responses.ts +10 -1
- package/src/registry/oauth/callback-server.ts +97 -9
- package/src/registry/oauth/types.ts +14 -0
- package/src/types.ts +18 -0
- package/src/usage/claude.ts +28 -8
- package/src/utils/schema/CONSTRAINTS.md +1 -0
- package/src/utils/schema/normalize.ts +131 -0
- package/src/utils/validation.ts +295 -0
package/src/auth-storage.ts
CHANGED
|
@@ -16,7 +16,13 @@ import * as AIError from "./error";
|
|
|
16
16
|
import { isUsageLimitOutcome } from "./error/rate-limit";
|
|
17
17
|
import { getProviderDefinition, PASTE_CODE_LOGIN_PROVIDERS } from "./registry";
|
|
18
18
|
import { getOAuthApiKey, getOAuthProvider, refreshOAuthToken } from "./registry/oauth";
|
|
19
|
-
import type {
|
|
19
|
+
import type {
|
|
20
|
+
OAuthAuthInfo,
|
|
21
|
+
OAuthController,
|
|
22
|
+
OAuthCredentials,
|
|
23
|
+
OAuthProvider,
|
|
24
|
+
OAuthProviderId,
|
|
25
|
+
} from "./registry/oauth/types";
|
|
20
26
|
import { getEnvApiKey, getEnvApiKeyName } from "./stream";
|
|
21
27
|
import type { Provider } from "./types";
|
|
22
28
|
import type {
|
|
@@ -117,6 +123,18 @@ export interface StoredAuthCredential {
|
|
|
117
123
|
disabledCause: string | null;
|
|
118
124
|
}
|
|
119
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
|
+
|
|
120
138
|
/**
|
|
121
139
|
* Per-credential health record returned by {@link AuthStorage.checkCredentials}.
|
|
122
140
|
*
|
|
@@ -299,6 +317,16 @@ export interface AuthCredentialStore {
|
|
|
299
317
|
getCache(key: string, options?: { includeExpired?: boolean }): string | null;
|
|
300
318
|
setCache(key: string, value: string, expiresAtSec: number): void;
|
|
301
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[];
|
|
302
330
|
/**
|
|
303
331
|
* Append usage-limit snapshots for trend history. Optional: stores without
|
|
304
332
|
* durable storage (e.g. the broker remote store) omit it and recording is
|
|
@@ -981,6 +1009,11 @@ export class AuthStorage {
|
|
|
981
1009
|
} catch {
|
|
982
1010
|
// Best-effort.
|
|
983
1011
|
}
|
|
1012
|
+
try {
|
|
1013
|
+
this.#store.cleanExpiredCredentialBlocks?.(Date.now());
|
|
1014
|
+
} catch {
|
|
1015
|
+
// Best-effort.
|
|
1016
|
+
}
|
|
984
1017
|
this.#usageFetch = options.usageFetch ?? fetch;
|
|
985
1018
|
this.#usageRequestTimeoutMs = options.usageRequestTimeoutMs ?? DEFAULT_USAGE_REQUEST_TIMEOUT_MS;
|
|
986
1019
|
this.#refreshOAuthCredentialOverride = options.refreshOAuthCredential;
|
|
@@ -1299,13 +1332,13 @@ export class AuthStorage {
|
|
|
1299
1332
|
return blockScope ? `${providerKey}\0${blockScope}` : providerKey;
|
|
1300
1333
|
}
|
|
1301
1334
|
|
|
1302
|
-
/** Returns block expiry timestamp for a credential/key pair, cleaning up expired entries. */
|
|
1303
|
-
#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 {
|
|
1304
1337
|
const backoffMap = this.#credentialBackoff.get(backoffKey);
|
|
1305
1338
|
if (!backoffMap) return undefined;
|
|
1306
1339
|
const blockedUntil = backoffMap.get(credentialIndex);
|
|
1307
1340
|
if (!blockedUntil) return undefined;
|
|
1308
|
-
if (blockedUntil <=
|
|
1341
|
+
if (blockedUntil <= nowMs) {
|
|
1309
1342
|
backoffMap.delete(credentialIndex);
|
|
1310
1343
|
if (backoffMap.size === 0) {
|
|
1311
1344
|
this.#credentialBackoff.delete(backoffKey);
|
|
@@ -1315,28 +1348,80 @@ export class AuthStorage {
|
|
|
1315
1348
|
return blockedUntil;
|
|
1316
1349
|
}
|
|
1317
1350
|
|
|
1318
|
-
|
|
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. */
|
|
1319
1372
|
#getCredentialBlockedUntil(
|
|
1373
|
+
provider: string,
|
|
1320
1374
|
providerKey: string,
|
|
1321
1375
|
credentialIndex: number,
|
|
1322
1376
|
blockScope: string | undefined = undefined,
|
|
1323
1377
|
): number | undefined {
|
|
1324
|
-
const
|
|
1325
|
-
|
|
1326
|
-
|
|
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;
|
|
1327
1410
|
}
|
|
1328
1411
|
|
|
1329
1412
|
/** Checks if a credential is temporarily blocked due to usage limits. */
|
|
1330
1413
|
#isCredentialBlocked(
|
|
1414
|
+
provider: string,
|
|
1331
1415
|
providerKey: string,
|
|
1332
1416
|
credentialIndex: number,
|
|
1333
1417
|
blockScope: string | undefined = undefined,
|
|
1334
1418
|
): boolean {
|
|
1335
|
-
return this.#getCredentialBlockedUntil(providerKey, credentialIndex, blockScope) !== undefined;
|
|
1419
|
+
return this.#getCredentialBlockedUntil(provider, providerKey, credentialIndex, blockScope) !== undefined;
|
|
1336
1420
|
}
|
|
1337
1421
|
|
|
1338
1422
|
/** Marks a credential as blocked until the specified time. */
|
|
1339
1423
|
#markCredentialBlocked(
|
|
1424
|
+
provider: string,
|
|
1340
1425
|
providerKey: string,
|
|
1341
1426
|
credentialIndex: number,
|
|
1342
1427
|
blockedUntilMs: number,
|
|
@@ -1345,8 +1430,31 @@ export class AuthStorage {
|
|
|
1345
1430
|
const backoffKey = this.#toScopedBackoffKey(providerKey, blockScope);
|
|
1346
1431
|
const backoffMap = this.#credentialBackoff.get(backoffKey) ?? new Map<number, number>();
|
|
1347
1432
|
const existing = backoffMap.get(credentialIndex) ?? 0;
|
|
1348
|
-
|
|
1433
|
+
const nextBlockedUntil = Math.max(existing, blockedUntilMs);
|
|
1434
|
+
backoffMap.set(credentialIndex, nextBlockedUntil);
|
|
1349
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
|
+
}
|
|
1350
1458
|
}
|
|
1351
1459
|
|
|
1352
1460
|
/** Records which credential was used for a session (for rate-limit switching). */
|
|
@@ -1463,7 +1571,7 @@ export class AuthStorage {
|
|
|
1463
1571
|
|
|
1464
1572
|
for (const idx of order) {
|
|
1465
1573
|
const candidate = credentials[idx];
|
|
1466
|
-
if (!this.#isCredentialBlocked(providerKey, candidate.index)) {
|
|
1574
|
+
if (!this.#isCredentialBlocked(provider, providerKey, candidate.index)) {
|
|
1467
1575
|
return candidate;
|
|
1468
1576
|
}
|
|
1469
1577
|
}
|
|
@@ -1863,7 +1971,7 @@ export class AuthStorage {
|
|
|
1863
1971
|
provider: OAuthProviderId,
|
|
1864
1972
|
ctrl: OAuthController & {
|
|
1865
1973
|
/** onAuth is required by auth-storage but optional in OAuthController */
|
|
1866
|
-
onAuth: (info:
|
|
1974
|
+
onAuth: (info: OAuthAuthInfo) => void;
|
|
1867
1975
|
/** onPrompt is required for some providers (github-copilot, openai-codex) */
|
|
1868
1976
|
onPrompt: (prompt: { message: string; placeholder?: string }) => Promise<string>;
|
|
1869
1977
|
},
|
|
@@ -2977,7 +3085,7 @@ export class AuthStorage {
|
|
|
2977
3085
|
}
|
|
2978
3086
|
}
|
|
2979
3087
|
|
|
2980
|
-
this.#markCredentialBlocked(providerKey, sessionCredential.index, blockedUntil, blockScope);
|
|
3088
|
+
this.#markCredentialBlocked(provider, providerKey, sessionCredential.index, blockedUntil, blockScope);
|
|
2981
3089
|
|
|
2982
3090
|
const remainingCredentials = this.#getCredentialsForProvider(provider)
|
|
2983
3091
|
.map((credential, index) => ({ credential, index }))
|
|
@@ -2988,7 +3096,12 @@ export class AuthStorage {
|
|
|
2988
3096
|
|
|
2989
3097
|
let retryAtMs: number | undefined;
|
|
2990
3098
|
for (const candidate of remainingCredentials) {
|
|
2991
|
-
const candidateBlockedUntil = this.#getCredentialBlockedUntil(
|
|
3099
|
+
const candidateBlockedUntil = this.#getCredentialBlockedUntil(
|
|
3100
|
+
provider,
|
|
3101
|
+
providerKey,
|
|
3102
|
+
candidate.index,
|
|
3103
|
+
blockScope,
|
|
3104
|
+
);
|
|
2992
3105
|
if (candidateBlockedUntil === undefined) return { switched: true };
|
|
2993
3106
|
if (retryAtMs === undefined || candidateBlockedUntil < retryAtMs) retryAtMs = candidateBlockedUntil;
|
|
2994
3107
|
}
|
|
@@ -3168,7 +3281,12 @@ export class AuthStorage {
|
|
|
3168
3281
|
args.order.map(async idx => {
|
|
3169
3282
|
const selection = args.credentials[idx];
|
|
3170
3283
|
if (!selection) return null;
|
|
3171
|
-
const blockedUntil = this.#getCredentialBlockedUntil(
|
|
3284
|
+
const blockedUntil = this.#getCredentialBlockedUntil(
|
|
3285
|
+
args.provider,
|
|
3286
|
+
args.providerKey,
|
|
3287
|
+
selection.index,
|
|
3288
|
+
args.blockScope,
|
|
3289
|
+
);
|
|
3172
3290
|
if (blockedUntil !== undefined) return { selection, usage: null, usageChecked: false, blockedUntil };
|
|
3173
3291
|
const usage = await this.#getUsageReport(args.provider, selection.credential, {
|
|
3174
3292
|
...args.options,
|
|
@@ -3205,7 +3323,13 @@ export class AuthStorage {
|
|
|
3205
3323
|
if (!blocked && scopedLimits && this.#isUsageLimitReached(scopedLimits)) {
|
|
3206
3324
|
const resetAtMs = this.#getUsageResetAtMs(scopedLimits, nowMs);
|
|
3207
3325
|
blockedUntil = resetAtMs ?? Date.now() + AuthStorage.#defaultBackoffMs;
|
|
3208
|
-
this.#markCredentialBlocked(
|
|
3326
|
+
this.#markCredentialBlocked(
|
|
3327
|
+
args.provider,
|
|
3328
|
+
args.providerKey,
|
|
3329
|
+
selection.index,
|
|
3330
|
+
blockedUntil,
|
|
3331
|
+
args.blockScope,
|
|
3332
|
+
);
|
|
3209
3333
|
blocked = true;
|
|
3210
3334
|
}
|
|
3211
3335
|
const windows = usage ? strategy.findWindowLimits(usage, args.rankingContext) : undefined;
|
|
@@ -3269,7 +3393,7 @@ export class AuthStorage {
|
|
|
3269
3393
|
// with the most headroom proactively and fall back intelligently when rate-limited.
|
|
3270
3394
|
const sessionPreferredIsAvailable =
|
|
3271
3395
|
sessionPreferredIndex !== undefined &&
|
|
3272
|
-
!this.#isCredentialBlocked(providerKey, sessionPreferredIndex, blockScope);
|
|
3396
|
+
!this.#isCredentialBlocked(provider, providerKey, sessionPreferredIndex, blockScope);
|
|
3273
3397
|
const shouldRank = checkUsage && (!sessionPreferredIsAvailable || requiresProModel);
|
|
3274
3398
|
const rankingOrder = shouldRank && sessionId ? credentials.map((_credential, index) => index) : order;
|
|
3275
3399
|
const candidates = shouldRank
|
|
@@ -3292,7 +3416,7 @@ export class AuthStorage {
|
|
|
3292
3416
|
if (sessionPreferredIndex !== undefined && !requiresProModel) {
|
|
3293
3417
|
const sessionPreferredCandidate = candidates.findIndex(
|
|
3294
3418
|
candidate =>
|
|
3295
|
-
!this.#isCredentialBlocked(providerKey, candidate.selection.index, blockScope) &&
|
|
3419
|
+
!this.#isCredentialBlocked(provider, providerKey, candidate.selection.index, blockScope) &&
|
|
3296
3420
|
candidate.selection.index === sessionPreferredIndex,
|
|
3297
3421
|
);
|
|
3298
3422
|
if (sessionPreferredCandidate > 0) {
|
|
@@ -3397,7 +3521,7 @@ export class AuthStorage {
|
|
|
3397
3521
|
if (resolved) return resolved;
|
|
3398
3522
|
}
|
|
3399
3523
|
|
|
3400
|
-
if (fallback && this.#isCredentialBlocked(providerKey, fallback.selection.index, blockScope)) {
|
|
3524
|
+
if (fallback && this.#isCredentialBlocked(provider, providerKey, fallback.selection.index, blockScope)) {
|
|
3401
3525
|
return this.#tryOAuthCredential(provider, fallback.selection, providerKey, sessionId, options, {
|
|
3402
3526
|
checkUsage,
|
|
3403
3527
|
allowBlocked: true,
|
|
@@ -3560,7 +3684,7 @@ export class AuthStorage {
|
|
|
3560
3684
|
blockScope,
|
|
3561
3685
|
allowFallback = true,
|
|
3562
3686
|
} = usageOptions;
|
|
3563
|
-
if (!allowBlocked && this.#isCredentialBlocked(providerKey, selection.index, blockScope)) {
|
|
3687
|
+
if (!allowBlocked && this.#isCredentialBlocked(provider, providerKey, selection.index, blockScope)) {
|
|
3564
3688
|
return undefined;
|
|
3565
3689
|
}
|
|
3566
3690
|
|
|
@@ -3597,6 +3721,7 @@ export class AuthStorage {
|
|
|
3597
3721
|
if (this.#isUsageLimitReached(scopedLimits)) {
|
|
3598
3722
|
const resetAtMs = this.#getUsageResetAtMs(scopedLimits, Date.now());
|
|
3599
3723
|
this.#markCredentialBlocked(
|
|
3724
|
+
provider,
|
|
3600
3725
|
providerKey,
|
|
3601
3726
|
selection.index,
|
|
3602
3727
|
resetAtMs ?? Date.now() + AuthStorage.#defaultBackoffMs,
|
|
@@ -3673,6 +3798,7 @@ export class AuthStorage {
|
|
|
3673
3798
|
if (this.#isUsageLimitReached(scopedLimits)) {
|
|
3674
3799
|
const resetAtMs = this.#getUsageResetAtMs(scopedLimits, Date.now());
|
|
3675
3800
|
this.#markCredentialBlocked(
|
|
3801
|
+
provider,
|
|
3676
3802
|
providerKey,
|
|
3677
3803
|
selection.index,
|
|
3678
3804
|
resetAtMs ?? Date.now() + AuthStorage.#defaultBackoffMs,
|
|
@@ -3749,7 +3875,7 @@ export class AuthStorage {
|
|
|
3749
3875
|
}
|
|
3750
3876
|
} else {
|
|
3751
3877
|
// Block temporarily for transient failures (5 minutes)
|
|
3752
|
-
this.#markCredentialBlocked(providerKey, selection.index, Date.now() + 5 * 60 * 1000);
|
|
3878
|
+
this.#markCredentialBlocked(provider, providerKey, selection.index, Date.now() + 5 * 60 * 1000);
|
|
3753
3879
|
}
|
|
3754
3880
|
}
|
|
3755
3881
|
|
|
@@ -4148,6 +4274,15 @@ export class AuthStorage {
|
|
|
4148
4274
|
* that `markUsageLimitReached` set for the now-obsolete reset time.
|
|
4149
4275
|
*/
|
|
4150
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
|
+
|
|
4151
4286
|
const index = this.#getStoredCredentials(provider).findIndex(entry => entry.id === credentialId);
|
|
4152
4287
|
if (index < 0) return;
|
|
4153
4288
|
const providerKey = this.#getProviderTypeKey(provider, "oauth");
|
|
@@ -4207,6 +4342,7 @@ export class AuthStorage {
|
|
|
4207
4342
|
|
|
4208
4343
|
this.#clearSessionCredential(provider, sessionId);
|
|
4209
4344
|
this.#markCredentialBlocked(
|
|
4345
|
+
provider,
|
|
4210
4346
|
this.#getProviderTypeKey(provider, matched.type),
|
|
4211
4347
|
matched.index,
|
|
4212
4348
|
Date.now() + AuthStorage.#defaultBackoffMs,
|
|
@@ -4269,11 +4405,16 @@ export class AuthStorage {
|
|
|
4269
4405
|
(credential, index) =>
|
|
4270
4406
|
credential.type === sessionCredential.type &&
|
|
4271
4407
|
index !== sessionCredential.index &&
|
|
4272
|
-
!this.#isCredentialBlocked(providerKey, index),
|
|
4408
|
+
!this.#isCredentialBlocked(provider, providerKey, index),
|
|
4273
4409
|
);
|
|
4274
4410
|
const target = this.#getStoredCredentials(provider)[sessionCredential.index];
|
|
4275
4411
|
this.#clearSessionCredential(provider, sessionId);
|
|
4276
|
-
this.#markCredentialBlocked(
|
|
4412
|
+
this.#markCredentialBlocked(
|
|
4413
|
+
provider,
|
|
4414
|
+
providerKey,
|
|
4415
|
+
sessionCredential.index,
|
|
4416
|
+
Date.now() + AuthStorage.#defaultBackoffMs,
|
|
4417
|
+
);
|
|
4277
4418
|
|
|
4278
4419
|
if (target) {
|
|
4279
4420
|
const markSuspect = this.#store.markCredentialSuspect?.bind(this.#store);
|
|
@@ -4499,6 +4640,33 @@ export class AuthStorage {
|
|
|
4499
4640
|
});
|
|
4500
4641
|
}
|
|
4501
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
|
+
|
|
4502
4670
|
/**
|
|
4503
4671
|
* Describe where the active credential for a provider came from.
|
|
4504
4672
|
*
|
|
@@ -4564,13 +4732,20 @@ type AuthRow = {
|
|
|
4564
4732
|
identity_key: string | null;
|
|
4565
4733
|
};
|
|
4566
4734
|
|
|
4735
|
+
type CredentialBlockRow = {
|
|
4736
|
+
credential_id: number;
|
|
4737
|
+
provider_key: string;
|
|
4738
|
+
block_scope: string;
|
|
4739
|
+
blocked_until_ms: number;
|
|
4740
|
+
};
|
|
4741
|
+
|
|
4567
4742
|
type SerializedCredentialRecord = {
|
|
4568
4743
|
credentialType: AuthCredential["type"];
|
|
4569
4744
|
data: string;
|
|
4570
4745
|
identityKey: string | null;
|
|
4571
4746
|
};
|
|
4572
4747
|
|
|
4573
|
-
const AUTH_SCHEMA_VERSION =
|
|
4748
|
+
const AUTH_SCHEMA_VERSION = 5;
|
|
4574
4749
|
const SQLITE_NOW_EPOCH = "CAST(strftime('%s','now') AS INTEGER)";
|
|
4575
4750
|
|
|
4576
4751
|
/**
|
|
@@ -4770,6 +4945,11 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
4770
4945
|
#getCacheIncludingExpiredStmt: Statement;
|
|
4771
4946
|
#upsertCacheStmt: Statement;
|
|
4772
4947
|
#deleteExpiredCacheStmt: Statement;
|
|
4948
|
+
#getCredentialBlockStmt: Statement;
|
|
4949
|
+
#listCredentialBlocksByCredentialStmt: Statement;
|
|
4950
|
+
#upsertCredentialBlockStmt: Statement;
|
|
4951
|
+
#deleteCredentialBlocksStmt: Statement;
|
|
4952
|
+
#deleteExpiredCredentialBlocksStmt: Statement;
|
|
4773
4953
|
#insertUsageHistoryStmt: Statement;
|
|
4774
4954
|
#insertUsageCostStmt: Statement;
|
|
4775
4955
|
#listUsageCostsStmt: Statement;
|
|
@@ -4815,6 +4995,23 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
4815
4995
|
"INSERT INTO cache (key, value, expires_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, expires_at = excluded.expires_at",
|
|
4816
4996
|
);
|
|
4817
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
|
+
);
|
|
4818
5015
|
this.#insertUsageHistoryStmt = this.#db.prepare(
|
|
4819
5016
|
"INSERT INTO usage_history (recorded_at, provider, account_key, email, account_id, limit_id, label, window_label, used_fraction, status, resets_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
4820
5017
|
);
|
|
@@ -4926,6 +5123,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
4926
5123
|
|
|
4927
5124
|
if (!this.#authCredentialsTableExists()) {
|
|
4928
5125
|
this.#createAuthCredentialsTable();
|
|
5126
|
+
this.#createAuthCredentialBlocksTable();
|
|
4929
5127
|
this.#writeAuthSchemaVersion(AUTH_SCHEMA_VERSION);
|
|
4930
5128
|
return;
|
|
4931
5129
|
}
|
|
@@ -4942,6 +5140,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
4942
5140
|
}
|
|
4943
5141
|
|
|
4944
5142
|
this.#createAuthCredentialIndexes();
|
|
5143
|
+
this.#createAuthCredentialBlocksTable();
|
|
4945
5144
|
this.#backfillCredentialIdentityKeys();
|
|
4946
5145
|
// Rewriting an already-current version row is a no-op write transaction
|
|
4947
5146
|
// on every boot; only persist when the recorded version actually changes.
|
|
@@ -5025,6 +5224,20 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
5025
5224
|
`);
|
|
5026
5225
|
}
|
|
5027
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
|
+
|
|
5028
5241
|
#migrateAuthSchema(fromVersion: number): void {
|
|
5029
5242
|
if (fromVersion < 1) {
|
|
5030
5243
|
this.#migrateAuthSchemaV0ToV1();
|
|
@@ -5035,6 +5248,9 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
5035
5248
|
if (fromVersion < 4) {
|
|
5036
5249
|
this.#migrateAuthSchemaV3ToV4();
|
|
5037
5250
|
}
|
|
5251
|
+
if (fromVersion < 5) {
|
|
5252
|
+
this.#migrateAuthSchemaV4ToV5();
|
|
5253
|
+
}
|
|
5038
5254
|
}
|
|
5039
5255
|
|
|
5040
5256
|
#migrateAuthSchemaV0ToV1(): void {
|
|
@@ -5121,6 +5337,13 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
5121
5337
|
migrate();
|
|
5122
5338
|
}
|
|
5123
5339
|
|
|
5340
|
+
#migrateAuthSchemaV4ToV5(): void {
|
|
5341
|
+
const migrate = this.#db.transaction(() => {
|
|
5342
|
+
this.#createAuthCredentialBlocksTable();
|
|
5343
|
+
});
|
|
5344
|
+
migrate();
|
|
5345
|
+
}
|
|
5346
|
+
|
|
5124
5347
|
#backfillCredentialIdentityKeys(): void {
|
|
5125
5348
|
const selectRowsStmt = this.#db.prepare(
|
|
5126
5349
|
"SELECT id, provider, credential_type, data, disabled_cause, identity_key FROM auth_credentials WHERE identity_key IS NULL ORDER BY id ASC",
|
|
@@ -5386,6 +5609,54 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
5386
5609
|
}
|
|
5387
5610
|
}
|
|
5388
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
|
+
|
|
5389
5660
|
recordUsageSnapshots(entries: UsageHistoryEntry[]): void {
|
|
5390
5661
|
try {
|
|
5391
5662
|
for (const entry of entries) {
|
|
@@ -5579,6 +5850,11 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
5579
5850
|
this.#getCacheIncludingExpiredStmt.finalize();
|
|
5580
5851
|
this.#upsertCacheStmt.finalize();
|
|
5581
5852
|
this.#deleteExpiredCacheStmt.finalize();
|
|
5853
|
+
this.#getCredentialBlockStmt.finalize();
|
|
5854
|
+
this.#listCredentialBlocksByCredentialStmt.finalize();
|
|
5855
|
+
this.#upsertCredentialBlockStmt.finalize();
|
|
5856
|
+
this.#deleteCredentialBlocksStmt.finalize();
|
|
5857
|
+
this.#deleteExpiredCredentialBlocksStmt.finalize();
|
|
5582
5858
|
this.#insertUsageHistoryStmt.finalize();
|
|
5583
5859
|
this.#lastUsageHistoryStmt.finalize();
|
|
5584
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
|
}
|
|
@@ -3311,7 +3311,12 @@ export function convertOpenAICodexResponsesTools(
|
|
|
3311
3311
|
name: tool.name,
|
|
3312
3312
|
description: tool.description || "",
|
|
3313
3313
|
parameters,
|
|
3314
|
-
|
|
3314
|
+
// See openai-responses.ts::convertTools — explicit `strict: false` is
|
|
3315
|
+
// preserved on the wire because some backends distinguish it from
|
|
3316
|
+
// omitted (#4336). `strict: true` still requires enforcement success,
|
|
3317
|
+
// and the `PI_NO_STRICT` global bypass MUST suppress the flag entirely
|
|
3318
|
+
// so Codex proxies that reject the `strict` key stay silent.
|
|
3319
|
+
...(effectiveStrict ? { strict: true } : !NO_STRICT && tool.strict === false ? { strict: false } : {}),
|
|
3315
3320
|
};
|
|
3316
3321
|
});
|
|
3317
3322
|
}
|
|
@@ -2115,6 +2115,17 @@ function convertTools(
|
|
|
2115
2115
|
return {
|
|
2116
2116
|
tools: adaptedTools.map(({ tool, baseParameters, parameters, strict }) => {
|
|
2117
2117
|
const includeStrict = toolStrictMode === "all_strict" || (toolStrictMode === "mixed" && strict);
|
|
2118
|
+
// `strict: false` is semantically distinct from omitted `strict` on some
|
|
2119
|
+
// backends: with it absent, optional properties may be over-filled with
|
|
2120
|
+
// placeholder values (#4336). Preserve the author's explicit `false`,
|
|
2121
|
+
// but only in "mixed" mode against a provider that understands the
|
|
2122
|
+
// field — the `all_strict → none` collapse and `supportsStrictMode:
|
|
2123
|
+
// false` paths deliberately keep the wire flag uniformly absent.
|
|
2124
|
+
const includeExplicitFalse =
|
|
2125
|
+
!includeStrict &&
|
|
2126
|
+
tool.strict === false &&
|
|
2127
|
+
toolStrictMode === "mixed" &&
|
|
2128
|
+
compat.supportsStrictMode !== false;
|
|
2118
2129
|
const wireParameters = includeStrict ? parameters : baseParameters;
|
|
2119
2130
|
return {
|
|
2120
2131
|
type: "function",
|
|
@@ -2128,7 +2139,7 @@ function convertTools(
|
|
|
2128
2139
|
? (normalizeSchemaForMoonshot(wireParameters) as Record<string, unknown>)
|
|
2129
2140
|
: wireParameters,
|
|
2130
2141
|
// Only include strict if provider supports it. Some reject unknown fields.
|
|
2131
|
-
...(includeStrict
|
|
2142
|
+
...(includeStrict ? { strict: true } : includeExplicitFalse ? { strict: false } : {}),
|
|
2132
2143
|
},
|
|
2133
2144
|
};
|
|
2134
2145
|
}),
|
|
@@ -987,7 +987,16 @@ export function convertTools(
|
|
|
987
987
|
name: tool.name,
|
|
988
988
|
description: tool.description || "",
|
|
989
989
|
parameters,
|
|
990
|
-
|
|
990
|
+
// `strict: false` and an omitted `strict` are NOT equivalent for every
|
|
991
|
+
// OpenAI-compat backend — some over-fill optional args when the flag is
|
|
992
|
+
// absent (#4336). Preserve the author's explicit `false` only while the
|
|
993
|
+
// Responses strict field is enabled; compatibility disables and
|
|
994
|
+
// strict-schema fallback retries rely on uniformly absent flags.
|
|
995
|
+
...(effectiveStrict
|
|
996
|
+
? { strict: true }
|
|
997
|
+
: !NO_STRICT && strictMode && tool.strict === false
|
|
998
|
+
? { strict: false }
|
|
999
|
+
: {}),
|
|
991
1000
|
} as OpenAITool);
|
|
992
1001
|
}
|
|
993
1002
|
return out;
|