codex-multi-auth 2.11.0 → 2.13.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 (63) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/README.md +7 -1
  3. package/dist/lib/accounts.d.ts +75 -0
  4. package/dist/lib/accounts.d.ts.map +1 -1
  5. package/dist/lib/accounts.js +213 -42
  6. package/dist/lib/capability-policy.d.ts.map +1 -1
  7. package/dist/lib/capability-policy.js +2 -1
  8. package/dist/lib/codex-manager/account-manager-commands.d.ts.map +1 -1
  9. package/dist/lib/codex-manager/account-manager-commands.js +1 -0
  10. package/dist/lib/codex-manager/commands/limits.d.ts +21 -0
  11. package/dist/lib/codex-manager/commands/limits.d.ts.map +1 -0
  12. package/dist/lib/codex-manager/commands/limits.js +158 -0
  13. package/dist/lib/codex-manager/help.d.ts.map +1 -1
  14. package/dist/lib/codex-manager/help.js +1 -0
  15. package/dist/lib/codex-manager/persist-selected-account.d.ts.map +1 -1
  16. package/dist/lib/codex-manager/persist-selected-account.js +14 -0
  17. package/dist/lib/codex-manager.d.ts.map +1 -1
  18. package/dist/lib/codex-manager.js +12 -0
  19. package/dist/lib/config.d.ts.map +1 -1
  20. package/dist/lib/config.js +2 -1
  21. package/dist/lib/constants.d.ts +9 -0
  22. package/dist/lib/constants.d.ts.map +1 -1
  23. package/dist/lib/constants.js +29 -0
  24. package/dist/lib/entitlement-cache.d.ts.map +1 -1
  25. package/dist/lib/entitlement-cache.js +2 -1
  26. package/dist/lib/forecast.d.ts.map +1 -1
  27. package/dist/lib/forecast.js +1 -14
  28. package/dist/lib/policy/runtime-policy.d.ts +9 -1
  29. package/dist/lib/policy/runtime-policy.d.ts.map +1 -1
  30. package/dist/lib/policy/runtime-policy.js +1 -0
  31. package/dist/lib/preemptive-quota-scheduler.d.ts +12 -0
  32. package/dist/lib/preemptive-quota-scheduler.d.ts.map +1 -1
  33. package/dist/lib/preemptive-quota-scheduler.js +21 -0
  34. package/dist/lib/redaction.d.ts +17 -0
  35. package/dist/lib/redaction.d.ts.map +1 -0
  36. package/dist/lib/redaction.js +28 -0
  37. package/dist/lib/request/error-classification.js +2 -2
  38. package/dist/lib/runtime/rotation-storage-meta.d.ts.map +1 -1
  39. package/dist/lib/runtime/rotation-storage-meta.js +4 -1
  40. package/dist/lib/runtime-rotation-proxy.d.ts +13 -0
  41. package/dist/lib/runtime-rotation-proxy.d.ts.map +1 -1
  42. package/dist/lib/runtime-rotation-proxy.js +47 -7
  43. package/dist/lib/storage.d.ts +20 -2
  44. package/dist/lib/storage.d.ts.map +1 -1
  45. package/dist/lib/storage.js +69 -25
  46. package/dist/lib/usage/ledger.d.ts +1 -1
  47. package/dist/lib/usage/ledger.d.ts.map +1 -1
  48. package/dist/lib/usage/ledger.js +9 -0
  49. package/dist/lib/usage/pricing.d.ts +16 -1
  50. package/dist/lib/usage/pricing.d.ts.map +1 -1
  51. package/dist/lib/usage/pricing.js +46 -8
  52. package/dist/lib/usage/redaction.d.ts +1 -1
  53. package/dist/lib/usage/redaction.d.ts.map +1 -1
  54. package/dist/lib/usage/redaction.js +13 -0
  55. package/dist/lib/usage/types.d.ts +33 -0
  56. package/dist/lib/usage/types.d.ts.map +1 -1
  57. package/dist/lib/usage/types.js +28 -1
  58. package/dist/lib/usage/usage-extraction.d.ts +2 -1
  59. package/dist/lib/usage/usage-extraction.d.ts.map +1 -1
  60. package/dist/lib/usage/usage-extraction.js +44 -4
  61. package/package.json +1 -1
  62. package/scripts/codex-routing.js +1 -0
  63. package/scripts/codex.js +157 -2
@@ -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;AAkCvB,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAkDtE,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;AA8gBD;;;;;;;;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,CAmLrC"}
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;AAkCvB,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAkDtE,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;;;;;;;GAOG;AACH,wBAAgB,+BAA+B,CAC9C,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,GACC,MAAM,CAmBR;AAED,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,CAER;AA8gBD;;;;;;;;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,CAmLrC"}
@@ -80,7 +80,15 @@ const PINNED_PERMANENT_SKIP_REASONS = new Set([
80
80
  ]);
81
81
  const DEFAULT_QUOTA_REMAINING_THRESHOLD = 10;
82
82
  /** @internal Stable identity key for in-memory quota snapshots across reloads. */
83
- export function buildQuotaScheduleKey(account, family, model) {
83
+ /**
84
+ * Quota-scheduler key prefix identifying ONE account across every family and
85
+ * model key it owns.
86
+ *
87
+ * The trailing separator is part of the contract: without it the prefix
88
+ * `account:email:foo` also matches `account:email:foobar:codex`, so clearing
89
+ * one account's observations would silently clear a neighbour's.
90
+ */
91
+ export function buildQuotaScheduleAccountPrefix(account) {
84
92
  const emailKey = normalizeEmailKey(account.email);
85
93
  const accountId = account.accountId?.trim();
86
94
  const refreshToken = account.refreshToken?.trim() ?? "";
@@ -97,7 +105,10 @@ export function buildQuotaScheduleKey(account, family, model) {
97
105
  : accountId
98
106
  ? `id:${accountId}${recordDiscriminator}`
99
107
  : `refresh:${createHash("sha256").update(refreshToken).digest("hex")}${recordDiscriminator}`;
100
- return `account:${accountIdentity}:${model ?? family}`;
108
+ return `account:${accountIdentity}:`;
109
+ }
110
+ export function buildQuotaScheduleKey(account, family, model) {
111
+ return `${buildQuotaScheduleAccountPrefix(account)}${model ?? family}`;
101
112
  }
102
113
  const DEFAULT_MAX_RUNTIME_ACCOUNT_ATTEMPTS = 4;
103
114
  // This is a hard safety ceiling over every pinned selection pass, including
@@ -862,7 +873,37 @@ async function handleRequestInner(state, req, res, traceId) {
862
873
  // otherwise glue an in-flight chat thread to the previously selected
863
874
  // account. The proxy itself never bumps the generation, so its own
864
875
  // debounced disk writes do not clear affinity. See issue #474.
865
- const storageMeta = readStorageMetaFromDisk();
876
+ /** Reconcile once before selection or recording a new upstream observation. */
877
+ const reconcileManualSelection = () => {
878
+ const meta = readStorageMetaFromDisk();
879
+ for (const manager of state.knownAccountManagers) {
880
+ manager.applyManualSelection(meta);
881
+ }
882
+ if (meta.affinityGeneration > state.lastObservedAffinityGeneration) {
883
+ const switchedAccount = meta.pinnedAccountIndex === null
884
+ ? null
885
+ : accountManager.getAccountByIndex(meta.pinnedAccountIndex);
886
+ if (switchedAccount) {
887
+ state.preemptiveQuotaScheduler.clearByPrefix(buildQuotaScheduleAccountPrefix(switchedAccount));
888
+ }
889
+ else if (meta.pinnedAccountIndex !== null) {
890
+ // A pin we cannot resolve: this long-lived proxy re-reads only
891
+ // pin/gen, never the account list, so a `login` that appended an
892
+ // account before the `switch` leaves the new index out of range
893
+ // here. The generation only bumps on the NEXT user-initiated
894
+ // switch, so skipping the clear and advancing anyway would strand
895
+ // the pre-switch quota observation forever, which is the exact
896
+ // deferral this reconciliation exists to end. Drop every cached
897
+ // observation instead: it only costs a re-probe, and real 429
898
+ // windows live on the accounts themselves, not in this cache.
899
+ state.preemptiveQuotaScheduler.clearAll();
900
+ }
901
+ state.sessionAffinityStore?.clearAll();
902
+ state.lastObservedAffinityGeneration = meta.affinityGeneration;
903
+ }
904
+ return meta;
905
+ };
906
+ const storageMeta = reconcileManualSelection();
866
907
  // The ephemeral --account pin (issue #623) takes precedence over the
867
908
  // persisted `switch` pin for this invocation, without ever mutating disk
868
909
  // state. Use `??` (not `||`) so a forced index of 0 is honored. When set,
@@ -899,10 +940,6 @@ async function handleRequestInner(state, req, res, traceId) {
899
940
  if (isPinned) {
900
941
  transientAttemptLimit = Math.max(1, Math.min(state.maxRuntimeAccountAttempts, MAX_PINNED_TRANSIENT_ATTEMPTS));
901
942
  }
902
- if (storageMeta.affinityGeneration > state.lastObservedAffinityGeneration) {
903
- state.sessionAffinityStore?.clearAll();
904
- state.lastObservedAffinityGeneration = storageMeta.affinityGeneration;
905
- }
906
943
  let runtimeSelectionIterations = 0;
907
944
  while ((isPinned || attemptedIndexes.size < accountCount) &&
908
945
  transientAttempts < transientAttemptLimit &&
@@ -1153,6 +1190,7 @@ async function handleRequestInner(state, req, res, traceId) {
1153
1190
  noteRotation();
1154
1191
  continue;
1155
1192
  }
1193
+ reconcileManualSelection();
1156
1194
  const quotaSnapshot = readQuotaSchedulerSnapshot(upstream.headers, upstream.status, state.now());
1157
1195
  if (quotaSnapshot) {
1158
1196
  state.preemptiveQuotaScheduler.update(quotaScheduleKey, quotaSnapshot);
@@ -1162,6 +1200,8 @@ async function handleRequestInner(state, req, res, traceId) {
1162
1200
  const retryAfterMs = parseRetryAfterHeaderMs(upstream.headers, state.now()) ??
1163
1201
  parseRetryAfterBodyMs(bodyText, state.now()) ??
1164
1202
  60_000;
1203
+ // Reading the body awaited I/O; a switch may have landed meanwhile.
1204
+ reconcileManualSelection();
1165
1205
  state.preemptiveQuotaScheduler.markRateLimited(quotaScheduleKey, retryAfterMs, state.now());
1166
1206
  // A 429 is the upstream quota signal for the attempted account, so
1167
1207
  // keep the consumed runtime token drained.
@@ -109,9 +109,27 @@ export declare function bumpStorageAffinityGeneration(storage: {
109
109
  * persisting routine state (rate-limit hits, cooldowns, etc.) so a CLI
110
110
  * `switch`/`unpin` that landed between proxy startup and the save is not
111
111
  * clobbered. Returns `pinnedAccountIndex: undefined` and
112
- * `affinityGeneration: 0` on any failure. See issue #474.
112
+ * `affinityGeneration: 0` on any failure by default. See issue #474.
113
+ *
114
+ * `strict` callers additionally need to know when the metadata could not be
115
+ * OBSERVED AT ALL, because persisting a stale in-memory pin over a newer disk
116
+ * pin is exactly the #474 regression. Strict mode therefore throws for the two
117
+ * cases where a newer selection may be hiding behind the failure:
118
+ * - the file exists but cannot be read (EBUSY/EPERM/EACCES: on Windows an AV
119
+ * scanner or indexer can hold accounts.json open), and
120
+ * - the bytes are not valid JSON (a torn read of a concurrent atomic write).
121
+ *
122
+ * A MISSING file and a successfully parsed file with INVALID field values are
123
+ * deliberately not failures, in either mode. Neither can be concealing a newer
124
+ * pin (there is no file, or we can see the whole file and the field is
125
+ * unusable), and both are self-repaired by the very write that follows. Making
126
+ * them throw would wedge persistence permanently, since recreating or repairing
127
+ * storage goes through the same save path, and it would take every rate-limit
128
+ * window, cooldown and rotated refresh token in memory down with it.
113
129
  */
114
- export declare function readPinAndGenFromDisk(path: string): {
130
+ export declare function readPinAndGenFromDisk(path: string, options?: {
131
+ strict?: boolean;
132
+ }): {
115
133
  pinnedAccountIndex: number | undefined;
116
134
  affinityGeneration: number;
117
135
  };
@@ -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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"}
@@ -898,7 +898,8 @@ export function normalizeAccountStorage(data) {
898
898
  return undefined;
899
899
  if (typeof raw !== "number" ||
900
900
  !Number.isFinite(raw) ||
901
- !Number.isInteger(raw) ||
901
+ // See readAffinityGenerationFromDisk: an unsafe integer wedges the bump.
902
+ !Number.isSafeInteger(raw) ||
902
903
  raw < 0) {
903
904
  log.warn("Dropping invalid affinityGeneration from storage", {
904
905
  value: raw,
@@ -941,7 +942,11 @@ export function readAffinityGenerationFromDisk(path) {
941
942
  const generation = parsed.affinityGeneration;
942
943
  if (typeof generation === "number" &&
943
944
  Number.isFinite(generation) &&
944
- Number.isInteger(generation) &&
945
+ // Safe, not merely integral: past 2^53 `Math.max(gen, disk) + 1 === gen`,
946
+ // so `bumpStorageAffinityGeneration` silently stops advancing and no
947
+ // running proxy ever observes another `switch`. Rejecting the value
948
+ // lets the next bump write 1 and repair the file.
949
+ Number.isSafeInteger(generation) &&
945
950
  generation >= 0) {
946
951
  return generation;
947
952
  }
@@ -978,7 +983,12 @@ export function bumpStorageAffinityGeneration(storage, storagePath) {
978
983
  // No resolvable storage path (never configured). The in-memory counter
979
984
  // is still bumped below; only the lost-update guard is unavailable.
980
985
  }
981
- const inMemoryGeneration = storage.affinityGeneration ?? 0;
986
+ // Guard the in-memory side too, not just the disk read. Past 2^53
987
+ // `Math.max(gen, disk) + 1 === gen`, so an unsafe value here would write the
988
+ // same generation forever and no running proxy would observe another
989
+ // `switch`. Treating it as 0 makes the next write repair the counter.
990
+ const rawInMemory = storage.affinityGeneration ?? 0;
991
+ const inMemoryGeneration = Number.isSafeInteger(rawInMemory) && rawInMemory >= 0 ? rawInMemory : 0;
982
992
  const next = Math.max(inMemoryGeneration, diskGeneration) + 1;
983
993
  storage.affinityGeneration = next;
984
994
  return next;
@@ -990,34 +1000,68 @@ export function bumpStorageAffinityGeneration(storage, storagePath) {
990
1000
  * persisting routine state (rate-limit hits, cooldowns, etc.) so a CLI
991
1001
  * `switch`/`unpin` that landed between proxy startup and the save is not
992
1002
  * clobbered. Returns `pinnedAccountIndex: undefined` and
993
- * `affinityGeneration: 0` on any failure. See issue #474.
1003
+ * `affinityGeneration: 0` on any failure by default. See issue #474.
1004
+ *
1005
+ * `strict` callers additionally need to know when the metadata could not be
1006
+ * OBSERVED AT ALL, because persisting a stale in-memory pin over a newer disk
1007
+ * pin is exactly the #474 regression. Strict mode therefore throws for the two
1008
+ * cases where a newer selection may be hiding behind the failure:
1009
+ * - the file exists but cannot be read (EBUSY/EPERM/EACCES: on Windows an AV
1010
+ * scanner or indexer can hold accounts.json open), and
1011
+ * - the bytes are not valid JSON (a torn read of a concurrent atomic write).
1012
+ *
1013
+ * A MISSING file and a successfully parsed file with INVALID field values are
1014
+ * deliberately not failures, in either mode. Neither can be concealing a newer
1015
+ * pin (there is no file, or we can see the whole file and the field is
1016
+ * unusable), and both are self-repaired by the very write that follows. Making
1017
+ * them throw would wedge persistence permanently, since recreating or repairing
1018
+ * storage goes through the same save path, and it would take every rate-limit
1019
+ * window, cooldown and rotated refresh token in memory down with it.
994
1020
  */
995
- export function readPinAndGenFromDisk(path) {
1021
+ export function readPinAndGenFromDisk(path, options) {
1022
+ const unselected = {
1023
+ pinnedAccountIndex: undefined,
1024
+ affinityGeneration: 0,
1025
+ };
996
1026
  if (!existsSync(path)) {
997
- return { pinnedAccountIndex: undefined, affinityGeneration: 0 };
1027
+ return { ...unselected };
998
1028
  }
1029
+ let parsed;
999
1030
  try {
1000
- const bytes = readFileSync(path);
1001
- const parsed = JSON.parse(bytes.toString("utf8"));
1002
- const rawPin = parsed.pinnedAccountIndex;
1003
- const pinnedAccountIndex = typeof rawPin === "number" &&
1004
- Number.isFinite(rawPin) &&
1005
- Number.isInteger(rawPin) &&
1006
- rawPin >= 0
1007
- ? rawPin
1008
- : undefined;
1009
- const rawGen = parsed.affinityGeneration;
1010
- const affinityGeneration = typeof rawGen === "number" &&
1011
- Number.isFinite(rawGen) &&
1012
- Number.isInteger(rawGen) &&
1013
- rawGen >= 0
1014
- ? rawGen
1015
- : 0;
1016
- return { pinnedAccountIndex, affinityGeneration };
1031
+ parsed = JSON.parse(readFileSync(path, "utf8"));
1017
1032
  }
1018
- catch {
1019
- return { pinnedAccountIndex: undefined, affinityGeneration: 0 };
1033
+ catch (error) {
1034
+ // The file was deleted or renamed between existsSync and the read; same
1035
+ // reasoning as the missing-file branch above.
1036
+ if (error.code === "ENOENT") {
1037
+ return { ...unselected };
1038
+ }
1039
+ if (options?.strict) {
1040
+ throw new Error("Unable to read account selection metadata", {
1041
+ cause: error,
1042
+ });
1043
+ }
1044
+ return { ...unselected };
1045
+ }
1046
+ if (!isRecord(parsed)) {
1047
+ return { ...unselected };
1020
1048
  }
1049
+ const rawPin = parsed.pinnedAccountIndex;
1050
+ const pinnedAccountIndex = typeof rawPin === "number" &&
1051
+ Number.isFinite(rawPin) &&
1052
+ Number.isInteger(rawPin) &&
1053
+ rawPin >= 0
1054
+ ? rawPin
1055
+ : undefined;
1056
+ const rawGen = parsed.affinityGeneration;
1057
+ const affinityGeneration = typeof rawGen === "number" &&
1058
+ Number.isFinite(rawGen) &&
1059
+ // See readAffinityGenerationFromDisk: an unsafe integer wedges the bump.
1060
+ Number.isSafeInteger(rawGen) &&
1061
+ rawGen >= 0
1062
+ ? rawGen
1063
+ : 0;
1064
+ return { pinnedAccountIndex, affinityGeneration };
1021
1065
  }
1022
1066
  /**
1023
1067
  * Loads OAuth accounts from disk storage.
@@ -1,4 +1,4 @@
1
- import type { UsageLedgerAppendInput, UsageLedgerPaths, UsageLedgerQuery, UsageLedgerRow, UsageSummary, UsageSummaryQuery } from "./types.js";
1
+ import { type UsageLedgerAppendInput, type UsageLedgerPaths, type UsageLedgerQuery, type UsageLedgerRow, type UsageSummary, type UsageSummaryQuery } from "./types.js";
2
2
  export declare function getUsageLedgerPaths(): UsageLedgerPaths;
3
3
  export declare function appendUsageLedgerRow(input: UsageLedgerAppendInput): Promise<UsageLedgerRow>;
4
4
  export declare function readUsageLedgerRows(query?: UsageLedgerQuery): Promise<UsageLedgerRow[]>;
@@ -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;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"}
1
+ {"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../../../lib/usage/ledger.ts"],"names":[],"mappings":"AASA,OAAO,EAEN,KAAK,sBAAsB,EAE3B,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EAErB,KAAK,cAAc,EAEnB,KAAK,YAAY,EAGjB,KAAK,iBAAiB,EAEtB,MAAM,YAAY,CAAC;AAqOpB,wBAAgB,mBAAmB,IAAI,gBAAgB,CAMtD;AAED,wBAAsB,oBAAoB,CACzC,KAAK,EAAE,sBAAsB,GAC3B,OAAO,CAAC,cAAc,CAAC,CASzB;AAuKD,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"}
@@ -4,6 +4,7 @@ import { getCodexMultiAuthDir } from "../runtime-paths.js";
4
4
  import { logWarn } from "../logger.js";
5
5
  import { isRecord, sleep } from "../utils.js";
6
6
  import { normalizeUsageLedgerRow, usageRowToJsonLine, } from "./redaction.js";
7
+ import { isKnownServiceTier, } from "./types.js";
7
8
  const USAGE_DIR_NAME = "usage";
8
9
  const USAGE_LEDGER_FILE_NAME = "usage-ledger.jsonl";
9
10
  const RETRYABLE_FS_CODES = new Set(["EBUSY", "EPERM"]);
@@ -287,6 +288,14 @@ function normalizeParsedUsageRow(value) {
287
288
  Number.isFinite(value.tokens.totalTokens)
288
289
  ? Math.max(0, Math.trunc(value.tokens.totalTokens))
289
290
  : 0,
291
+ // Validated against the union rather than passed through: the file is
292
+ // on disk and can be edited, and an unrecognised string would reach the
293
+ // pricer as a tier it has no rate for. Rebuilding only the numeric
294
+ // fields dropped this entirely, so a persisted Fast row read back as
295
+ // standard in `usage` reports.
296
+ ...(isKnownServiceTier(value.tokens.serviceTier)
297
+ ? { serviceTier: value.tokens.serviceTier }
298
+ : {}),
290
299
  };
291
300
  const account = isRecord(value.account)
292
301
  ? {
@@ -1,9 +1,24 @@
1
- import type { UsageTokenCounts } from "./types.js";
1
+ import type { UsageServiceTier, UsageTokenCounts } from "./types.js";
2
2
  export interface UsageModelPricing {
3
3
  inputUsdPerMillion: number;
4
4
  outputUsdPerMillion: number;
5
5
  cachedInputUsdPerMillion?: number;
6
6
  reasoningUsdPerMillion?: number;
7
+ /**
8
+ * Rates for non-standard service tiers, keyed by `UsageServiceTier`.
9
+ *
10
+ * Every entry in `MODEL_PRICING` is a STANDARD-tier rate. OpenAI's Fast tier
11
+ * costs more (2x for GPT-6 Astra), so pricing a Fast session off the standard
12
+ * row under-counts it by half and lets a `maxCostUsd` cap overrun.
13
+ *
14
+ * A tier absent from this map is deliberately NOT approximated from the
15
+ * standard rate. Only Astra's Fast multiplier is published; inventing one for
16
+ * the rest would move a budget's trip point on a guess, which is the failure
17
+ * this file exists to avoid. `estimateUsageCostUsd` reports an unlisted tier
18
+ * as unknown cost instead, so a budget fails closed the same way it does for
19
+ * an unpriced model.
20
+ */
21
+ serviceTiers?: Partial<Record<UsageServiceTier, UsageModelPricing>>;
7
22
  }
8
23
  /**
9
24
  * Models the router can normalize to that have no published rate here yet.
@@ -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;AAwED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,wBAAwB,0MAkB3B,CAAC;AAOX,wBAAgB,oBAAoB,CACnC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC9B,iBAAiB,GAAG,IAAI,CAiB1B;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,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAErE,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;IAChC;;;;;;;;;;;;;OAaG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,gBAAgB,EAAE,iBAAiB,CAAC,CAAC,CAAC;CACpE;AAyFD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,wBAAwB,0MAkB3B,CAAC;AAOX,wBAAgB,oBAAoB,CACnC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC9B,iBAAiB,GAAG,IAAI,CAiB1B;AAmBD,wBAAgB,oBAAoB,CACnC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAChC,MAAM,EAAE,gBAAgB,GACtB,MAAM,GAAG,IAAI,CA8Bf;AAED,wBAAgB,qBAAqB,IAAI,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAEzE"}
@@ -1,16 +1,33 @@
1
1
  const MODEL_PRICING = {
2
2
  // GPT-6 Astra, published at the 2026-09-03 launch: $10 / 1M input,
3
- // $50 / 1M output on the standard service tier. Fast mode is 2x standard
4
- // ($20 / $100); this table has no service-tier dimension, so it prices the
5
- // standard tier and a Fast-mode session is under-counted by half. No cached
6
- // input rate was published, so `cachedInputUsdPerMillion` is deliberately
7
- // absent: `estimateUsageCostUsd` then bills cached tokens at the full input
8
- // rate, which over-states cost. That is the safe direction for a
9
- // `maxCostUsd` budget — it trips early, never late.
3
+ // $50 / 1M output on the standard service tier, and $20 / $100 on Fast,
4
+ // carried in `serviceTiers` below.
5
+ //
6
+ // The cached-input rate is the platform-wide 90% cached discount, which every
7
+ // other row in this table already encodes at exactly input/10. It shipped
8
+ // absent at first, on the reasoning that no Astra-specific cached figure was
9
+ // published; that made Astra the only model billing cached tokens at the FULL
10
+ // input rate, over-stating a cache-heavy session tenfold and tripping a
11
+ // `maxCostUsd` cap far too early. Over-stating is the safer direction than
12
+ // under-stating, but a 10x error blocks legitimate work, and the discount is
13
+ // a uniform platform rate rather than a per-model guess.
10
14
  "gpt-6-astra": {
11
15
  inputUsdPerMillion: 10,
12
16
  outputUsdPerMillion: 50,
17
+ cachedInputUsdPerMillion: 1,
13
18
  reasoningUsdPerMillion: 50,
19
+ serviceTiers: {
20
+ // Published at launch alongside the standard rate: Fast mode is up to
21
+ // 2.5x the speed at 2x the price, $20 / $100 per 1M. This is the only
22
+ // tier multiplier OpenAI has published for any model in this table,
23
+ // which is why it is the only one listed anywhere in this file.
24
+ priority: {
25
+ inputUsdPerMillion: 20,
26
+ outputUsdPerMillion: 100,
27
+ cachedInputUsdPerMillion: 2,
28
+ reasoningUsdPerMillion: 100,
29
+ },
30
+ },
14
31
  },
15
32
  "gpt-5-codex": {
16
33
  inputUsdPerMillion: 1.25,
@@ -122,8 +139,29 @@ export function getUsageModelPricing(model) {
122
139
  }
123
140
  return MODEL_PRICING[normalized] ?? null;
124
141
  }
142
+ /**
143
+ * Pick the rate that applies to a response's service tier.
144
+ *
145
+ * Returns `null` when the tier is real but this table has no rate for it, which
146
+ * the caller turns into an unknown cost. `standard` and an absent tier both use
147
+ * the base row, because every entry in `MODEL_PRICING` is a standard-tier rate.
148
+ */
149
+ function resolveServiceTierPricing(pricing, serviceTier) {
150
+ if (!serviceTier || serviceTier === "standard") {
151
+ return pricing;
152
+ }
153
+ return pricing.serviceTiers?.[serviceTier] ?? null;
154
+ }
125
155
  export function estimateUsageCostUsd(model, tokens) {
126
- const pricing = getUsageModelPricing(model);
156
+ const basePricing = getUsageModelPricing(model);
157
+ if (!basePricing) {
158
+ return null;
159
+ }
160
+ // Resolve the tier BEFORE any arithmetic. A response billed at a tier this
161
+ // table has no rate for must report unknown cost, not a standard-tier
162
+ // figure: under-counting is what lets a `maxCostUsd` cap overrun, and
163
+ // `evaluateBudgetGuard` already knows how to fail closed on `null`.
164
+ const pricing = resolveServiceTierPricing(basePricing, tokens.serviceTier);
127
165
  if (!pricing) {
128
166
  return null;
129
167
  }
@@ -1,4 +1,4 @@
1
- import type { UsageLedgerAccountRef, UsageLedgerAppendInput, UsageLedgerRow } from "./types.js";
1
+ import { type UsageLedgerAccountRef, type UsageLedgerAppendInput, type UsageLedgerRow } from "./types.js";
2
2
  export declare function hashUsageIdentifier(value: string): string;
3
3
  export declare function createUsageAccountRef(input: {
4
4
  accountId?: string | null;
@@ -1 +1 @@
1
- {"version":3,"file":"redaction.d.ts","sourceRoot":"","sources":["../../../lib/usage/redaction.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACX,qBAAqB,EACrB,sBAAsB,EAGtB,cAAc,EAGd,MAAM,YAAY,CAAC;AA2EpB,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE;IAC5C,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B,GAAG,qBAAqB,GAAG,IAAI,CAkB/B;AAsBD,wBAAgB,uBAAuB,CACtC,KAAK,EAAE,sBAAsB,GAC3B,cAAc,CA0BhB;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,CAE9D"}
1
+ {"version":3,"file":"redaction.d.ts","sourceRoot":"","sources":["../../../lib/usage/redaction.ts"],"names":[],"mappings":"AACA,OAAO,EAEN,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAG3B,KAAK,cAAc,EAGnB,MAAM,YAAY,CAAC;AA2EpB,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE;IAC5C,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B,GAAG,qBAAqB,GAAG,IAAI,CAkB/B;AAkCD,wBAAgB,uBAAuB,CACtC,KAAK,EAAE,sBAAsB,GAC3B,cAAc,CA0BhB;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,CAE9D"}
@@ -1,4 +1,5 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
+ import { isKnownServiceTier, } from "./types.js";
2
3
  import { estimateUsageCostUsd } from "./pricing.js";
3
4
  const VALID_SOURCES = new Set([
4
5
  "runtime-proxy",
@@ -98,6 +99,18 @@ function normalizeTokens(input) {
98
99
  totalTokens: providedTotal === null
99
100
  ? computedTotal
100
101
  : Math.max(0, Math.trunc(providedTotal)),
102
+ // Carried through so `estimateUsageCostUsd` below can refuse to price a
103
+ // tier it has no rate for. Dropping it here would silently restore the
104
+ // standard-rate under-count this field exists to prevent.
105
+ //
106
+ // Validated, not passed through on truthiness. Every other field in this
107
+ // normaliser is checked against its allowed set (`normalizeSource`,
108
+ // `normalizeOperation`, `normalizeOutcome`), and the ledger READ path
109
+ // already rejects an unknown tier. Accepting one here made the write
110
+ // path the only unvalidated way onto a row.
111
+ ...(isKnownServiceTier(input.serviceTier)
112
+ ? { serviceTier: input.serviceTier }
113
+ : {}),
101
114
  };
102
115
  }
103
116
  export function normalizeUsageLedgerRow(input) {
@@ -1,12 +1,40 @@
1
1
  export type UsageLedgerSource = "runtime-proxy" | "plugin-host" | "local-bridge" | "cli" | "unknown";
2
2
  export type UsageLedgerOperation = "responses" | "models" | "thread-goal" | "auth-refresh" | "diagnostic" | "unknown";
3
3
  export type UsageLedgerOutcome = "success" | "failure" | "blocked" | "cancelled";
4
+ /**
5
+ * The service tier a response was actually billed at.
6
+ *
7
+ * OpenAI's Fast/priority tier costs more than standard (2x for GPT-6 Astra), so
8
+ * a rate table with no tier dimension under-counts such a session. `standard`
9
+ * and `default` are the same thing on the wire; anything this union does not
10
+ * name is normalised to `unknown`, which is treated as unpriced rather than
11
+ * assumed cheap.
12
+ */
13
+ export declare const USAGE_SERVICE_TIERS: readonly ["standard", "priority", "flex", "batch", "scale", "unknown"];
14
+ export type UsageServiceTier = (typeof USAGE_SERVICE_TIERS)[number];
15
+ /**
16
+ * Narrow an unvalidated value to a known tier.
17
+ *
18
+ * The ledger is JSONL on disk and can be hand-edited, so a value read back from
19
+ * it is untrusted. An unrecognised string reaching the pricer would be treated
20
+ * as a tier with no rate and silently make every affected row unpriced.
21
+ */
22
+ export declare function isKnownServiceTier(value: unknown): value is UsageServiceTier;
4
23
  export interface UsageTokenCounts {
5
24
  inputTokens: number;
6
25
  outputTokens: number;
7
26
  cachedInputTokens: number;
8
27
  reasoningTokens: number;
9
28
  totalTokens: number;
29
+ /**
30
+ * Rides on the token counts rather than a separate parameter so it reaches
31
+ * the ledger through every existing path: the `onUsage` callback in
32
+ * `response-handler.ts` carries this shape, and both the streaming scanner
33
+ * and the non-streaming branch already funnel through
34
+ * `extractResponsesUsage`. Absent when the response did not report one,
35
+ * which is the common case and is priced as `standard`.
36
+ */
37
+ serviceTier?: UsageServiceTier;
10
38
  }
11
39
  export interface UsageLedgerAccountRef {
12
40
  accountHash?: string;
@@ -50,6 +78,11 @@ export interface UsageLedgerAppendInput {
50
78
  cachedInputTokens?: number | null;
51
79
  reasoningTokens?: number | null;
52
80
  totalTokens?: number | null;
81
+ /**
82
+ * The tier the response reported. Absent means standard, which is what every
83
+ * rate in `MODEL_PRICING` is quoted at.
84
+ */
85
+ serviceTier?: UsageServiceTier;
53
86
  costUsd?: number | null;
54
87
  }
55
88
  export type UsageSummaryGroupBy = "model" | "account" | "project" | "outcome" | "day";
@@ -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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"}
@@ -1 +1,28 @@
1
- export {};
1
+ /**
2
+ * The service tier a response was actually billed at.
3
+ *
4
+ * OpenAI's Fast/priority tier costs more than standard (2x for GPT-6 Astra), so
5
+ * a rate table with no tier dimension under-counts such a session. `standard`
6
+ * and `default` are the same thing on the wire; anything this union does not
7
+ * name is normalised to `unknown`, which is treated as unpriced rather than
8
+ * assumed cheap.
9
+ */
10
+ export const USAGE_SERVICE_TIERS = [
11
+ "standard",
12
+ "priority",
13
+ "flex",
14
+ "batch",
15
+ "scale",
16
+ "unknown",
17
+ ];
18
+ /**
19
+ * Narrow an unvalidated value to a known tier.
20
+ *
21
+ * The ledger is JSONL on disk and can be hand-edited, so a value read back from
22
+ * it is untrusted. An unrecognised string reaching the pricer would be treated
23
+ * as a tier with no rate and silently make every affected row unpriced.
24
+ */
25
+ export function isKnownServiceTier(value) {
26
+ return (typeof value === "string" &&
27
+ USAGE_SERVICE_TIERS.includes(value));
28
+ }
@@ -17,7 +17,8 @@ import type { UsageTokenCounts } from "./types.js";
17
17
  export declare function extractUsageTokenCounts(usage: unknown): UsageTokenCounts | null;
18
18
  /**
19
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.
20
+ * bare response object (non-streaming) or wrapped in a stream event envelope,
21
+ * and attach the tier it was billed at.
21
22
  */
22
23
  export declare function extractResponsesUsage(payload: unknown): UsageTokenCounts | null;
23
24
  export interface UsageStreamScanner {
@@ -1 +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"}
1
+ {"version":3,"file":"usage-extraction.d.ts","sourceRoot":"","sources":["../../../lib/usage/usage-extraction.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,gBAAgB,EAAE,MAAM,YAAY,CAAC;AA+BrE;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,uBAAuB,CACtC,KAAK,EAAE,OAAO,GACZ,gBAAgB,GAAG,IAAI,CA+BzB;AAoCD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,gBAAgB,GAAG,IAAI,CAa/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"}