codex-multi-auth 2.8.7 → 2.9.1
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/.codex-plugin/plugin.json +1 -1
- package/README.md +14 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -0
- package/dist/lib/account-policy.d.ts +3 -1
- package/dist/lib/account-policy.d.ts.map +1 -1
- package/dist/lib/account-policy.js +14 -1
- package/dist/lib/budget-guard.d.ts +1 -0
- package/dist/lib/budget-guard.d.ts.map +1 -1
- package/dist/lib/budget-guard.js +13 -3
- package/dist/lib/codex-manager/account-pool-write.d.ts +8 -0
- package/dist/lib/codex-manager/account-pool-write.d.ts.map +1 -1
- package/dist/lib/codex-manager/account-pool-write.js +1 -1
- package/dist/lib/codex-manager/help.d.ts +2 -0
- package/dist/lib/codex-manager/help.d.ts.map +1 -1
- package/dist/lib/codex-manager/help.js +38 -1
- package/dist/lib/codex-manager/login-flow.d.ts +3 -0
- package/dist/lib/codex-manager/login-flow.d.ts.map +1 -1
- package/dist/lib/codex-manager/login-flow.js +80 -6
- package/dist/lib/codex-manager/login-menu-actions.d.ts.map +1 -1
- package/dist/lib/codex-manager/login-menu-actions.js +41 -5
- package/dist/lib/codex-manager/login-oauth.d.ts +38 -2
- package/dist/lib/codex-manager/login-oauth.d.ts.map +1 -1
- package/dist/lib/codex-manager/login-oauth.js +205 -23
- package/dist/lib/policy/runtime-policy.d.ts +7 -0
- package/dist/lib/policy/runtime-policy.d.ts.map +1 -1
- package/dist/lib/policy/runtime-policy.js +1 -0
- package/dist/lib/request/fetch-helpers.d.ts +7 -0
- package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
- package/dist/lib/request/fetch-helpers.js +1 -1
- package/dist/lib/request/response-handler.d.ts +3 -1
- package/dist/lib/request/response-handler.d.ts.map +1 -1
- package/dist/lib/request/response-handler.js +26 -5
- package/dist/lib/request/stream-failover-runtime.d.ts +8 -1
- package/dist/lib/request/stream-failover-runtime.d.ts.map +1 -1
- package/dist/lib/request/stream-failover-runtime.js +9 -1
- package/dist/lib/request/stream-failover.d.ts.map +1 -1
- package/dist/lib/request/stream-failover.js +41 -0
- package/dist/lib/runtime/account-check.d.ts.map +1 -1
- package/dist/lib/runtime/account-check.js +8 -1
- package/dist/lib/runtime-rotation-proxy.d.ts.map +1 -1
- package/dist/lib/runtime-rotation-proxy.js +46 -3
- package/dist/lib/storage.d.ts +19 -0
- package/dist/lib/storage.d.ts.map +1 -1
- package/dist/lib/storage.js +32 -0
- package/dist/lib/usage/index.d.ts +2 -0
- package/dist/lib/usage/index.d.ts.map +1 -1
- package/dist/lib/usage/index.js +2 -0
- package/dist/lib/usage/ledger.d.ts.map +1 -1
- package/dist/lib/usage/ledger.js +7 -0
- package/dist/lib/usage/pricing.d.ts +15 -0
- package/dist/lib/usage/pricing.d.ts.map +1 -1
- package/dist/lib/usage/pricing.js +24 -0
- package/dist/lib/usage/stream-usage-deferral.d.ts +39 -0
- package/dist/lib/usage/stream-usage-deferral.d.ts.map +1 -0
- package/dist/lib/usage/stream-usage-deferral.js +54 -0
- package/dist/lib/usage/types.d.ts +6 -0
- package/dist/lib/usage/types.d.ts.map +1 -1
- package/dist/lib/usage/usage-extraction.d.ts +42 -0
- package/dist/lib/usage/usage-extraction.d.ts.map +1 -0
- package/dist/lib/usage/usage-extraction.js +178 -0
- package/package.json +1 -1
|
@@ -11,6 +11,8 @@ import { setCodexCliActiveSelection } from "../codex-cli/writer.js";
|
|
|
11
11
|
import { createLogger } from "../logger.js";
|
|
12
12
|
import { MODEL_FAMILIES } from "../prompts/codex.js";
|
|
13
13
|
import { findMatchingAccountIndex, withAccountStorageTransaction, } from "../storage.js";
|
|
14
|
+
import { cloneAccountStorageForPersistence } from "../storage/account-persistence.js";
|
|
15
|
+
import { CodexValidationError } from "../errors.js";
|
|
14
16
|
import { UI_COPY } from "../ui/ui-copy.js";
|
|
15
17
|
import { applyAccountPoolResults, } from "./account-pool-write.js";
|
|
16
18
|
import { stylePromptText } from "./formatters/index.js";
|
|
@@ -39,7 +41,7 @@ export function isAbortError(error) {
|
|
|
39
41
|
*
|
|
40
42
|
* @internal
|
|
41
43
|
*/
|
|
42
|
-
export function resolveAccountSelection(tokens, orgOverride) {
|
|
44
|
+
export function resolveAccountSelection(tokens, orgOverride, targetAccountId) {
|
|
43
45
|
const candidates = getAccountIdCandidates(tokens.access, tokens.idToken);
|
|
44
46
|
// Surface every workspace/organization exposed by the token so the saved
|
|
45
47
|
// account can track them (issue #491/#512). Without this, same-email
|
|
@@ -54,18 +56,50 @@ export function resolveAccountSelection(tokens, orgOverride) {
|
|
|
54
56
|
isDefault: candidate.isDefault,
|
|
55
57
|
}))
|
|
56
58
|
: undefined;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
59
|
+
// A targeted re-authentication names one saved row, which is a stricter and
|
|
60
|
+
// more specific instruction than the ambient CODEX_AUTH_ACCOUNT_ID env var
|
|
61
|
+
// that resolveOrgOverride also honours. Consulting the override first let an
|
|
62
|
+
// unrelated exported org id hijack `login --account <n>` and then fail the
|
|
63
|
+
// identity guard with a misleading "does not match" message. An explicit
|
|
64
|
+
// `--org` cannot reach here alongside a target: parseAuthLoginArgs rejects
|
|
65
|
+
// that combination outright.
|
|
66
|
+
if (targetAccountId) {
|
|
67
|
+
const targetedCandidate = candidates.find((candidate) => candidate.accountId === targetAccountId);
|
|
68
|
+
if (targetedCandidate) {
|
|
69
|
+
return {
|
|
70
|
+
...tokens,
|
|
71
|
+
accountIdOverride: targetedCandidate.accountId,
|
|
72
|
+
// `id_token` candidates auto-follow the access token inside
|
|
73
|
+
// resolveRequestAccountId, which would rewrite this binding back to
|
|
74
|
+
// the access-token account and make the caller's own identity guard
|
|
75
|
+
// reject the write. Pin those as an explicit selection; a `token`
|
|
76
|
+
// candidate already IS the access-token account, and an `org`
|
|
77
|
+
// candidate never auto-follows, so both keep their source.
|
|
78
|
+
accountIdSource: targetedCandidate.source === "id_token"
|
|
79
|
+
? "manual"
|
|
80
|
+
: targetedCandidate.source,
|
|
81
|
+
accountLabel: targetedCandidate.label,
|
|
82
|
+
workspaces,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// The user signed in as somebody else. Fall through to the generic
|
|
86
|
+
// selection so persistAccountPool refuses the write with the precise
|
|
87
|
+
// identity-mismatch message instead of a confusing override error.
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
const override = resolveOrgOverride(orgOverride);
|
|
91
|
+
if (override) {
|
|
92
|
+
// Prefer the token candidate's human label for the chosen org so the
|
|
93
|
+
// saved row is identifiable, falling back to a bare manual binding.
|
|
94
|
+
const matched = candidates.find((candidate) => candidate.accountId === override);
|
|
95
|
+
return {
|
|
96
|
+
...tokens,
|
|
97
|
+
accountIdOverride: override,
|
|
98
|
+
accountIdSource: "manual",
|
|
99
|
+
accountLabel: matched?.label,
|
|
100
|
+
workspaces,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
69
103
|
}
|
|
70
104
|
if (candidates.length === 0) {
|
|
71
105
|
return tokens;
|
|
@@ -300,8 +334,81 @@ export async function runSignInFlow(forceNewLogin, signInMode, options = {}) {
|
|
|
300
334
|
}
|
|
301
335
|
return runOAuthFlow(forceNewLogin, signInMode);
|
|
302
336
|
}
|
|
337
|
+
function sameExpectedLoginIdentity(write, expected) {
|
|
338
|
+
const expectedAccountId = expected.accountId?.trim();
|
|
339
|
+
const incomingAccountId = write.accountId?.trim();
|
|
340
|
+
const expectedEmail = sanitizeEmail(expected.email);
|
|
341
|
+
const incomingEmail = sanitizeEmail(write.email);
|
|
342
|
+
// When the saved row already carries the provider identity it is
|
|
343
|
+
// authoritative and the incoming login must match it: the id survives an
|
|
344
|
+
// email change, and falling back to a shared email across workspaces would
|
|
345
|
+
// let a targeted re-auth overwrite the wrong saved account.
|
|
346
|
+
if (expectedAccountId) {
|
|
347
|
+
return incomingAccountId === expectedAccountId;
|
|
348
|
+
}
|
|
349
|
+
// A pre-#491 row saved before workspace tracking carries no account id at
|
|
350
|
+
// all. Fresh tokens essentially always carry one, so demanding the id on both
|
|
351
|
+
// sides made every legacy row permanently un-refreshable through --account.
|
|
352
|
+
// Email is the only identity such a row has, and the caller already resolved
|
|
353
|
+
// it to a single unambiguous row before starting the sign-in.
|
|
354
|
+
return Boolean(expectedEmail && incomingEmail === expectedEmail);
|
|
355
|
+
}
|
|
356
|
+
function matchesExpectedAccount(account, expected) {
|
|
357
|
+
if (!account)
|
|
358
|
+
return false;
|
|
359
|
+
if (expected.recordId && account.recordId) {
|
|
360
|
+
return account.recordId === expected.recordId;
|
|
361
|
+
}
|
|
362
|
+
const expectedAccountId = expected.accountId?.trim();
|
|
363
|
+
const candidateAccountId = account.accountId?.trim();
|
|
364
|
+
if (expectedAccountId || candidateAccountId) {
|
|
365
|
+
return Boolean(expectedAccountId &&
|
|
366
|
+
candidateAccountId &&
|
|
367
|
+
expectedAccountId === candidateAccountId);
|
|
368
|
+
}
|
|
369
|
+
const expectedEmail = sanitizeEmail(expected.email);
|
|
370
|
+
const candidateEmail = sanitizeEmail(account.email);
|
|
371
|
+
if (expectedEmail || candidateEmail) {
|
|
372
|
+
return Boolean(expectedEmail && candidateEmail === expectedEmail);
|
|
373
|
+
}
|
|
374
|
+
return account.refreshToken === expected.refreshToken;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Locate the saved row a targeted re-authentication must write.
|
|
378
|
+
*
|
|
379
|
+
* Resolution is deliberately identity-first and refresh-token-last. The runtime
|
|
380
|
+
* rotation proxy rewrites a stored refreshToken whenever it refreshes an account
|
|
381
|
+
* (AccountManager.commitRefreshedAuth), so a sign-in that overlaps a live codex
|
|
382
|
+
* session would otherwise "lose" its own target and throw away a completed
|
|
383
|
+
* OAuth. `recordId` is exact when both sides have one, the caller's position is
|
|
384
|
+
* honoured only while the row sitting there is still the same identity, and
|
|
385
|
+
* findMatchingAccountIndex supplies the same composite/email/refresh-token
|
|
386
|
+
* ladder the rest of the pool already resolves identities with.
|
|
387
|
+
*/
|
|
388
|
+
function resolveExpectedAccountIndex(accounts, expected, hintedIndex) {
|
|
389
|
+
if (expected.recordId) {
|
|
390
|
+
const byRecordId = accounts.findIndex((account) => account?.recordId === expected.recordId);
|
|
391
|
+
if (byRecordId >= 0)
|
|
392
|
+
return byRecordId;
|
|
393
|
+
}
|
|
394
|
+
if (validSelectionIndex(hintedIndex, accounts) &&
|
|
395
|
+
matchesExpectedAccount(accounts[hintedIndex], expected)) {
|
|
396
|
+
return hintedIndex;
|
|
397
|
+
}
|
|
398
|
+
return findMatchingAccountIndex(accounts, {
|
|
399
|
+
accountId: expected.accountId,
|
|
400
|
+
email: expected.email,
|
|
401
|
+
refreshToken: expected.refreshToken,
|
|
402
|
+
}, { allowUniqueAccountIdFallbackWithoutEmail: true });
|
|
403
|
+
}
|
|
404
|
+
function validSelectionIndex(index, accounts) {
|
|
405
|
+
return (typeof index === "number" &&
|
|
406
|
+
Number.isInteger(index) &&
|
|
407
|
+
index >= 0 &&
|
|
408
|
+
index < accounts.length);
|
|
409
|
+
}
|
|
303
410
|
/** @internal */
|
|
304
|
-
export async function persistAccountPool(results, replaceAll) {
|
|
411
|
+
export async function persistAccountPool(results, replaceAll, options = {}) {
|
|
305
412
|
if (results.length === 0)
|
|
306
413
|
return null;
|
|
307
414
|
return await withAccountStorageTransaction(async (loadedStorage, persist) => {
|
|
@@ -324,26 +431,101 @@ export async function persistAccountPool(results, replaceAll) {
|
|
|
324
431
|
accessToken: result.access,
|
|
325
432
|
expiresAt: result.expires,
|
|
326
433
|
workspaces: result.workspaces,
|
|
434
|
+
// A targeted re-auth only tops up credentials for the row the user
|
|
435
|
+
// named; it must not silently re-enable an account they disabled.
|
|
436
|
+
preserveEnabledState: Boolean(options.expectedAccount),
|
|
327
437
|
now,
|
|
328
438
|
};
|
|
329
439
|
});
|
|
440
|
+
if (options.expectedAccount && writes.length !== 1) {
|
|
441
|
+
throw new CodexValidationError("Targeted re-authentication accepts exactly one OAuth result. No saved credentials were changed.");
|
|
442
|
+
}
|
|
443
|
+
const expectedAccountIndex = options.expectedAccount && stored
|
|
444
|
+
? resolveExpectedAccountIndex(stored.accounts, options.expectedAccount, options.expectedAccountIndex)
|
|
445
|
+
: undefined;
|
|
446
|
+
if (options.expectedAccount && expectedAccountIndex === undefined) {
|
|
447
|
+
throw new CodexValidationError("The requested Codex account is no longer present. No saved credentials were changed.");
|
|
448
|
+
}
|
|
449
|
+
const expectedAccount = options.expectedAccount;
|
|
450
|
+
if (expectedAccount &&
|
|
451
|
+
writes.some((write) => !sameExpectedLoginIdentity(write, expectedAccount))) {
|
|
452
|
+
throw new CodexValidationError("The authenticated Codex identity does not match the account requested for re-authentication. No saved credentials were changed.");
|
|
453
|
+
}
|
|
330
454
|
const { accounts, activeIndex, outcome } = applyAccountPoolResults({
|
|
331
455
|
existing,
|
|
332
456
|
writes,
|
|
333
457
|
priorActiveIndex: stored?.activeIndex,
|
|
334
|
-
findMatchingAccountIndex
|
|
458
|
+
findMatchingAccountIndex: expectedAccountIndex === undefined
|
|
459
|
+
? findMatchingAccountIndex
|
|
460
|
+
: () => expectedAccountIndex,
|
|
335
461
|
});
|
|
462
|
+
const nextActiveIndex = options.preserveSelection &&
|
|
463
|
+
validSelectionIndex(stored?.activeIndex, accounts)
|
|
464
|
+
? stored.activeIndex
|
|
465
|
+
: activeIndex;
|
|
466
|
+
const priorActiveIndex = stored?.activeIndex;
|
|
336
467
|
const activeIndexByFamily = {};
|
|
337
468
|
for (const family of MODEL_FAMILIES) {
|
|
338
|
-
activeIndexByFamily[family]
|
|
469
|
+
const storedFamilyIndex = stored?.activeIndexByFamily?.[family];
|
|
470
|
+
const priorFamilyIndex = storedFamilyIndex ?? priorActiveIndex;
|
|
471
|
+
if (!validSelectionIndex(priorFamilyIndex, accounts)) {
|
|
472
|
+
activeIndexByFamily[family] = nextActiveIndex;
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
if (options.preserveSelection) {
|
|
476
|
+
activeIndexByFamily[family] = priorFamilyIndex;
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
// A family the user deliberately pointed at a DIFFERENT account than
|
|
480
|
+
// the global selection is a setting of its own, and a plain login must
|
|
481
|
+
// not silently drag it onto the account just signed in: adding a third
|
|
482
|
+
// account used to move a deliberate `codex-max -> account 2` binding
|
|
483
|
+
// onto account 3. Families that were merely following the global
|
|
484
|
+
// selection keep following it, which is what makes a plain login still
|
|
485
|
+
// feel like "this new account is now the one in use".
|
|
486
|
+
//
|
|
487
|
+
// Indices stay meaningful here because login only updates a row in
|
|
488
|
+
// place or appends one; it never reorders or removes the pool.
|
|
489
|
+
const followedGlobalSelection = storedFamilyIndex === undefined || storedFamilyIndex === priorActiveIndex;
|
|
490
|
+
activeIndexByFamily[family] = followedGlobalSelection
|
|
491
|
+
? nextActiveIndex
|
|
492
|
+
: priorFamilyIndex;
|
|
339
493
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
494
|
+
// Reuse the shared persistence clone so the pin/affinity carry-over rule
|
|
495
|
+
// (issue #474) keeps living in exactly one place instead of being
|
|
496
|
+
// re-derived at every write site with a slightly different validity check.
|
|
497
|
+
const nextStorage = cloneAccountStorageForPersistence(stored);
|
|
498
|
+
nextStorage.accounts = accounts;
|
|
499
|
+
nextStorage.activeIndex = nextActiveIndex;
|
|
500
|
+
nextStorage.activeIndexByFamily = activeIndexByFamily;
|
|
501
|
+
// A selection-preserving login leaves the pool exactly where it was, so a
|
|
502
|
+
// manual `switch <n>` pin survives it: login only updates a row in place
|
|
503
|
+
// or appends one and never reorders, so the raw position still points at
|
|
504
|
+
// the row the user pinned even when two rows share an identity.
|
|
505
|
+
//
|
|
506
|
+
// A plain login is the opposite. It moves activeIndex onto the account
|
|
507
|
+
// just signed in and publishes that account to ~/.codex/auth.json.
|
|
508
|
+
// Carrying an old pin through it would leave storage claiming one account
|
|
509
|
+
// is active while the runtime proxy routes every request to another
|
|
510
|
+
// (runtime-rotation-proxy resolves pinnedAccountIndex first) -- the
|
|
511
|
+
// contradiction `status` reports as "runtime using N but pin requests M".
|
|
512
|
+
if (!options.preserveSelection ||
|
|
513
|
+
!validSelectionIndex(nextStorage.pinnedAccountIndex, accounts)) {
|
|
514
|
+
delete nextStorage.pinnedAccountIndex;
|
|
515
|
+
}
|
|
516
|
+
await persist(nextStorage);
|
|
517
|
+
if (outcome === null)
|
|
518
|
+
return null;
|
|
519
|
+
return {
|
|
520
|
+
outcome,
|
|
521
|
+
accountIndex: activeIndex,
|
|
522
|
+
activeIndex: nextActiveIndex,
|
|
523
|
+
// The Codex CLI reads the `codex` family selection, so that -- not the
|
|
524
|
+
// bare global index -- decides whether this write owns the native
|
|
525
|
+
// ~/.codex/auth.json credentials.
|
|
526
|
+
isActiveAccount: activeIndex === (activeIndexByFamily.codex ?? nextActiveIndex),
|
|
527
|
+
accountEnabled: accounts[activeIndex]?.enabled !== false,
|
|
528
|
+
};
|
|
347
529
|
});
|
|
348
530
|
}
|
|
349
531
|
/** @internal */
|
|
@@ -7,6 +7,13 @@ export interface RuntimePolicyAccount {
|
|
|
7
7
|
index: number;
|
|
8
8
|
accountId?: string | null;
|
|
9
9
|
email?: string | null;
|
|
10
|
+
/**
|
|
11
|
+
* Only used to key account policy for an account with no accountId and no
|
|
12
|
+
* email. Without it the runtime would key such an account as "unknown"
|
|
13
|
+
* while the CLI keys it by refresh-token digest, and pause/drain/tag state
|
|
14
|
+
* written by one would be invisible to the other.
|
|
15
|
+
*/
|
|
16
|
+
refreshToken?: string | null;
|
|
10
17
|
}
|
|
11
18
|
export interface RuntimePolicyDecision {
|
|
12
19
|
allowed: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-policy.d.ts","sourceRoot":"","sources":["../../../lib/policy/runtime-policy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAErE,OAAO,EAGN,KAAK,kBAAkB,EACvB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAKN,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,EACrB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAEN,KAAK,4BAA4B,EACjC,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACN,oBAAoB,EAGpB,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,MAAM,mBAAmB,CAAC;AAE3B,MAAM,WAAW,oBAAoB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"runtime-policy.d.ts","sourceRoot":"","sources":["../../../lib/policy/runtime-policy.ts"],"names":[],"mappings":"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"}
|
|
@@ -98,6 +98,7 @@ export async function evaluateRuntimePolicy(input) {
|
|
|
98
98
|
const accountKey = getAccountPolicyKey({
|
|
99
99
|
accountId: account.accountId ?? undefined,
|
|
100
100
|
email: account.email ?? undefined,
|
|
101
|
+
refreshToken: account.refreshToken ?? undefined,
|
|
101
102
|
}, account.index);
|
|
102
103
|
const accountPolicy = input.state.accountPolicies.accounts[accountKey];
|
|
103
104
|
let boost = 0;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { type FastSessionInputTrimPlan } from "./request-transformer.js";
|
|
6
6
|
import type { UserConfig, RequestBody } from "../types.js";
|
|
7
|
+
import type { UsageTokenCounts } from "../usage/types.js";
|
|
7
8
|
export { shouldRefreshToken, refreshAndUpdateToken } from "./token-refresh.js";
|
|
8
9
|
export { DEFAULT_UNSUPPORTED_CODEX_FALLBACK_CHAIN, extractUnsupportedCodexModelFromText, getUnsupportedCodexModelInfo, resolveUnsupportedCodexFallbackModel, shouldFallbackToGpt52OnUnsupportedGpt53, isEntitlementError, isWorkspaceDisabledError, createEntitlementErrorResponse, } from "./error-classification.js";
|
|
9
10
|
export type { EntitlementError, UnsupportedCodexModelInfo, ResolveUnsupportedCodexFallbackOptions, } from "./error-classification.js";
|
|
@@ -70,6 +71,12 @@ export declare function handleErrorResponse(response: Response, options?: ErrorH
|
|
|
70
71
|
*/
|
|
71
72
|
export declare function handleSuccessResponse(response: Response, isStreaming: boolean, options?: {
|
|
72
73
|
onResponseId?: (responseId: string) => void;
|
|
74
|
+
/**
|
|
75
|
+
* Reports the upstream token counts once they are known, for the usage
|
|
76
|
+
* ledger. Fires from the SSE tee on the streaming path and from the
|
|
77
|
+
* assembled response on the non-streaming path.
|
|
78
|
+
*/
|
|
79
|
+
onUsage?: (usage: UsageTokenCounts) => void;
|
|
73
80
|
streamStallTimeoutMs?: number;
|
|
74
81
|
}): Promise<Response>;
|
|
75
82
|
//# sourceMappingURL=fetch-helpers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch-helpers.d.ts","sourceRoot":"","sources":["../../../lib/request/fetch-helpers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,EAIN,KAAK,wBAAwB,EAC7B,MAAM,0BAA0B,CAAC;AAMlC,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"fetch-helpers.d.ts","sourceRoot":"","sources":["../../../lib/request/fetch-helpers.ts"],"names":[],"mappings":"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"}
|
|
@@ -155,7 +155,7 @@ export async function handleSuccessResponse(response, isStreaming, options) {
|
|
|
155
155
|
return await convertSseToJson(response, responseHeaders, options);
|
|
156
156
|
}
|
|
157
157
|
// For streaming requests (streamText), return stream as-is
|
|
158
|
-
return attachResponseIdCapture(response, responseHeaders, options?.onResponseId);
|
|
158
|
+
return attachResponseIdCapture(response, responseHeaders, options?.onResponseId, options?.onUsage);
|
|
159
159
|
}
|
|
160
160
|
async function safeReadBody(response) {
|
|
161
161
|
try {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { UsageTokenCounts } from "../usage/types.js";
|
|
1
2
|
/**
|
|
2
3
|
* Convert SSE stream response to JSON for generateText()
|
|
3
4
|
* @param response - Fetch response with SSE stream
|
|
@@ -6,6 +7,7 @@
|
|
|
6
7
|
*/
|
|
7
8
|
export declare function convertSseToJson(response: Response, headers: Headers, options?: {
|
|
8
9
|
onResponseId?: (responseId: string) => void;
|
|
10
|
+
onUsage?: (usage: UsageTokenCounts) => void;
|
|
9
11
|
streamStallTimeoutMs?: number;
|
|
10
12
|
}): Promise<Response>;
|
|
11
13
|
/**
|
|
@@ -21,5 +23,5 @@ export declare function ensureContentType(headers: Headers): Headers;
|
|
|
21
23
|
* @returns True if response should be considered empty/malformed
|
|
22
24
|
*/
|
|
23
25
|
export declare function isEmptyResponse(body: unknown): boolean;
|
|
24
|
-
export declare function attachResponseIdCapture(response: Response, headers: Headers, onResponseId?: (responseId: string) => void): Response;
|
|
26
|
+
export declare function attachResponseIdCapture(response: Response, headers: Headers, onResponseId?: (responseId: string) => void, onUsage?: (usage: UsageTokenCounts) => void): Response;
|
|
25
27
|
//# sourceMappingURL=response-handler.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"response-handler.d.ts","sourceRoot":"","sources":["../../../lib/request/response-handler.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"response-handler.d.ts","sourceRoot":"","sources":["../../../lib/request/response-handler.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAqwB1D;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACrC,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE;IACT,YAAY,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAC5C,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC9B,GACC,OAAO,CAAC,QAAQ,CAAC,CAkInB;AAwED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAQ3D;AA2BD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CA4CtD;AAED,wBAAgB,uBAAuB,CACtC,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,OAAO,EAChB,YAAY,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,EAC3C,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,GACzC,QAAQ,CAiBV"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createLogger, logRequest, LOGGING_ENABLED } from "../logger.js";
|
|
2
2
|
import { HTTP_STATUS, PLUGIN_NAME } from "../constants.js";
|
|
3
3
|
import { isRecord } from "../utils.js";
|
|
4
|
+
import { extractResponsesUsage } from "../usage/usage-extraction.js";
|
|
4
5
|
const log = createLogger("response-handler");
|
|
5
6
|
const MAX_SSE_SIZE = 10 * 1024 * 1024; // 10MB limit to prevent memory exhaustion
|
|
6
7
|
const DEFAULT_STREAM_STALL_TIMEOUT_MS = 45_000;
|
|
@@ -663,6 +664,15 @@ export async function convertSseToJson(response, headers, options) {
|
|
|
663
664
|
});
|
|
664
665
|
}
|
|
665
666
|
logStreamDiagnostics(finalResponse);
|
|
667
|
+
if (options?.onUsage) {
|
|
668
|
+
// The non-streaming branch already holds the assembled response, so
|
|
669
|
+
// the token counts come straight off it. Reported even though the
|
|
670
|
+
// caller sees JSON rather than SSE: the usage ledger (and therefore
|
|
671
|
+
// the maxTokens / maxCostUsd budget caps) needs both shapes.
|
|
672
|
+
const usage = extractResponsesUsage(finalResponse);
|
|
673
|
+
if (usage)
|
|
674
|
+
options.onUsage(usage);
|
|
675
|
+
}
|
|
666
676
|
// Return as plain JSON (not SSE)
|
|
667
677
|
const jsonHeaders = new Headers(headers);
|
|
668
678
|
jsonHeaders.set("content-type", "application/json; charset=utf-8");
|
|
@@ -685,7 +695,7 @@ export async function convertSseToJson(response, headers, options) {
|
|
|
685
695
|
reader.releaseLock();
|
|
686
696
|
}
|
|
687
697
|
}
|
|
688
|
-
function createResponseIdCapturingStream(body, onResponseId) {
|
|
698
|
+
function createResponseIdCapturingStream(body, onResponseId, onUsage) {
|
|
689
699
|
const decoder = new TextDecoder();
|
|
690
700
|
let bufferedText = "";
|
|
691
701
|
let loggedMalformedStreamChunk = false;
|
|
@@ -709,7 +719,18 @@ function createResponseIdCapturingStream(body, onResponseId) {
|
|
|
709
719
|
continue;
|
|
710
720
|
try {
|
|
711
721
|
const data = JSON.parse(payload);
|
|
712
|
-
|
|
722
|
+
if (onResponseId) {
|
|
723
|
+
maybeCaptureResponseEvent(state, data, onResponseId);
|
|
724
|
+
}
|
|
725
|
+
if (onUsage) {
|
|
726
|
+
// Reuse the JSON this loop already parsed rather than scanning
|
|
727
|
+
// the stream twice. Without these counts every usage-ledger row
|
|
728
|
+
// lands with zero tokens and zero cost, which silently disables
|
|
729
|
+
// the maxTokens / maxCostUsd budget caps.
|
|
730
|
+
const usage = extractResponsesUsage(data);
|
|
731
|
+
if (usage)
|
|
732
|
+
onUsage(usage);
|
|
733
|
+
}
|
|
713
734
|
if (state.encounteredError)
|
|
714
735
|
break;
|
|
715
736
|
}
|
|
@@ -812,15 +833,15 @@ export function isEmptyResponse(body) {
|
|
|
812
833
|
}
|
|
813
834
|
return false;
|
|
814
835
|
}
|
|
815
|
-
export function attachResponseIdCapture(response, headers, onResponseId) {
|
|
816
|
-
if (!response.body || !onResponseId) {
|
|
836
|
+
export function attachResponseIdCapture(response, headers, onResponseId, onUsage) {
|
|
837
|
+
if (!response.body || (!onResponseId && !onUsage)) {
|
|
817
838
|
return new Response(response.body, {
|
|
818
839
|
status: response.status,
|
|
819
840
|
statusText: response.statusText,
|
|
820
841
|
headers,
|
|
821
842
|
});
|
|
822
843
|
}
|
|
823
|
-
return new Response(createResponseIdCapturingStream(response.body, onResponseId), {
|
|
844
|
+
return new Response(createResponseIdCapturingStream(response.body, onResponseId, onUsage), {
|
|
824
845
|
status: response.status,
|
|
825
846
|
statusText: response.statusText,
|
|
826
847
|
headers,
|
|
@@ -4,5 +4,12 @@ export declare const HOP_BY_HOP_HEADERS: Set<string>;
|
|
|
4
4
|
export declare function responseHeadersForClient(upstreamHeaders: Headers): Record<string, string>;
|
|
5
5
|
export declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number, onTimeout: () => void, message: string): Promise<T>;
|
|
6
6
|
export declare function readErrorBody(response: Response, timeoutMs: number, maxBytes?: number): Promise<string>;
|
|
7
|
-
export declare function forwardStreamingResponse(upstream: Response, res: ServerResponse, status: RuntimeRotationProxyStatus, onStreamError: () => void, streamStallTimeoutMs: number
|
|
7
|
+
export declare function forwardStreamingResponse(upstream: Response, res: ServerResponse, status: RuntimeRotationProxyStatus, onStreamError: () => void, streamStallTimeoutMs: number,
|
|
8
|
+
/**
|
|
9
|
+
* Observe each forwarded chunk. Used to recover the upstream `usage` totals
|
|
10
|
+
* for the usage ledger without buffering the body; it must not mutate the
|
|
11
|
+
* chunk and is called before the write, so a slow observer delays the
|
|
12
|
+
* forward. Implementations are expected to swallow their own errors.
|
|
13
|
+
*/
|
|
14
|
+
onChunk?: (chunk: Uint8Array) => void): Promise<boolean>;
|
|
8
15
|
//# sourceMappingURL=stream-failover-runtime.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stream-failover-runtime.d.ts","sourceRoot":"","sources":["../../../lib/request/stream-failover-runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,qCAAqC,CAAC;AAEtF,eAAO,MAAM,kBAAkB,aAW7B,CAAC;AAWH,wBAAgB,wBAAwB,CAAC,eAAe,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUzF;AAED,wBAAsB,WAAW,CAAC,CAAC,EAClC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,IAAI,EACrB,OAAO,EAAE,MAAM,GACb,OAAO,CAAC,CAAC,CAAC,CAmBZ;AAED,wBAAsB,aAAa,CAClC,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,MAAM,EACjB,QAAQ,SAAc,GACpB,OAAO,CAAC,MAAM,CAAC,CAuDjB;AAoBD,wBAAsB,wBAAwB,CAC7C,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,cAAc,EACnB,MAAM,EAAE,0BAA0B,EAClC,aAAa,EAAE,MAAM,IAAI,EACzB,oBAAoB,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"stream-failover-runtime.d.ts","sourceRoot":"","sources":["../../../lib/request/stream-failover-runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,qCAAqC,CAAC;AAEtF,eAAO,MAAM,kBAAkB,aAW7B,CAAC;AAWH,wBAAgB,wBAAwB,CAAC,eAAe,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUzF;AAED,wBAAsB,WAAW,CAAC,CAAC,EAClC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,IAAI,EACrB,OAAO,EAAE,MAAM,GACb,OAAO,CAAC,CAAC,CAAC,CAmBZ;AAED,wBAAsB,aAAa,CAClC,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,MAAM,EACjB,QAAQ,SAAc,GACpB,OAAO,CAAC,MAAM,CAAC,CAuDjB;AAoBD,wBAAsB,wBAAwB,CAC7C,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,cAAc,EACnB,MAAM,EAAE,0BAA0B,EAClC,aAAa,EAAE,MAAM,IAAI,EACzB,oBAAoB,EAAE,MAAM;AAC5B;;;;;GAKG;AACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,GACnC,OAAO,CAAC,OAAO,CAAC,CAsDlB"}
|
|
@@ -128,7 +128,14 @@ function waitForDrain(res) {
|
|
|
128
128
|
res.once("error", settle);
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
|
-
export async function forwardStreamingResponse(upstream, res, status, onStreamError, streamStallTimeoutMs
|
|
131
|
+
export async function forwardStreamingResponse(upstream, res, status, onStreamError, streamStallTimeoutMs,
|
|
132
|
+
/**
|
|
133
|
+
* Observe each forwarded chunk. Used to recover the upstream `usage` totals
|
|
134
|
+
* for the usage ledger without buffering the body; it must not mutate the
|
|
135
|
+
* chunk and is called before the write, so a slow observer delays the
|
|
136
|
+
* forward. Implementations are expected to swallow their own errors.
|
|
137
|
+
*/
|
|
138
|
+
onChunk) {
|
|
132
139
|
status.streamsStarted += 1;
|
|
133
140
|
res.writeHead(upstream.status, responseHeadersForClient(upstream.headers));
|
|
134
141
|
if (!upstream.body) {
|
|
@@ -149,6 +156,7 @@ export async function forwardStreamingResponse(upstream, res, status, onStreamEr
|
|
|
149
156
|
if (done)
|
|
150
157
|
break;
|
|
151
158
|
if (value && value.byteLength > 0) {
|
|
159
|
+
onChunk?.(value);
|
|
152
160
|
// If the response is already finished (clean end by a concurrent
|
|
153
161
|
// close-then-reader-cancel path), stop writing. Do NOT guard on
|
|
154
162
|
// res.destroyed here: a socket-error-during-backpressure scenario sets
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stream-failover.d.ts","sourceRoot":"","sources":["../../../lib/request/stream-failover.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,qBAAqB;IACrC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAkGD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CACpC,eAAe,EAAE,QAAQ,EACzB,mBAAmB,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,EACxF,OAAO,GAAE,qBAA0B,GACjC,QAAQ,
|
|
1
|
+
{"version":3,"file":"stream-failover.d.ts","sourceRoot":"","sources":["../../../lib/request/stream-failover.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,qBAAqB;IACrC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAkGD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CACpC,eAAe,EAAE,QAAQ,EACzB,mBAAmB,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,EACxF,OAAO,GAAE,qBAA0B,GACjC,QAAQ,CAuKV"}
|
|
@@ -117,6 +117,34 @@ export function withStreamingFailover(initialResponse, getFallbackResponse, opti
|
|
|
117
117
|
}
|
|
118
118
|
let closed = false;
|
|
119
119
|
let releaseCurrentReaderForCancel = null;
|
|
120
|
+
// Backpressure. `pump()` runs from `start()` as a free-running loop, so
|
|
121
|
+
// without a demand signal it enqueues every chunk upstream delivers as fast
|
|
122
|
+
// as it arrives and the whole stream buffers in memory for a slow consumer.
|
|
123
|
+
// `pull()` (and cancellation) release the loop; the sibling forwarder in
|
|
124
|
+
// stream-failover-runtime.ts gets the same guarantee from waitForDrain.
|
|
125
|
+
let demandWaiter = null;
|
|
126
|
+
let demandPending = false;
|
|
127
|
+
const signalDemand = () => {
|
|
128
|
+
if (demandWaiter) {
|
|
129
|
+
demandWaiter();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// `pull()` can fire before the loop parks. Latch it, or the next
|
|
133
|
+
// awaitDemand() would block on a signal that already happened.
|
|
134
|
+
demandPending = true;
|
|
135
|
+
};
|
|
136
|
+
const awaitDemand = () => {
|
|
137
|
+
if (demandPending || closed) {
|
|
138
|
+
demandPending = false;
|
|
139
|
+
return Promise.resolve();
|
|
140
|
+
}
|
|
141
|
+
return new Promise((resolve) => {
|
|
142
|
+
demandWaiter = () => {
|
|
143
|
+
demandWaiter = null;
|
|
144
|
+
resolve();
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
};
|
|
120
148
|
const body = new ReadableStream({
|
|
121
149
|
start(controller) {
|
|
122
150
|
let currentReader = initialResponse.body?.getReader() ?? null;
|
|
@@ -174,6 +202,13 @@ export function withStreamingFailover(initialResponse, getFallbackResponse, opti
|
|
|
174
202
|
if (result.value && result.value.byteLength > 0) {
|
|
175
203
|
emittedBytes += result.value.byteLength;
|
|
176
204
|
controller.enqueue(result.value);
|
|
205
|
+
// Park until the consumer asks for more. desiredSize is
|
|
206
|
+
// null once the controller is errored or closed, and the
|
|
207
|
+
// loop's own `closed` checks cover that case.
|
|
208
|
+
if (typeof controller.desiredSize === "number" &&
|
|
209
|
+
controller.desiredSize <= 0) {
|
|
210
|
+
await awaitDemand();
|
|
211
|
+
}
|
|
177
212
|
}
|
|
178
213
|
}
|
|
179
214
|
catch (error) {
|
|
@@ -206,8 +241,14 @@ export function withStreamingFailover(initialResponse, getFallbackResponse, opti
|
|
|
206
241
|
}
|
|
207
242
|
});
|
|
208
243
|
},
|
|
244
|
+
pull() {
|
|
245
|
+
signalDemand();
|
|
246
|
+
},
|
|
209
247
|
cancel() {
|
|
210
248
|
closed = true;
|
|
249
|
+
// Release a parked pump so it observes `closed` and unwinds instead
|
|
250
|
+
// of holding the upstream reader open for a demand that never comes.
|
|
251
|
+
signalDemand();
|
|
211
252
|
if (releaseCurrentReaderForCancel) {
|
|
212
253
|
void releaseCurrentReaderForCancel();
|
|
213
254
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account-check.d.ts","sourceRoot":"","sources":["../../../lib/runtime/account-check.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EACN,KAAK,gBAAgB,
|
|
1
|
+
{"version":3,"file":"account-check.d.ts","sourceRoot":"","sources":["../../../lib/runtime/account-check.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EACN,KAAK,gBAAgB,EAErB,KAAK,wBAAwB,EAE7B,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAChE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AACzE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAE7D,wBAAsB,sBAAsB,CAC3C,SAAS,EAAE,OAAO,EAClB,IAAI,EAAE;IACL,aAAa,EAAE,CACd,OAAO,EAAE,gBAAgB,GAAG,IAAI,KAC5B,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IACtC,YAAY,EAAE,MAAM,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IACrD,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;IAC3C,mBAAmB,EAAE,MAAM,OAAO,CAAC;QAClC,OAAO,EAAE,CAAC,CAAC;QACX,QAAQ,EAAE,wBAAwB,EAAE,CAAC;KACrC,CAAC,CAAC;IACH,8BAA8B,EAAE,CAAC,cAAc,EAAE;QAChD,OAAO,EAAE,CAAC,CAAC;QACX,QAAQ,EAAE,wBAAwB,EAAE,CAAC;KACrC,KAAK,wBAAwB,CAAC;IAC/B,2BAA2B,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,KAAK,OAAO,CAChE;QACA,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,WAAW,EAAE,MAAM,CAAC;QACpB,SAAS,CAAC,EAAE,MAAM,CAAC;KAClB,GACD,IAAI,GACJ,SAAS,CACX,CAAC;IACF,gBAAgB,EAAE,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,KAAK,MAAM,GAAG,SAAS,CAAC;IAC1E,8BAA8B,EAAE,CAC/B,MAAM,EAAE,eAAe,GAAG,SAAS,EACnC,gBAAgB,EAAE,MAAM,GAAG,SAAS,KAChC,OAAO,CAAC;IACb,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,KAAK,MAAM,GAAG,SAAS,CAAC;IACjE,mBAAmB,EAAE,CACpB,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,KACxB,MAAM,GAAG,SAAS,CAAC;IACxB,aAAa,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;IAC9D,yBAAyB,EAAE,CAC1B,OAAO,EAAE,OAAO,CAAC,WAAW,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAA;KAAE,CAAC,KAC7C,OAAO,CAAC;IACb,uBAAuB,EAAE,CAAC,MAAM,EAAE;QACjC,SAAS,EAAE,MAAM,CAAC;QAClB,WAAW,EAAE,MAAM,CAAC;KACpB,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAClC,uBAAuB,EAAE,CACxB,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,eAAe,EAAE,eAAe,GAAG,SAAS,EAC5C,cAAc,EAAE,MAAM,GAAG,SAAS,KAC9B,MAAM,GAAG,SAAS,CAAC;IACxB,oBAAoB,EAAE,CAAC,QAAQ,EAAE,kBAAkB,KAAK,MAAM,CAAC;IAC/D,yBAAyB,EAAE,CAC1B,OAAO,EAAE,gBAAgB,EACzB,aAAa,EAAE,SAAS,WAAW,EAAE,KACjC,IAAI,CAAC;IACV,cAAc,EAAE,SAAS,WAAW,EAAE,CAAC;IACvC,YAAY,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,6BAA6B,EAAE,MAAM,IAAI,CAAC;IAC1C,mBAAmB,EAAE,CAAC,OAAO,EAAE;QAC9B,OAAO,EAAE,CAAC,CAAC;QACX,QAAQ,EAAE,wBAAwB,EAAE,CAAC;KACrC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpB,+BAA+B,CAAC,EAAE,CACjC,cAAc,EAAE,gBAAgB,EAChC,cAAc,EAAE;QACf,OAAO,EAAE,CAAC,CAAC;QACX,QAAQ,EAAE,wBAAwB,EAAE,CAAC;KACrC,KACG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC,GACC,OAAO,CAAC,IAAI,CAAC,CA2Sf"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { maskEmail } from "../logger.js";
|
|
2
2
|
import { CODEX_UNAVAILABLE_PROBE_NOTE } from "../quota-probe.js";
|
|
3
3
|
import { isCodexUnavailableError } from "../errors.js";
|
|
4
|
-
import { reconcilePinnedAccountIndex, } from "../storage.js";
|
|
4
|
+
import { bumpStorageAffinityGeneration, reconcilePinnedAccountIndex, } from "../storage.js";
|
|
5
5
|
export async function runRuntimeAccountCheck(deepProbe, deps) {
|
|
6
6
|
const loadedStorage = await deps.hydrateEmails(await deps.loadAccounts());
|
|
7
7
|
const workingStorage = loadedStorage
|
|
@@ -195,6 +195,13 @@ export async function runRuntimeAccountCheck(deepProbe, deps) {
|
|
|
195
195
|
workingStorage.accounts = workingStorage.accounts.filter((account) => !state.removeFromActive.has(account.refreshToken));
|
|
196
196
|
deps.clampRuntimeActiveIndices(workingStorage, deps.MODEL_FAMILIES);
|
|
197
197
|
workingStorage.pinnedAccountIndex = reconcilePinnedAccountIndex(pinnedAccount, workingStorage.accounts);
|
|
198
|
+
// The filter can drop several accounts at once, shifting every index
|
|
199
|
+
// after each hole. Session affinity remembers accounts BY INDEX, so
|
|
200
|
+
// without a generation bump a live proxy keeps routing in-flight
|
|
201
|
+
// sessions to whatever now occupies the old slot — and this path is
|
|
202
|
+
// AUTOMATIC (revoked-token accounts are removed without a prompt), so
|
|
203
|
+
// nothing else invalidates the affinity map. See issue #474.
|
|
204
|
+
bumpStorageAffinityGeneration(workingStorage);
|
|
198
205
|
state.storageChanged = true;
|
|
199
206
|
}
|
|
200
207
|
const persistAccountAndFlaggedStorage = deps.persistAccountAndFlaggedStorage;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-rotation-proxy.d.ts","sourceRoot":"","sources":["../../lib/runtime-rotation-proxy.ts"],"names":[],"mappings":"AAGA,OAAO,EAIN,KAAK,cAAc,EACnB,MAAM,eAAe,CAAC;AA8BvB,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"runtime-rotation-proxy.d.ts","sourceRoot":"","sources":["../../lib/runtime-rotation-proxy.ts"],"names":[],"mappings":"AAGA,OAAO,EAIN,KAAK,cAAc,EACnB,MAAM,eAAe,CAAC;AA8BvB,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AA6CtE,OAAO,KAAK,EAKX,2BAA2B,EAC3B,0BAA0B,EAE1B,MAAM,oCAAoC,CAAC;AAe5C,YAAY,EACX,2BAA2B,EAC3B,0BAA0B,EAC1B,0BAA0B,GAC1B,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EACN,+BAA+B,EAC/B,0BAA0B,GAC1B,MAAM,kCAAkC,CAAC;AAC1C,YAAY,EAAE,0BAA0B,EAAE,MAAM,kCAAkC,CAAC;AACnF,OAAO,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAC;AACxE,OAAO,EACN,+BAA+B,EAC/B,8BAA8B,EAC9B,uBAAuB,EACvB,uBAAuB,GACvB,MAAM,oCAAoC,CAAC;AAC5C,YAAY,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AA8DtE,kFAAkF;AAClF,wBAAgB,qBAAqB,CACpC,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,WAAW,GAAG,OAAO,GAAG,cAAc,CAAC,GAAG;IACvE,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB,EACD,MAAM,EAAE,WAAW,EACnB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GACnB,MAAM,CAmBR;AA8bD;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CAC1C,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GACvC,MAAM,GAAG,IAAI,CAUf;AAED,wBAAsB,yBAAyB,CAC9C,OAAO,EAAE,2BAA2B,GAClC,OAAO,CAAC,0BAA0B,CAAC,CA4KrC"}
|