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
package/dist/modelInfo.js CHANGED
@@ -15,17 +15,26 @@
15
15
  * baseline without requiring a redeploy when vendor prices change.
16
16
  * - Longest-prefix matching handles variants like `claude-opus-4.5-20250514`
17
17
  * against a key like `claude-opus-4.5`. Same pattern used in modelContext.ts.
18
+ * - A consumer layer (`registerModelPricing`) sits beside the catalog and
19
+ * imported layers and survives their resets.
20
+ * - Every table-derived value is computed lazily, so a tree-shaken bundle that
21
+ * only uses the parsing/display helpers does not retain the pricing table.
18
22
  *
19
23
  * @module modelInfo
20
24
  */
21
25
  Object.defineProperty(exports, "__esModule", { value: true });
22
- exports.MODEL_PROVIDER_IDS = void 0;
26
+ exports.formatCost = exports.MODEL_PROVIDER_IDS = void 0;
23
27
  exports._setPricingOverrides = _setPricingOverrides;
24
28
  exports._getPricingOverrides = _getPricingOverrides;
25
29
  exports._clearPricingOverrides = _clearPricingOverrides;
26
30
  exports._setImportedResolvedPricing = _setImportedResolvedPricing;
27
31
  exports._clearImportedResolvedPricing = _clearImportedResolvedPricing;
32
+ exports.registerModelPricing = registerModelPricing;
33
+ exports.clearRegisteredModelPricing = clearRegisteredModelPricing;
34
+ exports.getPricingCatalogInfo = getPricingCatalogInfo;
28
35
  exports._getModelPricingIds = _getModelPricingIds;
36
+ exports.canonicalModelId = canonicalModelId;
37
+ exports.isMovingModelAlias = isMovingModelAlias;
29
38
  exports.parseModelId = parseModelId;
30
39
  exports.resolveModelPricing = resolveModelPricing;
31
40
  exports.getModelPricing = getModelPricing;
@@ -34,15 +43,19 @@ exports.calculateCostWithPricing = calculateCostWithPricing;
34
43
  exports.calculateCost = calculateCost;
35
44
  exports.calculateCostWithProvenance = calculateCostWithProvenance;
36
45
  exports.mergeCostSources = mergeCostSources;
46
+ exports.costSourceFromPricingProvenance = costSourceFromPricingProvenance;
47
+ exports.mergeCostComponents = mergeCostComponents;
37
48
  exports.shortModelName = shortModelName;
38
49
  exports.getModelDisplayInfo = getModelDisplayInfo;
39
50
  exports.compareModelIds = compareModelIds;
40
51
  exports.sortModelIds = sortModelIds;
41
- exports.formatCost = formatCost;
42
52
  const modelContext_1 = require("./modelContext");
43
53
  const modelAliases_1 = require("./modelAliases");
54
+ const ownKey_1 = require("./ownKey");
44
55
  var providerIds_1 = require("./providerIds");
45
56
  Object.defineProperty(exports, "MODEL_PROVIDER_IDS", { enumerable: true, get: function () { return providerIds_1.MODEL_PROVIDER_IDS; } });
57
+ var formatting_1 = require("./formatting");
58
+ Object.defineProperty(exports, "formatCost", { enumerable: true, get: function () { return formatting_1.formatCost; } });
46
59
  // ── Static Pricing Table ──
47
60
  /**
48
61
  * Static baseline pricing. Keys are model-ID prefixes; lookup uses
@@ -644,21 +657,38 @@ const PRICING_TABLE = {
644
657
  cacheReadCostPerMillion: 7.5,
645
658
  },
646
659
  };
647
- /** Static-table keys sorted longest-first so longest-prefix wins. */
648
- const STATIC_SORTED_KEYS = Object.keys(PRICING_TABLE).sort((a, b) => b.length - a.length);
660
+ /**
661
+ * Static-table keys sorted longest-first so longest-prefix wins. Computed on
662
+ * first lookup rather than at module load: a top-level call would keep the
663
+ * whole table alive in tree-shaken bundles that never price anything.
664
+ */
665
+ let staticSortedKeys = null;
666
+ function getStaticSortedKeys() {
667
+ if (!staticSortedKeys) {
668
+ staticSortedKeys = Object.keys(PRICING_TABLE).sort((a, b) => b.length - a.length);
669
+ }
670
+ return staticSortedKeys;
671
+ }
649
672
  // ── Override Table (populated at runtime by pricingCatalog) ──
650
673
  let overrideTable = {};
651
674
  let overrideSortedKeys = [];
675
+ let overrideBasis = null;
652
676
  let importedTable = {};
653
677
  /**
654
678
  * Internal: replace the runtime override map. Called by `pricingCatalog.ts`
655
679
  * after a successful LiteLLM hydration. Not part of the stable public API,
656
680
  * but exported so the Node-only catalog module can wire itself in without
657
681
  * creating a circular import.
682
+ *
683
+ * `basis` records how the map was obtained for `getPricingCatalogInfo()`;
684
+ * omitted, a non-empty map reports as `litellm` with no `fetchedAt`. Every
685
+ * call replaces the previous basis.
658
686
  */
659
- function _setPricingOverrides(overrides) {
687
+ function _setPricingOverrides(overrides, basis) {
660
688
  overrideTable = Object.fromEntries(Object.entries(overrides).map(([key, value]) => [key.trim().toLowerCase(), { ...value }]));
661
689
  overrideSortedKeys = Object.keys(overrideTable).sort((a, b) => b.length - a.length);
690
+ overrideBasis = basis ? { ...basis } : null;
691
+ invalidatePricingDigest();
662
692
  }
663
693
  /** Internal: snapshot current overrides (test + diagnostics). */
664
694
  function _getPricingOverrides() {
@@ -668,6 +698,8 @@ function _getPricingOverrides() {
668
698
  function _clearPricingOverrides() {
669
699
  overrideTable = {};
670
700
  overrideSortedKeys = [];
701
+ overrideBasis = null;
702
+ invalidatePricingDigest();
671
703
  }
672
704
  /**
673
705
  * Internal: hydrate exact pricing resolutions exported by another realm.
@@ -676,10 +708,210 @@ function _clearPricingOverrides() {
676
708
  */
677
709
  function _setImportedResolvedPricing(values) {
678
710
  importedTable = { ...importedTable, ...values };
711
+ invalidatePricingDigest();
679
712
  }
680
713
  /** Internal test hook. */
681
714
  function _clearImportedResolvedPricing() {
682
715
  importedTable = {};
716
+ invalidatePricingDigest();
717
+ }
718
+ // A Map (created on first registration) so keys such as `__proto__` stay data.
719
+ let registeredTable = null;
720
+ function isValidModelPricing(value) {
721
+ if (!value || typeof value !== 'object')
722
+ return false;
723
+ const pricing = value;
724
+ return [
725
+ pricing.inputCostPerMillion,
726
+ pricing.outputCostPerMillion,
727
+ pricing.cacheWriteCostPerMillion,
728
+ pricing.cacheReadCostPerMillion,
729
+ ].every((item) => typeof item === 'number' && Number.isFinite(item) && item >= 0);
730
+ }
731
+ /**
732
+ * Register consumer pricing for model ids, e.g. negotiated or private-deployment
733
+ * rates. This layer is separate from the runtime catalog and imported layers:
734
+ * `_setPricingOverrides`, `hydratePricingCatalog`, and
735
+ * `importResolvedModelCatalog` never reset it; only
736
+ * `clearRegisteredModelPricing` does.
737
+ *
738
+ * Keys are trimmed, lowercased, and stripped of `[1m]`, like the other tables.
739
+ * Matching is exact, never by prefix, against the looked-up id, its registered
740
+ * alias target, and its `canonicalModelId()` form — so a `claude-opus-5-5`
741
+ * entry also answers `claude-opus-5-5-20261001` and
742
+ * `us.anthropic.claude-opus-5-5-20261001-v1:0`.
743
+ *
744
+ * Successive calls merge; re-registering a key replaces its price and mode.
745
+ * Entries whose four rates are not all finite, nonnegative numbers are
746
+ * skipped. Results carry `provenance.source: 'registered'`.
747
+ *
748
+ * Registered entries are realm-local configuration and are kept out of
749
+ * cross-realm snapshots: they are not listed by the catalog id enumeration,
750
+ * and `resolveModelPricing(id, { includeRegistered: false })` is the
751
+ * resolution a snapshot should carry. An exported price would otherwise be
752
+ * pinned as an exact import in the other realm and outlive
753
+ * `clearRegisteredModelPricing` there. Register in each realm instead.
754
+ */
755
+ function registerModelPricing(entries, options) {
756
+ const onlyIfMissing = options?.onlyIfMissing === true;
757
+ let changed = false;
758
+ for (const [key, value] of Object.entries(entries ?? {})) {
759
+ const id = key
760
+ .replace(/\[1m\]/gi, '')
761
+ .trim()
762
+ .toLowerCase();
763
+ if (!id || !isValidModelPricing(value))
764
+ continue;
765
+ registeredTable ??= new Map();
766
+ registeredTable.set(id, {
767
+ pricing: {
768
+ inputCostPerMillion: value.inputCostPerMillion,
769
+ outputCostPerMillion: value.outputCostPerMillion,
770
+ cacheWriteCostPerMillion: value.cacheWriteCostPerMillion,
771
+ cacheReadCostPerMillion: value.cacheReadCostPerMillion,
772
+ },
773
+ onlyIfMissing,
774
+ });
775
+ changed = true;
776
+ }
777
+ if (changed)
778
+ invalidatePricingDigest();
779
+ }
780
+ /** Remove every entry added by `registerModelPricing`. */
781
+ function clearRegisteredModelPricing() {
782
+ if (registeredTable && registeredTable.size > 0)
783
+ invalidatePricingDigest();
784
+ registeredTable = null;
785
+ }
786
+ function findRegisteredPricing(rawId, aliasId) {
787
+ if (!registeredTable || registeredTable.size === 0)
788
+ return null;
789
+ const canonical = canonicalModelId(rawId);
790
+ for (const id of [rawId, aliasId, canonical, (0, modelAliases_1.resolveModelAlias)(canonical)]) {
791
+ const entry = registeredTable.get(id);
792
+ if (entry)
793
+ return { id, entry };
794
+ }
795
+ return null;
796
+ }
797
+ // ── Pricing Basis ──
798
+ let pricingDigestMemo = null;
799
+ function invalidatePricingDigest() {
800
+ pricingDigestMemo = null;
801
+ }
802
+ /** JSON with object keys sorted at every level; `undefined` members are dropped. */
803
+ function canonicalJson(value) {
804
+ if (value === null || typeof value !== 'object') {
805
+ return JSON.stringify(value) ?? 'null';
806
+ }
807
+ if (Array.isArray(value))
808
+ return `[${value.map(canonicalJson).join(',')}]`;
809
+ const record = value;
810
+ const keys = Object.keys(record)
811
+ .filter((key) => record[key] !== undefined)
812
+ .sort();
813
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(',')}}`;
814
+ }
815
+ /**
816
+ * 64-bit FNV-1a over the UTF-8 bytes of `text`, as 16 lowercase hex digits.
817
+ * Uses four 16-bit limbs so it runs on plain numbers in every JS runtime.
818
+ */
819
+ function fnv1a64Hex(text) {
820
+ // Offset basis 0xcbf29ce484222325, least significant limb first.
821
+ let h0 = 0x2325;
822
+ let h1 = 0x8422;
823
+ let h2 = 0x9ce4;
824
+ let h3 = 0xcbf2;
825
+ const mix = (byte) => {
826
+ h0 ^= byte;
827
+ // Multiply by the FNV prime 0x100000001b3 = 2^40 + 0x1b3, mod 2^64.
828
+ const t0 = h0 * 0x1b3;
829
+ const t1 = h1 * 0x1b3 + (t0 >>> 16);
830
+ const t2 = h2 * 0x1b3 + (h0 << 8) + (t1 >>> 16);
831
+ const t3 = h3 * 0x1b3 + (h1 << 8) + (t2 >>> 16);
832
+ h0 = t0 & 0xffff;
833
+ h1 = t1 & 0xffff;
834
+ h2 = t2 & 0xffff;
835
+ h3 = t3 & 0xffff;
836
+ };
837
+ for (let i = 0; i < text.length; i++) {
838
+ let code = text.charCodeAt(i);
839
+ if (code >= 0xd800 && code <= 0xdfff) {
840
+ const low = code <= 0xdbff && i + 1 < text.length ? text.charCodeAt(i + 1) : 0;
841
+ if (low >= 0xdc00 && low <= 0xdfff) {
842
+ code = 0x10000 + ((code - 0xd800) << 10) + (low - 0xdc00);
843
+ i++;
844
+ }
845
+ else {
846
+ // A lone surrogate encodes as U+FFFD, as TextEncoder does.
847
+ code = 0xfffd;
848
+ }
849
+ }
850
+ if (code < 0x80) {
851
+ mix(code);
852
+ }
853
+ else if (code < 0x800) {
854
+ mix(0xc0 | (code >> 6));
855
+ mix(0x80 | (code & 0x3f));
856
+ }
857
+ else if (code < 0x10000) {
858
+ mix(0xe0 | (code >> 12));
859
+ mix(0x80 | ((code >> 6) & 0x3f));
860
+ mix(0x80 | (code & 0x3f));
861
+ }
862
+ else {
863
+ mix(0xf0 | (code >> 18));
864
+ mix(0x80 | ((code >> 12) & 0x3f));
865
+ mix(0x80 | ((code >> 6) & 0x3f));
866
+ mix(0x80 | (code & 0x3f));
867
+ }
868
+ }
869
+ return [h3, h2, h1, h0].map((limb) => limb.toString(16).padStart(4, '0')).join('');
870
+ }
871
+ function getPricingDigest() {
872
+ if (pricingDigestMemo === null) {
873
+ const imported = {};
874
+ for (const [id, entry] of Object.entries(importedTable)) {
875
+ imported[id] = { pricing: entry.pricing, provenance: entry.provenance };
876
+ }
877
+ const registered = {};
878
+ for (const [id, entry] of registeredTable ?? []) {
879
+ registered[id] = { pricing: entry.pricing, onlyIfMissing: entry.onlyIfMissing };
880
+ }
881
+ const payload = canonicalJson({
882
+ v: 1,
883
+ static: PRICING_TABLE,
884
+ catalog: overrideTable,
885
+ imported,
886
+ registered,
887
+ });
888
+ pricingDigestMemo = `fnv1a64:${fnv1a64Hex(payload)}`;
889
+ }
890
+ return pricingDigestMemo;
891
+ }
892
+ /**
893
+ * Describe the pricing basis behind current cost estimates: which layer the
894
+ * prices come from, when the catalog was fetched, and a digest that changes
895
+ * whenever any active price changes. Pure and cheap after the first call (the
896
+ * digest is memoized until a table changes).
897
+ */
898
+ function getPricingCatalogInfo() {
899
+ const catalogEntries = overrideSortedKeys.length;
900
+ let source = 'static';
901
+ if (catalogEntries > 0) {
902
+ source = overrideBasis?.source ?? 'litellm';
903
+ }
904
+ else if (Object.keys(importedTable).length > 0) {
905
+ source = 'imported';
906
+ }
907
+ const fetchedAt = catalogEntries > 0 ? overrideBasis?.fetchedAt : undefined;
908
+ return {
909
+ source,
910
+ ...(fetchedAt ? { fetchedAt } : {}),
911
+ digest: getPricingDigest(),
912
+ catalogEntries,
913
+ registeredEntries: registeredTable?.size ?? 0,
914
+ };
683
915
  }
684
916
  /** Internal: list every ID known to the pricing resolver. */
685
917
  function _getModelPricingIds() {
@@ -689,6 +921,86 @@ function _getModelPricingIds() {
689
921
  ...Object.keys(importedTable),
690
922
  ]));
691
923
  }
924
+ // ── Model ID Canonicalization ──
925
+ // Bedrock: optional cross-region prefix (`us.`, `eu.`, `apac.`, `global.`, …)
926
+ // then the vendor segment, e.g. `us.anthropic.claude-opus-5-5-20261001-v1:0`.
927
+ const BEDROCK_ANTHROPIC_RE = /^(?:[a-z]{2,6}(?:-[a-z]{2,4})?\.)?anthropic\./;
928
+ // Bedrock revision tag (`-v1:0`, `-v1`), only after a version or date digit so
929
+ // legacy names such as `claude-v2:1` and `claude-instant-v1` keep theirs.
930
+ const BEDROCK_REVISION_RE = /([0-9])-v[0-9]+(?::[0-9]+)?$/;
931
+ const VERTEX_VERSION_RE = /@(?:[0-9]{8}|latest)$/;
932
+ const LATEST_SUFFIX_RE = /-latest$/;
933
+ const DASHED_DATE_SUFFIX_RE = /-[0-9]{4}-[0-9]{2}-[0-9]{2}$/;
934
+ const COMPACT_DATE_SUFFIX_RE = /-[0-9]{8}$/;
935
+ /**
936
+ * Reduce a model id to the bare model name it denotes, for grouping and
937
+ * matching across hosts and snapshots.
938
+ *
939
+ * Trims and lowercases, then removes: `[1m]`; every leading `segment/`
940
+ * provider or region prefix (`azure/us/gpt-6.1-terra` → `gpt-6.1-terra`);
941
+ * Bedrock forms (`us.anthropic.claude-opus-5-5-20261001-v1:0` →
942
+ * `claude-opus-5-5`, also `eu.`/`apac.`/`global.` and bare `anthropic.`);
943
+ * Vertex `@YYYYMMDD`; trailing `-YYYYMMDD` and `-YYYY-MM-DD` dates; `-latest`.
944
+ * Meaningful suffixes (`-mini`, `-codex`, `-sol`, `-terra`, `-high`, …) stay.
945
+ *
946
+ * Pure and syntactic: registered aliases are not applied (see
947
+ * `resolveModelAlias`). Pricing lookups do not use this to reach other
948
+ * models' prices — dated snapshots and deployments can bill differently — only
949
+ * to match `registerModelPricing` entries.
950
+ */
951
+ function canonicalModelId(id) {
952
+ let value = String(id ?? '')
953
+ .trim()
954
+ .toLowerCase()
955
+ .replace(/\[1m\]/g, '')
956
+ .trim();
957
+ const slash = value.lastIndexOf('/');
958
+ if (slash >= 0 && slash < value.length - 1)
959
+ value = value.slice(slash + 1);
960
+ const bedrock = BEDROCK_ANTHROPIC_RE.exec(value);
961
+ if (bedrock && value.length > bedrock[0].length) {
962
+ value = value.slice(bedrock[0].length).replace(BEDROCK_REVISION_RE, '$1');
963
+ }
964
+ // Suffixes can stack (`-20250101-latest`), so strip until nothing changes.
965
+ for (;;) {
966
+ const next = value
967
+ .replace(VERTEX_VERSION_RE, '')
968
+ .replace(LATEST_SUFFIX_RE, '')
969
+ .replace(DASHED_DATE_SUFFIX_RE, '')
970
+ .replace(COMPACT_DATE_SUFFIX_RE, '');
971
+ if (next === value || !next)
972
+ break;
973
+ value = next;
974
+ }
975
+ return value;
976
+ }
977
+ // Names an agent CLI re-points at a different concrete model over time.
978
+ // An object literal rather than a Set keeps it free for tree-shaking.
979
+ const MOVING_MODEL_ALIASES = {
980
+ default: true,
981
+ auto: true,
982
+ latest: true,
983
+ sonnet: true,
984
+ opus: true,
985
+ haiku: true,
986
+ fable: true,
987
+ opusplan: true,
988
+ };
989
+ /**
990
+ * True for model names an agent CLI resolves to a different concrete model
991
+ * over time (`default`, `auto`, `latest`, `sonnet`, `opus`, `haiku`, `fable`,
992
+ * and Claude Code's `opusplan`), so a recorded value names a moving target
993
+ * rather than a model. Case-insensitive, trimmed, and `[1m]`-tolerant
994
+ * (`sonnet[1m]`). Dated or versioned ids are never moving aliases.
995
+ */
996
+ function isMovingModelAlias(id) {
997
+ const value = String(id ?? '')
998
+ .trim()
999
+ .toLowerCase()
1000
+ .replace(/\[1m\]/g, '')
1001
+ .trim();
1002
+ return Object.prototype.hasOwnProperty.call(MOVING_MODEL_ALIASES, value);
1003
+ }
692
1004
  // ── Model ID Parsing ──
693
1005
  // Anthropic writes the minor version with a hyphen in modern IDs
694
1006
  // (`claude-sonnet-4-6-20260321`) and a dot in older ones
@@ -700,6 +1012,14 @@ const LEGACY_CLAUDE_RE = /^claude-([0-9]+(?:[-.][0-9]+)?)-(haiku|sonnet|opus)(?:
700
1012
  const GPT_RE = /^gpt-([0-9][0-9.A-Za-z-]*)/i;
701
1013
  const O_SERIES_RE = /^o([0-9]+)(-mini|-pro)?/i;
702
1014
  const GEMINI_RE = /^gemini-([0-9][0-9.A-Za-z-]*)/i;
1015
+ // GPT tier names, read from the parsed version (`6.1-sol`, `6-luna-2026-01-01`).
1016
+ const GPT_TIER_VARIANT_RE = /^[0-9]+(?:\.[0-9]+)*-(astra|sol|terra|luna)(?=-|$)/;
1017
+ const GPT_TIER_VARIANT_RANK = {
1018
+ astra: 0,
1019
+ sol: 1,
1020
+ terra: 2,
1021
+ luna: 3,
1022
+ };
703
1023
  /**
704
1024
  * Parses a model ID into {provider, family, version}.
705
1025
  *
@@ -731,7 +1051,13 @@ function parseModelId(modelId) {
731
1051
  }
732
1052
  const gpt = normalized.match(GPT_RE);
733
1053
  if (gpt) {
734
- return { provider: 'openai', family: 'gpt', version: gpt[1] };
1054
+ const version = gpt[1];
1055
+ const variant = GPT_TIER_VARIANT_RE.exec(version)?.[1];
1056
+ // The key is only present when set, so untiered results stay deep-equal
1057
+ // to what earlier releases returned.
1058
+ return variant
1059
+ ? { provider: 'openai', family: 'gpt', version, variant }
1060
+ : { provider: 'openai', family: 'gpt', version };
735
1061
  }
736
1062
  const oSeries = normalized.match(O_SERIES_RE);
737
1063
  if (oSeries) {
@@ -753,24 +1079,45 @@ function findLongestPrefix(keys, modelId) {
753
1079
  }
754
1080
  return null;
755
1081
  }
756
- /** Resolve pricing and disclose whether the result was inherited by prefix. */
757
- function resolveModelPricing(modelId) {
1082
+ /**
1083
+ * Resolve pricing and disclose which layer answered and whether the result was
1084
+ * inherited by prefix.
1085
+ *
1086
+ * Order: registered entries (default mode); exact imported, catalog, and
1087
+ * static matches; registered `onlyIfMissing` entries; catalog then static
1088
+ * prefix matches; unpriced.
1089
+ */
1090
+ function resolveModelPricing(modelId, options) {
758
1091
  const stripped = modelId.replace(/\[1m\]/gi, '');
759
1092
  const rawId = stripped.trim().toLowerCase();
760
- const canonicalModelId = (0, modelAliases_1.resolveModelAlias)(stripped);
1093
+ const aliasId = (0, modelAliases_1.resolveModelAlias)(stripped);
761
1094
  // The raw id is tried before its alias target at every step: pricing
762
1095
  // recorded for the alias id itself is more specific than the canonical
763
1096
  // fallback, so registering an alias never orphans it.
764
- const candidateIds = rawId === canonicalModelId ? [canonicalModelId] : [rawId, canonicalModelId];
765
- const imported = importedTable[rawId] ?? importedTable[canonicalModelId];
1097
+ const candidateIds = rawId === aliasId ? [aliasId] : [rawId, aliasId];
1098
+ const registered = options?.includeRegistered === false ? null : findRegisteredPricing(rawId, aliasId);
1099
+ const registeredResult = (match) => ({
1100
+ modelId,
1101
+ canonicalModelId: aliasId,
1102
+ pricing: { ...match.entry.pricing },
1103
+ provenance: {
1104
+ source: 'registered',
1105
+ match: 'exact',
1106
+ matchedModelId: match.id,
1107
+ inheritedByPrefix: false,
1108
+ },
1109
+ });
1110
+ if (registered && !registered.entry.onlyIfMissing)
1111
+ return registeredResult(registered);
1112
+ const imported = (0, ownKey_1.ownValue)(importedTable, rawId) ?? (0, ownKey_1.ownValue)(importedTable, aliasId);
766
1113
  if (imported)
767
- return { ...imported, modelId, canonicalModelId };
1114
+ return { ...imported, modelId, canonicalModelId: aliasId };
768
1115
  for (const id of candidateIds) {
769
- const exactOverride = overrideTable[id];
1116
+ const exactOverride = (0, ownKey_1.ownValue)(overrideTable, id);
770
1117
  if (exactOverride) {
771
1118
  return {
772
1119
  modelId,
773
- canonicalModelId,
1120
+ canonicalModelId: aliasId,
774
1121
  pricing: exactOverride,
775
1122
  provenance: {
776
1123
  source: 'catalog',
@@ -782,11 +1129,11 @@ function resolveModelPricing(modelId) {
782
1129
  }
783
1130
  }
784
1131
  for (const id of candidateIds) {
785
- const exactStatic = PRICING_TABLE[id];
1132
+ const exactStatic = (0, ownKey_1.ownValue)(PRICING_TABLE, id);
786
1133
  if (exactStatic) {
787
1134
  return {
788
1135
  modelId,
789
- canonicalModelId,
1136
+ canonicalModelId: aliasId,
790
1137
  pricing: exactStatic,
791
1138
  provenance: {
792
1139
  source: 'static',
@@ -797,12 +1144,15 @@ function resolveModelPricing(modelId) {
797
1144
  };
798
1145
  }
799
1146
  }
1147
+ // A gap-filling registration answers once no layer has an exact price.
1148
+ if (registered)
1149
+ return registeredResult(registered);
800
1150
  for (const id of candidateIds) {
801
1151
  const overridePrefix = findLongestPrefix(overrideSortedKeys, id);
802
1152
  if (overridePrefix) {
803
1153
  return {
804
1154
  modelId,
805
- canonicalModelId,
1155
+ canonicalModelId: aliasId,
806
1156
  pricing: overrideTable[overridePrefix],
807
1157
  provenance: {
808
1158
  source: 'catalog',
@@ -814,11 +1164,11 @@ function resolveModelPricing(modelId) {
814
1164
  }
815
1165
  }
816
1166
  for (const id of candidateIds) {
817
- const staticPrefix = findLongestPrefix(STATIC_SORTED_KEYS, id);
1167
+ const staticPrefix = findLongestPrefix(getStaticSortedKeys(), id);
818
1168
  if (staticPrefix) {
819
1169
  return {
820
1170
  modelId,
821
- canonicalModelId,
1171
+ canonicalModelId: aliasId,
822
1172
  pricing: PRICING_TABLE[staticPrefix],
823
1173
  provenance: {
824
1174
  source: 'static',
@@ -831,7 +1181,7 @@ function resolveModelPricing(modelId) {
831
1181
  }
832
1182
  return {
833
1183
  modelId,
834
- canonicalModelId,
1184
+ canonicalModelId: aliasId,
835
1185
  pricing: null,
836
1186
  provenance: { source: 'none', match: 'none', inheritedByPrefix: false },
837
1187
  };
@@ -839,9 +1189,11 @@ function resolveModelPricing(modelId) {
839
1189
  /**
840
1190
  * Gets pricing for a model ID.
841
1191
  *
842
- * Lookup order:
843
- * 1. Runtime override map (from LiteLLM catalog hydration).
844
- * 2. Static PRICING_TABLE.
1192
+ * Lookup order (see `resolveModelPricing` for provenance):
1193
+ * 1. Registered entries (`registerModelPricing`), exact matches only.
1194
+ * 2. Imported resolutions, then the runtime override map (from LiteLLM
1195
+ * catalog hydration), then the static PRICING_TABLE — exact before prefix,
1196
+ * with gap-filling registered entries between the two.
845
1197
  * 3. `null` — unknown model. Callers MUST handle this.
846
1198
  *
847
1199
  * The ID is first looked up verbatim (minus the "[1m]" suffix) — override
@@ -933,22 +1285,47 @@ function mergeCostSources(a, b) {
933
1285
  };
934
1286
  return rank[a] >= rank[b] ? a : b;
935
1287
  }
936
- // ── Display ──
937
- function normalizeModelId(modelId) {
938
- return modelId
939
- .trim()
940
- .toLowerCase()
941
- .replace(/^[a-z]+\//, '')
942
- .replace(/-\d{8}$/, '')
943
- .replace(/-latest$/, '')
944
- .replace(/\[1m\]/gi, '');
1288
+ /**
1289
+ * Map the provenance `calculateNormalizedUsageCost` reports to the display
1290
+ * cost source: provider-reported costs are `reported`, costs computed from
1291
+ * explicit or catalog pricing are `estimated`, and unpriced stays `unpriced`.
1292
+ */
1293
+ function costSourceFromPricingProvenance(provenance) {
1294
+ switch (provenance) {
1295
+ case 'provider-reported':
1296
+ return 'reported';
1297
+ case 'explicit-pricing':
1298
+ case 'model-catalog':
1299
+ return 'estimated';
1300
+ default:
1301
+ return 'unpriced';
1302
+ }
1303
+ }
1304
+ /**
1305
+ * Merge the cost sources of a rollup's components, least certain winning (as
1306
+ * `mergeCostSources`). Components without a `source` have not been seen yet
1307
+ * and are ignored, and an `unpriced` component that contributed no tokens
1308
+ * does not taint the total. Returns `undefined` when nothing counted.
1309
+ */
1310
+ function mergeCostComponents(components) {
1311
+ let merged;
1312
+ for (const component of components) {
1313
+ const source = component?.source;
1314
+ if (!source)
1315
+ continue;
1316
+ if (source === 'unpriced' && !(component.tokens > 0))
1317
+ continue;
1318
+ merged = merged ? mergeCostSources(merged, source) : source;
1319
+ }
1320
+ return merged;
945
1321
  }
1322
+ // ── Display ──
946
1323
  /**
947
1324
  * Short display label for model IDs. Keeps historical Claude family labels
948
1325
  * compact while normalizing common OpenAI labels.
949
1326
  */
950
1327
  function shortModelName(modelId) {
951
- const normalized = normalizeModelId(modelId);
1328
+ const normalized = canonicalModelId(modelId);
952
1329
  const parsed = parseModelId(normalized);
953
1330
  if (normalized.includes('codex'))
954
1331
  return 'Codex';
@@ -987,7 +1364,7 @@ function versionRank(version) {
987
1364
  */
988
1365
  function getModelDisplayInfo(modelId) {
989
1366
  const parsed = parseModelId(modelId);
990
- const normalized = normalizeModelId(modelId);
1367
+ const normalized = canonicalModelId(modelId);
991
1368
  let rank = 1_000;
992
1369
  if (parsed?.provider === 'anthropic') {
993
1370
  rank = (CLAUDE_FAMILY_RANK[parsed.family] ?? 9) * 100 + versionRank(parsed.version);
@@ -1013,29 +1390,37 @@ function getModelDisplayInfo(modelId) {
1013
1390
  rank,
1014
1391
  };
1015
1392
  }
1016
- /** Compare two model IDs using shared provider/family ranking rules. */
1393
+ /**
1394
+ * Order two GPT tier variants of the same family and numeric version
1395
+ * (astra > sol > terra > luna); 0 for every other pair.
1396
+ */
1397
+ function compareTierVariants(a, b) {
1398
+ const left = parseModelId(a);
1399
+ const right = parseModelId(b);
1400
+ if (!left?.variant || !right?.variant)
1401
+ return 0;
1402
+ if (left.provider !== right.provider || left.family !== right.family)
1403
+ return 0;
1404
+ if (versionRank(left.version) !== versionRank(right.version))
1405
+ return 0;
1406
+ return ((GPT_TIER_VARIANT_RANK[left.variant] ?? Number.MAX_SAFE_INTEGER) -
1407
+ (GPT_TIER_VARIANT_RANK[right.variant] ?? Number.MAX_SAFE_INTEGER));
1408
+ }
1409
+ /**
1410
+ * Compare two model IDs using shared provider/family ranking rules. Within a
1411
+ * family and numeric version, GPT tiers rank astra > sol > terra > luna.
1412
+ */
1017
1413
  function compareModelIds(a, b) {
1018
1414
  const left = getModelDisplayInfo(a);
1019
1415
  const right = getModelDisplayInfo(b);
1020
1416
  if (left.rank !== right.rank)
1021
1417
  return left.rank - right.rank;
1418
+ const variantOrder = compareTierVariants(a, b);
1419
+ if (variantOrder !== 0)
1420
+ return variantOrder;
1022
1421
  return a.localeCompare(b);
1023
1422
  }
1024
1423
  /** Return a sorted copy of model IDs using shared provider/family ranking rules. */
1025
1424
  function sortModelIds(modelIds) {
1026
1425
  return [...modelIds].sort(compareModelIds);
1027
1426
  }
1028
- /**
1029
- * Formats a USD cost as a currency string.
1030
- *
1031
- * - `null` / `undefined` → `"—"` (honest "pricing unavailable").
1032
- * - `< $0.01` → 4 decimals, so sub-cent costs are still visible.
1033
- * - otherwise → 2 decimals.
1034
- */
1035
- function formatCost(cost) {
1036
- if (cost === null || cost === undefined)
1037
- return '—';
1038
- if (cost < 0.01)
1039
- return `$${cost.toFixed(4)}`;
1040
- return `$${cost.toFixed(2)}`;
1041
- }