codex-multi-auth 2.8.7 → 2.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/README.md +14 -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/budget-guard.d.ts +1 -0
  9. package/dist/lib/budget-guard.d.ts.map +1 -1
  10. package/dist/lib/budget-guard.js +13 -3
  11. package/dist/lib/codex-manager/account-pool-write.d.ts +8 -0
  12. package/dist/lib/codex-manager/account-pool-write.d.ts.map +1 -1
  13. package/dist/lib/codex-manager/account-pool-write.js +1 -1
  14. package/dist/lib/codex-manager/help.d.ts +2 -0
  15. package/dist/lib/codex-manager/help.d.ts.map +1 -1
  16. package/dist/lib/codex-manager/help.js +38 -1
  17. package/dist/lib/codex-manager/login-flow.d.ts +3 -0
  18. package/dist/lib/codex-manager/login-flow.d.ts.map +1 -1
  19. package/dist/lib/codex-manager/login-flow.js +80 -6
  20. package/dist/lib/codex-manager/login-menu-actions.d.ts.map +1 -1
  21. package/dist/lib/codex-manager/login-menu-actions.js +41 -5
  22. package/dist/lib/codex-manager/login-oauth.d.ts +38 -2
  23. package/dist/lib/codex-manager/login-oauth.d.ts.map +1 -1
  24. package/dist/lib/codex-manager/login-oauth.js +205 -23
  25. package/dist/lib/policy/runtime-policy.d.ts +7 -0
  26. package/dist/lib/policy/runtime-policy.d.ts.map +1 -1
  27. package/dist/lib/policy/runtime-policy.js +1 -0
  28. package/dist/lib/request/fetch-helpers.d.ts +7 -0
  29. package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
  30. package/dist/lib/request/fetch-helpers.js +1 -1
  31. package/dist/lib/request/response-handler.d.ts +3 -1
  32. package/dist/lib/request/response-handler.d.ts.map +1 -1
  33. package/dist/lib/request/response-handler.js +26 -5
  34. package/dist/lib/request/stream-failover-runtime.d.ts +8 -1
  35. package/dist/lib/request/stream-failover-runtime.d.ts.map +1 -1
  36. package/dist/lib/request/stream-failover-runtime.js +9 -1
  37. package/dist/lib/request/stream-failover.d.ts.map +1 -1
  38. package/dist/lib/request/stream-failover.js +41 -0
  39. package/dist/lib/runtime/account-check.d.ts.map +1 -1
  40. package/dist/lib/runtime/account-check.js +8 -1
  41. package/dist/lib/runtime-rotation-proxy.d.ts.map +1 -1
  42. package/dist/lib/runtime-rotation-proxy.js +46 -3
  43. package/dist/lib/storage.d.ts +19 -0
  44. package/dist/lib/storage.d.ts.map +1 -1
  45. package/dist/lib/storage.js +32 -0
  46. package/dist/lib/usage/index.d.ts +2 -0
  47. package/dist/lib/usage/index.d.ts.map +1 -1
  48. package/dist/lib/usage/index.js +2 -0
  49. package/dist/lib/usage/ledger.d.ts.map +1 -1
  50. package/dist/lib/usage/ledger.js +7 -0
  51. package/dist/lib/usage/pricing.d.ts +15 -0
  52. package/dist/lib/usage/pricing.d.ts.map +1 -1
  53. package/dist/lib/usage/pricing.js +24 -0
  54. package/dist/lib/usage/stream-usage-deferral.d.ts +39 -0
  55. package/dist/lib/usage/stream-usage-deferral.d.ts.map +1 -0
  56. package/dist/lib/usage/stream-usage-deferral.js +54 -0
  57. package/dist/lib/usage/types.d.ts +6 -0
  58. package/dist/lib/usage/types.d.ts.map +1 -1
  59. package/dist/lib/usage/usage-extraction.d.ts +42 -0
  60. package/dist/lib/usage/usage-extraction.d.ts.map +1 -0
  61. package/dist/lib/usage/usage-extraction.js +178 -0
  62. package/package.json +1 -1
@@ -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";
@@ -1 +1 @@
1
- {"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../../../lib/usage/ledger.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EACX,sBAAsB,EAEtB,gBAAgB,EAChB,gBAAgB,EAEhB,cAAc,EAEd,YAAY,EAGZ,iBAAiB,EAEjB,MAAM,YAAY,CAAC;AAqOpB,wBAAgB,mBAAmB,IAAI,gBAAgB,CAMtD;AAED,wBAAsB,oBAAoB,CACzC,KAAK,EAAE,sBAAsB,GAC3B,OAAO,CAAC,cAAc,CAAC,CASzB;AA+JD,wBAAsB,mBAAmB,CACxC,KAAK,GAAE,gBAAqB,GAC1B,OAAO,CAAC,cAAc,EAAE,CAAC,CAc3B;AAgDD,wBAAgB,kBAAkB,CACjC,IAAI,EAAE,cAAc,EAAE,EACtB,KAAK,GAAE,iBAAsB,GAC3B,YAAY,CAwBd;AAED,wBAAsB,oBAAoB,CACzC,KAAK,GAAE,iBAAsB,GAC3B,OAAO,CAAC,YAAY,CAAC,CAEvB;AAED,wBAAsB,iBAAiB,CAAC,OAAO,GAAE;IAChD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA2B9B;AAED,wBAAgB,6BAA6B,IAAI,IAAI,CAEpD"}
1
+ {"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../../../lib/usage/ledger.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EACX,sBAAsB,EAEtB,gBAAgB,EAChB,gBAAgB,EAEhB,cAAc,EAEd,YAAY,EAGZ,iBAAiB,EAEjB,MAAM,YAAY,CAAC;AAqOpB,wBAAgB,mBAAmB,IAAI,gBAAgB,CAMtD;AAED,wBAAsB,oBAAoB,CACzC,KAAK,EAAE,sBAAsB,GAC3B,OAAO,CAAC,cAAc,CAAC,CASzB;AA+JD,wBAAsB,mBAAmB,CACxC,KAAK,GAAE,gBAAqB,GAC1B,OAAO,CAAC,cAAc,EAAE,CAAC,CAc3B;AAuDD,wBAAgB,kBAAkB,CACjC,IAAI,EAAE,cAAc,EAAE,EACtB,KAAK,GAAE,iBAAsB,GAC3B,YAAY,CAwBd;AAED,wBAAsB,oBAAoB,CACzC,KAAK,GAAE,iBAAsB,GAC3B,OAAO,CAAC,YAAY,CAAC,CAEvB;AAED,wBAAsB,iBAAiB,CAAC,OAAO,GAAE;IAChD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA2B9B;AAED,wBAAgB,6BAA6B,IAAI,IAAI,CAEpD"}
@@ -405,6 +405,7 @@ function createBucket(key) {
405
405
  reasoningTokens: 0,
406
406
  totalTokens: 0,
407
407
  costUsd: 0,
408
+ unpricedRequests: 0,
408
409
  };
409
410
  }
410
411
  function getBucketKey(row, by) {
@@ -437,6 +438,12 @@ function addRowToBucket(bucket, row) {
437
438
  bucket.reasoningTokens += row.tokens.reasoningTokens;
438
439
  bucket.totalTokens += row.tokens.totalTokens;
439
440
  bucket.costUsd = Number((bucket.costUsd + (row.costUsd ?? 0)).toFixed(8));
441
+ // Only rows that actually consumed tokens count as unpriced spend. A blocked
442
+ // or cancelled request carries no tokens and no cost, and must not make a
443
+ // cost budget unevaluable.
444
+ if (row.costUsd === null && row.tokens.totalTokens > 0) {
445
+ bucket.unpricedRequests += 1;
446
+ }
440
447
  }
441
448
  export function summarizeUsageRows(rows, query = {}) {
442
449
  const by = query.by ?? "model";
@@ -5,6 +5,21 @@ export interface UsageModelPricing {
5
5
  cachedInputUsdPerMillion?: number;
6
6
  reasoningUsdPerMillion?: number;
7
7
  }
8
+ /**
9
+ * Models the router can normalize to that have no published rate here yet.
10
+ *
11
+ * Their cost is deliberately reported as unknown (`null`) rather than guessed:
12
+ * a wrong dollar figure is worse than no figure, and it would silently move a
13
+ * `maxCostUsd` budget's trip point. Nothing may treat an unknown cost as zero —
14
+ * `evaluateBudgetGuard` refuses a cost budget while unknown-cost usage is in
15
+ * the window instead of counting it as free (that under-count made cost caps
16
+ * unenforceable for exactly these models, every `pro` tier among them).
17
+ *
18
+ * Add a real rate to MODEL_PRICING and delete the entry here once published;
19
+ * `test/usage-pricing-coverage.test.ts` fails if a NEW routable model appears
20
+ * in neither list.
21
+ */
22
+ export declare const UNPRICED_ROUTABLE_MODELS: readonly ["gpt-5.1", "gpt-5.2-pro", "gpt-5.4-mini", "gpt-5.4-nano", "gpt-5.4-pro", "gpt-5.5-pro", "gpt-5-mini", "gpt-5-nano"];
8
23
  export declare function getUsageModelPricing(model: string | null | undefined): UsageModelPricing | null;
9
24
  export declare function estimateUsageCostUsd(model: string | null | undefined, tokens: UsageTokenCounts): number | null;
10
25
  export declare function listUsageModelPricing(): Record<string, UsageModelPricing>;
@@ -1 +1 @@
1
- {"version":3,"file":"pricing.d.ts","sourceRoot":"","sources":["../../../lib/usage/pricing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,MAAM,WAAW,iBAAiB;IACjC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,sBAAsB,CAAC,EAAE,MAAM,CAAC;CAChC;AAgED,wBAAgB,oBAAoB,CACnC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC9B,iBAAiB,GAAG,IAAI,CAM1B;AAED,wBAAgB,oBAAoB,CACnC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAChC,MAAM,EAAE,gBAAgB,GACtB,MAAM,GAAG,IAAI,CAqBf;AAED,wBAAgB,qBAAqB,IAAI,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAEzE"}
1
+ {"version":3,"file":"pricing.d.ts","sourceRoot":"","sources":["../../../lib/usage/pricing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,MAAM,WAAW,iBAAiB;IACjC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,sBAAsB,CAAC,EAAE,MAAM,CAAC;CAChC;AA2DD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,wBAAwB,+HAS3B,CAAC;AAOX,wBAAgB,oBAAoB,CACnC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC9B,iBAAiB,GAAG,IAAI,CAM1B;AAED,wBAAgB,oBAAoB,CACnC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAChC,MAAM,EAAE,gBAAgB,GACtB,MAAM,GAAG,IAAI,CAqBf;AAED,wBAAgB,qBAAqB,IAAI,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAEzE"}
@@ -54,6 +54,30 @@ const MODEL_PRICING = {
54
54
  reasoningUsdPerMillion: 6,
55
55
  },
56
56
  };
57
+ /**
58
+ * Models the router can normalize to that have no published rate here yet.
59
+ *
60
+ * Their cost is deliberately reported as unknown (`null`) rather than guessed:
61
+ * a wrong dollar figure is worse than no figure, and it would silently move a
62
+ * `maxCostUsd` budget's trip point. Nothing may treat an unknown cost as zero —
63
+ * `evaluateBudgetGuard` refuses a cost budget while unknown-cost usage is in
64
+ * the window instead of counting it as free (that under-count made cost caps
65
+ * unenforceable for exactly these models, every `pro` tier among them).
66
+ *
67
+ * Add a real rate to MODEL_PRICING and delete the entry here once published;
68
+ * `test/usage-pricing-coverage.test.ts` fails if a NEW routable model appears
69
+ * in neither list.
70
+ */
71
+ export const UNPRICED_ROUTABLE_MODELS = [
72
+ "gpt-5.1",
73
+ "gpt-5.2-pro",
74
+ "gpt-5.4-mini",
75
+ "gpt-5.4-nano",
76
+ "gpt-5.4-pro",
77
+ "gpt-5.5-pro",
78
+ "gpt-5-mini",
79
+ "gpt-5-nano",
80
+ ];
57
81
  function normalizeModelName(model) {
58
82
  const trimmed = model?.trim().toLowerCase();
59
83
  return trimmed && trimmed.length > 0 ? trimmed : null;
@@ -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
+ }
@@ -74,6 +74,12 @@ export interface UsageSummaryBucket {
74
74
  reasoningTokens: number;
75
75
  totalTokens: number;
76
76
  costUsd: number;
77
+ /**
78
+ * Requests that consumed tokens but could not be priced, so `costUsd` is an
79
+ * UNDER-count by an unknown amount. A cost budget cannot be evaluated while
80
+ * this is above zero; see `evaluateBudgetGuard`.
81
+ */
82
+ unpricedRequests: number;
77
83
  }
78
84
  export interface UsageSummary {
79
85
  since: number | null;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../lib/usage/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../lib/usage/types.ts"],"names":[],"mappings":"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"}
@@ -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.1",
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",