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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-multi-auth",
3
- "version": "2.8.6",
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.6.md](docs/releases/v2.8.6.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 {
@@ -40,6 +40,33 @@ export interface PinnedUnavailableContext {
40
40
  pinSource?: "forced" | "manual" | null;
41
41
  /** Epoch ms when the blocking record ends (rate limit or cooldown). */
42
42
  resetAtMs?: number | null;
43
+ /**
44
+ * Which class of record supplies `resetAtMs`. That deadline is the max
45
+ * across every gating record, so a `rate-limited` skip reason can carry a
46
+ * deadline supplied by a breaker or cooldown that ends later, and the
47
+ * quota phrasing is only true when the rate limit itself is that bound.
48
+ *
49
+ * - `"rate-limit"` — the rate-limit records bound it; word it as a reset.
50
+ * - `"other"` — a cooldown or breaker ends later and bounds it instead.
51
+ * - `"unknown"` (the default) — the caller did not measure; trust the
52
+ * skip reason.
53
+ *
54
+ * Deliberately a value rather than the presence of a second deadline
55
+ * field. The previous shape branched on `"rateLimitResetAtMs" in context`,
56
+ * so a caller that spread the key with an `undefined` value got the
57
+ * opposite wording from one that omitted it — a contract no type could
58
+ * express and no compiler could check.
59
+ */
60
+ recoveryBound?: "rate-limit" | "other" | "unknown";
61
+ /**
62
+ * The pin's CURRENT runtime skip reason, re-read from account state at the
63
+ * moment the 503 is built. `accountSkipReasons` holds the SELECTION
64
+ * verdict, and on the dominant path the retry loop has already overwritten
65
+ * it with its own bookkeeping token ("already-attempted") — wording the
66
+ * sentence from that is what issue #675 reported. Omitted when the caller
67
+ * did not re-read runtime state.
68
+ */
69
+ currentSkipReason?: string | null;
43
70
  now?: number;
44
71
  }
45
72
  export declare function buildPinnedUnavailableErrorBody(pinnedIndex: number | null | undefined, accountSkipReasons: ReadonlyMap<number, string>, context?: PinnedUnavailableContext): PinnedUnavailableErrorBody;
@@ -1 +1 @@
1
- {"version":3,"file":"rate-limit-decision.d.ts","sourceRoot":"","sources":["../../../lib/request/rate-limit-decision.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AAC5E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAe/C,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAGlE;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,OAAO,CAAC,WAAW,EAAE;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC,GAAG,OAAO,CAWjG;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAapF;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAoBlF;AAYD,wBAAgB,0BAA0B,CAAC,gBAAgB,EAAE,MAAM,GAAG,MAAM,CAuB3E;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkBxE;AA0BD,wBAAgB,4BAA4B,CAC3C,OAAO,EAAE,OAAO,EAChB,kBAAkB,EAAE,MAAM,EAC1B,GAAG,EAAE,MAAM,GACT,MAAM,CAcR;AAED,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAE1E;AAED;;;;;;GAMG;AACH,MAAM,WAAW,0BAA0B;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,kCAAkC,CAAC;IACzC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,mEAAmE;IACnE,UAAU,EAAE,QAAQ,GAAG,QAAQ,GAAG,IAAI,CAAC;IACvC,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7C;AAQD,MAAM,WAAW,wBAAwB;IACxC;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,IAAI,CAAC;IACvC,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED,wBAAgB,+BAA+B,CAC9C,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACtC,kBAAkB,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/C,OAAO,CAAC,EAAE,wBAAwB,GAChC,0BAA0B,CAwD5B"}
1
+ {"version":3,"file":"rate-limit-decision.d.ts","sourceRoot":"","sources":["../../../lib/request/rate-limit-decision.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AAE5E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAe/C,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAGlE;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,OAAO,CAAC,WAAW,EAAE;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC,GAAG,OAAO,CAWjG;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAapF;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAoBlF;AAYD,wBAAgB,0BAA0B,CAAC,gBAAgB,EAAE,MAAM,GAAG,MAAM,CAuB3E;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkBxE;AA0BD,wBAAgB,4BAA4B,CAC3C,OAAO,EAAE,OAAO,EAChB,kBAAkB,EAAE,MAAM,EAC1B,GAAG,EAAE,MAAM,GACT,MAAM,CAcR;AAED,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAE1E;AAED;;;;;;GAMG;AACH,MAAM,WAAW,0BAA0B;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,kCAAkC,CAAC;IACzC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,mEAAmE;IACnE,UAAU,EAAE,QAAQ,GAAG,QAAQ,GAAG,IAAI,CAAC;IACvC,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7C;AAQD,MAAM,WAAW,wBAAwB;IACxC;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,IAAI,CAAC;IACvC,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B;;;;;;;;;;;;;;;;OAgBG;IACH,aAAa,CAAC,EAAE,YAAY,GAAG,OAAO,GAAG,SAAS,CAAC;IACnD;;;;;;;OAOG;IACH,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAwID,wBAAgB,+BAA+B,CAC9C,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACtC,kBAAkB,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/C,OAAO,CAAC,EAAE,wBAAwB,GAChC,0BAA0B,CA8E5B"}
@@ -177,12 +177,107 @@ export function normalizeExhaustionStatus(reason) {
177
177
  * throws RangeError (ECMAScript time-value limit, ±100,000,000 days).
178
178
  */
179
179
  const MAX_ECMASCRIPT_TIME_VALUE = 8_640_000_000_000_000;
180
+ const DEADLINE_SENTENCES = {
181
+ "available-again": (resetAt) => `the account is expected to be available again at ${resetAt}`,
182
+ "next-attempt": (resetAt) => `the next attempt is allowed at ${resetAt}`,
183
+ "cooldown-ends": (resetAt) => `the cooldown ends at ${resetAt}`,
184
+ "rate-limit-reset": (resetAt) => `the rate limit resets at ${resetAt}`,
185
+ };
186
+ /**
187
+ * Every cooldown variant is bounded by the same `coolingDownUntil` field, so
188
+ * they all word their deadline the same way. Two of them used to say "the next
189
+ * attempt is allowed at" while the rest said "the cooldown ends at", which
190
+ * reads as two different mechanisms to an operator comparing two 503s.
191
+ *
192
+ * `satisfies` is what keeps this honest: a new CooldownReason becomes a
193
+ * compile error here rather than falling through to the verbatim arm below,
194
+ * which would print the raw `cooling-down:<reason>` token in the sentence —
195
+ * the internal-token leak issue #675 was filed about.
196
+ */
197
+ const COOLDOWN_DESCRIPTIONS = {
198
+ "auth-failure": {
199
+ parenthetical: "cooling down after authentication failures",
200
+ deadlineNoun: "cooldown-ends",
201
+ },
202
+ "network-error": {
203
+ parenthetical: "cooling down after network errors",
204
+ deadlineNoun: "cooldown-ends",
205
+ },
206
+ "server-error": {
207
+ parenthetical: "cooling down after upstream server errors",
208
+ deadlineNoun: "cooldown-ends",
209
+ },
210
+ "rate-limit": {
211
+ parenthetical: "cooling down after a rate limit",
212
+ deadlineNoun: "cooldown-ends",
213
+ },
214
+ };
215
+ const BLOCKER_DESCRIPTIONS = new Map([
216
+ [
217
+ "rate-limited",
218
+ // Upgraded to "rate-limit-reset" below when the caller confirms the
219
+ // rate limit is what bounds the advertised deadline.
220
+ { parenthetical: "rate-limited", deadlineNoun: "available-again" },
221
+ ],
222
+ [
223
+ "circuit-open",
224
+ {
225
+ parenthetical: "paused after repeated upstream errors",
226
+ deadlineNoun: "next-attempt",
227
+ },
228
+ ],
229
+ ["cooling-down", { parenthetical: "cooling down", deadlineNoun: "cooldown-ends" }],
230
+ ...Object.entries(COOLDOWN_DESCRIPTIONS).map(([reason, description]) => [
231
+ `cooling-down:${reason}`,
232
+ description,
233
+ ]),
234
+ ]);
235
+ /** Whether the sentence has real wording for this token, or must echo it raw. */
236
+ function isDescribedBlocker(skipReason) {
237
+ return BLOCKER_DESCRIPTIONS.has(skipReason);
238
+ }
239
+ /**
240
+ * The human sentence derives its parenthetical and its deadline noun from the
241
+ * blocker class. The deadline is max(rate-limit reset, cooldown end, breaker
242
+ * next-attempt), so calling it a "limit" is only true for the rate-limit
243
+ * class: during a provider outage a tripped breaker or a server-error
244
+ * cooldown printed "the recorded limit resets at …", and operators read a
245
+ * backend incident as a blown subscription quota. Only the message changes —
246
+ * the machine-readable `reason` keeps the raw skip token.
247
+ */
248
+ function describePinnedBlocker(skipReason, rateLimitBoundsRecovery) {
249
+ if (skipReason === null) {
250
+ return {
251
+ parenthetical: null,
252
+ deadline: DEADLINE_SENTENCES["available-again"],
253
+ };
254
+ }
255
+ const described = BLOCKER_DESCRIPTIONS.get(skipReason);
256
+ if (described === undefined) {
257
+ // Permanent blockers never reach the deadline clause (the call site
258
+ // suppresses their reset time), and future or internal tokens stay
259
+ // legible verbatim.
260
+ return {
261
+ parenthetical: skipReason,
262
+ deadline: DEADLINE_SENTENCES["available-again"],
263
+ };
264
+ }
265
+ // A breaker or cooldown can outlive the rate limit; the deadline is the max
266
+ // of every gating record, so it is only worded as the limit's own reset
267
+ // when the rate limit actually supplies it.
268
+ const deadlineNoun = skipReason === "rate-limited" && rateLimitBoundsRecovery
269
+ ? "rate-limit-reset"
270
+ : described.deadlineNoun;
271
+ return {
272
+ parenthetical: described.parenthetical,
273
+ deadline: DEADLINE_SENTENCES[deadlineNoun],
274
+ };
275
+ }
180
276
  export function buildPinnedUnavailableErrorBody(pinnedIndex, accountSkipReasons, context) {
181
277
  const normalizedPinnedIndex = typeof pinnedIndex === "number" ? pinnedIndex : null;
182
278
  const skipReason = normalizedPinnedIndex !== null
183
279
  ? accountSkipReasons.get(normalizedPinnedIndex) ?? null
184
280
  : null;
185
- const reasonSuffix = skipReason ? ` (${skipReason})` : "";
186
281
  // On the desync path the pin index is unknown (null); claiming "account 1"
187
282
  // there would contradict the machine-readable pinnedAccountIndex: null.
188
283
  const accountPhrase = normalizedPinnedIndex === null
@@ -206,7 +301,27 @@ export function buildPinnedUnavailableErrorBody(pinnedIndex, accountSkipReasons,
206
301
  const now = context?.now ?? Date.now();
207
302
  const retryAfterMs = resetAtMs !== null ? Math.max(0, resetAtMs - now) : null;
208
303
  const resetAt = resetAtMs !== null ? new Date(resetAtMs).toISOString() : null;
209
- const waitSuffix = resetAt !== null ? `; the recorded limit resets at ${resetAt}` : "";
304
+ // "unknown" (the default) means the caller did not measure which record
305
+ // bounds the deadline — older callers and unit seams — so trust the skip
306
+ // reason. Only a measured "other" demotes to the neutral phrasing, which
307
+ // stays true for a live rate limit either way.
308
+ const rateLimitBoundsRecovery = (context?.recoveryBound ?? "unknown") !== "other";
309
+ // The machine-readable `reason` below stays the recorded SELECTION verdict;
310
+ // only the human sentence follows the blocker actually gating the pin. A
311
+ // recorded verdict that names a real blocker class wins, because the
312
+ // selection-only classes ("missing", "policy-blocked") have no
313
+ // runtime-state equivalent and would be lost. Otherwise the re-read runtime
314
+ // state is the truth and the recorded token is the retry loop's own
315
+ // bookkeeping — the dominant path in issue #675, where a 503/429 on the pin
316
+ // re-enters selection and records "already-attempted" over the real class.
317
+ const describedSkipReason = skipReason !== null && isDescribedBlocker(skipReason)
318
+ ? skipReason
319
+ : context?.currentSkipReason ?? skipReason;
320
+ const blocker = describePinnedBlocker(describedSkipReason, rateLimitBoundsRecovery);
321
+ const reasonSuffix = blocker.parenthetical
322
+ ? ` (${blocker.parenthetical})`
323
+ : "";
324
+ const waitSuffix = resetAt !== null ? `; ${blocker.deadline(resetAt)}` : "";
210
325
  // A forced pin belongs to the launching process, not to the persisted pin
211
326
  // state, so `unpin` would clear nothing — say what actually helps.
212
327
  const remedy = pinSource === "forced"
@@ -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"}