@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.
- package/README.md +2 -2
- package/dist/advisories.d.ts.map +1 -1
- package/dist/advisories.js +27 -4
- package/dist/advisories.js.map +1 -1
- package/dist/band.d.ts.map +1 -1
- package/dist/band.js +12 -0
- package/dist/band.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +7 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +8 -0
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/types.d.ts +17 -0
- package/dist/i18n/types.d.ts.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/langsmith.d.ts +104 -0
- package/dist/langsmith.d.ts.map +1 -0
- package/dist/langsmith.js +270 -0
- package/dist/langsmith.js.map +1 -0
- package/dist/levers.d.ts +1 -1
- package/dist/levers.js +2 -2
- package/dist/levers.js.map +1 -1
- package/dist/optimize.d.ts +2 -2
- package/dist/optimize.d.ts.map +1 -1
- package/dist/optimize.js +15 -1
- package/dist/optimize.js.map +1 -1
- package/dist/pricing.d.ts +98 -3
- package/dist/pricing.d.ts.map +1 -1
- package/dist/pricing.js +393 -35
- package/dist/pricing.js.map +1 -1
- package/dist/receipt.d.ts +253 -0
- package/dist/receipt.d.ts.map +1 -0
- package/dist/receipt.js +111 -0
- package/dist/receipt.js.map +1 -0
- package/dist/tokenizer.d.ts +4 -2
- package/dist/tokenizer.d.ts.map +1 -1
- package/dist/tokenizer.js +4 -2
- package/dist/tokenizer.js.map +1 -1
- package/dist/types.d.ts +148 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/advisories.ts +34 -4
- package/src/band.ts +12 -0
- package/src/i18n/en.ts +9 -0
- package/src/i18n/es.ts +10 -0
- package/src/i18n/types.ts +18 -0
- package/src/index.ts +14 -1
- package/src/langsmith.ts +315 -0
- package/src/levers.ts +3 -3
- package/src/optimize.ts +16 -0
- package/src/pricing.ts +430 -30
- package/src/receipt.ts +351 -0
- package/src/tokenizer.ts +4 -2
- 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
|
-
|
|
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
|
-
|
|
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
|
package/dist/pricing.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pricing.d.ts","sourceRoot":"","sources":["../src/pricing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,
|
|
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
|
-
|
|
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:
|
|
129
|
-
outputPerMTok:
|
|
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
|
-
|
|
135
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
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
|
|
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;
|