@trazum/core 1.82.0 → 1.85.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 (58) hide show
  1. package/README.md +2 -2
  2. package/dist/advisories.d.ts.map +1 -1
  3. package/dist/advisories.js +27 -4
  4. package/dist/advisories.js.map +1 -1
  5. package/dist/band.d.ts.map +1 -1
  6. package/dist/band.js +12 -0
  7. package/dist/band.js.map +1 -1
  8. package/dist/i18n/en.d.ts.map +1 -1
  9. package/dist/i18n/en.js +7 -0
  10. package/dist/i18n/en.js.map +1 -1
  11. package/dist/i18n/es.d.ts.map +1 -1
  12. package/dist/i18n/es.js +8 -0
  13. package/dist/i18n/es.js.map +1 -1
  14. package/dist/i18n/types.d.ts +17 -0
  15. package/dist/i18n/types.d.ts.map +1 -1
  16. package/dist/index.d.ts +6 -2
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +4 -2
  19. package/dist/index.js.map +1 -1
  20. package/dist/langsmith.d.ts +104 -0
  21. package/dist/langsmith.d.ts.map +1 -0
  22. package/dist/langsmith.js +270 -0
  23. package/dist/langsmith.js.map +1 -0
  24. package/dist/levers.d.ts +1 -1
  25. package/dist/levers.js +2 -2
  26. package/dist/levers.js.map +1 -1
  27. package/dist/optimize.d.ts +2 -2
  28. package/dist/optimize.d.ts.map +1 -1
  29. package/dist/optimize.js +15 -1
  30. package/dist/optimize.js.map +1 -1
  31. package/dist/pricing.d.ts +98 -3
  32. package/dist/pricing.d.ts.map +1 -1
  33. package/dist/pricing.js +393 -35
  34. package/dist/pricing.js.map +1 -1
  35. package/dist/receipt.d.ts +253 -0
  36. package/dist/receipt.d.ts.map +1 -0
  37. package/dist/receipt.js +111 -0
  38. package/dist/receipt.js.map +1 -0
  39. package/dist/tokenizer.d.ts +4 -2
  40. package/dist/tokenizer.d.ts.map +1 -1
  41. package/dist/tokenizer.js +4 -2
  42. package/dist/tokenizer.js.map +1 -1
  43. package/dist/types.d.ts +148 -1
  44. package/dist/types.d.ts.map +1 -1
  45. package/package.json +1 -1
  46. package/src/advisories.ts +34 -4
  47. package/src/band.ts +12 -0
  48. package/src/i18n/en.ts +9 -0
  49. package/src/i18n/es.ts +10 -0
  50. package/src/i18n/types.ts +18 -0
  51. package/src/index.ts +14 -1
  52. package/src/langsmith.ts +315 -0
  53. package/src/levers.ts +3 -3
  54. package/src/optimize.ts +16 -0
  55. package/src/pricing.ts +430 -30
  56. package/src/receipt.ts +351 -0
  57. package/src/tokenizer.ts +4 -2
  58. package/src/types.ts +144 -0
package/dist/pricing.d.ts CHANGED
@@ -7,7 +7,33 @@ import type { ModelPricing } from './types.js';
7
7
  * Amazon Bedrock and Vertex AI pricing is set by each partner and is NOT the
8
8
  * pricing in this table.
9
9
  */
10
- export declare const PRICING_LAST_REVIEWED = "2026-06-24";
10
+ /**
11
+ * When each provider's prices were last checked against that provider's own
12
+ * published table.
13
+ *
14
+ * One date for the whole catalogue was the wrong shape, and it cost a real
15
+ * error. Seven providers publish independently and are checked independently;
16
+ * a single date forces a partial review to either overstate itself by moving
17
+ * the date for everyone, or throw itself away by leaving it. What happened is
18
+ * the second: this constant was written on 2026-08-04 already reading
19
+ * 2026-06-24, and never moved again.
20
+ *
21
+ * Meanwhile Anthropic cancelled the increase that was going to take Sonnet 5
22
+ * from its 2/10 introductory price to 3/15 on 2026-09-01, and made 2/10
23
+ * standard. The catalogue still carried the promotion with its end date, so on
24
+ * 2026-09-01 every Sonnet 5 figure this tool printed would have risen 50% with
25
+ * no code change and no way for a reader to tell. A price nobody charges is
26
+ * the one error this product cannot make, and it was four days out on a timer.
27
+ *
28
+ * So: a date per provider, checked against that provider's own page, and the
29
+ * catalogue's headline date derived as the oldest of them rather than typed.
30
+ */
31
+ export declare const PROVIDER_REVIEWED: Readonly<Record<string, string>>;
32
+ /**
33
+ * The catalogue's age is its oldest provider's, because a reader asking how
34
+ * old this table is wants the answer for the worst part of it, not the best.
35
+ */
36
+ export declare const PRICING_LAST_REVIEWED: string;
11
37
  /** Cost multipliers relative to the input price. */
12
38
  /**
13
39
  * The defaults, which are Anthropic's numbers.
@@ -75,6 +101,25 @@ export interface PricingCatalogue {
75
101
  export declare function indexModels(models: ModelPricing[]): Map<string, ModelPricing>;
76
102
  /** The prices compiled into this release. */
77
103
  export declare const BUNDLED_CATALOGUE: PricingCatalogue;
104
+ /**
105
+ * Whether this model may be offered to a reader as something to move to.
106
+ *
107
+ * One home for a two-part rule that used to be written in four places as one
108
+ * part. `recommendable: false` — a private programme, a waitlist — was honoured
109
+ * at every one of them. `retired` is the stronger statement: the provider
110
+ * answered a real request for the id with an error, so a switch to it is not a
111
+ * worse recommendation, it is one that cannot be carried out at all.
112
+ *
113
+ * Written as a function rather than as a second `&&` at four call sites,
114
+ * because the fifth call site is always the one that gets written with only the
115
+ * first half. `pricing.test.js` fails a source file that filters on
116
+ * `recommendable` without coming through here, and that guard was proved by
117
+ * planting exactly that filter.
118
+ *
119
+ * Pricing is a separate question and stays answered: a retired model keeps its
120
+ * price, because a log full of its calls records money that was really spent.
121
+ */
122
+ export declare function isOffered(model: ModelPricing): boolean;
78
123
  /** Looks a model up in a specific catalogue. */
79
124
  export declare function modelFrom(catalogue: PricingCatalogue, id: string): ModelPricing;
80
125
  /**
@@ -93,11 +138,46 @@ export declare function cheapestOfTierIn(catalogue: PricingCatalogue, tier: Mode
93
138
  provider?: string): ModelPricing;
94
139
  export declare function getModel(id: string): ModelPricing;
95
140
  export declare function listModels(): ModelPricing[];
96
- export declare function effectivePricing(model: ModelPricing, on?: Date): {
141
+ /** Effective price on a given date, applying any live promotion. */
142
+ /**
143
+ * Memoised per model, because this runs once per record when a usage log is
144
+ * priced: a 200k-line profile called it 200k times, and the `new Date(...)`
145
+ * parse of the promo's end date alone was 0.7 of the 2.4 seconds the whole
146
+ * profile took — measured with --cpu-prof, not guessed. The cache keys on
147
+ * the model object (a WeakMap, so an overlay's models are collected with
148
+ * the overlay) and stores the parsed end-of-promo instant plus the two
149
+ * possible answers; `on` still decides which answer applies on every call,
150
+ * so behaviour is unchanged to the millisecond.
151
+ */
152
+ interface Rates {
97
153
  inputPerMTok: number;
98
154
  outputPerMTok: number;
99
155
  promoApplied: boolean;
100
- };
156
+ tier: {
157
+ applied: string | null;
158
+ decided: boolean;
159
+ because: string;
160
+ } | null;
161
+ }
162
+ /**
163
+ * What one call is billed at, and what decided it.
164
+ *
165
+ * `tier` is the field this grew for, and it carries three states rather than
166
+ * two. A model with no tiers gets `null`. A model whose condition could be
167
+ * evaluated gets the tier that applied, or the base rate with `applied: null`
168
+ * and `decided: true`. A model whose condition could **not** be evaluated —
169
+ * a size tier priced without a token count — gets `decided: false`, the
170
+ * **dearer** of the candidate rates, and `because` naming what would settle it.
171
+ *
172
+ * The dearer one is not a shrug. A cost figure that is a ceiling can prove
173
+ * "under budget" and can never surprise a bill; a floor chosen for looking
174
+ * better is the flattering direction this product spends its time refusing.
175
+ * The reader still has to be told, which is what `decided: false` is for, and
176
+ * `pricing.test.js` fails a tiered model whose report drops it.
177
+ */
178
+ export declare function effectivePricing(model: ModelPricing, on?: Date, context?: {
179
+ inputTokens?: number;
180
+ }): Rates;
101
181
  /** Cheapest model of each capability tier, for recommendations. */
102
182
  export declare function cheapestOfTier(tier: ModelPricing['tier']): ModelPricing;
103
183
  /**
@@ -116,5 +196,20 @@ export declare function cheapestOfTier(tier: ModelPricing['tier']): ModelPricing
116
196
  * a date — an overlay supplies this string, and a wrong one should read as
117
197
  * unknown rather than as a confident number computed from `NaN`.
118
198
  */
199
+ /**
200
+ * The age at which this product stops treating its own price table as current.
201
+ *
202
+ * Three surfaces warn a reader that the figures above may be off — `profile`,
203
+ * the MCP report, the browser's bill — and each of them had typed `45` into
204
+ * its own comparison, with four locale sentences stating it again in prose.
205
+ * Seven copies of one claim, and nothing that would notice when they
206
+ * disagreed; the sentence a reader sees names the number, so a drift between
207
+ * them would have been a surface telling the reader one threshold and applying
208
+ * another.
209
+ *
210
+ * The value is what those surfaces already published, not a new judgement.
211
+ */
212
+ export declare const STALE_PRICING_DAYS = 45;
119
213
  export declare function reviewAgeDays(lastReviewed: string, now: Date): number | null;
214
+ export {};
120
215
  //# sourceMappingURL=pricing.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pricing.d.ts","sourceRoot":"","sources":["../src/pricing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,eAAe,CAAC;AAElD,oDAAoD;AACpD;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB;IAC3B,uCAAuC;aACvC,YAAY,EAAE,IAAI;IAClB,qCAAqC;aACrC,YAAY,EAAE,CAAG;IACjB,2CAA2C;aAC3C,SAAS,EAAE,GAAG;IACd,mDAAmD;aACnD,KAAK,EAAE,GAAG;CACF,CAAC;AAeX,wBAAgB,cAAc,CAAC,KAAK,EAAE,YAAY,GAAG;IACnD,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CAYA;AAED,eAAO,MAAM,MAAM,EAAE,YAAY,EAqQhC,CAAC;AAEF,eAAO,MAAM,aAAa,kBAAkB,CAAC;AAE7C;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAChC,+DAA+D;IAC/D,YAAY,EAAE,MAAM,CAAC;IACrB,+EAA+E;IAC/E,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,0EAA0E;IAC1E,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAK7E;AAiBD,6CAA6C;AAC7C,eAAO,MAAM,iBAAiB,EAAE,gBAA+D,CAAC;AAEhG,gDAAgD;AAChD,wBAAgB,SAAS,CAAC,SAAS,EAAE,gBAAgB,EAAE,EAAE,EAAE,MAAM,GAAG,YAAY,CAQ/E;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,gBAAgB,EAC3B,IAAI,EAAE,YAAY,CAAC,MAAM,CAAC,EAC1B,EAAE,GAAE,IAAiB;AACrB;;;;;GAKG;AACH,QAAQ,CAAC,EAAE,MAAM,GAChB,YAAY,CAcd;AAED,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,CAEjD;AAED,wBAAgB,UAAU,IAAI,YAAY,EAAE,CAE3C;AAsBD,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,YAAY,EACnB,EAAE,GAAE,IAAiB,GACpB;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,OAAO,CAAA;CAAE,CAuBxE;AAED,mEAAmE;AACnE,wBAAgB,cAAc,CAAC,IAAI,EAAE,YAAY,CAAC,MAAM,CAAC,GAAG,YAAY,CAKvE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAU5E"}
1
+ {"version":3,"file":"pricing.d.ts","sourceRoot":"","sources":["../src/pricing.ts"],"names":[],"mappings":"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"}
package/dist/pricing.js CHANGED
@@ -6,7 +6,54 @@
6
6
  * Amazon Bedrock and Vertex AI pricing is set by each partner and is NOT the
7
7
  * pricing in this table.
8
8
  */
9
- export const PRICING_LAST_REVIEWED = '2026-06-24';
9
+ /**
10
+ * When each provider's prices were last checked against that provider's own
11
+ * published table.
12
+ *
13
+ * One date for the whole catalogue was the wrong shape, and it cost a real
14
+ * error. Seven providers publish independently and are checked independently;
15
+ * a single date forces a partial review to either overstate itself by moving
16
+ * the date for everyone, or throw itself away by leaving it. What happened is
17
+ * the second: this constant was written on 2026-08-04 already reading
18
+ * 2026-06-24, and never moved again.
19
+ *
20
+ * Meanwhile Anthropic cancelled the increase that was going to take Sonnet 5
21
+ * from its 2/10 introductory price to 3/15 on 2026-09-01, and made 2/10
22
+ * standard. The catalogue still carried the promotion with its end date, so on
23
+ * 2026-09-01 every Sonnet 5 figure this tool printed would have risen 50% with
24
+ * no code change and no way for a reader to tell. A price nobody charges is
25
+ * the one error this product cannot make, and it was four days out on a timer.
26
+ *
27
+ * So: a date per provider, checked against that provider's own page, and the
28
+ * catalogue's headline date derived as the oldest of them rather than typed.
29
+ */
30
+ export const PROVIDER_REVIEWED = Object.freeze({
31
+ /* platform.claude.com/docs/en/about-claude/pricing, read 2026-08-27. */
32
+ anthropic: '2026-08-27',
33
+ openai: '2026-06-24',
34
+ /* ai.google.dev/gemini-api/docs/pricing, read 2026-08-28. */
35
+ google: '2026-08-28',
36
+ moonshot: '2026-06-24',
37
+ /* api-docs.deepseek.com/quick_start/pricing, read 2026-08-28. */
38
+ deepseek: '2026-08-28',
39
+ xai: '2026-06-24',
40
+ /*
41
+ * `mistral.ai/pricing`, read 2026-08-28 — by a human, because the page
42
+ * renders its table in the browser and serves a fetch only an FAQ example
43
+ * with no model id attached.
44
+ *
45
+ * This date stayed at 2026-06-24 for a few hours after the other two moved,
46
+ * which is the point of having one per provider: looking and finding nothing
47
+ * servable is not the same as checking, and moving the date then would have
48
+ * said it was.
49
+ */
50
+ mistral: '2026-08-28',
51
+ });
52
+ /**
53
+ * The catalogue's age is its oldest provider's, because a reader asking how
54
+ * old this table is wants the answer for the worst part of it, not the best.
55
+ */
56
+ export const PRICING_LAST_REVIEWED = Object.values(PROVIDER_REVIEWED).reduce((oldest, date) => (date < oldest ? date : oldest));
10
57
  /** Cost multipliers relative to the input price. */
11
58
  /**
12
59
  * The defaults, which are Anthropic's numbers.
@@ -125,14 +172,14 @@ export const MODELS = [
125
172
  id: 'claude-sonnet-5',
126
173
  provider: 'anthropic',
127
174
  displayName: 'Claude Sonnet 5',
128
- inputPerMTok: 3,
129
- outputPerMTok: 15,
175
+ inputPerMTok: 2,
176
+ outputPerMTok: 10,
130
177
  contextWindow: 1_000_000,
131
178
  cacheMinTokens: 1024,
132
179
  capability: 'mid',
133
180
  tier: 'sonnet',
134
- promo: { inputPerMTok: 2, outputPerMTok: 10, until: '2026-08-31' },
135
- notes: 'Introductory pricing of 2/10 until 2026-08-31; 3/15 afterwards.',
181
+ notes: 'Launched with 2/10 as introductory pricing through 2026-08-31; the scheduled'
182
+ + ' increase to 3/15 on 2026-09-01 was cancelled and 2/10 is the standard price.',
136
183
  },
137
184
  {
138
185
  id: 'claude-sonnet-4-6',
@@ -222,6 +269,11 @@ export const MODELS = [
222
269
  cacheMinTokens: 2048,
223
270
  caching: 'explicit',
224
271
  multipliers: { cacheRead: 0.25, cacheWrite5m: 1, cacheWrite1h: 1, batch: 0.5 },
272
+ /* Refused on 2026-08-28: still in `GET /v1beta/models`, and a 404 to a request. */
273
+ retired: {
274
+ on: '2026-08-28',
275
+ because: 'This model models/gemini-2.5-pro is no longer available to new users. Please update your code to use models/gemini-3.1-pro-preview for the latest features and improvements. We recommend you to use the Interactions API.',
276
+ },
225
277
  capability: 'large',
226
278
  tier: 'opus',
227
279
  notes: 'Context caching is billed for storage per hour as well as per read.',
@@ -236,9 +288,46 @@ export const MODELS = [
236
288
  cacheMinTokens: 1024,
237
289
  caching: 'explicit',
238
290
  multipliers: { cacheRead: 0.25, cacheWrite5m: 1, cacheWrite1h: 1, batch: 0.5 },
291
+ /* Refused on 2026-08-28: still in `GET /v1beta/models`, and a 404 to a request. */
292
+ retired: {
293
+ on: '2026-08-28',
294
+ because: 'This model models/gemini-2.5-flash is no longer available to new users. Please update your code to use models/gemini-3.6-flash for the latest features and improvements. We recommend you to use the Interactions API.',
295
+ },
239
296
  capability: 'mid',
240
297
  tier: 'sonnet',
241
298
  },
299
+ {
300
+ /*
301
+ * Google's own error message names this as the replacement for the two
302
+ * retired 2.5 models, and ai.google.dev/gemini-api/docs/pricing carries the
303
+ * rates, read 2026-08-28.
304
+ *
305
+ * The introductory price has a published end date and a published successor,
306
+ * which is exactly what `promo` is for and is the shape the catalogue nearly
307
+ * shipped Sonnet 5 wrong on: on 2027-01-01 every figure here doubles with no
308
+ * code change, and `pricing-review.test.js` fails before that arrives.
309
+ */
310
+ id: 'gemini-3.6-flash',
311
+ provider: 'google',
312
+ displayName: 'Gemini 3.6 Flash',
313
+ inputPerMTok: 1.5,
314
+ outputPerMTok: 7.5,
315
+ promo: { inputPerMTok: 0.75, outputPerMTok: 3.75, until: '2026-12-31' },
316
+ contextWindow: 1_048_576,
317
+ cacheMinTokens: 1024,
318
+ caching: 'explicit',
319
+ /*
320
+ * $0.075 against $0.75 inside the introductory window, and $0.15 against
321
+ * $1.50 after it: Google scales the cache price with the headline one, so
322
+ * a single ratio is right on both sides of 2027-01-01 and the multiplier
323
+ * does not need a date of its own.
324
+ */
325
+ multipliers: { cacheRead: 0.1, cacheWrite5m: 1, cacheWrite1h: 1, batch: 0.5 },
326
+ capability: 'mid',
327
+ tier: 'sonnet',
328
+ notes: 'Context caching is billed for storage per hour as well as per read:'
329
+ + ' $0.50 per 1M tokens per hour through 2026-12-31, $1.00 after.',
330
+ },
242
331
  // ------------------------------------------------------------------------
243
332
  // Moonshot
244
333
  // ------------------------------------------------------------------------
@@ -269,9 +358,69 @@ export const MODELS = [
269
358
  cacheMinTokens: 1024,
270
359
  caching: 'automatic',
271
360
  multipliers: { cacheRead: 0.1, cacheWrite5m: 1, cacheWrite1h: 1, batch: null },
361
+ /* Refused on 2026-08-28: the id itself is rejected, and the error names the survivors. */
362
+ retired: {
363
+ on: '2026-08-28',
364
+ because: 'The supported API model names are deepseek-v4-pro, deepseek-v4-flash, and deepseek-v4-flash-vision-exp, but you passed deepseek-v3.',
365
+ },
272
366
  capability: 'mid',
273
367
  tier: 'sonnet',
274
368
  },
369
+ {
370
+ /*
371
+ * DeepSeek V4 Flash, from api-docs.deepseek.com/quick_start/pricing, read
372
+ * 2026-08-28. It replaced `deepseek-v3`, which the API now refuses.
373
+ *
374
+ * The base rate is off-peak because off-peak is the common case: peak is
375
+ * 01:00-04:00 and 06:00-10:00 UTC on weekdays, which is 35 hours of 168.
376
+ * The peak tier is exactly double, published as such.
377
+ */
378
+ id: 'deepseek-v4-flash',
379
+ provider: 'deepseek',
380
+ displayName: 'DeepSeek V4 Flash',
381
+ inputPerMTok: 0.22,
382
+ outputPerMTok: 0.66,
383
+ tiers: [
384
+ {
385
+ id: 'peak',
386
+ when: { kind: 'utc-hours', hours: [1, 2, 3, 6, 7, 8, 9], weekdaysOnly: true },
387
+ inputPerMTok: 0.44,
388
+ outputPerMTok: 1.32,
389
+ },
390
+ ],
391
+ contextWindow: 1_000_000,
392
+ cacheMinTokens: 1024,
393
+ caching: 'automatic',
394
+ /* $0.007 cache hit against $0.22 input off-peak. */
395
+ multipliers: { cacheRead: 0.0318, cacheWrite5m: 1, cacheWrite1h: 1, batch: null },
396
+ capability: 'mid',
397
+ tier: 'sonnet',
398
+ notes: 'Maximum output 384K tokens.',
399
+ },
400
+ {
401
+ /* Same page, same clock. Pro is three times Flash at both ends. */
402
+ id: 'deepseek-v4-pro',
403
+ provider: 'deepseek',
404
+ displayName: 'DeepSeek V4 Pro',
405
+ inputPerMTok: 0.66,
406
+ outputPerMTok: 1.98,
407
+ tiers: [
408
+ {
409
+ id: 'peak',
410
+ when: { kind: 'utc-hours', hours: [1, 2, 3, 6, 7, 8, 9], weekdaysOnly: true },
411
+ inputPerMTok: 1.32,
412
+ outputPerMTok: 3.96,
413
+ },
414
+ ],
415
+ contextWindow: 1_000_000,
416
+ cacheMinTokens: 1024,
417
+ caching: 'automatic',
418
+ /* $0.022 cache hit against $0.66 input off-peak. */
419
+ multipliers: { cacheRead: 0.0333, cacheWrite5m: 1, cacheWrite1h: 1, batch: null },
420
+ capability: 'large',
421
+ tier: 'opus',
422
+ notes: 'Maximum output 384K tokens.',
423
+ },
275
424
  // ------------------------------------------------------------------------
276
425
  // xAI
277
426
  // ------------------------------------------------------------------------
@@ -301,10 +450,88 @@ export const MODELS = [
301
450
  cacheMinTokens: 0,
302
451
  caching: 'none',
303
452
  multipliers: { batch: 0.5 },
453
+ /* Refused on 2026-08-28: the id itself is rejected. */
454
+ retired: {
455
+ on: '2026-08-28',
456
+ because: 'Invalid model: mistral-large-2',
457
+ },
458
+ capability: 'large',
459
+ tier: 'opus',
460
+ notes: 'No prompt caching: reordering still helps readability but saves nothing here.',
461
+ },
462
+ {
463
+ /*
464
+ * Mistral Large 3, the replacement for the refused `mistral-large-2`.
465
+ *
466
+ * **Read by a human from `mistral.ai/pricing` on 2026-08-28, and that is the
467
+ * provenance.** The page renders its table in the browser, so a fetch sees
468
+ * only an FAQ example with no model id attached: this repository looked,
469
+ * found nothing servable, left the model unpriced and said so, and the table
470
+ * was then read off the page and handed over. A price is a reading of the
471
+ * provider's page; who did the reading is a fact about the reading, not a
472
+ * reason to accept a worse source.
473
+ *
474
+ * The identity is measured rather than inferred. `GET /v1/models` declares
475
+ * `mistral-large-latest` and `mistral-large-2512` as aliases of each other,
476
+ * both reporting the name `mistral-large-2512` and a 262,144 context, so the
477
+ * dated id is what a usage log carries and the moving pointer is the alias.
478
+ */
479
+ id: 'mistral-large-2512',
480
+ aliases: ['mistral-large-latest'],
481
+ provider: 'mistral',
482
+ displayName: 'Mistral Large 3',
483
+ inputPerMTok: 0.5,
484
+ outputPerMTok: 1.5,
485
+ contextWindow: 262_144,
486
+ cacheMinTokens: 0,
487
+ caching: 'none',
488
+ multipliers: { batch: 0.5 },
304
489
  capability: 'large',
305
490
  tier: 'opus',
306
491
  notes: 'No prompt caching: reordering still helps readability but saves nothing here.',
307
492
  },
493
+ {
494
+ /*
495
+ * Same page, same day, same reading.
496
+ *
497
+ * **This provider's ladder is not monotonic in price, and that is not a
498
+ * typo.** Medium 3.5 costs three times Large 3 on input and five times on
499
+ * output, because Mistral positions Large as the open-weight flagship and
500
+ * Medium as the model for long-horizon agentic work. `capability` follows
501
+ * the vendor's own description rather than the price, which is what the
502
+ * field means; the consequence is that a step down from `large` to `mid`
503
+ * here is dearer, and `levers.ts` simply does not offer a route whose saving
504
+ * is not above zero. Nothing to fix, and worth saying so before somebody
505
+ * "corrects" the ladder into producing a saving that does not exist.
506
+ */
507
+ id: 'mistral-medium-2604',
508
+ aliases: ['mistral-medium-latest', 'mistral-medium-3-5'],
509
+ provider: 'mistral',
510
+ displayName: 'Mistral Medium 3.5',
511
+ inputPerMTok: 1.5,
512
+ outputPerMTok: 7.5,
513
+ contextWindow: 262_144,
514
+ cacheMinTokens: 0,
515
+ caching: 'none',
516
+ multipliers: { batch: 0.5 },
517
+ capability: 'mid',
518
+ tier: 'sonnet',
519
+ },
520
+ {
521
+ /* Same page, same day. `mistral-small-2603` is what the API reports. */
522
+ id: 'mistral-small-2603',
523
+ aliases: ['mistral-small-latest'],
524
+ provider: 'mistral',
525
+ displayName: 'Mistral Small 4',
526
+ inputPerMTok: 0.15,
527
+ outputPerMTok: 0.6,
528
+ contextWindow: 262_144,
529
+ cacheMinTokens: 0,
530
+ caching: 'none',
531
+ multipliers: { batch: 0.5 },
532
+ capability: 'small',
533
+ tier: 'haiku',
534
+ },
308
535
  ];
309
536
  export const DEFAULT_MODEL = 'claude-opus-5';
310
537
  /**
@@ -336,6 +563,27 @@ function makeCatalogue(models, lastReviewed, overriddenModels = [], addedModels
336
563
  }
337
564
  /** The prices compiled into this release. */
338
565
  export const BUNDLED_CATALOGUE = makeCatalogue(MODELS, PRICING_LAST_REVIEWED);
566
+ /**
567
+ * Whether this model may be offered to a reader as something to move to.
568
+ *
569
+ * One home for a two-part rule that used to be written in four places as one
570
+ * part. `recommendable: false` — a private programme, a waitlist — was honoured
571
+ * at every one of them. `retired` is the stronger statement: the provider
572
+ * answered a real request for the id with an error, so a switch to it is not a
573
+ * worse recommendation, it is one that cannot be carried out at all.
574
+ *
575
+ * Written as a function rather than as a second `&&` at four call sites,
576
+ * because the fifth call site is always the one that gets written with only the
577
+ * first half. `pricing.test.js` fails a source file that filters on
578
+ * `recommendable` without coming through here, and that guard was proved by
579
+ * planting exactly that filter.
580
+ *
581
+ * Pricing is a separate question and stays answered: a retired model keeps its
582
+ * price, because a log full of its calls records money that was really spent.
583
+ */
584
+ export function isOffered(model) {
585
+ return model.recommendable !== false && model.retired === undefined;
586
+ }
339
587
  /** Looks a model up in a specific catalogue. */
340
588
  export function modelFrom(catalogue, id) {
341
589
  const model = catalogue.byId.get(id);
@@ -359,7 +607,7 @@ export function cheapestOfTierIn(catalogue, tier, on = new Date(),
359
607
  */
360
608
  provider) {
361
609
  const candidates = catalogue.models.filter((m) => m.tier === tier &&
362
- m.recommendable !== false &&
610
+ isOffered(m) &&
363
611
  (provider === undefined || m.provider === provider));
364
612
  const first = candidates[0];
365
613
  if (!first)
@@ -372,47 +620,143 @@ export function getModel(id) {
372
620
  export function listModels() {
373
621
  return [...MODELS];
374
622
  }
375
- /** Effective price on a given date, applying any live promotion. */
623
+ const pricingMemo = new WeakMap();
624
+ /** The two unconditional answers for one model, parsed once. */
625
+ function cache(model) {
626
+ const entry = {
627
+ untilMs: model.promo ? new Date(`${model.promo.until}T23:59:59.999Z`).getTime() : null,
628
+ promo: model.promo
629
+ ? Object.freeze({
630
+ inputPerMTok: model.promo.inputPerMTok,
631
+ outputPerMTok: model.promo.outputPerMTok,
632
+ promoApplied: true,
633
+ tier: null,
634
+ })
635
+ : null,
636
+ base: Object.freeze({
637
+ inputPerMTok: model.inputPerMTok,
638
+ outputPerMTok: model.outputPerMTok,
639
+ promoApplied: false,
640
+ tier: null,
641
+ }),
642
+ };
643
+ pricingMemo.set(model, entry);
644
+ return entry;
645
+ }
376
646
  /**
377
- * Memoised per model, because this runs once per record when a usage log is
378
- * priced: a 200k-line profile called it 200k times, and the `new Date(...)`
379
- * parse of the promo's end date alone was 0.7 of the 2.4 seconds the whole
380
- * profile took — measured with --cpu-prof, not guessed. The cache keys on
381
- * the model object (a WeakMap, so an overlay's models are collected with
382
- * the overlay) and stores the parsed end-of-promo instant plus the two
383
- * possible answers; `on` still decides which answer applies on every call,
384
- * so behaviour is unchanged to the millisecond.
647
+ * Whether one condition holds, for a call at `on` of `inputTokens` tokens.
648
+ *
649
+ * Returns `null` rather than `false` when the condition cannot be decided,
650
+ * because "this does not apply" and "nobody said" lead to different reports and
651
+ * collapsing them is the oldest mistake in this repository.
385
652
  */
386
- const pricingMemo = new WeakMap();
387
- export function effectivePricing(model, on = new Date()) {
388
- let memo = pricingMemo.get(model);
389
- if (memo === undefined) {
390
- memo = {
391
- untilMs: model.promo ? new Date(`${model.promo.until}T23:59:59.999Z`).getTime() : null,
392
- promo: model.promo
393
- ? {
394
- inputPerMTok: model.promo.inputPerMTok,
395
- outputPerMTok: model.promo.outputPerMTok,
396
- promoApplied: true,
397
- }
398
- : null,
399
- base: Object.freeze({
653
+ function holds(when, on, inputTokens) {
654
+ if (when.kind === 'utc-hours') {
655
+ const day = on.getUTCDay();
656
+ if (when.weekdaysOnly && (day === 0 || day === 6))
657
+ return false;
658
+ return when.hours.includes(on.getUTCHours());
659
+ }
660
+ if (inputTokens === undefined)
661
+ return null;
662
+ return inputTokens > when.tokens;
663
+ }
664
+ /**
665
+ * What one call is billed at, and what decided it.
666
+ *
667
+ * `tier` is the field this grew for, and it carries three states rather than
668
+ * two. A model with no tiers gets `null`. A model whose condition could be
669
+ * evaluated gets the tier that applied, or the base rate with `applied: null`
670
+ * and `decided: true`. A model whose condition could **not** be evaluated —
671
+ * a size tier priced without a token count — gets `decided: false`, the
672
+ * **dearer** of the candidate rates, and `because` naming what would settle it.
673
+ *
674
+ * The dearer one is not a shrug. A cost figure that is a ceiling can prove
675
+ * "under budget" and can never surprise a bill; a floor chosen for looking
676
+ * better is the flattering direction this product spends its time refusing.
677
+ * The reader still has to be told, which is what `decided: false` is for, and
678
+ * `pricing.test.js` fails a tiered model whose report drops it.
679
+ */
680
+ export function effectivePricing(model, on = new Date(), context = {}) {
681
+ const memo = pricingMemo.get(model) ?? cache(model);
682
+ /**
683
+ * Tiers first, and a promotion never runs on top of one.
684
+ *
685
+ * No provider here does both, and combining them would be this repository
686
+ * inventing a price out of two real ones — the composed figure the first rule
687
+ * in the doctrine is about. A model that grew both would fail the guard in
688
+ * `pricing.test.js` rather than quietly getting a multiplied rate.
689
+ */
690
+ if (model.tiers !== undefined && model.tiers.length > 0) {
691
+ const undecided = [];
692
+ for (const tier of model.tiers) {
693
+ const verdict = holds(tier.when, on, context.inputTokens);
694
+ if (verdict === true) {
695
+ return {
696
+ inputPerMTok: tier.inputPerMTok,
697
+ outputPerMTok: tier.outputPerMTok,
698
+ promoApplied: false,
699
+ tier: { applied: tier.id, decided: true, because: describe(tier.when) },
700
+ };
701
+ }
702
+ if (verdict === null)
703
+ undecided.push(tier);
704
+ }
705
+ if (undecided.length === 0) {
706
+ return {
707
+ ...memo.base,
708
+ tier: { applied: null, decided: true, because: 'no conditional rate applies' },
709
+ };
710
+ }
711
+ /* The dearest candidate, so the figure is a ceiling rather than a wish. */
712
+ const dearest = undecided.reduce((worst, tier) => tier.inputPerMTok > worst.inputPerMTok ? tier : worst);
713
+ const ceiling = dearest.inputPerMTok > model.inputPerMTok
714
+ ? {
715
+ inputPerMTok: dearest.inputPerMTok,
716
+ outputPerMTok: dearest.outputPerMTok,
717
+ applied: dearest.id,
718
+ }
719
+ : {
400
720
  inputPerMTok: model.inputPerMTok,
401
721
  outputPerMTok: model.outputPerMTok,
402
- promoApplied: false,
403
- }),
722
+ applied: null,
723
+ };
724
+ return {
725
+ inputPerMTok: ceiling.inputPerMTok,
726
+ outputPerMTok: ceiling.outputPerMTok,
727
+ promoApplied: false,
728
+ tier: {
729
+ applied: ceiling.applied,
730
+ decided: false,
731
+ because: `${describe(dearest.when)}, and the token count was not given`,
732
+ },
404
733
  };
405
- if (memo.promo)
406
- Object.freeze(memo.promo);
407
- pricingMemo.set(model, memo);
408
734
  }
409
735
  if (memo.untilMs !== null && on.getTime() <= memo.untilMs)
410
736
  return memo.promo;
411
737
  return memo.base;
412
738
  }
739
+ /** One condition as a sentence a reader can check against a provider's page. */
740
+ function describe(when) {
741
+ if (when.kind === 'input-tokens-above') {
742
+ return `prompts over ${when.tokens.toLocaleString('en-US')} input tokens`;
743
+ }
744
+ const windows = [];
745
+ let start = null;
746
+ const sorted = [...when.hours].sort((a, b) => a - b);
747
+ for (let i = 0; i < sorted.length; i += 1) {
748
+ if (start === null)
749
+ start = sorted[i];
750
+ if (sorted[i] + 1 !== sorted[i + 1]) {
751
+ windows.push(`${String(start).padStart(2, '0')}:00-${String(sorted[i] + 1).padStart(2, '0')}:00`);
752
+ start = null;
753
+ }
754
+ }
755
+ return `${windows.join(' and ')} UTC${when.weekdaysOnly ? ', Monday to Friday' : ''}`;
756
+ }
413
757
  /** Cheapest model of each capability tier, for recommendations. */
414
758
  export function cheapestOfTier(tier) {
415
- const candidates = MODELS.filter((m) => m.tier === tier && m.recommendable !== false);
759
+ const candidates = MODELS.filter((m) => m.tier === tier && isOffered(m));
416
760
  const first = candidates[0];
417
761
  if (!first)
418
762
  throw new Error(`No models for tier "${tier}"`);
@@ -434,6 +778,20 @@ export function cheapestOfTier(tier) {
434
778
  * a date — an overlay supplies this string, and a wrong one should read as
435
779
  * unknown rather than as a confident number computed from `NaN`.
436
780
  */
781
+ /**
782
+ * The age at which this product stops treating its own price table as current.
783
+ *
784
+ * Three surfaces warn a reader that the figures above may be off — `profile`,
785
+ * the MCP report, the browser's bill — and each of them had typed `45` into
786
+ * its own comparison, with four locale sentences stating it again in prose.
787
+ * Seven copies of one claim, and nothing that would notice when they
788
+ * disagreed; the sentence a reader sees names the number, so a drift between
789
+ * them would have been a surface telling the reader one threshold and applying
790
+ * another.
791
+ *
792
+ * The value is what those surfaces already published, not a new judgement.
793
+ */
794
+ export const STALE_PRICING_DAYS = 45;
437
795
  export function reviewAgeDays(lastReviewed, now) {
438
796
  if (!/^\d{4}-\d{2}-\d{2}$/.test(lastReviewed))
439
797
  return null;