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