codex-multi-auth 2.8.6 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/README.md +1 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +34 -0
  5. package/dist/lib/account-policy.d.ts +3 -1
  6. package/dist/lib/account-policy.d.ts.map +1 -1
  7. package/dist/lib/account-policy.js +14 -1
  8. package/dist/lib/codex-manager/login-flow.js +7 -0
  9. package/dist/lib/codex-manager/login-menu-actions.d.ts.map +1 -1
  10. package/dist/lib/codex-manager/login-menu-actions.js +7 -1
  11. package/dist/lib/policy/runtime-policy.d.ts +7 -0
  12. package/dist/lib/policy/runtime-policy.d.ts.map +1 -1
  13. package/dist/lib/policy/runtime-policy.js +1 -0
  14. package/dist/lib/request/fetch-helpers.d.ts +7 -0
  15. package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
  16. package/dist/lib/request/fetch-helpers.js +1 -1
  17. package/dist/lib/request/rate-limit-decision.d.ts +27 -0
  18. package/dist/lib/request/rate-limit-decision.d.ts.map +1 -1
  19. package/dist/lib/request/rate-limit-decision.js +117 -2
  20. package/dist/lib/request/response-handler.d.ts +3 -1
  21. package/dist/lib/request/response-handler.d.ts.map +1 -1
  22. package/dist/lib/request/response-handler.js +26 -5
  23. package/dist/lib/request/stream-failover-runtime.d.ts +8 -1
  24. package/dist/lib/request/stream-failover-runtime.d.ts.map +1 -1
  25. package/dist/lib/request/stream-failover-runtime.js +9 -1
  26. package/dist/lib/request/stream-failover.d.ts.map +1 -1
  27. package/dist/lib/request/stream-failover.js +41 -0
  28. package/dist/lib/runtime/account-check.d.ts.map +1 -1
  29. package/dist/lib/runtime/account-check.js +8 -1
  30. package/dist/lib/runtime/account-status.d.ts +39 -10
  31. package/dist/lib/runtime/account-status.d.ts.map +1 -1
  32. package/dist/lib/runtime/account-status.js +47 -42
  33. package/dist/lib/runtime-rotation-proxy.d.ts.map +1 -1
  34. package/dist/lib/runtime-rotation-proxy.js +73 -8
  35. package/dist/lib/storage.d.ts +19 -0
  36. package/dist/lib/storage.d.ts.map +1 -1
  37. package/dist/lib/storage.js +32 -0
  38. package/dist/lib/usage/index.d.ts +2 -0
  39. package/dist/lib/usage/index.d.ts.map +1 -1
  40. package/dist/lib/usage/index.js +2 -0
  41. package/dist/lib/usage/stream-usage-deferral.d.ts +39 -0
  42. package/dist/lib/usage/stream-usage-deferral.d.ts.map +1 -0
  43. package/dist/lib/usage/stream-usage-deferral.js +54 -0
  44. package/dist/lib/usage/usage-extraction.d.ts +42 -0
  45. package/dist/lib/usage/usage-extraction.d.ts.map +1 -0
  46. package/dist/lib/usage/usage-extraction.js +178 -0
  47. package/package.json +1 -1
  48. package/scripts/codex.js +300 -82
@@ -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":"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"}
1
+ {"version":3,"file":"account-check.d.ts","sourceRoot":"","sources":["../../../lib/runtime/account-check.ts"],"names":[],"mappings":"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"}
@@ -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;
@@ -7,22 +7,51 @@ export declare function resolveActiveIndex(storage: {
7
7
  export declare function getRateLimitResetTimeForFamily(account: {
8
8
  rateLimitResetTimes?: Record<string, number | undefined>;
9
9
  }, now: number, family: ModelFamily): number | null;
10
+ export interface AccountRecoveryBounds {
11
+ /**
12
+ * The moment the account becomes usable again: `rateLimitAtMs` widened by
13
+ * any active cooldown, because the account stays skipped while ANY gating
14
+ * record is active. Null when nothing bounds recovery.
15
+ */
16
+ recoveryAtMs: number | null;
17
+ /**
18
+ * The rate-limit records' own contribution to `recoveryAtMs`, cooldowns
19
+ * excluded. The pinned-503 uses it to decide whether the deadline it
20
+ * advertises is in fact the rate limit's own reset — with a breaker or
21
+ * cooldown ending later, the full recovery bound outlives the rate limit
22
+ * and must not be worded as its reset. Null when no rate-limit record
23
+ * gates the request.
24
+ */
25
+ rateLimitAtMs: number | null;
26
+ }
10
27
  /**
11
- * The moment the account becomes usable again for a `family`/`model`
12
- * request: the LATEST bound among the records that actually gate that
13
- * request plus any active cooldown. Two deliberate differences from
14
- * getRateLimitResetTimeForFamily, whose earliest-reset answer feeds wait
15
- * displays: the account stays skipped while ANY gating record is active,
16
- * so the earliest reset would send clients back into a 503 — and only the
17
- * keys selection consults (`family`, plus `family:<model>` when a model is
18
- * known; see isRateLimitedForFamily) may contribute, because another
19
- * model's record does not block this request and would overstate its
20
- * recovery. Null when nothing bounds recovery.
28
+ * Both recovery bounds for a `family`/`model` request, from ONE pass over
29
+ * `rateLimitResetTimes` against a single `now`.
30
+ *
31
+ * Two deliberate differences from getRateLimitResetTimeForFamily, whose
32
+ * earliest-reset answer feeds wait displays: the latest bound wins, because
33
+ * the earliest reset would send clients back into a 503 — and only the keys
34
+ * selection consults (`family`, plus `family:<model>` when a model is known;
35
+ * see isRateLimitedForFamily) may contribute, because another model's record
36
+ * does not block this request and would overstate its recovery.
37
+ *
38
+ * A caller that needs both bounds must take them from one call. Measuring
39
+ * them separately lets a record expire between the two walks, which reports
40
+ * a rate-limited account as if a cooldown bounded its recovery.
21
41
  */
42
+ export declare function getAccountRecoveryBoundsForFamily(account: {
43
+ rateLimitResetTimes?: Record<string, number | undefined>;
44
+ coolingDownUntil?: number;
45
+ }, now: number, family: ModelFamily, model?: string | null): AccountRecoveryBounds;
46
+ /** The `recoveryAtMs` bound alone; see getAccountRecoveryBoundsForFamily. */
22
47
  export declare function getAccountRecoveryTimeForFamily(account: {
23
48
  rateLimitResetTimes?: Record<string, number | undefined>;
24
49
  coolingDownUntil?: number;
25
50
  }, now: number, family: ModelFamily, model?: string | null): number | null;
51
+ /** The `rateLimitAtMs` bound alone; see getAccountRecoveryBoundsForFamily. */
52
+ export declare function getRateLimitRecoveryTimeForFamily(account: {
53
+ rateLimitResetTimes?: Record<string, number | undefined>;
54
+ }, now: number, family: ModelFamily, model?: string | null): number | null;
26
55
  export declare function formatRateLimitEntry(account: {
27
56
  rateLimitResetTimes?: Record<string, number | undefined>;
28
57
  }, now: number, formatWaitTime: (ms: number) => string, family?: ModelFamily): string | null;
@@ -1 +1 @@
1
- {"version":3,"file":"account-status.d.ts","sourceRoot":"","sources":["../../../lib/runtime/account-status.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD,wBAAgB,kBAAkB,CACjC,OAAO,EAAE;IACR,WAAW,EAAE,MAAM,CAAC;IACpB,mBAAmB,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC;IAC3D,QAAQ,EAAE,OAAO,EAAE,CAAC;CACpB,EACD,MAAM,GAAE,WAAqB,GAC3B,MAAM,CAOR;AAED,wBAAgB,8BAA8B,CAC7C,OAAO,EAAE;IAAE,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;CAAE,EACrE,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,WAAW,GACjB,MAAM,GAAG,IAAI,CAgBf;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,+BAA+B,CAC9C,OAAO,EAAE;IACR,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACzD,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC1B,EACD,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,WAAW,EACnB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GACnB,MAAM,GAAG,IAAI,CAcf;AAED,wBAAgB,oBAAoB,CACnC,OAAO,EAAE;IAAE,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;CAAE,EACrE,GAAG,EAAE,MAAM,EACX,cAAc,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,EACtC,MAAM,GAAE,WAAqB,GAC3B,MAAM,GAAG,IAAI,CAMf;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,6BAA6B,CAC5C,OAAO,EAAE;IAAE,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;CAAE,EACrE,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,MAAM,GACX,MAAM,GAAG,IAAI,CAcf"}
1
+ {"version":3,"file":"account-status.d.ts","sourceRoot":"","sources":["../../../lib/runtime/account-status.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD,wBAAgB,kBAAkB,CACjC,OAAO,EAAE;IACR,WAAW,EAAE,MAAM,CAAC;IACpB,mBAAmB,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC;IAC3D,QAAQ,EAAE,OAAO,EAAE,CAAC;CACpB,EACD,MAAM,GAAE,WAAqB,GAC3B,MAAM,CAOR;AAED,wBAAgB,8BAA8B,CAC7C,OAAO,EAAE;IAAE,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;CAAE,EACrE,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,WAAW,GACjB,MAAM,GAAG,IAAI,CAgBf;AAeD,MAAM,WAAW,qBAAqB;IACrC;;;;OAIG;IACH,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B;;;;;;;OAOG;IACH,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iCAAiC,CAChD,OAAO,EAAE;IACR,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACzD,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC1B,EACD,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,WAAW,EACnB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GACnB,qBAAqB,CAevB;AAED,6EAA6E;AAC7E,wBAAgB,+BAA+B,CAC9C,OAAO,EAAE;IACR,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACzD,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC1B,EACD,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,WAAW,EACnB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GACnB,MAAM,GAAG,IAAI,CAGf;AAED,8EAA8E;AAC9E,wBAAgB,iCAAiC,CAChD,OAAO,EAAE;IACR,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;CACzD,EACD,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,WAAW,EACnB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GACnB,MAAM,GAAG,IAAI,CAGf;AAED,wBAAgB,oBAAoB,CACnC,OAAO,EAAE;IAAE,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;CAAE,EACrE,GAAG,EAAE,MAAM,EACX,cAAc,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,EACtC,MAAM,GAAE,WAAqB,GAC3B,MAAM,GAAG,IAAI,CAMf;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,6BAA6B,CAC5C,OAAO,EAAE;IAAE,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;CAAE,EACrE,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,MAAM,GACX,MAAM,GAAG,IAAI,CAGf"}
@@ -26,36 +26,54 @@ export function getRateLimitResetTimeForFamily(account, now, family) {
26
26
  }
27
27
  return minReset;
28
28
  }
29
+ /** The value when it is still a live bound at `now`, otherwise null. */
30
+ function activeBound(value, now) {
31
+ if (typeof value !== "number" || !Number.isFinite(value))
32
+ return null;
33
+ return value > now ? value : null;
34
+ }
35
+ /** The later of two bounds, treating null as "does not bound". */
36
+ function laterBound(a, b) {
37
+ if (a === null)
38
+ return b;
39
+ if (b === null)
40
+ return a;
41
+ return a > b ? a : b;
42
+ }
29
43
  /**
30
- * The moment the account becomes usable again for a `family`/`model`
31
- * request: the LATEST bound among the records that actually gate that
32
- * request plus any active cooldown. Two deliberate differences from
33
- * getRateLimitResetTimeForFamily, whose earliest-reset answer feeds wait
34
- * displays: the account stays skipped while ANY gating record is active,
35
- * so the earliest reset would send clients back into a 503 — and only the
36
- * keys selection consults (`family`, plus `family:<model>` when a model is
37
- * known; see isRateLimitedForFamily) may contribute, because another
38
- * model's record does not block this request and would overstate its
39
- * recovery. Null when nothing bounds recovery.
44
+ * Both recovery bounds for a `family`/`model` request, from ONE pass over
45
+ * `rateLimitResetTimes` against a single `now`.
46
+ *
47
+ * Two deliberate differences from getRateLimitResetTimeForFamily, whose
48
+ * earliest-reset answer feeds wait displays: the latest bound wins, because
49
+ * the earliest reset would send clients back into a 503 — and only the keys
50
+ * selection consults (`family`, plus `family:<model>` when a model is known;
51
+ * see isRateLimitedForFamily) may contribute, because another model's record
52
+ * does not block this request and would overstate its recovery.
53
+ *
54
+ * A caller that needs both bounds must take them from one call. Measuring
55
+ * them separately lets a record expire between the two walks, which reports
56
+ * a rate-limited account as if a cooldown bounded its recovery.
40
57
  */
41
- export function getAccountRecoveryTimeForFamily(account, now, family, model) {
42
- let latest = null;
43
- const consider = (value) => {
44
- if (typeof value !== "number" || !Number.isFinite(value))
45
- return;
46
- if (value <= now)
47
- return;
48
- if (latest === null || value > latest)
49
- latest = value;
50
- };
58
+ export function getAccountRecoveryBoundsForFamily(account, now, family, model) {
51
59
  const times = account.rateLimitResetTimes;
52
- if (times) {
53
- consider(times[getQuotaKey(family)]);
54
- if (model)
55
- consider(times[getQuotaKey(family, model)]);
56
- }
57
- consider(account.coolingDownUntil);
58
- return latest;
60
+ const rateLimitAtMs = times
61
+ ? laterBound(activeBound(times[getQuotaKey(family)], now), model ? activeBound(times[getQuotaKey(family, model)], now) : null)
62
+ : null;
63
+ return {
64
+ rateLimitAtMs,
65
+ recoveryAtMs: laterBound(rateLimitAtMs, activeBound(account.coolingDownUntil, now)),
66
+ };
67
+ }
68
+ /** The `recoveryAtMs` bound alone; see getAccountRecoveryBoundsForFamily. */
69
+ export function getAccountRecoveryTimeForFamily(account, now, family, model) {
70
+ return getAccountRecoveryBoundsForFamily(account, now, family, model)
71
+ .recoveryAtMs;
72
+ }
73
+ /** The `rateLimitAtMs` bound alone; see getAccountRecoveryBoundsForFamily. */
74
+ export function getRateLimitRecoveryTimeForFamily(account, now, family, model) {
75
+ return getAccountRecoveryBoundsForFamily(account, now, family, model)
76
+ .rateLimitAtMs;
59
77
  }
60
78
  export function formatRateLimitEntry(account, now, formatWaitTime, family = "codex") {
61
79
  const resetAt = getRateLimitResetTimeForFamily(account, now, family);
@@ -86,19 +104,6 @@ export function formatRateLimitEntry(account, now, formatWaitTime, family = "cod
86
104
  * model key applies and should keep the family-wide union above.
87
105
  */
88
106
  export function getRateLimitResetTimeForModel(account, now, family, model) {
89
- const times = account.rateLimitResetTimes;
90
- if (!times)
91
- return null;
92
- let latest = null;
93
- const consider = (value) => {
94
- if (typeof value !== "number" || !Number.isFinite(value))
95
- return;
96
- if (value <= now)
97
- return;
98
- if (latest === null || value > latest)
99
- latest = value;
100
- };
101
- consider(times[getQuotaKey(family)]);
102
- consider(times[getQuotaKey(family, model)]);
103
- return latest;
107
+ return getAccountRecoveryBoundsForFamily(account, now, family, model)
108
+ .rateLimitAtMs;
104
109
  }
@@ -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;AA4CtE,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"}
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"}
@@ -8,6 +8,7 @@ import { getModelFamily } from "./prompts/codex.js";
8
8
  import { CURRENT_CODEX_MODEL } from "./request/helpers/model-map.js";
9
9
  import { mutateRuntimeObservabilitySnapshot, recordRuntimeAccountRecovery, recordRuntimePoolExhaustion, } from "./runtime/runtime-observability.js";
10
10
  import { createRuntimeUsageRecorder, evaluateRuntimePolicy, loadRuntimePolicyState, } from "./policy/runtime-policy.js";
11
+ import { createUsageStreamScanner } from "./usage/usage-extraction.js";
11
12
  import { isWorkspaceDisabledError } from "./request/fetch-helpers.js";
12
13
  import { PreemptiveQuotaScheduler, readQuotaSchedulerSnapshot, } from "./preemptive-quota-scheduler.js";
13
14
  import { createLogger, maskString, runWithCorrelationId } from "./logger.js";
@@ -15,7 +16,7 @@ import { CodexValidationError } from "./errors.js";
15
16
  import { normalizeEmailKey } from "./storage/identity.js";
16
17
  import { buildPinnedUnavailableErrorBody, buildTokenInvalidationBody, extractErrorCodeFromBody, isTokenInvalidationError, normalizeExhaustionStatus, parseRetryAfterBodyMs, parseRetryAfterHeaderMs, } from "./request/rate-limit-decision.js";
17
18
  import { forwardStreamingResponse, HOP_BY_HOP_HEADERS, readErrorBody, responseHeadersForClient, withTimeout, } from "./request/stream-failover-runtime.js";
18
- import { getAccountRecoveryTimeForFamily } from "./runtime/account-status.js";
19
+ import { getAccountRecoveryBoundsForFamily } from "./runtime/account-status.js";
19
20
  import { chooseAccount } from "./runtime/rotation-account-selection.js";
20
21
  import { createRotationProxyState, recoverStaleRuntimeState, } from "./runtime/rotation-proxy-state.js";
21
22
  import { readStorageMetaFromDisk } from "./runtime/rotation-storage-meta.js";
@@ -936,11 +937,35 @@ async function handleRequestInner(state, req, res, traceId) {
936
937
  upstream = await withTimeout(state.fetchImpl(upstreamUrl, upstreamRequestInit), state.fetchTimeoutMs, () => fetchAbortController.abort(), `upstream fetch timed out after ${state.fetchTimeoutMs}ms`);
937
938
  }
938
939
  catch (error) {
939
- state.status.lastError = error instanceof Error ? error.message : String(error);
940
+ // errors-logging-08: a custom fetchImpl, a proxy agent, or an undici
941
+ // cause chain can embed the request URL or credential material in the
942
+ // raw message, so mask before it reaches any state.status consumer.
943
+ const transportError = maskString(error instanceof Error ? error.message : String(error));
944
+ state.status.lastError = transportError;
945
+ // errors-logging-01: give the failure a structured, trace-correlated
946
+ // line instead of only a last-write-wins status string. Nothing else
947
+ // on this path reaches proxyLog, because the request does not throw.
948
+ proxyLog.error("upstream transport failure", {
949
+ traceId,
950
+ code: "codex_runtime_rotation_transport_error",
951
+ error: transportError,
952
+ });
940
953
  accountManager.refundToken(refreshed.account, context.family, context.model);
941
- accountManager.recordFailure(refreshed.account, context.family, context.model);
954
+ // A pre-header transport exception is a property of the network path,
955
+ // not of this account's credentials or quota, so it must NOT feed the
956
+ // account's circuit breaker / health tracker: that is what would
957
+ // otherwise retire a healthy account for an outage it did not cause.
958
+ // See #677.
959
+ //
960
+ // The short timed cooldown IS still applied. It is self-healing, it
961
+ // keeps an account whose upstream hangs from stalling 1/N of every
962
+ // later request for the full fetch timeout, and it is what gives the
963
+ // exhaustion 503 a non-zero `retry_after_ms` to back the client off
964
+ // with (`getMinWaitTimeForFamily` returns 0 while any account is
965
+ // still selectable).
942
966
  accountManager.markAccountCoolingDown(refreshed.account, state.networkErrorCooldownMs, "network-error");
943
967
  accountManager.saveToDiskDebounced();
968
+ accountSkipReasons.set(refreshed.account.index, "network-error");
944
969
  exhaustionReason = "network-error";
945
970
  transientAttempts += 1;
946
971
  transientExhaustionReason = "network-error";
@@ -1152,17 +1177,35 @@ async function handleRequestInner(state, req, res, traceId) {
1152
1177
  state.lastGlobalSwitchAt = state.now();
1153
1178
  }
1154
1179
  await persistRuntimeActiveAccount(accountManager, refreshed.account, context.family, isPinned && refreshed.account.index === pinnedIndex, state.schedulingStrategy);
1180
+ // Recover the upstream token counts as the body streams past. Without
1181
+ // them every ledger row lands with all-zero tokens and a zero cost, so
1182
+ // evaluateBudgetGuard compares `0 >= limit` for maxTokens/maxCostUsd
1183
+ // and those caps never fire — `budget set --cost 50` would allow
1184
+ // unlimited spend, with only --requests actually enforced.
1185
+ const usageScanner = createUsageStreamScanner({
1186
+ contentType: upstream.headers.get("content-type"),
1187
+ });
1155
1188
  const forwarded = await forwardStreamingResponse(upstream, res, state.status, () => {
1189
+ // Deliberately NOT the pre-header transport policy above,
1190
+ // which skips recordFailure (#677). By this point the
1191
+ // upstream accepted the request and began responding, so a
1192
+ // broken stream is a signal about THIS account's session
1193
+ // (upstream tearing it down) rather than about the shared
1194
+ // network path, and it should still credit the breaker.
1156
1195
  accountManager.recordFailure(refreshed.account, context.family, context.model);
1157
1196
  accountManager.markAccountCoolingDown(refreshed.account, state.networkErrorCooldownMs, "network-error");
1158
1197
  state.sessionAffinityStore?.forgetSession(context.sessionKey);
1159
1198
  accountManager.saveToDiskDebounced();
1160
- }, state.streamStallTimeoutMs);
1199
+ }, state.streamStallTimeoutMs, usageScanner.push);
1200
+ // A stream that broke mid-flight still bills for whatever the upstream
1201
+ // reported before the break, so record the counts on both outcomes.
1202
+ const usageTokens = usageScanner.result();
1161
1203
  await usageRecorder.record({
1162
1204
  outcome: forwarded ? "success" : "failure",
1163
1205
  statusCode: upstream.status,
1164
1206
  errorCode: forwarded ? null : "stream_forward_failed",
1165
1207
  account: refreshed.account,
1208
+ ...(usageTokens ?? {}),
1166
1209
  });
1167
1210
  return;
1168
1211
  }
@@ -1219,23 +1262,45 @@ async function handleRequestInner(state, req, res, traceId) {
1219
1262
  // recovery — and with several overlapping records the account stays
1220
1263
  // skipped until the LAST one expires, so the latest bound is the one
1221
1264
  // worth advertising.
1222
- const pinnedStateRecoveryAtMs = pinnedAccount === null
1265
+ //
1266
+ // One clock for the whole body. The recovery bound and the
1267
+ // rate-limit bound are compared against each other below, so
1268
+ // reading state.now() per lookup would let a record expire between
1269
+ // them and report a rate-limited pin as bounded by something else.
1270
+ const evaluatedAtMs = state.now();
1271
+ // Both bounds come from a single pass over the account's records.
1272
+ const pinnedRecoveryBounds = pinnedAccount === null
1223
1273
  ? null
1224
- : getAccountRecoveryTimeForFamily(pinnedAccount, state.now(), context.family, context.model);
1274
+ : getAccountRecoveryBoundsForFamily(pinnedAccount, evaluatedAtMs, context.family, context.model);
1275
+ const pinnedStateRecoveryAtMs = pinnedRecoveryBounds?.recoveryAtMs ?? null;
1225
1276
  // An open circuit outlives the short failure cooldowns that tripped
1226
1277
  // it; its deadline lives in the breaker, not the account record.
1227
1278
  const pinnedCircuitRecoveryAtMs = pinnedAccount === null
1228
1279
  ? null
1229
- : accountManager.getCircuitRecoveryTime(pinnedAccount, state.now());
1280
+ : accountManager.getCircuitRecoveryTime(pinnedAccount, evaluatedAtMs);
1230
1281
  const pinnedResetAtMs = pinnedBlockedPermanently ||
1231
1282
  (pinnedStateRecoveryAtMs === null && pinnedCircuitRecoveryAtMs === null)
1232
1283
  ? null
1233
1284
  : Math.max(pinnedStateRecoveryAtMs ?? 0, pinnedCircuitRecoveryAtMs ?? 0);
1285
+ // The message only words the recovery deadline as a rate-limit reset
1286
+ // when the rate-limit records are in fact what supplies it — a
1287
+ // breaker or cooldown can end later and then bounds it instead.
1288
+ const pinnedRateLimitResetAtMs = pinnedRecoveryBounds?.rateLimitAtMs ?? null;
1289
+ const recoveryBound = pinnedResetAtMs !== null &&
1290
+ pinnedRateLimitResetAtMs !== null &&
1291
+ pinnedRateLimitResetAtMs >= pinnedResetAtMs
1292
+ ? "rate-limit"
1293
+ : "other";
1234
1294
  const errorBody = buildPinnedUnavailableErrorBody(pinnedIndex, accountSkipReasons, {
1235
1295
  // typeof check so a forced index of 0 still reads as forced.
1236
1296
  pinSource: typeof state.forcedAccountIndex === "number" ? "forced" : "manual",
1237
1297
  resetAtMs: pinnedResetAtMs,
1238
- now: state.now(),
1298
+ recoveryBound,
1299
+ // The recorded verdict above is the retry loop's
1300
+ // "already-attempted" on the dominant path; the re-read runtime
1301
+ // state is what the operator-facing sentence should describe.
1302
+ currentSkipReason: pinnedCurrentSkipReason,
1303
+ now: evaluatedAtMs,
1239
1304
  });
1240
1305
  if (errorBody.reason === null) {
1241
1306
  state.status.lastError = `pinned-503 missing skip reason (pinnedIndex=${pinnedIndex})`;
@@ -83,6 +83,25 @@ export declare function normalizeAccountStorage(data: unknown): AccountStorageV3
83
83
  * helper only narrows the lost-update window. See issue #474.
84
84
  */
85
85
  export declare function readAffinityGenerationFromDisk(path: string): number;
86
+ /**
87
+ * Advance the session-affinity generation so a running runtime proxy drops its
88
+ * sticky session→account bindings on the next request.
89
+ *
90
+ * Session affinity remembers an account by ITS INDEX, so any mutation that
91
+ * reshuffles the pool (removing an account, restoring a backup) silently
92
+ * re-points every live session at a different account until the generation
93
+ * changes. `switch`/`best`/`unpin` already bump it; the reshuffling paths are
94
+ * the ones that need it most, because they move every index at once.
95
+ *
96
+ * Re-reads the on-disk generation first so concurrent CLI processes do not lose
97
+ * increments via lost-update on the load+mutate pair. `Math.max` keeps the
98
+ * counter monotonic: an extra bump only costs one additional affinity
99
+ * invalidation, while a missed bump lets the proxy cling to the wrong account.
100
+ * See issue #474.
101
+ */
102
+ export declare function bumpStorageAffinityGeneration(storage: {
103
+ affinityGeneration?: number;
104
+ }, storagePath?: string | null): number;
86
105
  /**
87
106
  * Synchronously reads both the top-level `pinnedAccountIndex` and
88
107
  * `affinityGeneration` fields from the accounts storage file. Used by
@@ -1 +1 @@
1
- {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../lib/storage.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../lib/storage.ts"],"names":[],"mappings":"AA+BA,OAAO,EACN,KAAK,cAAc,EAGnB,KAAK,iBAAiB,EAEtB,MAAM,8BAA8B,CAAC;AAkBtC,OAAO,EAEN,KAAK,oBAAoB,EACzB,MAAM,qBAAqB,CAAC;AAe7B,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EACN,sBAAsB,EACtB,cAAc,GACd,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACN,qBAAqB,EACrB,iBAAiB,GACjB,MAAM,uBAAuB,CAAC;AAa/B,OAAO,KAAK,EACX,iBAAiB,EACjB,gBAAgB,EAChB,cAAc,EACd,wBAAwB,EACxB,uBAAuB,EACvB,gBAAgB,EAChB,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EAEN,KAAK,kBAAkB,EACvB,MAAM,4BAA4B,CAAC;AA2CpC,YAAY,EACX,oBAAoB,EACpB,cAAc,EACd,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,wBAAwB,EACxB,uBAAuB,EACvB,cAAc,EACd,iBAAiB,EACjB,kBAAkB,GAClB,CAAC;AAcF,KAAK,WAAW,GAAG;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAiBF,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE9D;AAuWD,wBAAgB,4BAA4B,IAAI,MAAM,CAErD;AAED,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CA4C/D;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAO9D;AAED;;;GAGG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAMvC;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAarE;AAED,wBAAsB,yBAAyB,CAC9C,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAC7B,OAAO,CAAC,gBAAgB,CAAC,CAe3B;AAED,wBAAsB,iBAAiB,CACtC,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GAC3B,OAAO,CAAC,MAAM,CAAC,CAQjB;AAED,wBAAgB,sBAAsB,IAAI,MAAM,CAE/C;AA6LD,KAAK,mBAAmB,GAAG;IAC1B,wCAAwC,CAAC,EAAE,OAAO,CAAC;CACnD,CAAC;AA8IF,wBAAgB,wBAAwB,CACvC,CAAC,SAAS,IAAI,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,GAAG,cAAc,CAAC,EAEnE,QAAQ,EAAE,SAAS,CAAC,EAAE,EACtB,SAAS,EAAE,IAAI,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,GAAG,cAAc,CAAC,EACpE,OAAO,GAAE,mBAAwB,GAC/B,MAAM,GAAG,SAAS,CAkBpB;AAED;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CAC1C,aAAa,EACV,IAAI,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,GAAG,cAAc,CAAC,GACzD,SAAS,EACZ,YAAY,EAAE,SAAS,IAAI,CAC1B,WAAW,EACX,WAAW,GAAG,OAAO,GAAG,cAAc,CACtC,EAAE,GACD,MAAM,GAAG,SAAS,CAGpB;AAED,wBAAgB,4BAA4B,CAC3C,CAAC,SAAS,IAAI,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,GAAG,cAAc,CAAC,EAEnE,QAAQ,EAAE,SAAS,CAAC,EAAE,EACtB,SAAS,EAAE,IAAI,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,GAAG,cAAc,CAAC,EACpE,aAAa,SAAI,GACf,MAAM,CAOR;AAsCD;;;GAGG;AACH,wBAAgB,mBAAmB,CAClC,CAAC,SAAS;IACT,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,EACA,QAAQ,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE,CAEpB;AAED,wBAAgB,0BAA0B,CACzC,CAAC,SAAS;IACT,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,EACA,QAAQ,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE,CAEpB;AAoBD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACtC,IAAI,EAAE,OAAO,GACX,gBAAgB,GAAG,IAAI,CAoJzB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAoBnE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,6BAA6B,CAC5C,OAAO,EAAE;IAAE,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAAE,EACxC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GACzB,MAAM,CAaR;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG;IACpD,kBAAkB,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,kBAAkB,EAAE,MAAM,CAAC;CAC3B,CA8BA;AAED;;;;GAIG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAErE;AAED,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,cAAc,CAAC,CAajE;AAED,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,iBAAiB,CAAC,CASvE;AAED,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,oBAAoB,CAAC,CAiG1E;AAyaD,wBAAsB,6BAA6B,CAAC,CAAC,EACpD,OAAO,EAAE,CACR,OAAO,EAAE,gBAAgB,GAAG,IAAI,EAChC,OAAO,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,OAAO,CAAC,IAAI,CAAC,KACjD,OAAO,CAAC,CAAC,CAAC,GACb,OAAO,CAAC,CAAC,CAAC,CAMZ;AAED,wBAAsB,uCAAuC,CAAC,CAAC,EAC9D,OAAO,EAAE,CACR,OAAO,EAAE,gBAAgB,GAAG,IAAI,EAChC,OAAO,EAAE,CACR,cAAc,EAAE,gBAAgB,EAChC,cAAc,EAAE,uBAAuB,KACnC,OAAO,CAAC,IAAI,CAAC,EAClB,cAAc,EAAE,uBAAuB,KACnC,OAAO,CAAC,CAAC,CAAC,GACb,OAAO,CAAC,CAAC,CAAC,CAgDZ;AAED,wBAAsB,6BAA6B,CAAC,CAAC,EACpD,OAAO,EAAE,CACR,OAAO,EAAE,uBAAuB,EAChC,OAAO,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,OAAO,CAAC,IAAI,CAAC,KACxD,OAAO,CAAC,CAAC,CAAC,GACb,OAAO,CAAC,CAAC,CAAC,CAiCZ;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAM3E;AAED;;;GAGG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAcnD;AAED,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,uBAAuB,CAAC,CAgD5E;AAyBD,wBAAsB,mBAAmB,CACxC,OAAO,EAAE,uBAAuB,GAC9B,OAAO,CAAC,IAAI,CAAC,CAMf;AAED,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC,CAa1D;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CACnC,QAAQ,EAAE,MAAM,EAChB,KAAK,UAAO,EACZ,YAAY,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,GAC3D,OAAO,CAAC,IAAI,CAAC,CAgBf;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CACnC,QAAQ,EAAE,MAAM,GACd,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAe/D"}
@@ -951,6 +951,38 @@ export function readAffinityGenerationFromDisk(path) {
951
951
  return 0;
952
952
  }
953
953
  }
954
+ /**
955
+ * Advance the session-affinity generation so a running runtime proxy drops its
956
+ * sticky session→account bindings on the next request.
957
+ *
958
+ * Session affinity remembers an account by ITS INDEX, so any mutation that
959
+ * reshuffles the pool (removing an account, restoring a backup) silently
960
+ * re-points every live session at a different account until the generation
961
+ * changes. `switch`/`best`/`unpin` already bump it; the reshuffling paths are
962
+ * the ones that need it most, because they move every index at once.
963
+ *
964
+ * Re-reads the on-disk generation first so concurrent CLI processes do not lose
965
+ * increments via lost-update on the load+mutate pair. `Math.max` keeps the
966
+ * counter monotonic: an extra bump only costs one additional affinity
967
+ * invalidation, while a missed bump lets the proxy cling to the wrong account.
968
+ * See issue #474.
969
+ */
970
+ export function bumpStorageAffinityGeneration(storage, storagePath) {
971
+ let diskGeneration = 0;
972
+ try {
973
+ const path = storagePath ?? getStoragePath();
974
+ if (path)
975
+ diskGeneration = readAffinityGenerationFromDisk(path);
976
+ }
977
+ catch {
978
+ // No resolvable storage path (never configured). The in-memory counter
979
+ // is still bumped below; only the lost-update guard is unavailable.
980
+ }
981
+ const inMemoryGeneration = storage.affinityGeneration ?? 0;
982
+ const next = Math.max(inMemoryGeneration, diskGeneration) + 1;
983
+ storage.affinityGeneration = next;
984
+ return next;
985
+ }
954
986
  /**
955
987
  * Synchronously reads both the top-level `pinnedAccountIndex` and
956
988
  * `affinityGeneration` fields from the accounts storage file. Used by
@@ -2,4 +2,6 @@ export * from "./types.js";
2
2
  export * from "./pricing.js";
3
3
  export * from "./redaction.js";
4
4
  export * from "./ledger.js";
5
+ export * from "./usage-extraction.js";
6
+ export * from "./stream-usage-deferral.js";
5
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../lib/usage/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../lib/usage/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,uBAAuB,CAAC;AACtC,cAAc,4BAA4B,CAAC"}
@@ -2,3 +2,5 @@ export * from "./types.js";
2
2
  export * from "./pricing.js";
3
3
  export * from "./redaction.js";
4
4
  export * from "./ledger.js";
5
+ export * from "./usage-extraction.js";
6
+ export * from "./stream-usage-deferral.js";
@@ -0,0 +1,39 @@
1
+ import type { UsageTokenCounts } from "./types.js";
2
+ export interface StreamUsageDeferral<TRow> {
3
+ /** Latest counts reported by upstream, or null if none have arrived. */
4
+ captured: () => UsageTokenCounts | null;
5
+ /** Feed upstream-reported counts. Writes a deferred row immediately. */
6
+ onUsage: (usage: UsageTokenCounts) => void;
7
+ /**
8
+ * Hand over a row to be written once upstream reports its counts, or after
9
+ * `fallbackMs` without them. Only the first pending row is held.
10
+ */
11
+ defer: (completion: TRow) => void;
12
+ }
13
+ /**
14
+ * Holds a usage-ledger row open until a streaming response reports its tokens.
15
+ *
16
+ * A streaming request's row would otherwise be written when the request handler
17
+ * returns its Response — before the client has read a byte, and therefore
18
+ * before the upstream `usage` event exists. The ledger is append-only, so that
19
+ * row could never be corrected: it would stand at zero tokens and zero cost
20
+ * forever, which is what silently disables the `maxTokens` and `maxCostUsd`
21
+ * budget caps (`evaluateBudgetGuard` compares `0 >= limit`).
22
+ *
23
+ * The fallback timer is not optional bookkeeping. A client that disconnects
24
+ * mid-stream, or an upstream that never emits a terminal usage event, must
25
+ * still cost one row, because `maxRequests` counts rows — losing it would
26
+ * weaken a cap that currently works.
27
+ *
28
+ * Generic over the row type so this stays inside `lib/usage` rather than
29
+ * importing the policy layer that imports it back.
30
+ */
31
+ export declare function createStreamUsageDeferral<TRow extends object>(options: {
32
+ record: (completion: TRow & Partial<UsageTokenCounts>) => void;
33
+ fallbackMs: number;
34
+ /** Injectable for tests; defaults to an unref'd setTimeout. */
35
+ schedule?: (run: () => void, ms: number) => {
36
+ cancel: () => void;
37
+ };
38
+ }): StreamUsageDeferral<TRow>;
39
+ //# sourceMappingURL=stream-usage-deferral.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream-usage-deferral.d.ts","sourceRoot":"","sources":["../../../lib/usage/stream-usage-deferral.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,MAAM,WAAW,mBAAmB,CAAC,IAAI;IACxC,wEAAwE;IACxE,QAAQ,EAAE,MAAM,gBAAgB,GAAG,IAAI,CAAC;IACxC,wEAAwE;IACxE,OAAO,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAC3C;;;OAGG;IACH,KAAK,EAAE,CAAC,UAAU,EAAE,IAAI,KAAK,IAAI,CAAC;CAClC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,SAAS,MAAM,EAAE,OAAO,EAAE;IACvE,MAAM,EAAE,CAAC,UAAU,EAAE,IAAI,GAAG,OAAO,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;IAC/D,UAAU,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK;QAAE,MAAM,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC;CACnE,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAqC5B"}