@trazum/core 1.8.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 (223) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +163 -0
  3. package/dist/advisories.d.ts +23 -0
  4. package/dist/advisories.d.ts.map +1 -0
  5. package/dist/advisories.js +376 -0
  6. package/dist/advisories.js.map +1 -0
  7. package/dist/aws-sigv4.d.ts +88 -0
  8. package/dist/aws-sigv4.d.ts.map +1 -0
  9. package/dist/aws-sigv4.js +117 -0
  10. package/dist/aws-sigv4.js.map +1 -0
  11. package/dist/baseline.d.ts +171 -0
  12. package/dist/baseline.d.ts.map +1 -0
  13. package/dist/baseline.js +273 -0
  14. package/dist/baseline.js.map +1 -0
  15. package/dist/cache.d.ts +26 -0
  16. package/dist/cache.d.ts.map +1 -0
  17. package/dist/cache.js +28 -0
  18. package/dist/cache.js.map +1 -0
  19. package/dist/changes.d.ts +29 -0
  20. package/dist/changes.d.ts.map +1 -0
  21. package/dist/changes.js +142 -0
  22. package/dist/changes.js.map +1 -0
  23. package/dist/compare.d.ts +65 -0
  24. package/dist/compare.d.ts.map +1 -0
  25. package/dist/compare.js +58 -0
  26. package/dist/compare.js.map +1 -0
  27. package/dist/config-schema.d.ts +118 -0
  28. package/dist/config-schema.d.ts.map +1 -0
  29. package/dist/config-schema.js +315 -0
  30. package/dist/config-schema.js.map +1 -0
  31. package/dist/config.d.ts +47 -0
  32. package/dist/config.d.ts.map +1 -0
  33. package/dist/config.js +112 -0
  34. package/dist/config.js.map +1 -0
  35. package/dist/detect.d.ts +70 -0
  36. package/dist/detect.d.ts.map +1 -0
  37. package/dist/detect.js +228 -0
  38. package/dist/detect.js.map +1 -0
  39. package/dist/evaluate.d.ts +98 -0
  40. package/dist/evaluate.d.ts.map +1 -0
  41. package/dist/evaluate.js +110 -0
  42. package/dist/evaluate.js.map +1 -0
  43. package/dist/extract.d.ts +81 -0
  44. package/dist/extract.d.ts.map +1 -0
  45. package/dist/extract.js +280 -0
  46. package/dist/extract.js.map +1 -0
  47. package/dist/gcp-auth.d.ts +58 -0
  48. package/dist/gcp-auth.d.ts.map +1 -0
  49. package/dist/gcp-auth.js +113 -0
  50. package/dist/gcp-auth.js.map +1 -0
  51. package/dist/glob.d.ts +49 -0
  52. package/dist/glob.d.ts.map +1 -0
  53. package/dist/glob.js +154 -0
  54. package/dist/glob.js.map +1 -0
  55. package/dist/host.d.ts +30 -0
  56. package/dist/host.d.ts.map +1 -0
  57. package/dist/host.js +69 -0
  58. package/dist/host.js.map +1 -0
  59. package/dist/i18n/en.d.ts +4 -0
  60. package/dist/i18n/en.d.ts.map +1 -0
  61. package/dist/i18n/en.js +168 -0
  62. package/dist/i18n/en.js.map +1 -0
  63. package/dist/i18n/es.d.ts +4 -0
  64. package/dist/i18n/es.d.ts.map +1 -0
  65. package/dist/i18n/es.js +168 -0
  66. package/dist/i18n/es.js.map +1 -0
  67. package/dist/i18n/index.d.ts +36 -0
  68. package/dist/i18n/index.d.ts.map +1 -0
  69. package/dist/i18n/index.js +50 -0
  70. package/dist/i18n/index.js.map +1 -0
  71. package/dist/i18n/types.d.ts +180 -0
  72. package/dist/i18n/types.d.ts.map +1 -0
  73. package/dist/i18n/types.js +11 -0
  74. package/dist/i18n/types.js.map +1 -0
  75. package/dist/index.d.ts +66 -0
  76. package/dist/index.d.ts.map +1 -0
  77. package/dist/index.js +69 -0
  78. package/dist/index.js.map +1 -0
  79. package/dist/llm.d.ts +226 -0
  80. package/dist/llm.d.ts.map +1 -0
  81. package/dist/llm.js +485 -0
  82. package/dist/llm.js.map +1 -0
  83. package/dist/nearest.d.ts +20 -0
  84. package/dist/nearest.d.ts.map +1 -0
  85. package/dist/nearest.js +54 -0
  86. package/dist/nearest.js.map +1 -0
  87. package/dist/net.d.ts +90 -0
  88. package/dist/net.d.ts.map +1 -0
  89. package/dist/net.js +203 -0
  90. package/dist/net.js.map +1 -0
  91. package/dist/node.d.ts +32 -0
  92. package/dist/node.d.ts.map +1 -0
  93. package/dist/node.js +33 -0
  94. package/dist/node.js.map +1 -0
  95. package/dist/openrouter.d.ts +25 -0
  96. package/dist/openrouter.d.ts.map +1 -0
  97. package/dist/openrouter.js +72 -0
  98. package/dist/openrouter.js.map +1 -0
  99. package/dist/optimize.d.ts +38 -0
  100. package/dist/optimize.d.ts.map +1 -0
  101. package/dist/optimize.js +183 -0
  102. package/dist/optimize.js.map +1 -0
  103. package/dist/otlp.d.ts +91 -0
  104. package/dist/otlp.d.ts.map +1 -0
  105. package/dist/otlp.js +102 -0
  106. package/dist/otlp.js.map +1 -0
  107. package/dist/phrases.d.ts +169 -0
  108. package/dist/phrases.d.ts.map +1 -0
  109. package/dist/phrases.js +939 -0
  110. package/dist/phrases.js.map +1 -0
  111. package/dist/pricing-overlay.d.ts +55 -0
  112. package/dist/pricing-overlay.d.ts.map +1 -0
  113. package/dist/pricing-overlay.js +241 -0
  114. package/dist/pricing-overlay.js.map +1 -0
  115. package/dist/pricing.d.ts +115 -0
  116. package/dist/pricing.d.ts.map +1 -0
  117. package/dist/pricing.js +400 -0
  118. package/dist/pricing.js.map +1 -0
  119. package/dist/profile.d.ts +71 -0
  120. package/dist/profile.d.ts.map +1 -0
  121. package/dist/profile.js +55 -0
  122. package/dist/profile.js.map +1 -0
  123. package/dist/promptfoo.d.ts +58 -0
  124. package/dist/promptfoo.d.ts.map +1 -0
  125. package/dist/promptfoo.js +149 -0
  126. package/dist/promptfoo.js.map +1 -0
  127. package/dist/prune.d.ts +91 -0
  128. package/dist/prune.d.ts.map +1 -0
  129. package/dist/prune.js +110 -0
  130. package/dist/prune.js.map +1 -0
  131. package/dist/reorder.d.ts +82 -0
  132. package/dist/reorder.d.ts.map +1 -0
  133. package/dist/reorder.js +215 -0
  134. package/dist/reorder.js.map +1 -0
  135. package/dist/review.d.ts +54 -0
  136. package/dist/review.d.ts.map +1 -0
  137. package/dist/review.js +131 -0
  138. package/dist/review.js.map +1 -0
  139. package/dist/rules.d.ts +5 -0
  140. package/dist/rules.d.ts.map +1 -0
  141. package/dist/rules.js +279 -0
  142. package/dist/rules.js.map +1 -0
  143. package/dist/savings.d.ts +36 -0
  144. package/dist/savings.d.ts.map +1 -0
  145. package/dist/savings.js +83 -0
  146. package/dist/savings.js.map +1 -0
  147. package/dist/segment.d.ts +8 -0
  148. package/dist/segment.d.ts.map +1 -0
  149. package/dist/segment.js +74 -0
  150. package/dist/segment.js.map +1 -0
  151. package/dist/shared-prefix.d.ts +63 -0
  152. package/dist/shared-prefix.d.ts.map +1 -0
  153. package/dist/shared-prefix.js +151 -0
  154. package/dist/shared-prefix.js.map +1 -0
  155. package/dist/similarity.d.ts +13 -0
  156. package/dist/similarity.d.ts.map +1 -0
  157. package/dist/similarity.js +30 -0
  158. package/dist/similarity.js.map +1 -0
  159. package/dist/structure.d.ts +144 -0
  160. package/dist/structure.d.ts.map +1 -0
  161. package/dist/structure.js +455 -0
  162. package/dist/structure.js.map +1 -0
  163. package/dist/suggest.d.ts +100 -0
  164. package/dist/suggest.d.ts.map +1 -0
  165. package/dist/suggest.js +151 -0
  166. package/dist/suggest.js.map +1 -0
  167. package/dist/tokenizer.d.ts +57 -0
  168. package/dist/tokenizer.d.ts.map +1 -0
  169. package/dist/tokenizer.js +157 -0
  170. package/dist/tokenizer.js.map +1 -0
  171. package/dist/types.d.ts +296 -0
  172. package/dist/types.d.ts.map +1 -0
  173. package/dist/types.js +3 -0
  174. package/dist/types.js.map +1 -0
  175. package/dist/walk.d.ts +40 -0
  176. package/dist/walk.d.ts.map +1 -0
  177. package/dist/walk.js +95 -0
  178. package/dist/walk.js.map +1 -0
  179. package/package.json +56 -0
  180. package/src/advisories.ts +431 -0
  181. package/src/aws-sigv4.ts +174 -0
  182. package/src/baseline.ts +390 -0
  183. package/src/cache.ts +54 -0
  184. package/src/changes.ts +158 -0
  185. package/src/compare.ts +131 -0
  186. package/src/config-schema.ts +451 -0
  187. package/src/config.ts +161 -0
  188. package/src/detect.ts +312 -0
  189. package/src/evaluate.ts +188 -0
  190. package/src/extract.ts +336 -0
  191. package/src/gcp-auth.ts +166 -0
  192. package/src/glob.ts +160 -0
  193. package/src/host.ts +90 -0
  194. package/src/i18n/en.ts +236 -0
  195. package/src/i18n/es.ts +236 -0
  196. package/src/i18n/index.ts +68 -0
  197. package/src/i18n/types.ts +230 -0
  198. package/src/index.ts +228 -0
  199. package/src/llm.ts +708 -0
  200. package/src/nearest.ts +61 -0
  201. package/src/net.ts +233 -0
  202. package/src/node.ts +63 -0
  203. package/src/openrouter.ts +125 -0
  204. package/src/optimize.ts +228 -0
  205. package/src/otlp.ts +179 -0
  206. package/src/phrases.ts +1047 -0
  207. package/src/pricing-overlay.ts +319 -0
  208. package/src/pricing.ts +468 -0
  209. package/src/profile.ts +124 -0
  210. package/src/promptfoo.ts +213 -0
  211. package/src/prune.ts +211 -0
  212. package/src/reorder.ts +307 -0
  213. package/src/review.ts +180 -0
  214. package/src/rules.ts +324 -0
  215. package/src/savings.ts +121 -0
  216. package/src/segment.ts +106 -0
  217. package/src/shared-prefix.ts +198 -0
  218. package/src/similarity.ts +28 -0
  219. package/src/structure.ts +652 -0
  220. package/src/suggest.ts +254 -0
  221. package/src/tokenizer.ts +190 -0
  222. package/src/types.ts +323 -0
  223. package/src/walk.ts +117 -0
@@ -0,0 +1,319 @@
1
+ import { BUNDLED_CATALOGUE } from './pricing.js';
2
+ import { nearestName } from './nearest.js';
3
+ import type { PricingCatalogue } from './pricing.js';
4
+ import type { Capability, CachingMode, ModelPricing } from './types.js';
5
+
6
+ /**
7
+ * Local price corrections, so a price change does not require a library upgrade.
8
+ *
9
+ * This is the honest answer to "pricing data separated from the release cycle".
10
+ * A separate `@trazum/pricing` package would not have achieved it — you would
11
+ * still need to install something to get current numbers. A JSON file in your own
12
+ * repository does, and it keeps the two properties that matter: **the core still
13
+ * makes no network call**, and it still has no dependencies.
14
+ *
15
+ * The bundled catalogue remains the default, so Trazum is correct out of the box
16
+ * and an overlay is only needed once a published price moves.
17
+ *
18
+ * Validation is as strict as the config parser's, for the same reason: a typo'd
19
+ * model id in an overlay would silently price against the bundled number, and a
20
+ * budget decision made on a price nobody applied is the failure this whole file
21
+ * exists to prevent.
22
+ */
23
+
24
+ export const PRICING_OVERLAY_KEYS = ['lastReviewed', 'models'] as const;
25
+
26
+ export const PRICING_MODEL_KEYS = [
27
+ 'displayName',
28
+ 'inputPerMTok',
29
+ 'outputPerMTok',
30
+ 'contextWindow',
31
+ 'cacheMinTokens',
32
+ 'tier',
33
+ 'capability',
34
+ 'caching',
35
+ 'notes',
36
+ 'promo',
37
+ ] as const;
38
+
39
+ const PROMO_KEYS = ['inputPerMTok', 'outputPerMTok', 'until'] as const;
40
+
41
+ const TIERS: ModelPricing['tier'][] = ['frontier', 'opus', 'sonnet', 'haiku', 'unknown'];
42
+ const CAPABILITIES: Capability[] = ['small', 'mid', 'large', 'frontier', 'unknown'];
43
+ const CACHING_MODES: CachingMode[] = ['explicit', 'automatic', 'none', 'unknown'];
44
+
45
+ /** Largest overlay this will read — a price list, not a dataset. */
46
+ export const MAX_PRICING_BYTES = 64 * 1024;
47
+
48
+ export interface PricingOverlay {
49
+ /**
50
+ * When these prices were checked, as `YYYY-MM-DD`. Required: an overlay whose
51
+ * age is unknown is worse than the bundled catalogue, whose age is printed on
52
+ * every report.
53
+ */
54
+ lastReviewed: string;
55
+ /** Per-model corrections, keyed by model id. */
56
+ models: Record<string, Partial<Omit<ModelPricing, 'id'>>>;
57
+ }
58
+
59
+ export class PricingOverlayError extends Error {
60
+ constructor(
61
+ message: string,
62
+ readonly source: string,
63
+ ) {
64
+ super(`${source}: ${message}`);
65
+ this.name = 'PricingOverlayError';
66
+ }
67
+ }
68
+
69
+ const isPlainObject = (value: unknown): value is Record<string, unknown> =>
70
+ typeof value === 'object' && value !== null && !Array.isArray(value);
71
+
72
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
73
+
74
+ function rejectUnknownKeys(
75
+ object: Record<string, unknown>,
76
+ allowed: readonly string[],
77
+ source: string,
78
+ path: string,
79
+ ): void {
80
+ for (const key of Object.keys(object)) {
81
+ if (allowed.includes(key)) continue;
82
+ const nearest = nearestName(key, allowed);
83
+ throw new PricingOverlayError(
84
+ nearest
85
+ ? `unknown key "${path}${key}" — did you mean "${nearest}"?`
86
+ : `unknown key "${path}${key}". Known keys: ${allowed.join(', ')}`,
87
+ source,
88
+ );
89
+ }
90
+ }
91
+
92
+ function positiveNumber(value: unknown, label: string, source: string): number {
93
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
94
+ throw new PricingOverlayError(`"${label}" must be a number greater than 0`, source);
95
+ }
96
+ return value;
97
+ }
98
+
99
+ function integerAbove(value: unknown, label: string, source: string): number {
100
+ const n = positiveNumber(value, label, source);
101
+ if (!Number.isInteger(n)) {
102
+ throw new PricingOverlayError(`"${label}" must be a whole number`, source);
103
+ }
104
+ return n;
105
+ }
106
+
107
+ function parsePromo(raw: unknown, label: string, source: string): ModelPricing['promo'] {
108
+ if (!isPlainObject(raw)) {
109
+ throw new PricingOverlayError(`"${label}" must be an object`, source);
110
+ }
111
+ rejectUnknownKeys(raw, PROMO_KEYS, source, `${label}.`);
112
+
113
+ for (const key of PROMO_KEYS) {
114
+ if (raw[key] === undefined) {
115
+ throw new PricingOverlayError(
116
+ `"${label}" needs all of ${PROMO_KEYS.join(', ')} — a partial promotion has no meaning`,
117
+ source,
118
+ );
119
+ }
120
+ }
121
+ if (typeof raw.until !== 'string' || !ISO_DATE.test(raw.until)) {
122
+ throw new PricingOverlayError(`"${label}.until" must be a date like 2026-08-31`, source);
123
+ }
124
+
125
+ return {
126
+ inputPerMTok: positiveNumber(raw.inputPerMTok, `${label}.inputPerMTok`, source),
127
+ outputPerMTok: positiveNumber(raw.outputPerMTok, `${label}.outputPerMTok`, source),
128
+ until: raw.until,
129
+ };
130
+ }
131
+
132
+ function parseModel(
133
+ raw: unknown,
134
+ id: string,
135
+ source: string,
136
+ ): Partial<Omit<ModelPricing, 'id'>> {
137
+ if (!isPlainObject(raw)) {
138
+ throw new PricingOverlayError(`"models.${id}" must be an object`, source);
139
+ }
140
+ rejectUnknownKeys(raw, PRICING_MODEL_KEYS, source, `models.${id}.`);
141
+ if (Object.keys(raw).length === 0) {
142
+ throw new PricingOverlayError(
143
+ `"models.${id}" is empty — remove it, or say what it changes`,
144
+ source,
145
+ );
146
+ }
147
+
148
+ const model: Partial<Omit<ModelPricing, 'id'>> = {};
149
+
150
+ if (raw.displayName !== undefined) {
151
+ if (typeof raw.displayName !== 'string' || raw.displayName.trim() === '') {
152
+ throw new PricingOverlayError(`"models.${id}.displayName" must be a non-empty string`, source);
153
+ }
154
+ model.displayName = raw.displayName;
155
+ }
156
+ if (raw.inputPerMTok !== undefined) {
157
+ model.inputPerMTok = positiveNumber(raw.inputPerMTok, `models.${id}.inputPerMTok`, source);
158
+ }
159
+ if (raw.outputPerMTok !== undefined) {
160
+ model.outputPerMTok = positiveNumber(raw.outputPerMTok, `models.${id}.outputPerMTok`, source);
161
+ }
162
+ if (raw.contextWindow !== undefined) {
163
+ model.contextWindow = integerAbove(raw.contextWindow, `models.${id}.contextWindow`, source);
164
+ }
165
+ if (raw.cacheMinTokens !== undefined) {
166
+ // `null` is "this catalogue does not know", which is a different statement
167
+ // from any number — including zero, which claims caching from the first
168
+ // token. A live price feed knows what a model costs and nothing about how
169
+ // it caches, so the distinction has to be expressible.
170
+ model.cacheMinTokens =
171
+ raw.cacheMinTokens === null
172
+ ? null
173
+ : integerAbove(raw.cacheMinTokens, `models.${id}.cacheMinTokens`, source);
174
+ }
175
+ if (raw.capability !== undefined) {
176
+ if (typeof raw.capability !== 'string' || !CAPABILITIES.includes(raw.capability as Capability)) {
177
+ throw new PricingOverlayError(
178
+ `"models.${id}.capability" must be one of ${CAPABILITIES.join(', ')}`,
179
+ source,
180
+ );
181
+ }
182
+ model.capability = raw.capability as Capability;
183
+ }
184
+ if (raw.caching !== undefined) {
185
+ if (typeof raw.caching !== 'string' || !CACHING_MODES.includes(raw.caching as CachingMode)) {
186
+ throw new PricingOverlayError(
187
+ `"models.${id}.caching" must be one of ${CACHING_MODES.join(', ')}`,
188
+ source,
189
+ );
190
+ }
191
+ model.caching = raw.caching as CachingMode;
192
+ }
193
+ if (raw.tier !== undefined) {
194
+ if (typeof raw.tier !== 'string' || !TIERS.includes(raw.tier as ModelPricing['tier'])) {
195
+ throw new PricingOverlayError(
196
+ `"models.${id}.tier" must be one of ${TIERS.join(', ')}`,
197
+ source,
198
+ );
199
+ }
200
+ model.tier = raw.tier as ModelPricing['tier'];
201
+ }
202
+ if (raw.notes !== undefined) {
203
+ if (typeof raw.notes !== 'string') {
204
+ throw new PricingOverlayError(`"models.${id}.notes" must be a string`, source);
205
+ }
206
+ model.notes = raw.notes;
207
+ }
208
+ if (raw.promo !== undefined) {
209
+ // `null` is how you cancel a bundled promotion that has been withdrawn.
210
+ model.promo = raw.promo === null ? undefined : parsePromo(raw.promo, `models.${id}.promo`, source);
211
+ }
212
+
213
+ return model;
214
+ }
215
+
216
+ /** Validates an overlay document. Every failure throws, with the source named. */
217
+ export function parsePricingOverlay(raw: string, source = 'pricing overlay'): PricingOverlay {
218
+ let document: unknown;
219
+ try {
220
+ document = JSON.parse(raw);
221
+ } catch (error) {
222
+ const detail = error instanceof Error ? error.message : String(error);
223
+ throw new PricingOverlayError(`not valid JSON — ${detail}`, source);
224
+ }
225
+
226
+ if (!isPlainObject(document)) {
227
+ throw new PricingOverlayError('the top level must be an object', source);
228
+ }
229
+ rejectUnknownKeys(document, PRICING_OVERLAY_KEYS, source, '');
230
+
231
+ if (typeof document.lastReviewed !== 'string' || !ISO_DATE.test(document.lastReviewed)) {
232
+ throw new PricingOverlayError(
233
+ '"lastReviewed" is required and must be a date like 2026-06-24. ' +
234
+ 'An overlay of unknown age is worse than the bundled catalogue, whose age is printed.',
235
+ source,
236
+ );
237
+ }
238
+ if (!isPlainObject(document.models) || Object.keys(document.models).length === 0) {
239
+ throw new PricingOverlayError('"models" must be a non-empty object keyed by model id', source);
240
+ }
241
+
242
+ const models: Record<string, Partial<Omit<ModelPricing, 'id'>>> = {};
243
+ for (const [id, value] of Object.entries(document.models)) {
244
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(id)) {
245
+ throw new PricingOverlayError(`"${id}" is not a plausible model id`, source);
246
+ }
247
+ models[id] = parseModel(value, id, source);
248
+ }
249
+
250
+ return { lastReviewed: document.lastReviewed, models };
251
+ }
252
+
253
+ /**
254
+ * A new catalogue with the overlay applied. Neither argument is modified.
255
+ *
256
+ * **A model the bundled catalogue does not have must be complete.** Overriding
257
+ * one field of a known model is the common case and needs no more than that
258
+ * field; inventing a model needs every field, because a half-defined model would
259
+ * price at zero somewhere and report a saving that does not exist.
260
+ *
261
+ * `lastReviewed` becomes the *overlay's* date, and the report shows it — a
262
+ * catalogue is only as current as its most recently touched half, and claiming
263
+ * the bundled date over corrected prices would be a lie about provenance.
264
+ */
265
+ export function applyPricingOverlay(
266
+ base: PricingCatalogue,
267
+ overlay: PricingOverlay,
268
+ source = 'pricing overlay',
269
+ ): PricingCatalogue {
270
+ const overridden: string[] = [];
271
+ const added: string[] = [];
272
+
273
+ const models = base.models.map((model) => {
274
+ const patch = overlay.models[model.id];
275
+ if (!patch) return model;
276
+ overridden.push(model.id);
277
+ // `promo` is spread explicitly: a patch that sets it to undefined must remove
278
+ // a withdrawn promotion rather than be ignored as a missing key.
279
+ const merged: ModelPricing = { ...model, ...patch };
280
+ if ('promo' in patch && patch.promo === undefined) delete merged.promo;
281
+ return merged;
282
+ });
283
+
284
+ for (const [id, patch] of Object.entries(overlay.models)) {
285
+ if (base.byId.has(id)) continue;
286
+
287
+ // `capability` joins the list: it is a required field of `ModelPricing`, and
288
+ // an added model without one produced an object the type says cannot exist —
289
+ // the cast below was hiding it.
290
+ //
291
+ // `cacheMinTokens` is checked for `undefined` and not for falsiness, so an
292
+ // explicit `null` counts as supplied. That is the point of it: saying "not
293
+ // known" is an answer, leaving it out is not.
294
+ const missing = (['displayName', 'inputPerMTok', 'outputPerMTok', 'contextWindow', 'cacheMinTokens', 'tier', 'capability'] as const).filter(
295
+ (key) => patch[key] === undefined,
296
+ );
297
+ if (missing.length > 0) {
298
+ throw new PricingOverlayError(
299
+ `"models.${id}" is not in the bundled catalogue, so it has to be complete. Missing: ${missing.join(', ')}`,
300
+ source,
301
+ );
302
+ }
303
+ models.push({ id, ...patch } as ModelPricing);
304
+ added.push(id);
305
+ }
306
+
307
+ return {
308
+ models,
309
+ byId: new Map(models.map((m) => [m.id, m])),
310
+ lastReviewed: overlay.lastReviewed,
311
+ overriddenModels: overridden.sort(),
312
+ addedModels: added.sort(),
313
+ };
314
+ }
315
+
316
+ /** Convenience: the bundled catalogue with an overlay document applied. */
317
+ export function catalogueFromOverlay(raw: string, source = 'pricing overlay'): PricingCatalogue {
318
+ return applyPricingOverlay(BUNDLED_CATALOGUE, parsePricingOverlay(raw, source), source);
319
+ }