codex-multi-auth 2.8.7 → 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 (41) 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/response-handler.d.ts +3 -1
  18. package/dist/lib/request/response-handler.d.ts.map +1 -1
  19. package/dist/lib/request/response-handler.js +26 -5
  20. package/dist/lib/request/stream-failover-runtime.d.ts +8 -1
  21. package/dist/lib/request/stream-failover-runtime.d.ts.map +1 -1
  22. package/dist/lib/request/stream-failover-runtime.js +9 -1
  23. package/dist/lib/request/stream-failover.d.ts.map +1 -1
  24. package/dist/lib/request/stream-failover.js +41 -0
  25. package/dist/lib/runtime/account-check.d.ts.map +1 -1
  26. package/dist/lib/runtime/account-check.js +8 -1
  27. package/dist/lib/runtime-rotation-proxy.d.ts.map +1 -1
  28. package/dist/lib/runtime-rotation-proxy.js +46 -3
  29. package/dist/lib/storage.d.ts +19 -0
  30. package/dist/lib/storage.d.ts.map +1 -1
  31. package/dist/lib/storage.js +32 -0
  32. package/dist/lib/usage/index.d.ts +2 -0
  33. package/dist/lib/usage/index.d.ts.map +1 -1
  34. package/dist/lib/usage/index.js +2 -0
  35. package/dist/lib/usage/stream-usage-deferral.d.ts +39 -0
  36. package/dist/lib/usage/stream-usage-deferral.d.ts.map +1 -0
  37. package/dist/lib/usage/stream-usage-deferral.js +54 -0
  38. package/dist/lib/usage/usage-extraction.d.ts +42 -0
  39. package/dist/lib/usage/usage-extraction.d.ts.map +1 -0
  40. package/dist/lib/usage/usage-extraction.js +178 -0
  41. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-multi-auth",
3
- "version": "2.8.7",
3
+ "version": "2.9.0",
4
4
  "description": "Install and operate codex-multi-auth for the official @openai/codex CLI with multi-account OAuth rotation, switching, health checks, and recovery tools.",
5
5
  "interface": {
6
6
  "composerIcon": "./assets/codex-multi-auth-icon.svg"
package/README.md CHANGED
@@ -392,7 +392,7 @@ codex-multi-auth doctor --json
392
392
 
393
393
  ## Release Notes
394
394
 
395
- - Current stable: [docs/releases/v2.8.7.md](docs/releases/v2.8.7.md) — install via `npm i -g codex-multi-auth`
395
+ - Current stable: [docs/releases/v2.9.0.md](docs/releases/v2.9.0.md) — install via `npm i -g codex-multi-auth`
396
396
  - Previous stable: [docs/releases/v2.8.3.md](docs/releases/v2.8.3.md)
397
397
  - Previous stable: [docs/releases/v2.8.2.md](docs/releases/v2.8.2.md)
398
398
  - Previous stable: [docs/releases/v2.7.1.md](docs/releases/v2.7.1.md)
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,kBAAkB,CAAC;AA2V5D;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,iBAAiB,EAAE,MA2uI/B,CAAC;AAEF,eAAO,MAAM,gBAAgB,KAAoB,CAAC;AAElD,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,kBAAkB,CAAC;AAoW5D;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,iBAAiB,EAAE,MAuwI/B,CAAC;AAEF,eAAO,MAAM,gBAAgB,KAAoB,CAAC;AAElD,eAAe,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -38,6 +38,14 @@ import { LiveAccountSync } from "./lib/live-account-sync.js";
38
38
  import { clearCorrelationId, initLogger, logDebug, logError, logInfo, logRequest, logWarn, setCorrelationId, } from "./lib/logger.js";
39
39
  import { PreemptiveQuotaScheduler, readQuotaSchedulerSnapshot, } from "./lib/preemptive-quota-scheduler.js";
40
40
  import { createRuntimeUsageRecorder, evaluateRuntimePolicy, loadRuntimePolicyState, } from "./lib/policy/runtime-policy.js";
41
+ import { createStreamUsageDeferral } from "./lib/usage/stream-usage-deferral.js";
42
+ /**
43
+ * How long a streaming request's usage-ledger row may wait for the upstream
44
+ * `usage` event before it is written without token counts. Long enough not to
45
+ * truncate a genuinely slow completion; the timer is unref'd so it never holds
46
+ * the process open. See createStreamUsageDeferral.
47
+ */
48
+ const USAGE_STREAM_RECORD_FALLBACK_MS = 15 * 60_000;
41
49
  import { getModelFamily, MODEL_FAMILIES, prewarmCodexInstructions, } from "./lib/prompts/codex.js";
42
50
  import { prewarmHostCodexPrompt } from "./lib/prompts/host-codex-prompt.js";
43
51
  import { fetchCodexQuotaSnapshot, formatQuotaSnapshotLine, } from "./lib/quota-probe.js";
@@ -546,6 +554,18 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
546
554
  async fetch(input, init) {
547
555
  let usageRecorder = null;
548
556
  let usageCompletion = null;
557
+ // A non-streaming response reports its token counts while
558
+ // handleSuccessResponse is still assembling the body, so they
559
+ // are folded straight into usageCompletion below. A streaming
560
+ // one reports them only as the client drains the body, long
561
+ // after the `finally` that writes the row — so that row is
562
+ // handed to the deferral instead of written empty.
563
+ const usageDeferral = createStreamUsageDeferral({
564
+ record: (completion) => {
565
+ void usageRecorder?.record(completion);
566
+ },
567
+ fallbackMs: USAGE_STREAM_RECORD_FALLBACK_MS,
568
+ });
549
569
  try {
550
570
  if (cachedAccountManager &&
551
571
  cachedAccountManager !== accountManager) {
@@ -1594,6 +1614,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1594
1614
  sessionAffinityStore?.updateLastResponseId(sessionAffinityKey, responseId, Date.now(), sessionAffinityVersion);
1595
1615
  storedResponseIdForSuccess = true;
1596
1616
  },
1617
+ onUsage: usageDeferral.onUsage,
1597
1618
  streamStallTimeoutMs,
1598
1619
  });
1599
1620
  if (!isStreaming && emptyResponseMaxRetries > 0) {
@@ -1679,7 +1700,20 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1679
1700
  accountId: successAccountForResponse.accountId ?? null,
1680
1701
  email: successAccountForResponse.email ?? null,
1681
1702
  },
1703
+ // Already known for a non-streaming response, which
1704
+ // reported its usage while the body was being
1705
+ // assembled above.
1706
+ ...(usageDeferral.captured() ?? {}),
1682
1707
  };
1708
+ if (isStreaming && !usageDeferral.captured()) {
1709
+ // Writing the row in the `finally` below would stamp
1710
+ // zero tokens — the client has not read a byte yet —
1711
+ // and the ledger is append-only, so the real counts
1712
+ // could never correct it. Zero tokens is exactly what
1713
+ // left maxTokens / maxCostUsd unenforceable here.
1714
+ usageDeferral.defer(usageCompletion);
1715
+ usageCompletion = null;
1716
+ }
1683
1717
  if (lastCodexCliActiveSyncIndex !==
1684
1718
  successAccountForResponse.index) {
1685
1719
  void accountManager.syncCodexCliActiveSelectionForIndex(successAccountForResponse.index);
@@ -13,7 +13,9 @@ export interface AccountPolicyStore {
13
13
  accounts: Record<string, AccountPolicy>;
14
14
  }
15
15
  export declare function getAccountPolicyPath(): string;
16
- export declare function getAccountPolicyKey(account: Pick<AccountMetadataV3, "accountId" | "email">, _index?: number): string;
16
+ export declare function getAccountPolicyKey(account: Pick<AccountMetadataV3, "accountId" | "email"> & {
17
+ refreshToken?: string | null;
18
+ }, _index?: number): string;
17
19
  export declare function loadAccountPolicyStore(): Promise<AccountPolicyStore>;
18
20
  export declare function saveAccountPolicyStore(store: AccountPolicyStore): Promise<void>;
19
21
  export declare function upsertAccountPolicy(store: AccountPolicyStore, accountKey: string, mutate: (policy: AccountPolicy) => void, now?: number): AccountPolicy;
@@ -1 +1 @@
1
- {"version":3,"file":"account-policy.d.ts","sourceRoot":"","sources":["../../lib/account-policy.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAItD,MAAM,WAAW,aAAa;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IAClC,OAAO,EAAE,CAAC,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;CACxC;AAkFD,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C;AAED,wBAAgB,mBAAmB,CAClC,OAAO,EAAE,IAAI,CAAC,iBAAiB,EAAE,WAAW,GAAG,OAAO,CAAC,EACvD,MAAM,CAAC,EAAE,MAAM,GACb,MAAM,CAMR;AAED,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAa1E;AAED,wBAAsB,sBAAsB,CAC3C,KAAK,EAAE,kBAAkB,GACvB,OAAO,CAAC,IAAI,CAAC,CAsCf;AAED,wBAAgB,mBAAmB,CAClC,KAAK,EAAE,kBAAkB,EACzB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,EACvC,GAAG,SAAa,GACd,aAAa,CAgBf;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEtE;AAED,wBAAgB,oCAAoC,IAAI,IAAI,CAE3D"}
1
+ {"version":3,"file":"account-policy.d.ts","sourceRoot":"","sources":["../../lib/account-policy.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAItD,MAAM,WAAW,aAAa;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IAClC,OAAO,EAAE,CAAC,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;CACxC;AAkFD,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C;AAED,wBAAgB,mBAAmB,CAClC,OAAO,EAAE,IAAI,CAAC,iBAAiB,EAAE,WAAW,GAAG,OAAO,CAAC,GAAG;IACzD,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B,EACD,MAAM,CAAC,EAAE,MAAM,GACb,MAAM,CAmBR;AAED,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAa1E;AAED,wBAAsB,sBAAsB,CAC3C,KAAK,EAAE,kBAAkB,GACvB,OAAO,CAAC,IAAI,CAAC,CAsCf;AAED,wBAAgB,mBAAmB,CAClC,KAAK,EAAE,kBAAkB,EACzB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,EACvC,GAAG,SAAa,GACd,aAAa,CAgBf;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEtE;AAED,wBAAgB,oCAAoC,IAAI,IAAI,CAE3D"}
@@ -81,9 +81,22 @@ export function getAccountPolicyPath() {
81
81
  return join(getCodexMultiAuthDir(), ACCOUNT_POLICY_FILE_NAME);
82
82
  }
83
83
  export function getAccountPolicyKey(account, _index) {
84
+ // The key must be stable across processes and independent of the account's
85
+ // slot (policy state is persisted and survives reordering), so it is derived
86
+ // from identity, never from `_index`.
87
+ //
88
+ // An account whose accountId AND email are both missing — freshly imported,
89
+ // or not yet hydrated from its token — used to hash the literal "unknown",
90
+ // which gave EVERY such account the same key: pausing, draining or tagging
91
+ // one silently applied to all of them. Fall back to the refresh token
92
+ // instead, which every account has and no two accounts share. It is
93
+ // namespaced before hashing so a token value can never collide with an
94
+ // email or accountId of the same text, and only its digest is persisted.
95
+ // Mirrors getAccountIdentityKey's `allowRefreshFallback`.
96
+ const refreshFallback = account.refreshToken?.trim();
84
97
  const identity = account.accountId?.trim() ||
85
98
  account.email?.trim().toLowerCase() ||
86
- "unknown";
99
+ (refreshFallback ? `refresh:${refreshFallback}` : "unknown");
87
100
  return `sha256:${createHash("sha256").update(identity).digest("hex")}`;
88
101
  }
89
102
  export async function loadAccountPolicyStore() {
@@ -290,6 +290,13 @@ async function runAuthLoginFlow(loginOptions, deps) {
290
290
  parsed: targetIndex + 1,
291
291
  switchReason: "restore",
292
292
  preserveActiveIndexByFamily: true,
293
+ // A restore replaces the whole account list, so every
294
+ // index moves. Session affinity remembers accounts BY
295
+ // INDEX, so without this a live proxy keeps routing
296
+ // in-flight sessions to whatever now occupies the old
297
+ // slot. `switch`/`best` already bump for a single
298
+ // move; a restore moves everything. See issue #474.
299
+ bumpAffinityGeneration: true,
293
300
  });
294
301
  console.log(UI_COPY.oauth.restoreBackupLoaded(selectedBackup.fileName, restoredStorage.accounts.length));
295
302
  if (!synced) {
@@ -1 +1 @@
1
- {"version":3,"file":"login-menu-actions.d.ts","sourceRoot":"","sources":["../../../lib/codex-manager/login-menu-actions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAEjD,OAAO,EAEN,KAAK,gBAAgB,EAIrB,KAAK,kBAAkB,EAGvB,MAAM,eAAe,CAAC;AASvB,OAAO,EACN,KAAK,eAAe,EAKpB,MAAM,kBAAkB,CAAC;AAG1B;;;;;GAKG;AAEH,gBAAgB;AAChB,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE7D,gBAAgB;AAChB,wBAAsB,qBAAqB,CAC1C,YAAY,EAAE,kBAAkB,GAAG,IAAI,EACvC,sBAAsB,GAAE,MAAM,GAAG,IAAW,GAC1C,OAAO,CAAC,eAAe,CAAC,CA6D1B;AAED,gBAAgB;AAChB,wBAAsB,uBAAuB,CAC5C,YAAY,EAAE,kBAAkB,GAC9B,OAAO,CAAC,iBAAiB,CAAC,CA0C5B;AAED,gBAAgB;AAChB,wBAAsB,2BAA2B,CAChD,OAAO,EAAE,kBAAkB,EAAE,GAC3B,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAkCpC;AA8GD,gBAAgB;AAChB,wBAAsB,kBAAkB,CACvC,OAAO,EAAE,gBAAgB,EACzB,UAAU,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC,GACrD,OAAO,CAAC,IAAI,CAAC,CA0Hf"}
1
+ {"version":3,"file":"login-menu-actions.d.ts","sourceRoot":"","sources":["../../../lib/codex-manager/login-menu-actions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAEjD,OAAO,EAEN,KAAK,gBAAgB,EAKrB,KAAK,kBAAkB,EAGvB,MAAM,eAAe,CAAC;AASvB,OAAO,EACN,KAAK,eAAe,EAKpB,MAAM,kBAAkB,CAAC;AAG1B;;;;;GAKG;AAEH,gBAAgB;AAChB,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE7D,gBAAgB;AAChB,wBAAsB,qBAAqB,CAC1C,YAAY,EAAE,kBAAkB,GAAG,IAAI,EACvC,sBAAsB,GAAE,MAAM,GAAG,IAAW,GAC1C,OAAO,CAAC,eAAe,CAAC,CA6D1B;AAED,gBAAgB;AAChB,wBAAsB,uBAAuB,CAC5C,YAAY,EAAE,kBAAkB,GAC9B,OAAO,CAAC,iBAAiB,CAAC,CA0C5B;AAED,gBAAgB;AAChB,wBAAsB,2BAA2B,CAChD,OAAO,EAAE,kBAAkB,EAAE,GAC3B,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAkCpC;AA8GD,gBAAgB;AAChB,wBAAsB,kBAAkB,CACvC,OAAO,EAAE,gBAAgB,EACzB,UAAU,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC,GACrD,OAAO,CAAC,IAAI,CAAC,CAgIf"}
@@ -1,7 +1,7 @@
1
1
  import { stdin as input, stdout as output } from "node:process";
2
2
  import { sanitizeEmail } from "../accounts.js";
3
3
  import { MODEL_FAMILIES } from "../prompts/codex.js";
4
- import { findMatchingAccountIndex, loadAccounts, reconcilePinnedAccountIndex, setStoragePath, withAccountStorageTransaction, } from "../storage.js";
4
+ import { bumpStorageAffinityGeneration, findMatchingAccountIndex, loadAccounts, reconcilePinnedAccountIndex, setStoragePath, withAccountStorageTransaction, } from "../storage.js";
5
5
  import { UI_COPY } from "../ui/ui-copy.js";
6
6
  import { getUiRuntimeOptions } from "../ui/runtime.js";
7
7
  import { select } from "../ui/select.js";
@@ -249,6 +249,12 @@ export async function handleManageAction(storage, menuResult) {
249
249
  nextStorage.accounts.splice(nextIndex, 1);
250
250
  resetManageActionSelection(nextStorage, nextIndex);
251
251
  nextStorage.pinnedAccountIndex = reconcilePinnedAccountIndex(pinnedAccount, nextStorage.accounts);
252
+ // The splice shifts every index above the removed slot, and session
253
+ // affinity remembers accounts BY INDEX. Without a generation bump a
254
+ // live proxy keeps routing in-flight sessions to the account that
255
+ // slid into the old slot. Reconciling the pin is not enough — the
256
+ // affinity map is separate state. See issue #474.
257
+ bumpStorageAffinityGeneration(nextStorage);
252
258
  await persist(nextStorage);
253
259
  replaceManageActionStorage(storage, nextStorage);
254
260
  deleted = true;
@@ -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":"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"}
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;AAiB3D,OAAO,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC/E,OAAO,EACN,wCAAwC,EACxC,oCAAoC,EACpC,4BAA4B,EAC5B,oCAAoC,EACpC,uCAAuC,EACvC,kBAAkB,EAClB,wBAAwB,EACxB,8BAA8B,GAC9B,MAAM,2BAA2B,CAAC;AACnC,YAAY,EACX,gBAAgB,EAChB,yBAAyB,EACzB,sCAAsC,GACtC,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACN,iBAAiB,EACjB,kBAAkB,EAClB,yBAAyB,EACzB,2BAA2B,EAC3B,wBAAwB,GACxB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EAAE,0BAA0B,EAAE,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClD,YAAY,EACX,yBAAyB,EACzB,wBAAwB,GACxB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,aAAa;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;CACrB;AASD,MAAM,WAAW,8BAA8B;IAC9C,IAAI,EAAE,WAAW,CAAC;IAClB,WAAW,EAAE,WAAW,CAAC;IACzB,4BAA4B,CAAC,EAAE,wBAAwB,CAAC,MAAM,CAAC,CAAC;CAChE;AAED,MAAM,WAAW,mBAAmB;IAC5B,QAAQ,EAAE,QAAQ,CAAC;IACnB,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B,SAAS,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,oBAAoB;IACpC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,wBAAwB,CAC7C,IAAI,EAAE,WAAW,GAAG,SAAS,EAC7B,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,UAAU,EACtB,SAAS,UAAO,EAChB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,OAAO,CAAC,EAAE;IACT,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,mBAAmB,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC1C,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,6BAA6B,CAAC,EAAE,OAAO,CAAC;IACxC,wBAAwB,CAAC,EAAE,OAAO,CAAC;CACnC,GACC,OAAO,CAAC,8BAA8B,GAAG,SAAS,CAAC,CA4FrD;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CACjC,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,oBAAoB,GACnC,OAAO,CAAC,mBAAmB,CAAC,CA4C9B;AAED;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CACvC,QAAQ,EAAE,QAAQ,EAClB,WAAW,EAAE,OAAO,EACpB,OAAO,CAAC,EAAE;IACZ,YAAY,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC9B,GACC,OAAO,CAAC,QAAQ,CAAC,CAanB"}
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":"AAwwBA;;;;;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,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC9B,GACC,OAAO,CAAC,QAAQ,CAAC,CA2HnB;AA6DD;;;;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,GACzC,QAAQ,CAiBV"}
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
- maybeCaptureResponseEvent(state, data, onResponseId);
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): Promise<boolean>;
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,GAC1B,OAAO,CAAC,OAAO,CAAC,CAqDlB"}
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,CA4HV"}
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":"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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;
@@ -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";
@@ -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
  }
@@ -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":"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"}
@@ -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"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Holds a usage-ledger row open until a streaming response reports its tokens.
3
+ *
4
+ * A streaming request's row would otherwise be written when the request handler
5
+ * returns its Response — before the client has read a byte, and therefore
6
+ * before the upstream `usage` event exists. The ledger is append-only, so that
7
+ * row could never be corrected: it would stand at zero tokens and zero cost
8
+ * forever, which is what silently disables the `maxTokens` and `maxCostUsd`
9
+ * budget caps (`evaluateBudgetGuard` compares `0 >= limit`).
10
+ *
11
+ * The fallback timer is not optional bookkeeping. A client that disconnects
12
+ * mid-stream, or an upstream that never emits a terminal usage event, must
13
+ * still cost one row, because `maxRequests` counts rows — losing it would
14
+ * weaken a cap that currently works.
15
+ *
16
+ * Generic over the row type so this stays inside `lib/usage` rather than
17
+ * importing the policy layer that imports it back.
18
+ */
19
+ export function createStreamUsageDeferral(options) {
20
+ const schedule = options.schedule ??
21
+ ((run, ms) => {
22
+ const timer = setTimeout(run, ms);
23
+ // Never hold the process open for a row that is only a fallback.
24
+ timer.unref?.();
25
+ return { cancel: () => clearTimeout(timer) };
26
+ });
27
+ let captured = null;
28
+ let pending = null;
29
+ let timer = null;
30
+ const flush = (usage) => {
31
+ const completion = pending;
32
+ pending = null;
33
+ timer?.cancel();
34
+ timer = null;
35
+ if (!completion)
36
+ return;
37
+ options.record(usage ? { ...completion, ...usage } : completion);
38
+ };
39
+ return {
40
+ captured: () => captured,
41
+ onUsage: (usage) => {
42
+ captured = usage;
43
+ flush(usage);
44
+ },
45
+ defer: (completion) => {
46
+ // A second defer would orphan the first row's timer, so keep the row
47
+ // already in flight and let it settle on its own terms.
48
+ if (pending)
49
+ return;
50
+ pending = completion;
51
+ timer = schedule(() => flush(null), options.fallbackMs);
52
+ },
53
+ };
54
+ }
@@ -0,0 +1,42 @@
1
+ import type { UsageTokenCounts } from "./types.js";
2
+ /**
3
+ * Read an OpenAI Responses `usage` object into the ledger's token shape.
4
+ *
5
+ * Two containment rules drive the mapping, and getting either backwards
6
+ * mis-prices every row:
7
+ *
8
+ * - `input_tokens_details.cached_tokens` is a SUBSET of `input_tokens`, and
9
+ * `estimateUsageCostUsd` already subtracts it (`billableInputTokens`), so
10
+ * both are passed through raw.
11
+ * - `output_tokens_details.reasoning_tokens` is likewise a SUBSET of
12
+ * `output_tokens`, but `estimateUsageCostUsd` prices `outputTokens` and
13
+ * `reasoningTokens` as DISJOINT buckets and sums them. Passing the raw
14
+ * `output_tokens` alongside the reasoning count would bill reasoning twice,
15
+ * so the reasoning share is subtracted out here.
16
+ */
17
+ export declare function extractUsageTokenCounts(usage: unknown): UsageTokenCounts | null;
18
+ /**
19
+ * Pull the `usage` object out of a Responses payload, whether it arrived as a
20
+ * bare response object (non-streaming) or wrapped in a stream event envelope.
21
+ */
22
+ export declare function extractResponsesUsage(payload: unknown): UsageTokenCounts | null;
23
+ export interface UsageStreamScanner {
24
+ /** Feed one forwarded chunk. Never throws. */
25
+ push: (chunk: Uint8Array) => void;
26
+ /** Flush any trailing partial data and return the final counts, if any. */
27
+ result: () => UsageTokenCounts | null;
28
+ }
29
+ /**
30
+ * Observe a forwarded response body and recover its token counts.
31
+ *
32
+ * SSE bodies are scanned line by line so only the current partial line is
33
+ * retained; a full response can be gigabytes of deltas and must never be
34
+ * buffered. Non-SSE JSON bodies have no incremental structure to exploit, so
35
+ * they are accumulated up to `MAX_BUFFERED_BYTES` and parsed at the end —
36
+ * beyond that cap the scanner gives up on usage rather than grow without
37
+ * bound.
38
+ */
39
+ export declare function createUsageStreamScanner(options: {
40
+ contentType?: string | null;
41
+ }): UsageStreamScanner;
42
+ //# sourceMappingURL=usage-extraction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usage-extraction.d.ts","sourceRoot":"","sources":["../../../lib/usage/usage-extraction.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AA+BnD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,uBAAuB,CACtC,KAAK,EAAE,OAAO,GACZ,gBAAgB,GAAG,IAAI,CA+BzB;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,gBAAgB,GAAG,IAAI,CAQ/E;AAED,MAAM,WAAW,kBAAkB;IAClC,8CAA8C;IAC9C,IAAI,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAClC,2EAA2E;IAC3E,MAAM,EAAE,MAAM,gBAAgB,GAAG,IAAI,CAAC;CACtC;AAED;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE;IACjD,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B,GAAG,kBAAkB,CA6ErB"}
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Extracting token counts from upstream Responses API traffic.
3
+ *
4
+ * Without this the usage ledger records every request with all-zero tokens and
5
+ * a zero cost, which silently disables the `maxTokens` / `maxCostUsd` budget
6
+ * caps: `evaluateBudgetGuard` compares `0 >= limit`, which never fires, so
7
+ * `codex-multi-auth budget set --cost 50` allows unlimited spend and only
8
+ * `--requests` actually enforces anything.
9
+ */
10
+ /** Cap on retained bytes so a hostile or unterminated body cannot grow forever. */
11
+ const MAX_BUFFERED_BYTES = 1_048_576;
12
+ /** Terminal Responses stream events that carry the final `usage` object. */
13
+ const TERMINAL_EVENT_TYPES = new Set([
14
+ "response.completed",
15
+ "response.incomplete",
16
+ "response.failed",
17
+ "response.done",
18
+ ]);
19
+ function isRecord(value) {
20
+ return typeof value === "object" && value !== null && !Array.isArray(value);
21
+ }
22
+ function nonNegativeInteger(value) {
23
+ if (typeof value !== "number" || !Number.isFinite(value))
24
+ return 0;
25
+ return Math.max(0, Math.trunc(value));
26
+ }
27
+ /**
28
+ * Read an OpenAI Responses `usage` object into the ledger's token shape.
29
+ *
30
+ * Two containment rules drive the mapping, and getting either backwards
31
+ * mis-prices every row:
32
+ *
33
+ * - `input_tokens_details.cached_tokens` is a SUBSET of `input_tokens`, and
34
+ * `estimateUsageCostUsd` already subtracts it (`billableInputTokens`), so
35
+ * both are passed through raw.
36
+ * - `output_tokens_details.reasoning_tokens` is likewise a SUBSET of
37
+ * `output_tokens`, but `estimateUsageCostUsd` prices `outputTokens` and
38
+ * `reasoningTokens` as DISJOINT buckets and sums them. Passing the raw
39
+ * `output_tokens` alongside the reasoning count would bill reasoning twice,
40
+ * so the reasoning share is subtracted out here.
41
+ */
42
+ export function extractUsageTokenCounts(usage) {
43
+ if (!isRecord(usage))
44
+ return null;
45
+ const hasAnyCount = typeof usage.input_tokens === "number" ||
46
+ typeof usage.output_tokens === "number" ||
47
+ typeof usage.total_tokens === "number";
48
+ if (!hasAnyCount)
49
+ return null;
50
+ const inputTokens = nonNegativeInteger(usage.input_tokens);
51
+ const rawOutputTokens = nonNegativeInteger(usage.output_tokens);
52
+ const cachedInputTokens = isRecord(usage.input_tokens_details)
53
+ ? Math.min(inputTokens, nonNegativeInteger(usage.input_tokens_details.cached_tokens))
54
+ : 0;
55
+ const reasoningTokens = isRecord(usage.output_tokens_details)
56
+ ? Math.min(rawOutputTokens, nonNegativeInteger(usage.output_tokens_details.reasoning_tokens))
57
+ : 0;
58
+ const providedTotal = typeof usage.total_tokens === "number" && Number.isFinite(usage.total_tokens)
59
+ ? Math.max(0, Math.trunc(usage.total_tokens))
60
+ : inputTokens + rawOutputTokens;
61
+ return {
62
+ inputTokens,
63
+ outputTokens: rawOutputTokens - reasoningTokens,
64
+ cachedInputTokens,
65
+ reasoningTokens,
66
+ totalTokens: providedTotal,
67
+ };
68
+ }
69
+ /**
70
+ * Pull the `usage` object out of a Responses payload, whether it arrived as a
71
+ * bare response object (non-streaming) or wrapped in a stream event envelope.
72
+ */
73
+ export function extractResponsesUsage(payload) {
74
+ if (!isRecord(payload))
75
+ return null;
76
+ const direct = extractUsageTokenCounts(payload.usage);
77
+ if (direct)
78
+ return direct;
79
+ if (isRecord(payload.response)) {
80
+ return extractUsageTokenCounts(payload.response.usage);
81
+ }
82
+ return null;
83
+ }
84
+ /**
85
+ * Observe a forwarded response body and recover its token counts.
86
+ *
87
+ * SSE bodies are scanned line by line so only the current partial line is
88
+ * retained; a full response can be gigabytes of deltas and must never be
89
+ * buffered. Non-SSE JSON bodies have no incremental structure to exploit, so
90
+ * they are accumulated up to `MAX_BUFFERED_BYTES` and parsed at the end —
91
+ * beyond that cap the scanner gives up on usage rather than grow without
92
+ * bound.
93
+ */
94
+ export function createUsageStreamScanner(options) {
95
+ const sse = (options.contentType ?? "").toLowerCase().includes("text/event-stream");
96
+ const decoder = new TextDecoder("utf-8");
97
+ let pending = "";
98
+ let bufferedBytes = 0;
99
+ let overflowed = false;
100
+ let latest = null;
101
+ const consumeLine = (line) => {
102
+ const trimmed = line.trim();
103
+ if (!trimmed.startsWith("data:"))
104
+ return;
105
+ const payload = trimmed.slice("data:".length).trim();
106
+ if (payload.length === 0 || payload === "[DONE]")
107
+ return;
108
+ let parsed;
109
+ try {
110
+ parsed = JSON.parse(payload);
111
+ }
112
+ catch {
113
+ return;
114
+ }
115
+ // Prefer terminal events, which carry the authoritative totals, but fall
116
+ // back to any event that reports usage so a stream that ends on a
117
+ // non-standard terminal type still records something.
118
+ const type = isRecord(parsed) ? parsed.type : undefined;
119
+ const isTerminal = typeof type === "string" && TERMINAL_EVENT_TYPES.has(type);
120
+ const counts = extractResponsesUsage(parsed);
121
+ if (counts && (isTerminal || latest === null)) {
122
+ latest = counts;
123
+ }
124
+ };
125
+ return {
126
+ push: (chunk) => {
127
+ try {
128
+ if (overflowed || chunk.byteLength === 0)
129
+ return;
130
+ bufferedBytes += chunk.byteLength;
131
+ pending += decoder.decode(chunk, { stream: true });
132
+ if (!sse) {
133
+ if (bufferedBytes > MAX_BUFFERED_BYTES) {
134
+ overflowed = true;
135
+ pending = "";
136
+ }
137
+ return;
138
+ }
139
+ let newlineAt = pending.indexOf("\n");
140
+ while (newlineAt !== -1) {
141
+ consumeLine(pending.slice(0, newlineAt));
142
+ pending = pending.slice(newlineAt + 1);
143
+ newlineAt = pending.indexOf("\n");
144
+ }
145
+ // A single unterminated line this long is not SSE any more; stop
146
+ // retaining it rather than growing the buffer for the whole body.
147
+ if (pending.length > MAX_BUFFERED_BYTES) {
148
+ overflowed = true;
149
+ pending = "";
150
+ }
151
+ }
152
+ catch {
153
+ // Usage accounting must never break the forwarded response.
154
+ }
155
+ },
156
+ result: () => {
157
+ try {
158
+ if (overflowed)
159
+ return latest;
160
+ pending += decoder.decode();
161
+ if (sse) {
162
+ if (pending.length > 0)
163
+ consumeLine(pending);
164
+ pending = "";
165
+ return latest;
166
+ }
167
+ if (pending.trim().length === 0)
168
+ return latest;
169
+ const parsed = JSON.parse(pending);
170
+ pending = "";
171
+ return extractResponsesUsage(parsed) ?? latest;
172
+ }
173
+ catch {
174
+ return latest;
175
+ }
176
+ },
177
+ };
178
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-multi-auth",
3
- "version": "2.8.7",
3
+ "version": "2.9.0",
4
4
  "description": "Codex CLI multi-account OAuth manager with account switching, health checks, runtime rotation, diagnostics, and recovery tools for @openai/codex",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",