sidekick-shared 0.28.3 → 0.29.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 (151) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +188 -27
  3. package/dist/aggregation/EventAggregator.d.ts +19 -0
  4. package/dist/aggregation/EventAggregator.js +174 -35
  5. package/dist/aggregation/snapshot.d.ts +37 -8
  6. package/dist/aggregation/snapshot.js +31 -9
  7. package/dist/aggregation/types.d.ts +13 -0
  8. package/dist/authStatus.d.ts +54 -0
  9. package/dist/authStatus.js +91 -0
  10. package/dist/browser.d.ts +16 -6
  11. package/dist/browser.js +24 -2
  12. package/dist/codex.d.ts +23 -0
  13. package/dist/codex.js +44 -0
  14. package/dist/codexProfiles.d.ts +1 -0
  15. package/dist/codexProfiles.js +8 -11
  16. package/dist/context/sessionContext.js +4 -0
  17. package/dist/esm/aggregation/EventAggregator.js +174 -35
  18. package/dist/esm/aggregation/snapshot.js +31 -9
  19. package/dist/esm/authStatus.js +87 -0
  20. package/dist/esm/browser.js +5 -2
  21. package/dist/esm/codex.js +18 -0
  22. package/dist/esm/codexProfiles.js +8 -11
  23. package/dist/esm/context/sessionContext.js +4 -0
  24. package/dist/esm/formatters/toolSummary.js +4 -1
  25. package/dist/esm/formatting.js +10 -1
  26. package/dist/esm/index.js +11 -6
  27. package/dist/esm/modelCatalog.js +7 -5
  28. package/dist/esm/modelContext.js +4 -3
  29. package/dist/esm/modelInfo.js +425 -47
  30. package/dist/esm/node.js +9 -3
  31. package/dist/esm/observedContextWindows.js +2 -1
  32. package/dist/esm/ownKey.js +26 -0
  33. package/dist/esm/parsers/codexParser.js +351 -54
  34. package/dist/esm/parsers/codexRolloutOrigin.js +107 -0
  35. package/dist/esm/parsers/openCodeParser.js +85 -22
  36. package/dist/esm/parsers/subagentScanner.js +33 -12
  37. package/dist/esm/parsers/subagentTraceParser.js +97 -37
  38. package/dist/esm/pricing.js +18 -0
  39. package/dist/esm/pricingCatalog.js +17 -9
  40. package/dist/esm/promptHistory.js +66 -4
  41. package/dist/esm/providerFailure.js +124 -3
  42. package/dist/esm/providers/claudeCode.js +10 -2
  43. package/dist/esm/providers/codex.js +84 -62
  44. package/dist/esm/providers/codexDatabase.js +3 -19
  45. package/dist/esm/providers/codexRollouts.js +24 -1
  46. package/dist/esm/providers/openCode.js +9 -17
  47. package/dist/esm/report/htmlReportGenerator.js +42 -5
  48. package/dist/esm/report/transcriptParser.js +13 -11
  49. package/dist/esm/schemas/providerFailure.js +3 -1
  50. package/dist/esm/schemas/sessionEvent.js +35 -14
  51. package/dist/esm/schemas/transcript.js +4 -0
  52. package/dist/esm/sessionPromptHistory.js +2 -2
  53. package/dist/esm/statusline/payload.js +9 -0
  54. package/dist/esm/subagentSpawn.js +163 -0
  55. package/dist/esm/testing/credentialGuard.js +87 -5
  56. package/dist/esm/toolCallOrigins.js +310 -0
  57. package/dist/esm/transcript.js +46 -3
  58. package/dist/esm/turns/assistantTurn.js +3 -2
  59. package/dist/esm/usage/claudeUsageDedupe.js +68 -0
  60. package/dist/esm/usage/usageEvents.js +146 -17
  61. package/dist/esm/usage/usageReports.js +39 -6
  62. package/dist/esm/watchers/eventBridge.js +15 -0
  63. package/dist/esm/watchers/factory.js +5 -3
  64. package/dist/esm/watchers/jsonlTail.js +442 -58
  65. package/dist/esm/watchers/jsonlWatcher.js +51 -4
  66. package/dist/esm/watchers/providerReaderWatcher.js +13 -0
  67. package/dist/esm/zaiQuota.js +2 -1
  68. package/dist/formatters/toolSummary.js +4 -1
  69. package/dist/formatting.d.ts +26 -1
  70. package/dist/formatting.js +11 -1
  71. package/dist/index.d.ts +22 -11
  72. package/dist/index.js +44 -12
  73. package/dist/modelCatalog.js +7 -5
  74. package/dist/modelContext.js +4 -3
  75. package/dist/modelInfo.d.ts +185 -16
  76. package/dist/modelInfo.js +434 -49
  77. package/dist/node.d.ts +16 -5
  78. package/dist/node.js +33 -2
  79. package/dist/observedContextWindows.js +2 -1
  80. package/dist/ownKey.d.ts +16 -0
  81. package/dist/ownKey.js +30 -0
  82. package/dist/parsers/codexParser.d.ts +118 -0
  83. package/dist/parsers/codexParser.js +352 -54
  84. package/dist/parsers/codexRolloutOrigin.d.ts +49 -0
  85. package/dist/parsers/codexRolloutOrigin.js +111 -0
  86. package/dist/parsers/openCodeParser.d.ts +38 -0
  87. package/dist/parsers/openCodeParser.js +88 -22
  88. package/dist/parsers/subagentScanner.d.ts +19 -6
  89. package/dist/parsers/subagentScanner.js +33 -12
  90. package/dist/parsers/subagentTraceParser.d.ts +25 -5
  91. package/dist/parsers/subagentTraceParser.js +97 -37
  92. package/dist/pricing.d.ts +22 -0
  93. package/dist/pricing.js +50 -0
  94. package/dist/pricingCatalog.d.ts +10 -0
  95. package/dist/pricingCatalog.js +16 -8
  96. package/dist/promptHistory.d.ts +65 -4
  97. package/dist/promptHistory.js +68 -5
  98. package/dist/providerFailure.d.ts +44 -0
  99. package/dist/providerFailure.js +126 -4
  100. package/dist/providers/claudeCode.js +9 -1
  101. package/dist/providers/codex.d.ts +3 -22
  102. package/dist/providers/codex.js +85 -64
  103. package/dist/providers/codexDatabase.d.ts +3 -2
  104. package/dist/providers/codexDatabase.js +4 -22
  105. package/dist/providers/codexRollouts.d.ts +22 -0
  106. package/dist/providers/codexRollouts.js +24 -0
  107. package/dist/providers/openCode.js +8 -16
  108. package/dist/providers/types.d.ts +14 -0
  109. package/dist/report/htmlReportGenerator.js +42 -5
  110. package/dist/report/transcriptParser.d.ts +10 -1
  111. package/dist/report/transcriptParser.js +13 -11
  112. package/dist/schemas/index.d.ts +1 -0
  113. package/dist/schemas/providerFailure.d.ts +39 -0
  114. package/dist/schemas/providerFailure.js +2 -0
  115. package/dist/schemas/sessionEvent.d.ts +27 -10
  116. package/dist/schemas/sessionEvent.js +35 -14
  117. package/dist/schemas/transcript.d.ts +2 -0
  118. package/dist/schemas/transcript.js +4 -0
  119. package/dist/sessionPromptHistory.d.ts +7 -1
  120. package/dist/sessionPromptHistory.js +1 -1
  121. package/dist/statusline/payload.d.ts +10 -0
  122. package/dist/statusline/payload.js +14 -0
  123. package/dist/subagentSpawn.d.ts +80 -0
  124. package/dist/subagentSpawn.js +174 -0
  125. package/dist/testing/credentialGuard.d.ts +14 -0
  126. package/dist/testing/credentialGuard.js +87 -5
  127. package/dist/toolCallOrigins.d.ts +70 -0
  128. package/dist/toolCallOrigins.js +346 -0
  129. package/dist/transcript.d.ts +12 -0
  130. package/dist/transcript.js +46 -3
  131. package/dist/turns/assistantTurn.d.ts +1 -0
  132. package/dist/turns/assistantTurn.js +3 -2
  133. package/dist/types/opencode.d.ts +2 -0
  134. package/dist/usage/claudeUsageDedupe.d.ts +31 -0
  135. package/dist/usage/claudeUsageDedupe.js +68 -0
  136. package/dist/usage/usageEvents.d.ts +54 -6
  137. package/dist/usage/usageEvents.js +146 -17
  138. package/dist/usage/usageReports.d.ts +35 -4
  139. package/dist/usage/usageReports.js +39 -6
  140. package/dist/watchers/eventBridge.js +15 -0
  141. package/dist/watchers/factory.d.ts +6 -0
  142. package/dist/watchers/factory.js +5 -3
  143. package/dist/watchers/jsonlTail.d.ts +124 -4
  144. package/dist/watchers/jsonlTail.js +446 -59
  145. package/dist/watchers/jsonlWatcher.d.ts +18 -1
  146. package/dist/watchers/jsonlWatcher.js +50 -3
  147. package/dist/watchers/providerReaderWatcher.d.ts +7 -0
  148. package/dist/watchers/providerReaderWatcher.js +13 -0
  149. package/dist/watchers/types.d.ts +12 -0
  150. package/dist/zaiQuota.js +2 -1
  151. package/package.json +41 -1
@@ -14,12 +14,18 @@
14
14
  * baseline without requiring a redeploy when vendor prices change.
15
15
  * - Longest-prefix matching handles variants like `claude-opus-4.5-20250514`
16
16
  * against a key like `claude-opus-4.5`. Same pattern used in modelContext.ts.
17
+ * - A consumer layer (`registerModelPricing`) sits beside the catalog and
18
+ * imported layers and survives their resets.
19
+ * - Every table-derived value is computed lazily, so a tree-shaken bundle that
20
+ * only uses the parsing/display helpers does not retain the pricing table.
17
21
  *
18
22
  * @module modelInfo
19
23
  */
20
24
  import { getModelContextWindowSize } from './modelContext';
21
25
  import { resolveModelAlias } from './modelAliases';
26
+ import { ownValue } from './ownKey';
22
27
  export { MODEL_PROVIDER_IDS } from './providerIds';
28
+ export { formatCost } from './formatting';
23
29
  // ── Static Pricing Table ──
24
30
  /**
25
31
  * Static baseline pricing. Keys are model-ID prefixes; lookup uses
@@ -621,21 +627,38 @@ const PRICING_TABLE = {
621
627
  cacheReadCostPerMillion: 7.5,
622
628
  },
623
629
  };
624
- /** Static-table keys sorted longest-first so longest-prefix wins. */
625
- const STATIC_SORTED_KEYS = Object.keys(PRICING_TABLE).sort((a, b) => b.length - a.length);
630
+ /**
631
+ * Static-table keys sorted longest-first so longest-prefix wins. Computed on
632
+ * first lookup rather than at module load: a top-level call would keep the
633
+ * whole table alive in tree-shaken bundles that never price anything.
634
+ */
635
+ let staticSortedKeys = null;
636
+ function getStaticSortedKeys() {
637
+ if (!staticSortedKeys) {
638
+ staticSortedKeys = Object.keys(PRICING_TABLE).sort((a, b) => b.length - a.length);
639
+ }
640
+ return staticSortedKeys;
641
+ }
626
642
  // ── Override Table (populated at runtime by pricingCatalog) ──
627
643
  let overrideTable = {};
628
644
  let overrideSortedKeys = [];
645
+ let overrideBasis = null;
629
646
  let importedTable = {};
630
647
  /**
631
648
  * Internal: replace the runtime override map. Called by `pricingCatalog.ts`
632
649
  * after a successful LiteLLM hydration. Not part of the stable public API,
633
650
  * but exported so the Node-only catalog module can wire itself in without
634
651
  * creating a circular import.
652
+ *
653
+ * `basis` records how the map was obtained for `getPricingCatalogInfo()`;
654
+ * omitted, a non-empty map reports as `litellm` with no `fetchedAt`. Every
655
+ * call replaces the previous basis.
635
656
  */
636
- export function _setPricingOverrides(overrides) {
657
+ export function _setPricingOverrides(overrides, basis) {
637
658
  overrideTable = Object.fromEntries(Object.entries(overrides).map(([key, value]) => [key.trim().toLowerCase(), { ...value }]));
638
659
  overrideSortedKeys = Object.keys(overrideTable).sort((a, b) => b.length - a.length);
660
+ overrideBasis = basis ? { ...basis } : null;
661
+ invalidatePricingDigest();
639
662
  }
640
663
  /** Internal: snapshot current overrides (test + diagnostics). */
641
664
  export function _getPricingOverrides() {
@@ -645,6 +668,8 @@ export function _getPricingOverrides() {
645
668
  export function _clearPricingOverrides() {
646
669
  overrideTable = {};
647
670
  overrideSortedKeys = [];
671
+ overrideBasis = null;
672
+ invalidatePricingDigest();
648
673
  }
649
674
  /**
650
675
  * Internal: hydrate exact pricing resolutions exported by another realm.
@@ -653,10 +678,210 @@ export function _clearPricingOverrides() {
653
678
  */
654
679
  export function _setImportedResolvedPricing(values) {
655
680
  importedTable = { ...importedTable, ...values };
681
+ invalidatePricingDigest();
656
682
  }
657
683
  /** Internal test hook. */
658
684
  export function _clearImportedResolvedPricing() {
659
685
  importedTable = {};
686
+ invalidatePricingDigest();
687
+ }
688
+ // A Map (created on first registration) so keys such as `__proto__` stay data.
689
+ let registeredTable = null;
690
+ function isValidModelPricing(value) {
691
+ if (!value || typeof value !== 'object')
692
+ return false;
693
+ const pricing = value;
694
+ return [
695
+ pricing.inputCostPerMillion,
696
+ pricing.outputCostPerMillion,
697
+ pricing.cacheWriteCostPerMillion,
698
+ pricing.cacheReadCostPerMillion,
699
+ ].every((item) => typeof item === 'number' && Number.isFinite(item) && item >= 0);
700
+ }
701
+ /**
702
+ * Register consumer pricing for model ids, e.g. negotiated or private-deployment
703
+ * rates. This layer is separate from the runtime catalog and imported layers:
704
+ * `_setPricingOverrides`, `hydratePricingCatalog`, and
705
+ * `importResolvedModelCatalog` never reset it; only
706
+ * `clearRegisteredModelPricing` does.
707
+ *
708
+ * Keys are trimmed, lowercased, and stripped of `[1m]`, like the other tables.
709
+ * Matching is exact, never by prefix, against the looked-up id, its registered
710
+ * alias target, and its `canonicalModelId()` form — so a `claude-opus-5-5`
711
+ * entry also answers `claude-opus-5-5-20261001` and
712
+ * `us.anthropic.claude-opus-5-5-20261001-v1:0`.
713
+ *
714
+ * Successive calls merge; re-registering a key replaces its price and mode.
715
+ * Entries whose four rates are not all finite, nonnegative numbers are
716
+ * skipped. Results carry `provenance.source: 'registered'`.
717
+ *
718
+ * Registered entries are realm-local configuration and are kept out of
719
+ * cross-realm snapshots: they are not listed by the catalog id enumeration,
720
+ * and `resolveModelPricing(id, { includeRegistered: false })` is the
721
+ * resolution a snapshot should carry. An exported price would otherwise be
722
+ * pinned as an exact import in the other realm and outlive
723
+ * `clearRegisteredModelPricing` there. Register in each realm instead.
724
+ */
725
+ export function registerModelPricing(entries, options) {
726
+ const onlyIfMissing = options?.onlyIfMissing === true;
727
+ let changed = false;
728
+ for (const [key, value] of Object.entries(entries ?? {})) {
729
+ const id = key
730
+ .replace(/\[1m\]/gi, '')
731
+ .trim()
732
+ .toLowerCase();
733
+ if (!id || !isValidModelPricing(value))
734
+ continue;
735
+ registeredTable ??= new Map();
736
+ registeredTable.set(id, {
737
+ pricing: {
738
+ inputCostPerMillion: value.inputCostPerMillion,
739
+ outputCostPerMillion: value.outputCostPerMillion,
740
+ cacheWriteCostPerMillion: value.cacheWriteCostPerMillion,
741
+ cacheReadCostPerMillion: value.cacheReadCostPerMillion,
742
+ },
743
+ onlyIfMissing,
744
+ });
745
+ changed = true;
746
+ }
747
+ if (changed)
748
+ invalidatePricingDigest();
749
+ }
750
+ /** Remove every entry added by `registerModelPricing`. */
751
+ export function clearRegisteredModelPricing() {
752
+ if (registeredTable && registeredTable.size > 0)
753
+ invalidatePricingDigest();
754
+ registeredTable = null;
755
+ }
756
+ function findRegisteredPricing(rawId, aliasId) {
757
+ if (!registeredTable || registeredTable.size === 0)
758
+ return null;
759
+ const canonical = canonicalModelId(rawId);
760
+ for (const id of [rawId, aliasId, canonical, resolveModelAlias(canonical)]) {
761
+ const entry = registeredTable.get(id);
762
+ if (entry)
763
+ return { id, entry };
764
+ }
765
+ return null;
766
+ }
767
+ // ── Pricing Basis ──
768
+ let pricingDigestMemo = null;
769
+ function invalidatePricingDigest() {
770
+ pricingDigestMemo = null;
771
+ }
772
+ /** JSON with object keys sorted at every level; `undefined` members are dropped. */
773
+ function canonicalJson(value) {
774
+ if (value === null || typeof value !== 'object') {
775
+ return JSON.stringify(value) ?? 'null';
776
+ }
777
+ if (Array.isArray(value))
778
+ return `[${value.map(canonicalJson).join(',')}]`;
779
+ const record = value;
780
+ const keys = Object.keys(record)
781
+ .filter((key) => record[key] !== undefined)
782
+ .sort();
783
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(',')}}`;
784
+ }
785
+ /**
786
+ * 64-bit FNV-1a over the UTF-8 bytes of `text`, as 16 lowercase hex digits.
787
+ * Uses four 16-bit limbs so it runs on plain numbers in every JS runtime.
788
+ */
789
+ function fnv1a64Hex(text) {
790
+ // Offset basis 0xcbf29ce484222325, least significant limb first.
791
+ let h0 = 0x2325;
792
+ let h1 = 0x8422;
793
+ let h2 = 0x9ce4;
794
+ let h3 = 0xcbf2;
795
+ const mix = (byte) => {
796
+ h0 ^= byte;
797
+ // Multiply by the FNV prime 0x100000001b3 = 2^40 + 0x1b3, mod 2^64.
798
+ const t0 = h0 * 0x1b3;
799
+ const t1 = h1 * 0x1b3 + (t0 >>> 16);
800
+ const t2 = h2 * 0x1b3 + (h0 << 8) + (t1 >>> 16);
801
+ const t3 = h3 * 0x1b3 + (h1 << 8) + (t2 >>> 16);
802
+ h0 = t0 & 0xffff;
803
+ h1 = t1 & 0xffff;
804
+ h2 = t2 & 0xffff;
805
+ h3 = t3 & 0xffff;
806
+ };
807
+ for (let i = 0; i < text.length; i++) {
808
+ let code = text.charCodeAt(i);
809
+ if (code >= 0xd800 && code <= 0xdfff) {
810
+ const low = code <= 0xdbff && i + 1 < text.length ? text.charCodeAt(i + 1) : 0;
811
+ if (low >= 0xdc00 && low <= 0xdfff) {
812
+ code = 0x10000 + ((code - 0xd800) << 10) + (low - 0xdc00);
813
+ i++;
814
+ }
815
+ else {
816
+ // A lone surrogate encodes as U+FFFD, as TextEncoder does.
817
+ code = 0xfffd;
818
+ }
819
+ }
820
+ if (code < 0x80) {
821
+ mix(code);
822
+ }
823
+ else if (code < 0x800) {
824
+ mix(0xc0 | (code >> 6));
825
+ mix(0x80 | (code & 0x3f));
826
+ }
827
+ else if (code < 0x10000) {
828
+ mix(0xe0 | (code >> 12));
829
+ mix(0x80 | ((code >> 6) & 0x3f));
830
+ mix(0x80 | (code & 0x3f));
831
+ }
832
+ else {
833
+ mix(0xf0 | (code >> 18));
834
+ mix(0x80 | ((code >> 12) & 0x3f));
835
+ mix(0x80 | ((code >> 6) & 0x3f));
836
+ mix(0x80 | (code & 0x3f));
837
+ }
838
+ }
839
+ return [h3, h2, h1, h0].map((limb) => limb.toString(16).padStart(4, '0')).join('');
840
+ }
841
+ function getPricingDigest() {
842
+ if (pricingDigestMemo === null) {
843
+ const imported = {};
844
+ for (const [id, entry] of Object.entries(importedTable)) {
845
+ imported[id] = { pricing: entry.pricing, provenance: entry.provenance };
846
+ }
847
+ const registered = {};
848
+ for (const [id, entry] of registeredTable ?? []) {
849
+ registered[id] = { pricing: entry.pricing, onlyIfMissing: entry.onlyIfMissing };
850
+ }
851
+ const payload = canonicalJson({
852
+ v: 1,
853
+ static: PRICING_TABLE,
854
+ catalog: overrideTable,
855
+ imported,
856
+ registered,
857
+ });
858
+ pricingDigestMemo = `fnv1a64:${fnv1a64Hex(payload)}`;
859
+ }
860
+ return pricingDigestMemo;
861
+ }
862
+ /**
863
+ * Describe the pricing basis behind current cost estimates: which layer the
864
+ * prices come from, when the catalog was fetched, and a digest that changes
865
+ * whenever any active price changes. Pure and cheap after the first call (the
866
+ * digest is memoized until a table changes).
867
+ */
868
+ export function getPricingCatalogInfo() {
869
+ const catalogEntries = overrideSortedKeys.length;
870
+ let source = 'static';
871
+ if (catalogEntries > 0) {
872
+ source = overrideBasis?.source ?? 'litellm';
873
+ }
874
+ else if (Object.keys(importedTable).length > 0) {
875
+ source = 'imported';
876
+ }
877
+ const fetchedAt = catalogEntries > 0 ? overrideBasis?.fetchedAt : undefined;
878
+ return {
879
+ source,
880
+ ...(fetchedAt ? { fetchedAt } : {}),
881
+ digest: getPricingDigest(),
882
+ catalogEntries,
883
+ registeredEntries: registeredTable?.size ?? 0,
884
+ };
660
885
  }
661
886
  /** Internal: list every ID known to the pricing resolver. */
662
887
  export function _getModelPricingIds() {
@@ -666,6 +891,86 @@ export function _getModelPricingIds() {
666
891
  ...Object.keys(importedTable),
667
892
  ]));
668
893
  }
894
+ // ── Model ID Canonicalization ──
895
+ // Bedrock: optional cross-region prefix (`us.`, `eu.`, `apac.`, `global.`, …)
896
+ // then the vendor segment, e.g. `us.anthropic.claude-opus-5-5-20261001-v1:0`.
897
+ const BEDROCK_ANTHROPIC_RE = /^(?:[a-z]{2,6}(?:-[a-z]{2,4})?\.)?anthropic\./;
898
+ // Bedrock revision tag (`-v1:0`, `-v1`), only after a version or date digit so
899
+ // legacy names such as `claude-v2:1` and `claude-instant-v1` keep theirs.
900
+ const BEDROCK_REVISION_RE = /([0-9])-v[0-9]+(?::[0-9]+)?$/;
901
+ const VERTEX_VERSION_RE = /@(?:[0-9]{8}|latest)$/;
902
+ const LATEST_SUFFIX_RE = /-latest$/;
903
+ const DASHED_DATE_SUFFIX_RE = /-[0-9]{4}-[0-9]{2}-[0-9]{2}$/;
904
+ const COMPACT_DATE_SUFFIX_RE = /-[0-9]{8}$/;
905
+ /**
906
+ * Reduce a model id to the bare model name it denotes, for grouping and
907
+ * matching across hosts and snapshots.
908
+ *
909
+ * Trims and lowercases, then removes: `[1m]`; every leading `segment/`
910
+ * provider or region prefix (`azure/us/gpt-6.1-terra` → `gpt-6.1-terra`);
911
+ * Bedrock forms (`us.anthropic.claude-opus-5-5-20261001-v1:0` →
912
+ * `claude-opus-5-5`, also `eu.`/`apac.`/`global.` and bare `anthropic.`);
913
+ * Vertex `@YYYYMMDD`; trailing `-YYYYMMDD` and `-YYYY-MM-DD` dates; `-latest`.
914
+ * Meaningful suffixes (`-mini`, `-codex`, `-sol`, `-terra`, `-high`, …) stay.
915
+ *
916
+ * Pure and syntactic: registered aliases are not applied (see
917
+ * `resolveModelAlias`). Pricing lookups do not use this to reach other
918
+ * models' prices — dated snapshots and deployments can bill differently — only
919
+ * to match `registerModelPricing` entries.
920
+ */
921
+ export function canonicalModelId(id) {
922
+ let value = String(id ?? '')
923
+ .trim()
924
+ .toLowerCase()
925
+ .replace(/\[1m\]/g, '')
926
+ .trim();
927
+ const slash = value.lastIndexOf('/');
928
+ if (slash >= 0 && slash < value.length - 1)
929
+ value = value.slice(slash + 1);
930
+ const bedrock = BEDROCK_ANTHROPIC_RE.exec(value);
931
+ if (bedrock && value.length > bedrock[0].length) {
932
+ value = value.slice(bedrock[0].length).replace(BEDROCK_REVISION_RE, '$1');
933
+ }
934
+ // Suffixes can stack (`-20250101-latest`), so strip until nothing changes.
935
+ for (;;) {
936
+ const next = value
937
+ .replace(VERTEX_VERSION_RE, '')
938
+ .replace(LATEST_SUFFIX_RE, '')
939
+ .replace(DASHED_DATE_SUFFIX_RE, '')
940
+ .replace(COMPACT_DATE_SUFFIX_RE, '');
941
+ if (next === value || !next)
942
+ break;
943
+ value = next;
944
+ }
945
+ return value;
946
+ }
947
+ // Names an agent CLI re-points at a different concrete model over time.
948
+ // An object literal rather than a Set keeps it free for tree-shaking.
949
+ const MOVING_MODEL_ALIASES = {
950
+ default: true,
951
+ auto: true,
952
+ latest: true,
953
+ sonnet: true,
954
+ opus: true,
955
+ haiku: true,
956
+ fable: true,
957
+ opusplan: true,
958
+ };
959
+ /**
960
+ * True for model names an agent CLI resolves to a different concrete model
961
+ * over time (`default`, `auto`, `latest`, `sonnet`, `opus`, `haiku`, `fable`,
962
+ * and Claude Code's `opusplan`), so a recorded value names a moving target
963
+ * rather than a model. Case-insensitive, trimmed, and `[1m]`-tolerant
964
+ * (`sonnet[1m]`). Dated or versioned ids are never moving aliases.
965
+ */
966
+ export function isMovingModelAlias(id) {
967
+ const value = String(id ?? '')
968
+ .trim()
969
+ .toLowerCase()
970
+ .replace(/\[1m\]/g, '')
971
+ .trim();
972
+ return Object.prototype.hasOwnProperty.call(MOVING_MODEL_ALIASES, value);
973
+ }
669
974
  // ── Model ID Parsing ──
670
975
  // Anthropic writes the minor version with a hyphen in modern IDs
671
976
  // (`claude-sonnet-4-6-20260321`) and a dot in older ones
@@ -677,6 +982,14 @@ const LEGACY_CLAUDE_RE = /^claude-([0-9]+(?:[-.][0-9]+)?)-(haiku|sonnet|opus)(?:
677
982
  const GPT_RE = /^gpt-([0-9][0-9.A-Za-z-]*)/i;
678
983
  const O_SERIES_RE = /^o([0-9]+)(-mini|-pro)?/i;
679
984
  const GEMINI_RE = /^gemini-([0-9][0-9.A-Za-z-]*)/i;
985
+ // GPT tier names, read from the parsed version (`6.1-sol`, `6-luna-2026-01-01`).
986
+ const GPT_TIER_VARIANT_RE = /^[0-9]+(?:\.[0-9]+)*-(astra|sol|terra|luna)(?=-|$)/;
987
+ const GPT_TIER_VARIANT_RANK = {
988
+ astra: 0,
989
+ sol: 1,
990
+ terra: 2,
991
+ luna: 3,
992
+ };
680
993
  /**
681
994
  * Parses a model ID into {provider, family, version}.
682
995
  *
@@ -708,7 +1021,13 @@ export function parseModelId(modelId) {
708
1021
  }
709
1022
  const gpt = normalized.match(GPT_RE);
710
1023
  if (gpt) {
711
- return { provider: 'openai', family: 'gpt', version: gpt[1] };
1024
+ const version = gpt[1];
1025
+ const variant = GPT_TIER_VARIANT_RE.exec(version)?.[1];
1026
+ // The key is only present when set, so untiered results stay deep-equal
1027
+ // to what earlier releases returned.
1028
+ return variant
1029
+ ? { provider: 'openai', family: 'gpt', version, variant }
1030
+ : { provider: 'openai', family: 'gpt', version };
712
1031
  }
713
1032
  const oSeries = normalized.match(O_SERIES_RE);
714
1033
  if (oSeries) {
@@ -730,24 +1049,45 @@ function findLongestPrefix(keys, modelId) {
730
1049
  }
731
1050
  return null;
732
1051
  }
733
- /** Resolve pricing and disclose whether the result was inherited by prefix. */
734
- export function resolveModelPricing(modelId) {
1052
+ /**
1053
+ * Resolve pricing and disclose which layer answered and whether the result was
1054
+ * inherited by prefix.
1055
+ *
1056
+ * Order: registered entries (default mode); exact imported, catalog, and
1057
+ * static matches; registered `onlyIfMissing` entries; catalog then static
1058
+ * prefix matches; unpriced.
1059
+ */
1060
+ export function resolveModelPricing(modelId, options) {
735
1061
  const stripped = modelId.replace(/\[1m\]/gi, '');
736
1062
  const rawId = stripped.trim().toLowerCase();
737
- const canonicalModelId = resolveModelAlias(stripped);
1063
+ const aliasId = resolveModelAlias(stripped);
738
1064
  // The raw id is tried before its alias target at every step: pricing
739
1065
  // recorded for the alias id itself is more specific than the canonical
740
1066
  // fallback, so registering an alias never orphans it.
741
- const candidateIds = rawId === canonicalModelId ? [canonicalModelId] : [rawId, canonicalModelId];
742
- const imported = importedTable[rawId] ?? importedTable[canonicalModelId];
1067
+ const candidateIds = rawId === aliasId ? [aliasId] : [rawId, aliasId];
1068
+ const registered = options?.includeRegistered === false ? null : findRegisteredPricing(rawId, aliasId);
1069
+ const registeredResult = (match) => ({
1070
+ modelId,
1071
+ canonicalModelId: aliasId,
1072
+ pricing: { ...match.entry.pricing },
1073
+ provenance: {
1074
+ source: 'registered',
1075
+ match: 'exact',
1076
+ matchedModelId: match.id,
1077
+ inheritedByPrefix: false,
1078
+ },
1079
+ });
1080
+ if (registered && !registered.entry.onlyIfMissing)
1081
+ return registeredResult(registered);
1082
+ const imported = ownValue(importedTable, rawId) ?? ownValue(importedTable, aliasId);
743
1083
  if (imported)
744
- return { ...imported, modelId, canonicalModelId };
1084
+ return { ...imported, modelId, canonicalModelId: aliasId };
745
1085
  for (const id of candidateIds) {
746
- const exactOverride = overrideTable[id];
1086
+ const exactOverride = ownValue(overrideTable, id);
747
1087
  if (exactOverride) {
748
1088
  return {
749
1089
  modelId,
750
- canonicalModelId,
1090
+ canonicalModelId: aliasId,
751
1091
  pricing: exactOverride,
752
1092
  provenance: {
753
1093
  source: 'catalog',
@@ -759,11 +1099,11 @@ export function resolveModelPricing(modelId) {
759
1099
  }
760
1100
  }
761
1101
  for (const id of candidateIds) {
762
- const exactStatic = PRICING_TABLE[id];
1102
+ const exactStatic = ownValue(PRICING_TABLE, id);
763
1103
  if (exactStatic) {
764
1104
  return {
765
1105
  modelId,
766
- canonicalModelId,
1106
+ canonicalModelId: aliasId,
767
1107
  pricing: exactStatic,
768
1108
  provenance: {
769
1109
  source: 'static',
@@ -774,12 +1114,15 @@ export function resolveModelPricing(modelId) {
774
1114
  };
775
1115
  }
776
1116
  }
1117
+ // A gap-filling registration answers once no layer has an exact price.
1118
+ if (registered)
1119
+ return registeredResult(registered);
777
1120
  for (const id of candidateIds) {
778
1121
  const overridePrefix = findLongestPrefix(overrideSortedKeys, id);
779
1122
  if (overridePrefix) {
780
1123
  return {
781
1124
  modelId,
782
- canonicalModelId,
1125
+ canonicalModelId: aliasId,
783
1126
  pricing: overrideTable[overridePrefix],
784
1127
  provenance: {
785
1128
  source: 'catalog',
@@ -791,11 +1134,11 @@ export function resolveModelPricing(modelId) {
791
1134
  }
792
1135
  }
793
1136
  for (const id of candidateIds) {
794
- const staticPrefix = findLongestPrefix(STATIC_SORTED_KEYS, id);
1137
+ const staticPrefix = findLongestPrefix(getStaticSortedKeys(), id);
795
1138
  if (staticPrefix) {
796
1139
  return {
797
1140
  modelId,
798
- canonicalModelId,
1141
+ canonicalModelId: aliasId,
799
1142
  pricing: PRICING_TABLE[staticPrefix],
800
1143
  provenance: {
801
1144
  source: 'static',
@@ -808,7 +1151,7 @@ export function resolveModelPricing(modelId) {
808
1151
  }
809
1152
  return {
810
1153
  modelId,
811
- canonicalModelId,
1154
+ canonicalModelId: aliasId,
812
1155
  pricing: null,
813
1156
  provenance: { source: 'none', match: 'none', inheritedByPrefix: false },
814
1157
  };
@@ -816,9 +1159,11 @@ export function resolveModelPricing(modelId) {
816
1159
  /**
817
1160
  * Gets pricing for a model ID.
818
1161
  *
819
- * Lookup order:
820
- * 1. Runtime override map (from LiteLLM catalog hydration).
821
- * 2. Static PRICING_TABLE.
1162
+ * Lookup order (see `resolveModelPricing` for provenance):
1163
+ * 1. Registered entries (`registerModelPricing`), exact matches only.
1164
+ * 2. Imported resolutions, then the runtime override map (from LiteLLM
1165
+ * catalog hydration), then the static PRICING_TABLE — exact before prefix,
1166
+ * with gap-filling registered entries between the two.
822
1167
  * 3. `null` — unknown model. Callers MUST handle this.
823
1168
  *
824
1169
  * The ID is first looked up verbatim (minus the "[1m]" suffix) — override
@@ -910,22 +1255,47 @@ export function mergeCostSources(a, b) {
910
1255
  };
911
1256
  return rank[a] >= rank[b] ? a : b;
912
1257
  }
913
- // ── Display ──
914
- function normalizeModelId(modelId) {
915
- return modelId
916
- .trim()
917
- .toLowerCase()
918
- .replace(/^[a-z]+\//, '')
919
- .replace(/-\d{8}$/, '')
920
- .replace(/-latest$/, '')
921
- .replace(/\[1m\]/gi, '');
1258
+ /**
1259
+ * Map the provenance `calculateNormalizedUsageCost` reports to the display
1260
+ * cost source: provider-reported costs are `reported`, costs computed from
1261
+ * explicit or catalog pricing are `estimated`, and unpriced stays `unpriced`.
1262
+ */
1263
+ export function costSourceFromPricingProvenance(provenance) {
1264
+ switch (provenance) {
1265
+ case 'provider-reported':
1266
+ return 'reported';
1267
+ case 'explicit-pricing':
1268
+ case 'model-catalog':
1269
+ return 'estimated';
1270
+ default:
1271
+ return 'unpriced';
1272
+ }
1273
+ }
1274
+ /**
1275
+ * Merge the cost sources of a rollup's components, least certain winning (as
1276
+ * `mergeCostSources`). Components without a `source` have not been seen yet
1277
+ * and are ignored, and an `unpriced` component that contributed no tokens
1278
+ * does not taint the total. Returns `undefined` when nothing counted.
1279
+ */
1280
+ export function mergeCostComponents(components) {
1281
+ let merged;
1282
+ for (const component of components) {
1283
+ const source = component?.source;
1284
+ if (!source)
1285
+ continue;
1286
+ if (source === 'unpriced' && !(component.tokens > 0))
1287
+ continue;
1288
+ merged = merged ? mergeCostSources(merged, source) : source;
1289
+ }
1290
+ return merged;
922
1291
  }
1292
+ // ── Display ──
923
1293
  /**
924
1294
  * Short display label for model IDs. Keeps historical Claude family labels
925
1295
  * compact while normalizing common OpenAI labels.
926
1296
  */
927
1297
  export function shortModelName(modelId) {
928
- const normalized = normalizeModelId(modelId);
1298
+ const normalized = canonicalModelId(modelId);
929
1299
  const parsed = parseModelId(normalized);
930
1300
  if (normalized.includes('codex'))
931
1301
  return 'Codex';
@@ -964,7 +1334,7 @@ function versionRank(version) {
964
1334
  */
965
1335
  export function getModelDisplayInfo(modelId) {
966
1336
  const parsed = parseModelId(modelId);
967
- const normalized = normalizeModelId(modelId);
1337
+ const normalized = canonicalModelId(modelId);
968
1338
  let rank = 1_000;
969
1339
  if (parsed?.provider === 'anthropic') {
970
1340
  rank = (CLAUDE_FAMILY_RANK[parsed.family] ?? 9) * 100 + versionRank(parsed.version);
@@ -990,29 +1360,37 @@ export function getModelDisplayInfo(modelId) {
990
1360
  rank,
991
1361
  };
992
1362
  }
993
- /** Compare two model IDs using shared provider/family ranking rules. */
1363
+ /**
1364
+ * Order two GPT tier variants of the same family and numeric version
1365
+ * (astra > sol > terra > luna); 0 for every other pair.
1366
+ */
1367
+ function compareTierVariants(a, b) {
1368
+ const left = parseModelId(a);
1369
+ const right = parseModelId(b);
1370
+ if (!left?.variant || !right?.variant)
1371
+ return 0;
1372
+ if (left.provider !== right.provider || left.family !== right.family)
1373
+ return 0;
1374
+ if (versionRank(left.version) !== versionRank(right.version))
1375
+ return 0;
1376
+ return ((GPT_TIER_VARIANT_RANK[left.variant] ?? Number.MAX_SAFE_INTEGER) -
1377
+ (GPT_TIER_VARIANT_RANK[right.variant] ?? Number.MAX_SAFE_INTEGER));
1378
+ }
1379
+ /**
1380
+ * Compare two model IDs using shared provider/family ranking rules. Within a
1381
+ * family and numeric version, GPT tiers rank astra > sol > terra > luna.
1382
+ */
994
1383
  export function compareModelIds(a, b) {
995
1384
  const left = getModelDisplayInfo(a);
996
1385
  const right = getModelDisplayInfo(b);
997
1386
  if (left.rank !== right.rank)
998
1387
  return left.rank - right.rank;
1388
+ const variantOrder = compareTierVariants(a, b);
1389
+ if (variantOrder !== 0)
1390
+ return variantOrder;
999
1391
  return a.localeCompare(b);
1000
1392
  }
1001
1393
  /** Return a sorted copy of model IDs using shared provider/family ranking rules. */
1002
1394
  export function sortModelIds(modelIds) {
1003
1395
  return [...modelIds].sort(compareModelIds);
1004
1396
  }
1005
- /**
1006
- * Formats a USD cost as a currency string.
1007
- *
1008
- * - `null` / `undefined` → `"—"` (honest "pricing unavailable").
1009
- * - `< $0.01` → 4 decimals, so sub-cent costs are still visible.
1010
- * - otherwise → 2 decimals.
1011
- */
1012
- export function formatCost(cost) {
1013
- if (cost === null || cost === undefined)
1014
- return '—';
1015
- if (cost < 0.01)
1016
- return `$${cost.toFixed(4)}`;
1017
- return `$${cost.toFixed(2)}`;
1018
- }
package/dist/esm/node.js CHANGED
@@ -8,7 +8,8 @@
8
8
  export { hydratePricingCatalog, normalizeLiteLlmCatalog, normalizeLiteLlmContextWindows, LITELLM_CATALOG_URL, } from './pricingCatalog';
9
9
  export { loadObservedContextWindows, recordObservedContextWindow, getObservedContextWindowPath, } from './observedContextWindows';
10
10
  export { listRecentSessions, readSessionTranscript } from './sessionTranscripts';
11
- export { collectPromptHistory, isPromptHistorySessionSettled, parsePromptHistoryCursor, } from './promptHistory';
11
+ export { collectPromptHistory, isPromptHistorySessionSettled, parsePromptHistoryCursor, PROMPT_HISTORY_ORDINAL_MAX_RECORD_BYTES, } from './promptHistory';
12
+ export { findToolCallOrigins } from './toolCallOrigins';
12
13
  export { collectSessionPromptHistory } from './sessionPromptHistory';
13
14
  export { HUMAN_CLAUDE_ENTRYPOINTS, HUMAN_CODEX_SOURCES, humanPromptText, isHumanPrompt, } from './humanPrompt';
14
15
  export { computeSessionFileStats, firstUserPrompt, providerContextSizeFn, readSessionFileStats, } from './sessionStats';
@@ -35,7 +36,12 @@ export { CONFIG_DIR_ENV_VAR, getConfigDir, getConfigDirOverride, getDefaultConfi
35
36
  export { encodeWorkspacePath as encodeClaudeWorkspacePath, getSessionDirectory as getClaudeSessionDirectory, } from './parsers/sessionPathResolver';
36
37
  // Provider homes the CLIs read for this process
37
38
  export { getLiveClaudeHome, getDefaultClaudeHome, isDefaultClaudeHome, getClaudeProjectsDir, } from './claudePaths';
38
- export { getSystemCodexHome, getCodexMonitoringHomes, resolveSidekickCodexHome, getCodexExecutionEnv, getCodexSessionsDirs, CODEX_SESSIONS_SUBDIR, CODEX_ARCHIVED_SESSIONS_SUBDIR, } from './codexPaths';
39
+ export { getSystemCodexHome, getCodexMonitoringHomes, resolveSidekickCodexHome, getCodexExecutionEnv, getCodexSessionsDirs, getCodexCredentialStoreMode, forceFileCredentialStore, CODEX_SESSIONS_SUBDIR, CODEX_ARCHIVED_SESSIONS_SUBDIR, } from './codexPaths';
40
+ export { parseAuthJson as parseCodexAuthJson, readAuthIdentityFromRaw as readCodexAuthIdentity, readLastRefresh as readCodexLastRefresh, readAccessTokenExpiry as readCodexAccessTokenExpiry, identitiesMatch as codexIdentitiesMatch, codexRefreshFingerprint, compareCodexAuthFreshness, } from './codexAuth';
41
+ export { atomicWriteJson, atomicWriteJsonSync } from './writers/atomic';
42
+ export { parseClaudeAuthStatus, parseCodexLoginStatus } from './authStatus';
43
+ export { codexRolloutOrigin, codexSpawnParentFromSource } from './parsers/codexRolloutOrigin';
44
+ export { getPricingCatalogInfo, registerModelPricing, clearRegisteredModelPricing, } from './modelInfo';
39
45
  export { fetchProviderServiceStatus } from './providerServiceStatus';
40
46
  // Tool categories across providers
41
47
  export { classifyTool, TOOL_CATEGORIES } from './toolCategory';
@@ -43,4 +49,4 @@ export { classifyTool, TOOL_CATEGORIES } from './toolCategory';
43
49
  export { resolveWorkspaceRoots } from './workspaceRoots';
44
50
  export { discoverWorktreeSiblings, resolveWorktreeMainRepo } from './parsers/sessionPathResolver';
45
51
  // Bounded JSONL tail reader
46
- export { createJsonlTail, DEFAULT_JSONL_TAIL_MAX_LINE_BYTES, DEFAULT_JSONL_TAIL_READ_BYTES, } from './watchers/jsonlTail';
52
+ export { captureJsonlCheckpoint, createJsonlTail, DEFAULT_JSONL_TAIL_MAX_LINE_BYTES, DEFAULT_JSONL_TAIL_READ_BYTES, JSONL_TAIL_CHECKPOINT_VERSION, JsonlTailSymlinkError, verifyJsonlCheckpoint, } from './watchers/jsonlTail';
@@ -32,6 +32,7 @@
32
32
  import { promises as fs } from 'node:fs';
33
33
  import * as path from 'node:path';
34
34
  import { _setObservedContextWindows, _getObservedContextWindows } from './modelContext';
35
+ import { setOwnValue } from './ownKey';
35
36
  import { getConfigDir } from './paths';
36
37
  import { updateJsonStoreAtomic } from './writers/atomic';
37
38
  // ── Constants ──
@@ -80,7 +81,7 @@ function toOverrideMap(store, now = Date.now()) {
80
81
  const observedAt = typeof entry.observedAt === 'string' ? Date.parse(entry.observedAt) : NaN;
81
82
  if (Number.isFinite(observedAt) && now - observedAt > OBSERVED_CONTEXT_WINDOW_TTL_MS)
82
83
  continue;
83
- out[modelId] = window;
84
+ setOwnValue(out, modelId, window);
84
85
  }
85
86
  return out;
86
87
  }