@trazum/core 1.81.0 → 1.83.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 +34 -18
- package/dist/advisories.d.ts +33 -1
- package/dist/advisories.d.ts.map +1 -1
- package/dist/advisories.js +130 -20
- package/dist/advisories.js.map +1 -1
- package/dist/band.d.ts +131 -0
- package/dist/band.d.ts.map +1 -0
- package/dist/band.js +210 -0
- package/dist/band.js.map +1 -0
- package/dist/conform.d.ts +1 -1
- package/dist/conform.d.ts.map +1 -1
- package/dist/conform.js +84 -1
- package/dist/conform.js.map +1 -1
- package/dist/helicone.d.ts +92 -0
- package/dist/helicone.d.ts.map +1 -0
- package/dist/helicone.js +194 -0
- package/dist/helicone.js.map +1 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +16 -3
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +17 -3
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/types.d.ts +60 -1
- package/dist/i18n/types.d.ts.map +1 -1
- package/dist/index.d.ts +16 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -3
- 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/litellm.d.ts +98 -0
- package/dist/litellm.d.ts.map +1 -0
- package/dist/litellm.js +207 -0
- package/dist/litellm.js.map +1 -0
- package/dist/measured-profile.d.ts +4 -0
- package/dist/measured-profile.d.ts.map +1 -1
- package/dist/measured-profile.js +22 -1
- package/dist/measured-profile.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/schemas.d.ts.map +1 -1
- package/dist/schemas.js +36 -0
- package/dist/schemas.js.map +1 -1
- package/dist/tokenizer.d.ts +45 -49
- package/dist/tokenizer.d.ts.map +1 -1
- package/dist/tokenizer.js +100 -51
- package/dist/tokenizer.js.map +1 -1
- package/dist/types.d.ts +108 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/verdict-bridge.d.ts +113 -0
- package/dist/verdict-bridge.d.ts.map +1 -0
- package/dist/verdict-bridge.js +127 -0
- package/dist/verdict-bridge.js.map +1 -0
- package/package.json +1 -1
- package/src/advisories.ts +166 -19
- package/src/band.ts +213 -0
- package/src/conform.ts +100 -1
- package/src/helicone.ts +227 -0
- package/src/i18n/en.ts +19 -3
- package/src/i18n/es.ts +20 -3
- package/src/i18n/types.ts +63 -1
- package/src/index.ts +31 -2
- package/src/langsmith.ts +315 -0
- package/src/levers.ts +3 -3
- package/src/litellm.ts +243 -0
- package/src/measured-profile.ts +26 -2
- package/src/pricing.ts +430 -30
- package/src/receipt.ts +351 -0
- package/src/schemas.ts +36 -0
- package/src/tokenizer.ts +101 -51
- package/src/types.ts +104 -0
- package/src/verdict-bridge.ts +200 -0
package/src/pricing.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ModelPricing } from './types.js';
|
|
1
|
+
import type { ModelPricing, PricingTier, TierCondition } from './types.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Model and pricing catalogue (USD per million tokens).
|
|
@@ -8,7 +8,57 @@ import type { ModelPricing } from './types.js';
|
|
|
8
8
|
* Amazon Bedrock and Vertex AI pricing is set by each partner and is NOT the
|
|
9
9
|
* pricing in this table.
|
|
10
10
|
*/
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* When each provider's prices were last checked against that provider's own
|
|
13
|
+
* published table.
|
|
14
|
+
*
|
|
15
|
+
* One date for the whole catalogue was the wrong shape, and it cost a real
|
|
16
|
+
* error. Seven providers publish independently and are checked independently;
|
|
17
|
+
* a single date forces a partial review to either overstate itself by moving
|
|
18
|
+
* the date for everyone, or throw itself away by leaving it. What happened is
|
|
19
|
+
* the second: this constant was written on 2026-08-04 already reading
|
|
20
|
+
* 2026-06-24, and never moved again.
|
|
21
|
+
*
|
|
22
|
+
* Meanwhile Anthropic cancelled the increase that was going to take Sonnet 5
|
|
23
|
+
* from its 2/10 introductory price to 3/15 on 2026-09-01, and made 2/10
|
|
24
|
+
* standard. The catalogue still carried the promotion with its end date, so on
|
|
25
|
+
* 2026-09-01 every Sonnet 5 figure this tool printed would have risen 50% with
|
|
26
|
+
* no code change and no way for a reader to tell. A price nobody charges is
|
|
27
|
+
* the one error this product cannot make, and it was four days out on a timer.
|
|
28
|
+
*
|
|
29
|
+
* So: a date per provider, checked against that provider's own page, and the
|
|
30
|
+
* catalogue's headline date derived as the oldest of them rather than typed.
|
|
31
|
+
*/
|
|
32
|
+
export const PROVIDER_REVIEWED: Readonly<Record<string, string>> = Object.freeze({
|
|
33
|
+
/* platform.claude.com/docs/en/about-claude/pricing, read 2026-08-27. */
|
|
34
|
+
anthropic: '2026-08-27',
|
|
35
|
+
openai: '2026-06-24',
|
|
36
|
+
/* ai.google.dev/gemini-api/docs/pricing, read 2026-08-28. */
|
|
37
|
+
google: '2026-08-28',
|
|
38
|
+
moonshot: '2026-06-24',
|
|
39
|
+
/* api-docs.deepseek.com/quick_start/pricing, read 2026-08-28. */
|
|
40
|
+
deepseek: '2026-08-28',
|
|
41
|
+
xai: '2026-06-24',
|
|
42
|
+
/*
|
|
43
|
+
* `mistral.ai/pricing`, read 2026-08-28 — by a human, because the page
|
|
44
|
+
* renders its table in the browser and serves a fetch only an FAQ example
|
|
45
|
+
* with no model id attached.
|
|
46
|
+
*
|
|
47
|
+
* This date stayed at 2026-06-24 for a few hours after the other two moved,
|
|
48
|
+
* which is the point of having one per provider: looking and finding nothing
|
|
49
|
+
* servable is not the same as checking, and moving the date then would have
|
|
50
|
+
* said it was.
|
|
51
|
+
*/
|
|
52
|
+
mistral: '2026-08-28',
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The catalogue's age is its oldest provider's, because a reader asking how
|
|
57
|
+
* old this table is wants the answer for the worst part of it, not the best.
|
|
58
|
+
*/
|
|
59
|
+
export const PRICING_LAST_REVIEWED: string = Object.values(PROVIDER_REVIEWED).reduce(
|
|
60
|
+
(oldest, date) => (date < oldest ? date : oldest),
|
|
61
|
+
);
|
|
12
62
|
|
|
13
63
|
/** Cost multipliers relative to the input price. */
|
|
14
64
|
/**
|
|
@@ -138,14 +188,15 @@ export const MODELS: ModelPricing[] = [
|
|
|
138
188
|
id: 'claude-sonnet-5',
|
|
139
189
|
provider: 'anthropic',
|
|
140
190
|
displayName: 'Claude Sonnet 5',
|
|
141
|
-
inputPerMTok:
|
|
142
|
-
outputPerMTok:
|
|
191
|
+
inputPerMTok: 2,
|
|
192
|
+
outputPerMTok: 10,
|
|
143
193
|
contextWindow: 1_000_000,
|
|
144
194
|
cacheMinTokens: 1024,
|
|
145
195
|
capability: 'mid',
|
|
146
196
|
tier: 'sonnet',
|
|
147
|
-
|
|
148
|
-
|
|
197
|
+
notes:
|
|
198
|
+
'Launched with 2/10 as introductory pricing through 2026-08-31; the scheduled'
|
|
199
|
+
+ ' increase to 3/15 on 2026-09-01 was cancelled and 2/10 is the standard price.',
|
|
149
200
|
},
|
|
150
201
|
{
|
|
151
202
|
id: 'claude-sonnet-4-6',
|
|
@@ -237,6 +288,12 @@ export const MODELS: ModelPricing[] = [
|
|
|
237
288
|
cacheMinTokens: 2048,
|
|
238
289
|
caching: 'explicit',
|
|
239
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:
|
|
295
|
+
'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.',
|
|
296
|
+
},
|
|
240
297
|
capability: 'large',
|
|
241
298
|
tier: 'opus',
|
|
242
299
|
notes: 'Context caching is billed for storage per hour as well as per read.',
|
|
@@ -251,10 +308,50 @@ export const MODELS: ModelPricing[] = [
|
|
|
251
308
|
cacheMinTokens: 1024,
|
|
252
309
|
caching: 'explicit',
|
|
253
310
|
multipliers: { cacheRead: 0.25, cacheWrite5m: 1, cacheWrite1h: 1, batch: 0.5 },
|
|
311
|
+
/* Refused on 2026-08-28: still in `GET /v1beta/models`, and a 404 to a request. */
|
|
312
|
+
retired: {
|
|
313
|
+
on: '2026-08-28',
|
|
314
|
+
because:
|
|
315
|
+
'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.',
|
|
316
|
+
},
|
|
254
317
|
capability: 'mid',
|
|
255
318
|
tier: 'sonnet',
|
|
256
319
|
},
|
|
257
320
|
|
|
321
|
+
{
|
|
322
|
+
/*
|
|
323
|
+
* Google's own error message names this as the replacement for the two
|
|
324
|
+
* retired 2.5 models, and ai.google.dev/gemini-api/docs/pricing carries the
|
|
325
|
+
* rates, read 2026-08-28.
|
|
326
|
+
*
|
|
327
|
+
* The introductory price has a published end date and a published successor,
|
|
328
|
+
* which is exactly what `promo` is for and is the shape the catalogue nearly
|
|
329
|
+
* shipped Sonnet 5 wrong on: on 2027-01-01 every figure here doubles with no
|
|
330
|
+
* code change, and `pricing-review.test.js` fails before that arrives.
|
|
331
|
+
*/
|
|
332
|
+
id: 'gemini-3.6-flash',
|
|
333
|
+
provider: 'google',
|
|
334
|
+
displayName: 'Gemini 3.6 Flash',
|
|
335
|
+
inputPerMTok: 1.5,
|
|
336
|
+
outputPerMTok: 7.5,
|
|
337
|
+
promo: { inputPerMTok: 0.75, outputPerMTok: 3.75, until: '2026-12-31' },
|
|
338
|
+
contextWindow: 1_048_576,
|
|
339
|
+
cacheMinTokens: 1024,
|
|
340
|
+
caching: 'explicit',
|
|
341
|
+
/*
|
|
342
|
+
* $0.075 against $0.75 inside the introductory window, and $0.15 against
|
|
343
|
+
* $1.50 after it: Google scales the cache price with the headline one, so
|
|
344
|
+
* a single ratio is right on both sides of 2027-01-01 and the multiplier
|
|
345
|
+
* does not need a date of its own.
|
|
346
|
+
*/
|
|
347
|
+
multipliers: { cacheRead: 0.1, cacheWrite5m: 1, cacheWrite1h: 1, batch: 0.5 },
|
|
348
|
+
capability: 'mid',
|
|
349
|
+
tier: 'sonnet',
|
|
350
|
+
notes:
|
|
351
|
+
'Context caching is billed for storage per hour as well as per read:'
|
|
352
|
+
+ ' $0.50 per 1M tokens per hour through 2026-12-31, $1.00 after.',
|
|
353
|
+
},
|
|
354
|
+
|
|
258
355
|
// ------------------------------------------------------------------------
|
|
259
356
|
// Moonshot
|
|
260
357
|
// ------------------------------------------------------------------------
|
|
@@ -286,9 +383,70 @@ export const MODELS: ModelPricing[] = [
|
|
|
286
383
|
cacheMinTokens: 1024,
|
|
287
384
|
caching: 'automatic',
|
|
288
385
|
multipliers: { cacheRead: 0.1, cacheWrite5m: 1, cacheWrite1h: 1, batch: null },
|
|
386
|
+
/* Refused on 2026-08-28: the id itself is rejected, and the error names the survivors. */
|
|
387
|
+
retired: {
|
|
388
|
+
on: '2026-08-28',
|
|
389
|
+
because:
|
|
390
|
+
'The supported API model names are deepseek-v4-pro, deepseek-v4-flash, and deepseek-v4-flash-vision-exp, but you passed deepseek-v3.',
|
|
391
|
+
},
|
|
289
392
|
capability: 'mid',
|
|
290
393
|
tier: 'sonnet',
|
|
291
394
|
},
|
|
395
|
+
{
|
|
396
|
+
/*
|
|
397
|
+
* DeepSeek V4 Flash, from api-docs.deepseek.com/quick_start/pricing, read
|
|
398
|
+
* 2026-08-28. It replaced `deepseek-v3`, which the API now refuses.
|
|
399
|
+
*
|
|
400
|
+
* The base rate is off-peak because off-peak is the common case: peak is
|
|
401
|
+
* 01:00-04:00 and 06:00-10:00 UTC on weekdays, which is 35 hours of 168.
|
|
402
|
+
* The peak tier is exactly double, published as such.
|
|
403
|
+
*/
|
|
404
|
+
id: 'deepseek-v4-flash',
|
|
405
|
+
provider: 'deepseek',
|
|
406
|
+
displayName: 'DeepSeek V4 Flash',
|
|
407
|
+
inputPerMTok: 0.22,
|
|
408
|
+
outputPerMTok: 0.66,
|
|
409
|
+
tiers: [
|
|
410
|
+
{
|
|
411
|
+
id: 'peak',
|
|
412
|
+
when: { kind: 'utc-hours', hours: [1, 2, 3, 6, 7, 8, 9], weekdaysOnly: true },
|
|
413
|
+
inputPerMTok: 0.44,
|
|
414
|
+
outputPerMTok: 1.32,
|
|
415
|
+
},
|
|
416
|
+
],
|
|
417
|
+
contextWindow: 1_000_000,
|
|
418
|
+
cacheMinTokens: 1024,
|
|
419
|
+
caching: 'automatic',
|
|
420
|
+
/* $0.007 cache hit against $0.22 input off-peak. */
|
|
421
|
+
multipliers: { cacheRead: 0.0318, cacheWrite5m: 1, cacheWrite1h: 1, batch: null },
|
|
422
|
+
capability: 'mid',
|
|
423
|
+
tier: 'sonnet',
|
|
424
|
+
notes: 'Maximum output 384K tokens.',
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
/* Same page, same clock. Pro is three times Flash at both ends. */
|
|
428
|
+
id: 'deepseek-v4-pro',
|
|
429
|
+
provider: 'deepseek',
|
|
430
|
+
displayName: 'DeepSeek V4 Pro',
|
|
431
|
+
inputPerMTok: 0.66,
|
|
432
|
+
outputPerMTok: 1.98,
|
|
433
|
+
tiers: [
|
|
434
|
+
{
|
|
435
|
+
id: 'peak',
|
|
436
|
+
when: { kind: 'utc-hours', hours: [1, 2, 3, 6, 7, 8, 9], weekdaysOnly: true },
|
|
437
|
+
inputPerMTok: 1.32,
|
|
438
|
+
outputPerMTok: 3.96,
|
|
439
|
+
},
|
|
440
|
+
],
|
|
441
|
+
contextWindow: 1_000_000,
|
|
442
|
+
cacheMinTokens: 1024,
|
|
443
|
+
caching: 'automatic',
|
|
444
|
+
/* $0.022 cache hit against $0.66 input off-peak. */
|
|
445
|
+
multipliers: { cacheRead: 0.0333, cacheWrite5m: 1, cacheWrite1h: 1, batch: null },
|
|
446
|
+
capability: 'large',
|
|
447
|
+
tier: 'opus',
|
|
448
|
+
notes: 'Maximum output 384K tokens.',
|
|
449
|
+
},
|
|
292
450
|
|
|
293
451
|
// ------------------------------------------------------------------------
|
|
294
452
|
// xAI
|
|
@@ -320,10 +478,89 @@ export const MODELS: ModelPricing[] = [
|
|
|
320
478
|
cacheMinTokens: 0,
|
|
321
479
|
caching: 'none',
|
|
322
480
|
multipliers: { batch: 0.5 },
|
|
481
|
+
/* Refused on 2026-08-28: the id itself is rejected. */
|
|
482
|
+
retired: {
|
|
483
|
+
on: '2026-08-28',
|
|
484
|
+
because:
|
|
485
|
+
'Invalid model: mistral-large-2',
|
|
486
|
+
},
|
|
487
|
+
capability: 'large',
|
|
488
|
+
tier: 'opus',
|
|
489
|
+
notes: 'No prompt caching: reordering still helps readability but saves nothing here.',
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
/*
|
|
493
|
+
* Mistral Large 3, the replacement for the refused `mistral-large-2`.
|
|
494
|
+
*
|
|
495
|
+
* **Read by a human from `mistral.ai/pricing` on 2026-08-28, and that is the
|
|
496
|
+
* provenance.** The page renders its table in the browser, so a fetch sees
|
|
497
|
+
* only an FAQ example with no model id attached: this repository looked,
|
|
498
|
+
* found nothing servable, left the model unpriced and said so, and the table
|
|
499
|
+
* was then read off the page and handed over. A price is a reading of the
|
|
500
|
+
* provider's page; who did the reading is a fact about the reading, not a
|
|
501
|
+
* reason to accept a worse source.
|
|
502
|
+
*
|
|
503
|
+
* The identity is measured rather than inferred. `GET /v1/models` declares
|
|
504
|
+
* `mistral-large-latest` and `mistral-large-2512` as aliases of each other,
|
|
505
|
+
* both reporting the name `mistral-large-2512` and a 262,144 context, so the
|
|
506
|
+
* dated id is what a usage log carries and the moving pointer is the alias.
|
|
507
|
+
*/
|
|
508
|
+
id: 'mistral-large-2512',
|
|
509
|
+
aliases: ['mistral-large-latest'],
|
|
510
|
+
provider: 'mistral',
|
|
511
|
+
displayName: 'Mistral Large 3',
|
|
512
|
+
inputPerMTok: 0.5,
|
|
513
|
+
outputPerMTok: 1.5,
|
|
514
|
+
contextWindow: 262_144,
|
|
515
|
+
cacheMinTokens: 0,
|
|
516
|
+
caching: 'none',
|
|
517
|
+
multipliers: { batch: 0.5 },
|
|
323
518
|
capability: 'large',
|
|
324
519
|
tier: 'opus',
|
|
325
520
|
notes: 'No prompt caching: reordering still helps readability but saves nothing here.',
|
|
326
521
|
},
|
|
522
|
+
{
|
|
523
|
+
/*
|
|
524
|
+
* Same page, same day, same reading.
|
|
525
|
+
*
|
|
526
|
+
* **This provider's ladder is not monotonic in price, and that is not a
|
|
527
|
+
* typo.** Medium 3.5 costs three times Large 3 on input and five times on
|
|
528
|
+
* output, because Mistral positions Large as the open-weight flagship and
|
|
529
|
+
* Medium as the model for long-horizon agentic work. `capability` follows
|
|
530
|
+
* the vendor's own description rather than the price, which is what the
|
|
531
|
+
* field means; the consequence is that a step down from `large` to `mid`
|
|
532
|
+
* here is dearer, and `levers.ts` simply does not offer a route whose saving
|
|
533
|
+
* is not above zero. Nothing to fix, and worth saying so before somebody
|
|
534
|
+
* "corrects" the ladder into producing a saving that does not exist.
|
|
535
|
+
*/
|
|
536
|
+
id: 'mistral-medium-2604',
|
|
537
|
+
aliases: ['mistral-medium-latest', 'mistral-medium-3-5'],
|
|
538
|
+
provider: 'mistral',
|
|
539
|
+
displayName: 'Mistral Medium 3.5',
|
|
540
|
+
inputPerMTok: 1.5,
|
|
541
|
+
outputPerMTok: 7.5,
|
|
542
|
+
contextWindow: 262_144,
|
|
543
|
+
cacheMinTokens: 0,
|
|
544
|
+
caching: 'none',
|
|
545
|
+
multipliers: { batch: 0.5 },
|
|
546
|
+
capability: 'mid',
|
|
547
|
+
tier: 'sonnet',
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
/* Same page, same day. `mistral-small-2603` is what the API reports. */
|
|
551
|
+
id: 'mistral-small-2603',
|
|
552
|
+
aliases: ['mistral-small-latest'],
|
|
553
|
+
provider: 'mistral',
|
|
554
|
+
displayName: 'Mistral Small 4',
|
|
555
|
+
inputPerMTok: 0.15,
|
|
556
|
+
outputPerMTok: 0.6,
|
|
557
|
+
contextWindow: 262_144,
|
|
558
|
+
cacheMinTokens: 0,
|
|
559
|
+
caching: 'none',
|
|
560
|
+
multipliers: { batch: 0.5 },
|
|
561
|
+
capability: 'small',
|
|
562
|
+
tier: 'haiku',
|
|
563
|
+
},
|
|
327
564
|
];
|
|
328
565
|
|
|
329
566
|
export const DEFAULT_MODEL = 'claude-opus-5';
|
|
@@ -390,6 +627,28 @@ function makeCatalogue(
|
|
|
390
627
|
/** The prices compiled into this release. */
|
|
391
628
|
export const BUNDLED_CATALOGUE: PricingCatalogue = makeCatalogue(MODELS, PRICING_LAST_REVIEWED);
|
|
392
629
|
|
|
630
|
+
/**
|
|
631
|
+
* Whether this model may be offered to a reader as something to move to.
|
|
632
|
+
*
|
|
633
|
+
* One home for a two-part rule that used to be written in four places as one
|
|
634
|
+
* part. `recommendable: false` — a private programme, a waitlist — was honoured
|
|
635
|
+
* at every one of them. `retired` is the stronger statement: the provider
|
|
636
|
+
* answered a real request for the id with an error, so a switch to it is not a
|
|
637
|
+
* worse recommendation, it is one that cannot be carried out at all.
|
|
638
|
+
*
|
|
639
|
+
* Written as a function rather than as a second `&&` at four call sites,
|
|
640
|
+
* because the fifth call site is always the one that gets written with only the
|
|
641
|
+
* first half. `pricing.test.js` fails a source file that filters on
|
|
642
|
+
* `recommendable` without coming through here, and that guard was proved by
|
|
643
|
+
* planting exactly that filter.
|
|
644
|
+
*
|
|
645
|
+
* Pricing is a separate question and stays answered: a retired model keeps its
|
|
646
|
+
* price, because a log full of its calls records money that was really spent.
|
|
647
|
+
*/
|
|
648
|
+
export function isOffered(model: ModelPricing): boolean {
|
|
649
|
+
return model.recommendable !== false && model.retired === undefined;
|
|
650
|
+
}
|
|
651
|
+
|
|
393
652
|
/** Looks a model up in a specific catalogue. */
|
|
394
653
|
export function modelFrom(catalogue: PricingCatalogue, id: string): ModelPricing {
|
|
395
654
|
const model = catalogue.byId.get(id);
|
|
@@ -422,7 +681,7 @@ export function cheapestOfTierIn(
|
|
|
422
681
|
const candidates = catalogue.models.filter(
|
|
423
682
|
(m) =>
|
|
424
683
|
m.tier === tier &&
|
|
425
|
-
m
|
|
684
|
+
isOffered(m) &&
|
|
426
685
|
(provider === undefined || m.provider === provider),
|
|
427
686
|
);
|
|
428
687
|
const first = candidates[0];
|
|
@@ -453,46 +712,172 @@ export function listModels(): ModelPricing[] {
|
|
|
453
712
|
* possible answers; `on` still decides which answer applies on every call,
|
|
454
713
|
* so behaviour is unchanged to the millisecond.
|
|
455
714
|
*/
|
|
715
|
+
interface Rates {
|
|
716
|
+
inputPerMTok: number;
|
|
717
|
+
outputPerMTok: number;
|
|
718
|
+
promoApplied: boolean;
|
|
719
|
+
tier: { applied: string | null; decided: boolean; because: string } | null;
|
|
720
|
+
}
|
|
721
|
+
|
|
456
722
|
const pricingMemo = new WeakMap<
|
|
457
723
|
ModelPricing,
|
|
458
|
-
{
|
|
459
|
-
untilMs: number | null;
|
|
460
|
-
promo: { inputPerMTok: number; outputPerMTok: number; promoApplied: boolean } | null;
|
|
461
|
-
base: { inputPerMTok: number; outputPerMTok: number; promoApplied: boolean };
|
|
462
|
-
}
|
|
724
|
+
{ untilMs: number | null; promo: Rates | null; base: Rates }
|
|
463
725
|
>();
|
|
464
726
|
|
|
727
|
+
/** The two unconditional answers for one model, parsed once. */
|
|
728
|
+
function cache(model: ModelPricing): {
|
|
729
|
+
untilMs: number | null;
|
|
730
|
+
promo: Rates | null;
|
|
731
|
+
base: Rates;
|
|
732
|
+
} {
|
|
733
|
+
const entry = {
|
|
734
|
+
untilMs: model.promo ? new Date(`${model.promo.until}T23:59:59.999Z`).getTime() : null,
|
|
735
|
+
promo: model.promo
|
|
736
|
+
? Object.freeze({
|
|
737
|
+
inputPerMTok: model.promo.inputPerMTok,
|
|
738
|
+
outputPerMTok: model.promo.outputPerMTok,
|
|
739
|
+
promoApplied: true,
|
|
740
|
+
tier: null,
|
|
741
|
+
})
|
|
742
|
+
: null,
|
|
743
|
+
base: Object.freeze({
|
|
744
|
+
inputPerMTok: model.inputPerMTok,
|
|
745
|
+
outputPerMTok: model.outputPerMTok,
|
|
746
|
+
promoApplied: false,
|
|
747
|
+
tier: null,
|
|
748
|
+
}),
|
|
749
|
+
};
|
|
750
|
+
pricingMemo.set(model, entry);
|
|
751
|
+
return entry;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Whether one condition holds, for a call at `on` of `inputTokens` tokens.
|
|
756
|
+
*
|
|
757
|
+
* Returns `null` rather than `false` when the condition cannot be decided,
|
|
758
|
+
* because "this does not apply" and "nobody said" lead to different reports and
|
|
759
|
+
* collapsing them is the oldest mistake in this repository.
|
|
760
|
+
*/
|
|
761
|
+
function holds(
|
|
762
|
+
when: TierCondition,
|
|
763
|
+
on: Date,
|
|
764
|
+
inputTokens: number | undefined,
|
|
765
|
+
): boolean | null {
|
|
766
|
+
if (when.kind === 'utc-hours') {
|
|
767
|
+
const day = on.getUTCDay();
|
|
768
|
+
if (when.weekdaysOnly && (day === 0 || day === 6)) return false;
|
|
769
|
+
return when.hours.includes(on.getUTCHours());
|
|
770
|
+
}
|
|
771
|
+
if (inputTokens === undefined) return null;
|
|
772
|
+
return inputTokens > when.tokens;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* What one call is billed at, and what decided it.
|
|
777
|
+
*
|
|
778
|
+
* `tier` is the field this grew for, and it carries three states rather than
|
|
779
|
+
* two. A model with no tiers gets `null`. A model whose condition could be
|
|
780
|
+
* evaluated gets the tier that applied, or the base rate with `applied: null`
|
|
781
|
+
* and `decided: true`. A model whose condition could **not** be evaluated —
|
|
782
|
+
* a size tier priced without a token count — gets `decided: false`, the
|
|
783
|
+
* **dearer** of the candidate rates, and `because` naming what would settle it.
|
|
784
|
+
*
|
|
785
|
+
* The dearer one is not a shrug. A cost figure that is a ceiling can prove
|
|
786
|
+
* "under budget" and can never surprise a bill; a floor chosen for looking
|
|
787
|
+
* better is the flattering direction this product spends its time refusing.
|
|
788
|
+
* The reader still has to be told, which is what `decided: false` is for, and
|
|
789
|
+
* `pricing.test.js` fails a tiered model whose report drops it.
|
|
790
|
+
*/
|
|
465
791
|
export function effectivePricing(
|
|
466
792
|
model: ModelPricing,
|
|
467
793
|
on: Date = new Date(),
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
794
|
+
context: { inputTokens?: number } = {},
|
|
795
|
+
): Rates {
|
|
796
|
+
const memo = pricingMemo.get(model) ?? cache(model);
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Tiers first, and a promotion never runs on top of one.
|
|
800
|
+
*
|
|
801
|
+
* No provider here does both, and combining them would be this repository
|
|
802
|
+
* inventing a price out of two real ones — the composed figure the first rule
|
|
803
|
+
* in the doctrine is about. A model that grew both would fail the guard in
|
|
804
|
+
* `pricing.test.js` rather than quietly getting a multiplied rate.
|
|
805
|
+
*/
|
|
806
|
+
if (model.tiers !== undefined && model.tiers.length > 0) {
|
|
807
|
+
const undecided: PricingTier[] = [];
|
|
808
|
+
for (const tier of model.tiers) {
|
|
809
|
+
const verdict = holds(tier.when, on, context.inputTokens);
|
|
810
|
+
if (verdict === true) {
|
|
811
|
+
return {
|
|
812
|
+
inputPerMTok: tier.inputPerMTok,
|
|
813
|
+
outputPerMTok: tier.outputPerMTok,
|
|
814
|
+
promoApplied: false,
|
|
815
|
+
tier: { applied: tier.id, decided: true, because: describe(tier.when) },
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
if (verdict === null) undecided.push(tier);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if (undecided.length === 0) {
|
|
822
|
+
return {
|
|
823
|
+
...memo.base,
|
|
824
|
+
tier: { applied: null, decided: true, because: 'no conditional rate applies' },
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/* The dearest candidate, so the figure is a ceiling rather than a wish. */
|
|
829
|
+
const dearest = undecided.reduce((worst, tier) =>
|
|
830
|
+
tier.inputPerMTok > worst.inputPerMTok ? tier : worst,
|
|
831
|
+
);
|
|
832
|
+
const ceiling =
|
|
833
|
+
dearest.inputPerMTok > model.inputPerMTok
|
|
474
834
|
? {
|
|
475
|
-
inputPerMTok:
|
|
476
|
-
outputPerMTok:
|
|
477
|
-
|
|
835
|
+
inputPerMTok: dearest.inputPerMTok,
|
|
836
|
+
outputPerMTok: dearest.outputPerMTok,
|
|
837
|
+
applied: dearest.id,
|
|
478
838
|
}
|
|
479
|
-
:
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
839
|
+
: {
|
|
840
|
+
inputPerMTok: model.inputPerMTok,
|
|
841
|
+
outputPerMTok: model.outputPerMTok,
|
|
842
|
+
applied: null,
|
|
843
|
+
};
|
|
844
|
+
return {
|
|
845
|
+
inputPerMTok: ceiling.inputPerMTok,
|
|
846
|
+
outputPerMTok: ceiling.outputPerMTok,
|
|
847
|
+
promoApplied: false,
|
|
848
|
+
tier: {
|
|
849
|
+
applied: ceiling.applied,
|
|
850
|
+
decided: false,
|
|
851
|
+
because: `${describe(dearest.when)}, and the token count was not given`,
|
|
852
|
+
},
|
|
485
853
|
};
|
|
486
|
-
if (memo.promo) Object.freeze(memo.promo);
|
|
487
|
-
pricingMemo.set(model, memo);
|
|
488
854
|
}
|
|
855
|
+
|
|
489
856
|
if (memo.untilMs !== null && on.getTime() <= memo.untilMs) return memo.promo!;
|
|
490
857
|
return memo.base;
|
|
491
858
|
}
|
|
492
859
|
|
|
860
|
+
/** One condition as a sentence a reader can check against a provider's page. */
|
|
861
|
+
function describe(when: TierCondition): string {
|
|
862
|
+
if (when.kind === 'input-tokens-above') {
|
|
863
|
+
return `prompts over ${when.tokens.toLocaleString('en-US')} input tokens`;
|
|
864
|
+
}
|
|
865
|
+
const windows = [];
|
|
866
|
+
let start = null;
|
|
867
|
+
const sorted = [...when.hours].sort((a, b) => a - b);
|
|
868
|
+
for (let i = 0; i < sorted.length; i += 1) {
|
|
869
|
+
if (start === null) start = sorted[i]!;
|
|
870
|
+
if (sorted[i]! + 1 !== sorted[i + 1]) {
|
|
871
|
+
windows.push(`${String(start).padStart(2, '0')}:00-${String(sorted[i]! + 1).padStart(2, '0')}:00`);
|
|
872
|
+
start = null;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
return `${windows.join(' and ')} UTC${when.weekdaysOnly ? ', Monday to Friday' : ''}`;
|
|
876
|
+
}
|
|
877
|
+
|
|
493
878
|
/** Cheapest model of each capability tier, for recommendations. */
|
|
494
879
|
export function cheapestOfTier(tier: ModelPricing['tier']): ModelPricing {
|
|
495
|
-
const candidates = MODELS.filter((m) => m.tier === tier && m
|
|
880
|
+
const candidates = MODELS.filter((m) => m.tier === tier && isOffered(m));
|
|
496
881
|
const first = candidates[0];
|
|
497
882
|
if (!first) throw new Error(`No models for tier "${tier}"`);
|
|
498
883
|
return candidates.reduce((best, m) => (m.inputPerMTok < best.inputPerMTok ? m : best), first);
|
|
@@ -514,6 +899,21 @@ export function cheapestOfTier(tier: ModelPricing['tier']): ModelPricing {
|
|
|
514
899
|
* a date — an overlay supplies this string, and a wrong one should read as
|
|
515
900
|
* unknown rather than as a confident number computed from `NaN`.
|
|
516
901
|
*/
|
|
902
|
+
/**
|
|
903
|
+
* The age at which this product stops treating its own price table as current.
|
|
904
|
+
*
|
|
905
|
+
* Three surfaces warn a reader that the figures above may be off — `profile`,
|
|
906
|
+
* the MCP report, the browser's bill — and each of them had typed `45` into
|
|
907
|
+
* its own comparison, with four locale sentences stating it again in prose.
|
|
908
|
+
* Seven copies of one claim, and nothing that would notice when they
|
|
909
|
+
* disagreed; the sentence a reader sees names the number, so a drift between
|
|
910
|
+
* them would have been a surface telling the reader one threshold and applying
|
|
911
|
+
* another.
|
|
912
|
+
*
|
|
913
|
+
* The value is what those surfaces already published, not a new judgement.
|
|
914
|
+
*/
|
|
915
|
+
export const STALE_PRICING_DAYS = 45;
|
|
916
|
+
|
|
517
917
|
export function reviewAgeDays(lastReviewed: string, now: Date): number | null {
|
|
518
918
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(lastReviewed)) return null;
|
|
519
919
|
const then = Date.parse(`${lastReviewed}T00:00:00Z`);
|