sidekick-shared 0.24.5 → 0.26.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 (157) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/README.md +205 -52
  3. package/dist/accountChangeSignal.d.ts +3 -0
  4. package/dist/accountChangeSignal.js +19 -0
  5. package/dist/accountChanges.d.ts +21 -0
  6. package/dist/accountChanges.js +170 -0
  7. package/dist/accountRegistry.d.ts +25 -2
  8. package/dist/accountRegistry.js +41 -13
  9. package/dist/accountStatus.d.ts +9 -1
  10. package/dist/accountStatus.js +3 -3
  11. package/dist/accounts.d.ts +3 -1
  12. package/dist/accounts.js +84 -64
  13. package/dist/aggregation/EventAggregator.d.ts +15 -2
  14. package/dist/aggregation/EventAggregator.js +54 -9
  15. package/dist/aggregation/costProvenance.d.ts +14 -0
  16. package/dist/aggregation/costProvenance.js +35 -0
  17. package/dist/aggregation/snapshot.d.ts +13 -2
  18. package/dist/aggregation/snapshot.js +57 -6
  19. package/dist/aggregation/types.d.ts +30 -1
  20. package/dist/analytics/compactionLedger.js +15 -5
  21. package/dist/analytics/qualityScore.js +2 -1
  22. package/dist/browser.d.ts +19 -4
  23. package/dist/browser.js +43 -1
  24. package/dist/codexProfiles.d.ts +3 -1
  25. package/dist/codexProfiles.js +4 -2
  26. package/dist/codexQuota.d.ts +2 -0
  27. package/dist/codexQuota.js +11 -66
  28. package/dist/codexQuotaWatcher.d.ts +6 -0
  29. package/dist/codexQuotaWatcher.js +101 -47
  30. package/dist/failingTools.d.ts +37 -0
  31. package/dist/failingTools.js +70 -0
  32. package/dist/formatters/sessionDump.js +13 -7
  33. package/dist/formatting.d.ts +13 -0
  34. package/dist/formatting.js +35 -0
  35. package/dist/historicalStore.d.ts +47 -0
  36. package/dist/historicalStore.js +312 -0
  37. package/dist/index.d.ts +67 -23
  38. package/dist/index.js +138 -26
  39. package/dist/modelAliases.d.ts +13 -0
  40. package/dist/modelAliases.js +58 -0
  41. package/dist/modelCatalog.d.ts +32 -0
  42. package/dist/modelCatalog.js +145 -0
  43. package/dist/modelContext.d.ts +36 -3
  44. package/dist/modelContext.js +109 -34
  45. package/dist/modelInfo.d.ts +30 -1
  46. package/dist/modelInfo.js +116 -26
  47. package/dist/multiProviderQuotaService.d.ts +8 -0
  48. package/dist/multiProviderQuotaService.js +64 -3
  49. package/dist/node.d.ts +23 -4
  50. package/dist/node.js +50 -1
  51. package/dist/observedContextWindows.d.ts +7 -0
  52. package/dist/observedContextWindows.js +17 -2
  53. package/dist/observedSessionCollector.d.ts +72 -6
  54. package/dist/observedSessionCollector.js +440 -47
  55. package/dist/parsers/codexHistory.d.ts +4 -2
  56. package/dist/parsers/codexHistory.js +9 -2
  57. package/dist/parsers/sessionActivityDetector.d.ts +28 -0
  58. package/dist/parsers/sessionActivityDetector.js +110 -15
  59. package/dist/parsers/sessionPathResolver.d.ts +11 -0
  60. package/dist/parsers/sessionPathResolver.js +13 -10
  61. package/dist/peakHours.d.ts +34 -3
  62. package/dist/peakHours.js +92 -7
  63. package/dist/pricingCatalog.d.ts +7 -0
  64. package/dist/pricingCatalog.js +8 -5
  65. package/dist/providerIds.d.ts +11 -0
  66. package/dist/providerIds.js +9 -0
  67. package/dist/providerQuota.d.ts +3 -1
  68. package/dist/providerQuota.js +3 -0
  69. package/dist/providers/claudeCode.d.ts +10 -1
  70. package/dist/providers/claudeCode.js +211 -131
  71. package/dist/providers/codex.d.ts +20 -1
  72. package/dist/providers/codex.js +308 -244
  73. package/dist/providers/codexDatabase.d.ts +9 -0
  74. package/dist/providers/codexDatabase.js +107 -2
  75. package/dist/providers/detect.d.ts +2 -0
  76. package/dist/providers/detect.js +29 -24
  77. package/dist/providers/diagnostics.d.ts +13 -0
  78. package/dist/providers/diagnostics.js +88 -0
  79. package/dist/providers/factory.d.ts +20 -0
  80. package/dist/providers/factory.js +57 -0
  81. package/dist/providers/openCode.d.ts +22 -1
  82. package/dist/providers/openCode.js +460 -126
  83. package/dist/providers/openCodeDatabase.d.ts +10 -0
  84. package/dist/providers/openCodeDatabase.js +69 -0
  85. package/dist/providers/rolloutWalker.d.ts +53 -0
  86. package/dist/providers/rolloutWalker.js +209 -0
  87. package/dist/providers/types.d.ts +82 -2
  88. package/dist/providers/types.js +4 -0
  89. package/dist/quota.d.ts +34 -4
  90. package/dist/quota.js +37 -9
  91. package/dist/quotaHistory.d.ts +3 -2
  92. package/dist/quotaHistory.js +36 -35
  93. package/dist/quotaPoller.d.ts +13 -1
  94. package/dist/quotaPoller.js +65 -3
  95. package/dist/quotaResolver.d.ts +81 -0
  96. package/dist/quotaResolver.js +213 -0
  97. package/dist/quotaSnapshots.d.ts +15 -2
  98. package/dist/quotaSnapshots.js +25 -3
  99. package/dist/quotaThresholds.d.ts +60 -0
  100. package/dist/quotaThresholds.js +90 -0
  101. package/dist/report/htmlReportGenerator.js +10 -4
  102. package/dist/report/sessionReportInputs.d.ts +23 -0
  103. package/dist/report/sessionReportInputs.js +34 -0
  104. package/dist/schemas/accountManager.d.ts +2 -2
  105. package/dist/schemas/accountManager.js +2 -4
  106. package/dist/schemas/accountStatus.d.ts +3 -0
  107. package/dist/schemas/accountStatus.js +1 -0
  108. package/dist/schemas/index.d.ts +3 -0
  109. package/dist/schemas/index.js +16 -2
  110. package/dist/schemas/observedSessionV1.d.ts +27 -26
  111. package/dist/schemas/observedSessionV1.js +6 -4
  112. package/dist/schemas/quota.d.ts +141 -2
  113. package/dist/schemas/quota.js +20 -8
  114. package/dist/schemas/quotaHistory.d.ts +3 -2
  115. package/dist/schemas/quotaHistory.js +1 -1
  116. package/dist/schemas/sessionEvent.d.ts +6 -0
  117. package/dist/schemas/sessionEvent.js +1 -0
  118. package/dist/schemas/stateFile.d.ts +198 -0
  119. package/dist/schemas/stateFile.js +77 -0
  120. package/dist/schemas/tokenEstimation.d.ts +1 -1
  121. package/dist/schemas/transcript.js +2 -0
  122. package/dist/sessionFingerprint.d.ts +25 -0
  123. package/dist/sessionFingerprint.js +46 -0
  124. package/dist/sessionMonitor.d.ts +15 -0
  125. package/dist/sessionMonitor.js +142 -0
  126. package/dist/sessionPreviews.d.ts +33 -2
  127. package/dist/sessionPreviews.js +333 -6
  128. package/dist/sessionStats.d.ts +51 -0
  129. package/dist/sessionStats.js +166 -0
  130. package/dist/stateFile.d.ts +113 -0
  131. package/dist/stateFile.js +160 -0
  132. package/dist/statusline/claudeStatuslinePayload.d.ts +89 -0
  133. package/dist/statusline/claudeStatuslinePayload.js +152 -0
  134. package/dist/statusline/formatter.d.ts +13 -0
  135. package/dist/statusline/formatter.js +48 -6
  136. package/dist/statusline/index.d.ts +10 -2
  137. package/dist/statusline/index.js +19 -1
  138. package/dist/tokenSummary.d.ts +62 -0
  139. package/dist/tokenSummary.js +72 -0
  140. package/dist/transcript.d.ts +5 -0
  141. package/dist/transcript.js +2 -0
  142. package/dist/types/observedSessionV1.d.ts +8 -2
  143. package/dist/types/observedSessionV1.js +88 -25
  144. package/dist/types/sessionEvent.d.ts +2 -0
  145. package/dist/usage/billingBlocks.d.ts +85 -0
  146. package/dist/usage/billingBlocks.js +153 -0
  147. package/dist/usage/importSessionHistory.d.ts +53 -0
  148. package/dist/usage/importSessionHistory.js +88 -0
  149. package/dist/usage/usageEvents.d.ts +92 -0
  150. package/dist/usage/usageEvents.js +322 -0
  151. package/dist/usage/usageReports.d.ts +76 -0
  152. package/dist/usage/usageReports.js +208 -0
  153. package/dist/watchers/factory.d.ts +6 -0
  154. package/dist/watchers/factory.js +30 -25
  155. package/dist/zaiQuotaApi.d.ts +2 -0
  156. package/dist/zaiQuotaApi.js +2 -0
  157. package/package.json +22 -1
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveModelAlias = exports.registerModelAlias = exports.RESOLVED_MODEL_CATALOG_SCHEMA_VERSION = void 0;
4
+ exports.exportResolvedModelCatalog = exportResolvedModelCatalog;
5
+ exports.importResolvedModelCatalog = importResolvedModelCatalog;
6
+ const modelContext_1 = require("./modelContext");
7
+ const modelInfo_1 = require("./modelInfo");
8
+ const modelAliases_1 = require("./modelAliases");
9
+ Object.defineProperty(exports, "registerModelAlias", { enumerable: true, get: function () { return modelAliases_1.registerModelAlias; } });
10
+ Object.defineProperty(exports, "resolveModelAlias", { enumerable: true, get: function () { return modelAliases_1.resolveModelAlias; } });
11
+ exports.RESOLVED_MODEL_CATALOG_SCHEMA_VERSION = 1;
12
+ function normalizedIds(ids) {
13
+ if (ids) {
14
+ return Array.from(new Set(ids.map((id) => id.trim()).filter(Boolean)));
15
+ }
16
+ const aliases = (0, modelAliases_1._getModelAliases)();
17
+ return Array.from(new Set([
18
+ ...(0, modelContext_1._getModelContextIds)(),
19
+ ...(0, modelInfo_1._getModelPricingIds)(),
20
+ ...Object.keys(aliases),
21
+ ...Object.values(aliases),
22
+ ])).sort();
23
+ }
24
+ /**
25
+ * Export an entirely serializable catalog suitable for IPC to another JS realm.
26
+ * Each entry records published, observed, and tier-effective context windows,
27
+ * plus pricing match provenance (including prefix inheritance).
28
+ */
29
+ function exportResolvedModelCatalog(ids) {
30
+ const models = {};
31
+ const selectedIds = normalizedIds(ids);
32
+ for (const id of selectedIds) {
33
+ const canonicalId = (0, modelAliases_1.resolveModelAlias)(id);
34
+ models[id] = {
35
+ id,
36
+ canonicalId,
37
+ contextWindow: (0, modelContext_1.resolveModelContextWindow)(id),
38
+ pricing: (0, modelInfo_1.resolveModelPricing)(id),
39
+ };
40
+ }
41
+ const selectedCanonicalIds = new Set(Object.values(models).flatMap(({ id, canonicalId }) => [id, canonicalId]));
42
+ const aliases = Object.fromEntries(Object.entries((0, modelAliases_1._getModelAliases)()).filter(([alias, canonicalId]) => selectedCanonicalIds.has(alias) || selectedCanonicalIds.has(canonicalId)));
43
+ return { schemaVersion: exports.RESOLVED_MODEL_CATALOG_SCHEMA_VERSION, models, aliases };
44
+ }
45
+ /**
46
+ * Hydrate exact model resolutions exported by another bundle realm.
47
+ * Invalid entries are skipped and reported; import never partially mutates an
48
+ * entry or throws for malformed IPC data.
49
+ */
50
+ function importResolvedModelCatalog(snapshot) {
51
+ const diagnostics = [];
52
+ if (!isRecord(snapshot) || snapshot.schemaVersion !== exports.RESOLVED_MODEL_CATALOG_SCHEMA_VERSION) {
53
+ return { imported: 0, diagnostics: ['unsupported resolved model catalog snapshot'] };
54
+ }
55
+ const aliases = isStringRecord(snapshot.aliases) ? snapshot.aliases : {};
56
+ (0, modelAliases_1._importModelAliases)(aliases);
57
+ const contexts = {};
58
+ const prices = {};
59
+ const models = isRecord(snapshot.models) ? snapshot.models : {};
60
+ let imported = 0;
61
+ for (const [key, rawEntry] of Object.entries(models)) {
62
+ if (!isResolvedEntry(rawEntry)) {
63
+ diagnostics.push(`ignored invalid model catalog entry: ${key}`);
64
+ continue;
65
+ }
66
+ const entry = rawEntry;
67
+ const normalizedKey = key.trim().toLowerCase();
68
+ const canonicalId = entry.canonicalId.trim().toLowerCase();
69
+ const context = cloneContext(entry.contextWindow, entry.id, canonicalId);
70
+ const pricing = clonePricing(entry.pricing, entry.id, canonicalId);
71
+ // Unresolved facets are not importable facts: pinning a 'default' window
72
+ // or 'none' pricing would outrank data the importing realm can resolve
73
+ // (or later learn) on its own.
74
+ const contextResolved = context.provenance.source !== 'default';
75
+ const pricingResolved = pricing.provenance.source !== 'none';
76
+ if (!contextResolved && !pricingResolved) {
77
+ diagnostics.push(`skipped unresolved model catalog entry: ${key}`);
78
+ continue;
79
+ }
80
+ for (const id of new Set([normalizedKey, entry.id.trim().toLowerCase(), canonicalId])) {
81
+ if (!id)
82
+ continue;
83
+ if (contextResolved)
84
+ contexts[id] = context;
85
+ if (pricingResolved)
86
+ prices[id] = pricing;
87
+ }
88
+ imported++;
89
+ }
90
+ (0, modelContext_1._setImportedResolvedContextWindows)(contexts);
91
+ (0, modelInfo_1._setImportedResolvedPricing)(prices);
92
+ return { imported, diagnostics };
93
+ }
94
+ function cloneContext(value, modelId, canonicalModelId) {
95
+ return {
96
+ ...value,
97
+ modelId,
98
+ canonicalModelId,
99
+ provenance: { ...value.provenance },
100
+ };
101
+ }
102
+ function clonePricing(value, modelId, canonicalModelId) {
103
+ return {
104
+ ...value,
105
+ modelId,
106
+ canonicalModelId,
107
+ pricing: value.pricing ? { ...value.pricing } : null,
108
+ provenance: { ...value.provenance },
109
+ };
110
+ }
111
+ function isRecord(value) {
112
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
113
+ }
114
+ function isStringRecord(value) {
115
+ return isRecord(value) && Object.values(value).every((item) => typeof item === 'string');
116
+ }
117
+ function validPricing(value) {
118
+ if (!isRecord(value))
119
+ return false;
120
+ return [
121
+ value.inputCostPerMillion,
122
+ value.outputCostPerMillion,
123
+ value.cacheWriteCostPerMillion,
124
+ value.cacheReadCostPerMillion,
125
+ ].every((item) => typeof item === 'number' && Number.isFinite(item) && item >= 0);
126
+ }
127
+ function isResolvedEntry(value) {
128
+ if (!isRecord(value) || typeof value.id !== 'string' || typeof value.canonicalId !== 'string') {
129
+ return false;
130
+ }
131
+ const context = value.contextWindow;
132
+ const pricing = value.pricing;
133
+ if (!isRecord(context) || !isRecord(pricing))
134
+ return false;
135
+ if (typeof context.tierEffective !== 'number' ||
136
+ !Number.isFinite(context.tierEffective) ||
137
+ context.tierEffective <= 0 ||
138
+ (context.published !== null && typeof context.published !== 'number') ||
139
+ (context.observed !== null && typeof context.observed !== 'number') ||
140
+ !isRecord(context.provenance) ||
141
+ !isRecord(pricing.provenance)) {
142
+ return false;
143
+ }
144
+ return pricing.pricing === null || validPricing(pricing.pricing);
145
+ }
@@ -9,13 +9,19 @@
9
9
  * model, persisted across runs by `observedContextWindows.ts`. Reflects the
10
10
  * effective window for this account/tier, which can be well below the
11
11
  * model's published maximum.
12
- * 3. Catalog overrides — `max_input_tokens` from the LiteLLM catalog, hydrated
12
+ * 3. Imported resolutions — exact entries hydrated from another realm via
13
+ * `importResolvedModelCatalog()`. Below local observed data (a window this
14
+ * realm saw first-hand is fresher than a transferred snapshot), above the
15
+ * catalog.
16
+ * 4. Catalog overrides — `max_input_tokens` from the LiteLLM catalog, hydrated
13
17
  * by `pricingCatalog.ts`. Keeps new models correct without a code change.
14
- * 4. The static table below — offline baseline of last resort.
18
+ * 5. The static table below — offline baseline of last resort.
15
19
  *
16
20
  * Within that order, match quality comes first: every layer is tried for an
17
21
  * exact hit before any layer is tried for a prefix hit. See
18
- * `getModelContextWindowSize`.
22
+ * `getModelContextWindowSize`. The raw (alias) id is tried before its
23
+ * canonical target at every step, so registering an alias never orphans data
24
+ * recorded under the alias itself.
19
25
  *
20
26
  * Layers 2 and 3 are pushed in by Node-only modules through the `_set*` hooks,
21
27
  * so this file stays browser-safe (no node:fs, no fetch) and can be bundled for
@@ -23,6 +29,21 @@
23
29
  */
24
30
  /** Default context window size when model is unknown. */
25
31
  export declare const DEFAULT_CONTEXT_WINDOW = 200000;
32
+ export type ModelContextWindowSource = 'explicit-1m' | 'imported' | 'observed' | 'catalog' | 'static' | 'default';
33
+ export type ModelCatalogMatch = 'exact' | 'prefix' | 'default';
34
+ export interface ModelContextWindowProvenance {
35
+ source: ModelContextWindowSource;
36
+ match: ModelCatalogMatch;
37
+ matchedModelId?: string;
38
+ }
39
+ export interface ResolvedModelContextWindow {
40
+ modelId: string;
41
+ canonicalModelId: string;
42
+ published: number | null;
43
+ observed: number | null;
44
+ tierEffective: number;
45
+ provenance: ModelContextWindowProvenance;
46
+ }
26
47
  /**
27
48
  * Internal: replace the observed-context-window map. Called by
28
49
  * `observedContextWindows.ts` on load and on each newly observed value.
@@ -43,6 +64,18 @@ export declare function _setCatalogContextWindows(overrides: Record<string, numb
43
64
  export declare function _getCatalogContextWindows(): Record<string, number>;
44
65
  /** Internal: clear catalog overrides (used by tests and when disabling hydration). */
45
66
  export declare function _clearCatalogContextWindows(): void;
67
+ /**
68
+ * Internal: hydrate exact, already-resolved context data from another realm.
69
+ * Merges so successive partial imports accumulate (aliases already merge);
70
+ * use `_clearImportedResolvedContextWindows` to reset.
71
+ */
72
+ export declare function _setImportedResolvedContextWindows(values: Record<string, ResolvedModelContextWindow>): void;
73
+ /** Internal test hook. */
74
+ export declare function _clearImportedResolvedContextWindows(): void;
75
+ /** Internal: list every ID known to the context resolver. */
76
+ export declare function _getModelContextIds(): string[];
77
+ /** Resolve published, observed, and tier-effective context windows with provenance. */
78
+ export declare function resolveModelContextWindow(modelId?: string): ResolvedModelContextWindow;
46
79
  /**
47
80
  * Returns the context window size for a model ID.
48
81
  *
@@ -10,13 +10,19 @@
10
10
  * model, persisted across runs by `observedContextWindows.ts`. Reflects the
11
11
  * effective window for this account/tier, which can be well below the
12
12
  * model's published maximum.
13
- * 3. Catalog overrides — `max_input_tokens` from the LiteLLM catalog, hydrated
13
+ * 3. Imported resolutions — exact entries hydrated from another realm via
14
+ * `importResolvedModelCatalog()`. Below local observed data (a window this
15
+ * realm saw first-hand is fresher than a transferred snapshot), above the
16
+ * catalog.
17
+ * 4. Catalog overrides — `max_input_tokens` from the LiteLLM catalog, hydrated
14
18
  * by `pricingCatalog.ts`. Keeps new models correct without a code change.
15
- * 4. The static table below — offline baseline of last resort.
19
+ * 5. The static table below — offline baseline of last resort.
16
20
  *
17
21
  * Within that order, match quality comes first: every layer is tried for an
18
22
  * exact hit before any layer is tried for a prefix hit. See
19
- * `getModelContextWindowSize`.
23
+ * `getModelContextWindowSize`. The raw (alias) id is tried before its
24
+ * canonical target at every step, so registering an alias never orphans data
25
+ * recorded under the alias itself.
20
26
  *
21
27
  * Layers 2 and 3 are pushed in by Node-only modules through the `_set*` hooks,
22
28
  * so this file stays browser-safe (no node:fs, no fetch) and can be bundled for
@@ -30,7 +36,12 @@ exports._clearObservedContextWindows = _clearObservedContextWindows;
30
36
  exports._setCatalogContextWindows = _setCatalogContextWindows;
31
37
  exports._getCatalogContextWindows = _getCatalogContextWindows;
32
38
  exports._clearCatalogContextWindows = _clearCatalogContextWindows;
39
+ exports._setImportedResolvedContextWindows = _setImportedResolvedContextWindows;
40
+ exports._clearImportedResolvedContextWindows = _clearImportedResolvedContextWindows;
41
+ exports._getModelContextIds = _getModelContextIds;
42
+ exports.resolveModelContextWindow = resolveModelContextWindow;
33
43
  exports.getModelContextWindowSize = getModelContextWindowSize;
44
+ const modelAliases_1 = require("./modelAliases");
34
45
  /** Known model context window sizes (in tokens). */
35
46
  const MODEL_CONTEXT_SIZES = {
36
47
  // Claude — native 1M context (Fable 5, Opus 4.6+, Sonnet 4.6+)
@@ -92,6 +103,7 @@ let observedTable = {};
92
103
  let observedSortedKeys = [];
93
104
  let catalogTable = {};
94
105
  let catalogSortedKeys = [];
106
+ let importedTable = {};
95
107
  function sortKeysLongestFirst(table) {
96
108
  return Object.keys(table).sort((a, b) => b.length - a.length);
97
109
  }
@@ -145,20 +157,109 @@ function _clearCatalogContextWindows() {
145
157
  catalogTable = {};
146
158
  catalogSortedKeys = [];
147
159
  }
160
+ /**
161
+ * Internal: hydrate exact, already-resolved context data from another realm.
162
+ * Merges so successive partial imports accumulate (aliases already merge);
163
+ * use `_clearImportedResolvedContextWindows` to reset.
164
+ */
165
+ function _setImportedResolvedContextWindows(values) {
166
+ importedTable = { ...importedTable, ...values };
167
+ }
168
+ /** Internal test hook. */
169
+ function _clearImportedResolvedContextWindows() {
170
+ importedTable = {};
171
+ }
172
+ /** Internal: list every ID known to the context resolver. */
173
+ function _getModelContextIds() {
174
+ return Array.from(new Set([
175
+ ...Object.keys(MODEL_CONTEXT_SIZES),
176
+ ...Object.keys(catalogTable),
177
+ ...Object.keys(observedTable),
178
+ ...Object.keys(importedTable),
179
+ ]));
180
+ }
148
181
  // ── Lookup ──
149
182
  /** Exact match against one table. */
150
183
  function exactWindow(table, modelId) {
151
184
  const hit = table[modelId];
152
185
  return hit === undefined ? null : hit;
153
186
  }
154
- /** Longest-prefix match against one table. */
155
- function prefixWindow(table, sortedKeys, modelId) {
187
+ function exactWindowMatch(table, modelId) {
188
+ const exact = exactWindow(table, modelId);
189
+ return exact === null ? null : { value: exact, match: 'exact', matchedModelId: modelId };
190
+ }
191
+ function prefixWindowMatch(table, sortedKeys, modelId) {
156
192
  for (const key of sortedKeys) {
157
- if (modelId.startsWith(key))
158
- return table[key];
193
+ if (modelId !== key && modelId.startsWith(key)) {
194
+ return { value: table[key], match: 'prefix', matchedModelId: key };
195
+ }
159
196
  }
160
197
  return null;
161
198
  }
199
+ /** Resolve published, observed, and tier-effective context windows with provenance. */
200
+ function resolveModelContextWindow(modelId) {
201
+ const rawModelId = modelId ?? '';
202
+ const stripped = rawModelId
203
+ .replace(/\[1m\]/gi, '')
204
+ .trim()
205
+ .toLowerCase();
206
+ const canonicalModelId = (0, modelAliases_1.resolveModelAlias)(stripped);
207
+ if (/\[1m\]/i.test(rawModelId) || /\[1m\]/i.test(canonicalModelId)) {
208
+ return {
209
+ modelId: rawModelId,
210
+ canonicalModelId: canonicalModelId.replace(/\[1m\]/gi, ''),
211
+ published: 1_000_000,
212
+ observed: null,
213
+ tierEffective: 1_000_000,
214
+ provenance: { source: 'explicit-1m', match: 'exact', matchedModelId: canonicalModelId },
215
+ };
216
+ }
217
+ // The raw id is tried before its alias target at every step: data recorded
218
+ // for the alias id itself is more specific than the canonical fallback.
219
+ const candidateIds = stripped === canonicalModelId ? [canonicalModelId] : [stripped, canonicalModelId];
220
+ const firstMatch = (lookup) => {
221
+ for (const id of candidateIds) {
222
+ const hit = lookup(id);
223
+ if (hit)
224
+ return hit;
225
+ }
226
+ return null;
227
+ };
228
+ const observedExact = firstMatch((id) => exactWindowMatch(observedTable, id));
229
+ // Imported resolutions rank below local observed data: a window this realm
230
+ // saw first-hand is fresher than a snapshot transferred from another realm.
231
+ if (!observedExact) {
232
+ const imported = importedTable[stripped] ?? importedTable[canonicalModelId];
233
+ if (imported) {
234
+ return { ...imported, modelId: rawModelId, canonicalModelId };
235
+ }
236
+ }
237
+ const catalogExact = firstMatch((id) => exactWindowMatch(catalogTable, id));
238
+ const staticExact = firstMatch((id) => exactWindowMatch(MODEL_CONTEXT_SIZES, id));
239
+ const observedPrefix = firstMatch((id) => prefixWindowMatch(observedTable, observedSortedKeys, id));
240
+ const catalogPrefix = firstMatch((id) => prefixWindowMatch(catalogTable, catalogSortedKeys, id));
241
+ const staticPrefix = firstMatch((id) => prefixWindowMatch(MODEL_CONTEXT_SIZES, SORTED_KEYS, id));
242
+ const published = catalogExact ?? staticExact ?? catalogPrefix ?? staticPrefix;
243
+ const effective = observedExact ?? catalogExact ?? staticExact ?? observedPrefix ?? catalogPrefix ?? staticPrefix;
244
+ return {
245
+ modelId: rawModelId,
246
+ canonicalModelId,
247
+ published: published?.value ?? null,
248
+ observed: (observedExact ?? observedPrefix)?.value ?? null,
249
+ tierEffective: effective?.value ?? exports.DEFAULT_CONTEXT_WINDOW,
250
+ provenance: effective
251
+ ? {
252
+ source: effective === observedExact || effective === observedPrefix
253
+ ? 'observed'
254
+ : effective === catalogExact || effective === catalogPrefix
255
+ ? 'catalog'
256
+ : 'static',
257
+ match: effective.match,
258
+ matchedModelId: effective.matchedModelId,
259
+ }
260
+ : { source: 'default', match: 'default' },
261
+ };
262
+ }
162
263
  /**
163
264
  * Returns the context window size for a model ID.
164
265
  *
@@ -180,31 +281,5 @@ function prefixWindow(table, sortedKeys, modelId) {
180
281
  * of the static table's exact 1M.
181
282
  */
182
283
  function getModelContextWindowSize(modelId) {
183
- if (!modelId)
184
- return exports.DEFAULT_CONTEXT_WINDOW;
185
- // Claude Code tags the 1M-context variant with a "[1m]" suffix on the
186
- // model ID. If we see it, honor it regardless of the base family.
187
- if (/\[1m\]/i.test(modelId))
188
- return 1_000_000;
189
- // Strip the suffix if present, so the normal lookup still succeeds when
190
- // a caller passes e.g. "claude-opus-5[1m]" and we've already handled it.
191
- // Trim/lowercase so padded or mixed-case IDs match the lowercase table keys.
192
- const normalized = modelId
193
- .replace(/\[1m\]/gi, '')
194
- .trim()
195
- .toLowerCase();
196
- // Pass 1 — exact matches, most-trusted layer first.
197
- const exact = exactWindow(observedTable, normalized) ??
198
- exactWindow(catalogTable, normalized) ??
199
- exactWindow(MODEL_CONTEXT_SIZES, normalized);
200
- if (exact !== null)
201
- return exact;
202
- // Pass 2 — longest-prefix matches, same layer order. Resolves versioned IDs
203
- // that no layer spells out, e.g. "claude-opus-4-6-20250414".
204
- const prefix = prefixWindow(observedTable, observedSortedKeys, normalized) ??
205
- prefixWindow(catalogTable, catalogSortedKeys, normalized) ??
206
- prefixWindow(MODEL_CONTEXT_SIZES, SORTED_KEYS, normalized);
207
- if (prefix !== null)
208
- return prefix;
209
- return exports.DEFAULT_CONTEXT_WINDOW;
284
+ return resolveModelContextWindow(modelId).tierEffective;
210
285
  }
@@ -17,6 +17,8 @@
17
17
  *
18
18
  * @module modelInfo
19
19
  */
20
+ import type { ModelProviderId } from './providerIds';
21
+ export { MODEL_PROVIDER_IDS } from './providerIds';
20
22
  /** Pricing information for a model. All costs are per million tokens in USD. */
21
23
  export interface ModelPricing {
22
24
  inputCostPerMillion: number;
@@ -24,6 +26,21 @@ export interface ModelPricing {
24
26
  cacheWriteCostPerMillion: number;
25
27
  cacheReadCostPerMillion: number;
26
28
  }
29
+ export type ModelPricingSource = 'imported' | 'catalog' | 'static' | 'none';
30
+ export type ModelPricingMatch = 'exact' | 'prefix' | 'none';
31
+ export interface ModelPricingProvenance {
32
+ source: ModelPricingSource;
33
+ match: ModelPricingMatch;
34
+ matchedModelId?: string;
35
+ /** True when the price was inherited from a shorter model-ID prefix. */
36
+ inheritedByPrefix: boolean;
37
+ }
38
+ export interface ResolvedModelPricing {
39
+ modelId: string;
40
+ canonicalModelId: string;
41
+ pricing: ModelPricing | null;
42
+ provenance: ModelPricingProvenance;
43
+ }
27
44
  /**
28
45
  * Token usage for the legacy cost calculation contract.
29
46
  * @deprecated Use NormalizedUsage and calculateNormalizedUsageCost. This shape
@@ -42,7 +59,7 @@ export interface CostTokenUsage {
42
59
  reasoningTokens?: number;
43
60
  }
44
61
  /** Provider that hosts a model. "unknown" means we couldn't classify it. */
45
- export type ModelProvider = 'anthropic' | 'openai' | 'google' | 'unknown';
62
+ export type ModelProvider = ModelProviderId;
46
63
  /** Structured result of parsing a raw model ID. */
47
64
  export interface ParsedModelId {
48
65
  provider: ModelProvider;
@@ -93,6 +110,16 @@ export declare function _setPricingOverrides(overrides: Record<string, ModelPric
93
110
  export declare function _getPricingOverrides(): Record<string, ModelPricing>;
94
111
  /** Internal: clear overrides (used by tests and when disabling hydration). */
95
112
  export declare function _clearPricingOverrides(): void;
113
+ /**
114
+ * Internal: hydrate exact pricing resolutions exported by another realm.
115
+ * Merges so successive partial imports accumulate (aliases already merge);
116
+ * use `_clearImportedResolvedPricing` to reset.
117
+ */
118
+ export declare function _setImportedResolvedPricing(values: Record<string, ResolvedModelPricing>): void;
119
+ /** Internal test hook. */
120
+ export declare function _clearImportedResolvedPricing(): void;
121
+ /** Internal: list every ID known to the pricing resolver. */
122
+ export declare function _getModelPricingIds(): string[];
96
123
  /**
97
124
  * Parses a model ID into {provider, family, version}.
98
125
  *
@@ -103,6 +130,8 @@ export declare function _clearPricingOverrides(): void;
103
130
  * Returns null for anything else — callers should treat that as "unknown model".
104
131
  */
105
132
  export declare function parseModelId(modelId: string): ParsedModelId | null;
133
+ /** Resolve pricing and disclose whether the result was inherited by prefix. */
134
+ export declare function resolveModelPricing(modelId: string): ResolvedModelPricing;
106
135
  /**
107
136
  * Gets pricing for a model ID.
108
137
  *
package/dist/modelInfo.js CHANGED
@@ -19,10 +19,15 @@
19
19
  * @module modelInfo
20
20
  */
21
21
  Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.MODEL_PROVIDER_IDS = void 0;
22
23
  exports._setPricingOverrides = _setPricingOverrides;
23
24
  exports._getPricingOverrides = _getPricingOverrides;
24
25
  exports._clearPricingOverrides = _clearPricingOverrides;
26
+ exports._setImportedResolvedPricing = _setImportedResolvedPricing;
27
+ exports._clearImportedResolvedPricing = _clearImportedResolvedPricing;
28
+ exports._getModelPricingIds = _getModelPricingIds;
25
29
  exports.parseModelId = parseModelId;
30
+ exports.resolveModelPricing = resolveModelPricing;
26
31
  exports.getModelPricing = getModelPricing;
27
32
  exports.getModelInfo = getModelInfo;
28
33
  exports.calculateCostWithPricing = calculateCostWithPricing;
@@ -35,6 +40,9 @@ exports.compareModelIds = compareModelIds;
35
40
  exports.sortModelIds = sortModelIds;
36
41
  exports.formatCost = formatCost;
37
42
  const modelContext_1 = require("./modelContext");
43
+ const modelAliases_1 = require("./modelAliases");
44
+ var providerIds_1 = require("./providerIds");
45
+ Object.defineProperty(exports, "MODEL_PROVIDER_IDS", { enumerable: true, get: function () { return providerIds_1.MODEL_PROVIDER_IDS; } });
38
46
  // ── Static Pricing Table ──
39
47
  /**
40
48
  * Static baseline pricing. Keys are model-ID prefixes; lookup uses
@@ -412,6 +420,7 @@ const STATIC_SORTED_KEYS = Object.keys(PRICING_TABLE).sort((a, b) => b.length -
412
420
  // ── Override Table (populated at runtime by pricingCatalog) ──
413
421
  let overrideTable = {};
414
422
  let overrideSortedKeys = [];
423
+ let importedTable = {};
415
424
  /**
416
425
  * Internal: replace the runtime override map. Called by `pricingCatalog.ts`
417
426
  * after a successful LiteLLM hydration. Not part of the stable public API,
@@ -419,7 +428,7 @@ let overrideSortedKeys = [];
419
428
  * creating a circular import.
420
429
  */
421
430
  function _setPricingOverrides(overrides) {
422
- overrideTable = { ...overrides };
431
+ overrideTable = Object.fromEntries(Object.entries(overrides).map(([key, value]) => [key.trim().toLowerCase(), { ...value }]));
423
432
  overrideSortedKeys = Object.keys(overrideTable).sort((a, b) => b.length - a.length);
424
433
  }
425
434
  /** Internal: snapshot current overrides (test + diagnostics). */
@@ -431,6 +440,26 @@ function _clearPricingOverrides() {
431
440
  overrideTable = {};
432
441
  overrideSortedKeys = [];
433
442
  }
443
+ /**
444
+ * Internal: hydrate exact pricing resolutions exported by another realm.
445
+ * Merges so successive partial imports accumulate (aliases already merge);
446
+ * use `_clearImportedResolvedPricing` to reset.
447
+ */
448
+ function _setImportedResolvedPricing(values) {
449
+ importedTable = { ...importedTable, ...values };
450
+ }
451
+ /** Internal test hook. */
452
+ function _clearImportedResolvedPricing() {
453
+ importedTable = {};
454
+ }
455
+ /** Internal: list every ID known to the pricing resolver. */
456
+ function _getModelPricingIds() {
457
+ return Array.from(new Set([
458
+ ...Object.keys(PRICING_TABLE),
459
+ ...Object.keys(overrideTable),
460
+ ...Object.keys(importedTable),
461
+ ]));
462
+ }
434
463
  // ── Model ID Parsing ──
435
464
  // Anthropic writes the minor version with a hyphen in modern IDs
436
465
  // (`claude-sonnet-4-6-20260321`) and a dot in older ones
@@ -454,10 +483,7 @@ const GEMINI_RE = /^gemini-([0-9][0-9.A-Za-z-]*)/i;
454
483
  function parseModelId(modelId) {
455
484
  if (!modelId)
456
485
  return null;
457
- const normalized = modelId
458
- .replace(/\[1m\]/gi, '')
459
- .trim()
460
- .toLowerCase();
486
+ const normalized = (0, modelAliases_1.resolveModelAlias)(modelId.replace(/\[1m\]/gi, ''));
461
487
  const claude = normalized.match(CLAUDE_RE);
462
488
  if (claude) {
463
489
  return {
@@ -490,27 +516,96 @@ function parseModelId(modelId) {
490
516
  return null;
491
517
  }
492
518
  // ── Pricing Lookup ──
493
- /** Find the longest key in `keys` that is a prefix of `modelId`, or null. */
519
+ /** Find the longest shorter key in `keys` that is a prefix of `modelId`, or null. */
494
520
  function findLongestPrefix(keys, modelId) {
495
521
  for (const key of keys) {
496
- if (modelId === key || modelId.startsWith(key))
522
+ if (modelId !== key && modelId.startsWith(key))
497
523
  return key;
498
524
  }
499
525
  return null;
500
526
  }
501
- /** Override-then-static lookup, exact then longest-prefix at each stage. */
502
- function lookupPricing(modelId) {
503
- if (overrideTable[modelId])
504
- return overrideTable[modelId];
505
- const overridePrefix = findLongestPrefix(overrideSortedKeys, modelId);
506
- if (overridePrefix)
507
- return overrideTable[overridePrefix];
508
- if (PRICING_TABLE[modelId])
509
- return PRICING_TABLE[modelId];
510
- const staticPrefix = findLongestPrefix(STATIC_SORTED_KEYS, modelId);
511
- if (staticPrefix)
512
- return PRICING_TABLE[staticPrefix];
513
- return null;
527
+ /** Resolve pricing and disclose whether the result was inherited by prefix. */
528
+ function resolveModelPricing(modelId) {
529
+ const stripped = modelId.replace(/\[1m\]/gi, '');
530
+ const rawId = stripped.trim().toLowerCase();
531
+ const canonicalModelId = (0, modelAliases_1.resolveModelAlias)(stripped);
532
+ // The raw id is tried before its alias target at every step: pricing
533
+ // recorded for the alias id itself is more specific than the canonical
534
+ // fallback, so registering an alias never orphans it.
535
+ const candidateIds = rawId === canonicalModelId ? [canonicalModelId] : [rawId, canonicalModelId];
536
+ const imported = importedTable[rawId] ?? importedTable[canonicalModelId];
537
+ if (imported)
538
+ return { ...imported, modelId, canonicalModelId };
539
+ for (const id of candidateIds) {
540
+ const exactOverride = overrideTable[id];
541
+ if (exactOverride) {
542
+ return {
543
+ modelId,
544
+ canonicalModelId,
545
+ pricing: exactOverride,
546
+ provenance: {
547
+ source: 'catalog',
548
+ match: 'exact',
549
+ matchedModelId: id,
550
+ inheritedByPrefix: false,
551
+ },
552
+ };
553
+ }
554
+ }
555
+ for (const id of candidateIds) {
556
+ const exactStatic = PRICING_TABLE[id];
557
+ if (exactStatic) {
558
+ return {
559
+ modelId,
560
+ canonicalModelId,
561
+ pricing: exactStatic,
562
+ provenance: {
563
+ source: 'static',
564
+ match: 'exact',
565
+ matchedModelId: id,
566
+ inheritedByPrefix: false,
567
+ },
568
+ };
569
+ }
570
+ }
571
+ for (const id of candidateIds) {
572
+ const overridePrefix = findLongestPrefix(overrideSortedKeys, id);
573
+ if (overridePrefix) {
574
+ return {
575
+ modelId,
576
+ canonicalModelId,
577
+ pricing: overrideTable[overridePrefix],
578
+ provenance: {
579
+ source: 'catalog',
580
+ match: 'prefix',
581
+ matchedModelId: overridePrefix,
582
+ inheritedByPrefix: true,
583
+ },
584
+ };
585
+ }
586
+ }
587
+ for (const id of candidateIds) {
588
+ const staticPrefix = findLongestPrefix(STATIC_SORTED_KEYS, id);
589
+ if (staticPrefix) {
590
+ return {
591
+ modelId,
592
+ canonicalModelId,
593
+ pricing: PRICING_TABLE[staticPrefix],
594
+ provenance: {
595
+ source: 'static',
596
+ match: 'prefix',
597
+ matchedModelId: staticPrefix,
598
+ inheritedByPrefix: true,
599
+ },
600
+ };
601
+ }
602
+ }
603
+ return {
604
+ modelId,
605
+ canonicalModelId,
606
+ pricing: null,
607
+ provenance: { source: 'none', match: 'none', inheritedByPrefix: false },
608
+ };
514
609
  }
515
610
  /**
516
611
  * Gets pricing for a model ID.
@@ -530,12 +625,7 @@ function lookupPricing(modelId) {
530
625
  function getModelPricing(modelId) {
531
626
  if (!modelId)
532
627
  return null;
533
- const stripped = modelId.replace(/\[1m\]/gi, '');
534
- const direct = lookupPricing(stripped);
535
- if (direct)
536
- return direct;
537
- const normalized = stripped.trim().toLowerCase();
538
- return normalized === stripped ? null : lookupPricing(normalized);
628
+ return resolveModelPricing(modelId).pricing;
539
629
  }
540
630
  /**
541
631
  * Returns comprehensive metadata for a model ID.