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