@molecule/api-resource-ai-models 1.0.2 → 1.2.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 CHANGED
@@ -3,7 +3,7 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
3
3
  Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
4
4
  Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
5
5
  To change this document, edit the module-level JSDoc in src/index.ts.
6
- Generated: 2026-08-06T19:42:52.531Z
6
+ Generated: 2026-08-13T22:10:11.926Z
7
7
  -->
8
8
 
9
9
  # @molecule/api-resource-ai-models
@@ -260,6 +260,57 @@ interface ModelDefinition {
260
260
  windows: { startMinuteUtc: number; endMinuteUtc: number }[]
261
261
  multiplier: number
262
262
  }
263
+ /**
264
+ * A price change the provider has ANNOUNCED with a dated effective instant,
265
+ * staged ahead of time. Until `effectiveFrom` the model bills at the base
266
+ * rates above; from that instant on, these rates replace them.
267
+ *
268
+ * This exists because the freshness gate can only ever see prices that are
269
+ * ALREADY live: it diffs the catalog against models.dev's *current* rates, so
270
+ * a change announced today and effective in three days is invisible to it
271
+ * until after it lands — and the cron runs every 8h, so the catalog would
272
+ * under-meter for up to a third of a day at whatever the new rate is. Landing
273
+ * the new numbers early is not an option either: that over-bills every turn
274
+ * until the switch (the exact mistake the removed DeepSeek peak windows made
275
+ * for weeks against the free-tier default model). Staging with a timestamp is
276
+ * the only form that is correct on BOTH sides of the instant, and it needs no
277
+ * one awake at the switch.
278
+ *
279
+ * Applies to the BASE rates only — a `regionPricing` entry is a different
280
+ * host's rate card (a US re-host does not reprice because the native provider
281
+ * did) and is never touched by a scheduled change.
282
+ *
283
+ * `peakPricing` here, when declared, replaces the model's peak windows from
284
+ * the same instant; when omitted, the model's existing windows carry through
285
+ * unchanged. To schedule the END of peak pricing, declare an explicit
286
+ * `{ windows: [], multiplier: 1 }`.
287
+ *
288
+ * Resolution is `effectiveBaseRates()` / `effectivePeakPricing()`, and every
289
+ * consumer reaches it through `modelRegionRates()` / `priceMultiplierAt()` /
290
+ * the `withEffectivePricing()` projection the list handler serves — so a
291
+ * scheduled change lands everywhere at once with no follow-up edit. Once the
292
+ * instant has passed, fold the rates into the base fields and delete this
293
+ * (the freshness gate now verifies them against models.dev normally).
294
+ */
295
+ scheduledPricing?: {
296
+ /** ISO-8601 UTC instant the new rates take effect. */
297
+ effectiveFrom: string
298
+ /** Input price per million *uncached* tokens in USD, from `effectiveFrom`. */
299
+ inputPricePerMTok: number
300
+ /** Output price per million tokens in USD, from `effectiveFrom`. */
301
+ outputPricePerMTok: number
302
+ /** Prompt-cache *read* price per million tokens in USD, from `effectiveFrom`. */
303
+ cacheReadPricePerMTok: number
304
+ /** Prompt-cache *write* price per million tokens in USD, from `effectiveFrom`. */
305
+ cacheWritePricePerMTok: number
306
+ /** Peak-hour pricing from `effectiveFrom` (omitted → existing windows carry through). */
307
+ peakPricing?: {
308
+ windows: { startMinuteUtc: number; endMinuteUtc: number }[]
309
+ multiplier: number
310
+ }
311
+ /** Where the change was announced, for the re-verify pass after it lands. */
312
+ source?: string
313
+ }
263
314
  /**
264
315
  * Fast-mode ("priority speed") pricing — the per-MTok rates billed when a
265
316
  * request runs with the provider's fast/priority tier (e.g. Anthropic's
@@ -310,6 +361,35 @@ interface ModelDefinition {
310
361
  * or its past usage silently meters as free. Omit entirely for active models.
311
362
  */
312
363
  disabled?: boolean
364
+ /**
365
+ * The id of the NEWER-generation model that replaces this one, set on the
366
+ * OLDER entry and naming its successor (e.g. `qwen3.7-max` carries
367
+ * `supersededBy: 'qwen3.8-max'`).
368
+ *
369
+ * A superseded model is NOT selectable — like {@link disabled} it is excluded
370
+ * from `MODEL_IDS`, `getAvailableModels()`, the `GET /ai/models` listing and
371
+ * the client-side picker partitions, so a user is only ever offered the
372
+ * newest generation of a family. It differs from `disabled` in *why* and in
373
+ * what it points at: a disabled model is one the provider retired (it can no
374
+ * longer answer), whereas a superseded model is usually still served upstream
375
+ * and simply has nothing to offer over its successor — and the successor id
376
+ * is a real migration target, so a saved selection can resolve forward
377
+ * (`resolveSelectableModelId`) instead of silently falling back to the
378
+ * platform default.
379
+ *
380
+ * `getModel(id)` STILL returns a superseded entry — historical usage must stay
381
+ * priceable, so NEVER delete one.
382
+ *
383
+ * Set this ONLY when the successor covers the same TIER. A cheaper or
384
+ * specialist tier with no newer equivalent is NOT superseded merely because
385
+ * its version number is lower (Google's sole pro tier `gemini-3.1-pro-preview`
386
+ * alongside the newer flash flagship; `qwen3-coder-plus`; `kimi-k2.7-code`) —
387
+ * hiding those would leave a provider with no cheap option. Mark those
388
+ * {@link deprecatedAt} at most. The invariant is enforced by
389
+ * `__tests__/lookup.test.ts`: two selectable models of the same family at
390
+ * different versions fail unless listed there as a documented exception.
391
+ */
392
+ supersededBy?: string
313
393
  }
314
394
  ```
315
395
 
@@ -402,6 +482,25 @@ type EffortLevel = string
402
482
 
403
483
  ### Functions
404
484
 
485
+ #### `effectiveBaseRates(modelDef, at)`
486
+
487
+ A model's BASE token rates in effect at a given instant — the staged
488
+ {@link ModelDefinition.scheduledPricing} rates once their `effectiveFrom` has
489
+ passed, else the base fields.
490
+
491
+ These are the native provider's rates. A `regionPricing` override is a
492
+ different host's rate card and is resolved separately by
493
+ {@link modelRegionRates}.
494
+
495
+ ```typescript
496
+ function effectiveBaseRates(modelDef: ModelDefinition, at?: Date): ModelTokenRates
497
+ ```
498
+
499
+ - `modelDef` — The model definition.
500
+ - `at` — The instant to price at (defaults to now).
501
+
502
+ **Returns:** The base rates in effect at that instant.
503
+
405
504
  #### `effectiveModelRegion(modelDef, requested)`
406
505
 
407
506
  Resolve a model's effective processing region: the requested region when the
@@ -419,12 +518,32 @@ function effectiveModelRegion(modelDef: ModelDefinition | undefined, requested?:
419
518
 
420
519
  **Returns:** The effective region code.
421
520
 
521
+ #### `effectivePeakPricing(modelDef, at)`
522
+
523
+ A model's peak-hour pricing in effect at a given instant: the staged
524
+ {@link ModelDefinition.scheduledPricing} `peakPricing` once its
525
+ `effectiveFrom` has passed (when that entry declares one — an omitted one
526
+ leaves the existing windows in force), else the model's own `peakPricing`.
527
+
528
+ ```typescript
529
+ function effectivePeakPricing(
530
+ modelDef: ModelDefinition,
531
+ at?: Date,
532
+ ): { windows: { startMinuteUtc: number; endMinuteUtc: number }[]; multiplier: number } | undefined
533
+ ```
534
+
535
+ - `modelDef` — The model definition.
536
+ - `at` — The instant to evaluate (defaults to now).
537
+
538
+ **Returns:** The peak-pricing config in effect, or `undefined` when none is.
539
+
422
540
  #### `getAvailableModels(availableProviders)`
423
541
 
424
542
  Get models that are currently usable — filtered to only providers that are available.
425
543
 
426
544
  The caller passes in which provider IDs are active (i.e. have a bond wired).
427
- `disabled` models are excluded — they are never offered for selection.
545
+ Models that are not {@link isSelectableModel} — `disabled` or superseded by a
546
+ newer generation — are excluded; they are never offered for selection.
428
547
 
429
548
  ```typescript
430
549
  function getAvailableModels(
@@ -434,16 +553,16 @@ function getAvailableModels(
434
553
 
435
554
  - `availableProviders` — Set or array of provider IDs that have active bonds.
436
555
 
437
- **Returns:** Non-disabled models whose provider is in the available set.
556
+ **Returns:** Selectable models whose provider is in the available set.
438
557
 
439
558
  #### `getModel(id)`
440
559
 
441
560
  Look up a model definition by ID.
442
561
 
443
- Returns `disabled` models too: a saved selection or a historical usage row
444
- may reference a since-retired model, and it must stay priceable. Use
445
- {@link MODEL_IDS} / {@link getAvailableModels} (which exclude disabled
446
- models) to decide what is _selectable_.
562
+ Returns `disabled` and `supersededBy` models too: a saved selection or a
563
+ historical usage row may reference a since-retired or since-superseded model,
564
+ and it must stay priceable. Use {@link MODEL_IDS} / {@link getAvailableModels}
565
+ (or {@link isSelectableModel}) to decide what is _selectable_.
447
566
 
448
567
  ```typescript
449
568
  function getModel(id: string): ModelDefinition | undefined
@@ -465,6 +584,21 @@ function getModelsByProvider(provider: AIProviderID): readonly ModelDefinition[]
465
584
 
466
585
  **Returns:** Array of model definitions for that provider.
467
586
 
587
+ #### `isSelectableModel(model)`
588
+
589
+ Whether a model may be offered for selection: not `disabled` (retired
590
+ upstream) and not `supersededBy` a newer generation of its own family. Both
591
+ kinds stay in the catalog for pricing — this predicate is the single place
592
+ that decides _exposure_, so every listing/validation surface agrees.
593
+
594
+ ```typescript
595
+ function isSelectableModel(model: Pick<ModelDefinition, 'disabled' | 'supersededBy'>): boolean
596
+ ```
597
+
598
+ - `model` — The model definition (or the two flags from one).
599
+
600
+ **Returns:** True when the model may be listed and chosen.
601
+
468
602
  #### `list(_req, res)`
469
603
 
470
604
  Returns models whose `provider` has a bond registered under the `'ai'`
@@ -482,41 +616,95 @@ function list(_req: MoleculeRequest, res: MoleculeResponse): Promise<void>
482
616
  - `_req` — The request object (unused).
483
617
  - `res` — The response object.
484
618
 
485
- #### `modelRegionRates(modelDef, requested)`
619
+ #### `modelRegionRates(modelDef, requested, at)`
486
620
 
487
621
  The token rates for a model in a given processing region: the model's
488
622
  {@link ModelDefinition.regionPricing} override for the region when one
489
- exists, else the base rates (the native provider's list prices). Omitted
490
- cache fields in an override fall back to the override's input price (hosts
491
- with no cache discount / no write premium). The region is resolved via
492
- {@link effectiveModelRegion}, so callers may pass the raw user choice.
623
+ exists, else the base rates in effect at `at` (the native provider's list
624
+ prices, including any staged {@link ModelDefinition.scheduledPricing} change
625
+ that has landed). Omitted cache fields in an override fall back to the
626
+ override's input price (hosts with no cache discount / no write premium). The
627
+ region is resolved via {@link effectiveModelRegion}, so callers may pass the
628
+ raw user choice.
629
+
630
+ `at` defaults to NOW rather than being required, so an existing caller cannot
631
+ keep billing a superseded rate by omitting it — metering should still pass
632
+ each request's own timestamp, the same way it must for
633
+ {@link priceMultiplierAt}.
493
634
 
494
635
  ```typescript
495
- function modelRegionRates(modelDef: ModelDefinition, requested?: string): ModelTokenRates
636
+ function modelRegionRates(modelDef: ModelDefinition, requested?: string, at?: Date): ModelTokenRates
496
637
  ```
497
638
 
498
639
  - `modelDef` — The model definition.
499
640
  - `requested` — The user's per-model region choice, if any.
641
+ - `at` — The instant to price at (defaults to now).
500
642
 
501
643
  **Returns:** The region-effective rates.
502
644
 
503
- #### `priceMultiplierAt(modelDef, at)`
645
+ #### `priceMultiplierAt(modelDef, at, region)`
504
646
 
505
- The price multiplier in effect for a model at a given instant.
647
+ The price multiplier in effect for a model at a given instant, in a region.
506
648
 
507
649
  Consults the model's {@link ModelDefinition.peakPricing} windows (UTC,
508
650
  half-open, may wrap midnight). Metering MUST call this with each request's
509
651
  own timestamp so peak-hour usage bills at the provider's real rate — pricing
510
652
  everything at the flat rate silently under-meters peak traffic.
511
653
 
654
+ Peak windows belong to the NATIVE provider, so they apply only where the base
655
+ rates do. A region with a {@link ModelDefinition.regionPricing} override is a
656
+ different host billing its own complete rate card, including whether it has
657
+ time-of-day pricing at all — and re-hosts generally do not. Applying the
658
+ native provider's surcharge on top of a re-host's flat rates would over-bill
659
+ every turn in its windows (DeepSeek's 2× Beijing-hours pricing charged
660
+ against DeepInfra, which has no peak pricing).
661
+
512
662
  ```typescript
513
- function priceMultiplierAt(modelDef: ModelDefinition | undefined, at: Date): number
663
+ function priceMultiplierAt(modelDef: ModelDefinition | undefined, at: Date, region?: string): number
514
664
  ```
515
665
 
516
666
  - `modelDef` — The model definition (or undefined).
517
667
  - `at` — The instant the request was made.
668
+ - `region` — The user's per-model region choice, if any (omitted → the model's default region).
518
669
 
519
- **Returns:** The multiplier (`1` outside peak windows or when none are declared).
670
+ **Returns:** The multiplier (`1` outside peak windows, when none are declared, or in a region that prices off its own override).
671
+
672
+ #### `resolveSelectableModelId(id)`
673
+
674
+ Resolve a model id FORWARD to the selectable model that replaces it, following
675
+ the {@link ModelDefinition.supersededBy} chain (a saved `qwen3.7-max` →
676
+ `qwen3.8-max`). Lets a persisted selection keep the user's intent — the same
677
+ tier from the same provider — instead of falling back to the platform default
678
+ once the older generation stops being offered.
679
+
680
+ ```typescript
681
+ function resolveSelectableModelId(id: string): string | undefined
682
+ ```
683
+
684
+ - `id` — The persisted model id.
685
+
686
+ **Returns:** The selectable successor's id, the id itself when it is already selectable, or `undefined` for an unknown or `disabled` model (nothing to forward to).
687
+
688
+ #### `withEffectivePricing(modelDef, at)`
689
+
690
+ A model projected onto the pricing in effect at a given instant: the staged
691
+ {@link ModelDefinition.scheduledPricing} rates folded into the base fields
692
+ (and its peak windows into `peakPricing`) once effective, with the staged
693
+ entry stripped.
694
+
695
+ This is what the `GET /ai/models` handler serves, so a client renders the
696
+ rates that are actually billing right now without needing to resolve a
697
+ schedule against its own clock — the server's clock is the only one that
698
+ decides when a price change lands.
699
+
700
+ ```typescript
701
+ function withEffectivePricing(modelDef: ModelDefinition, at?: Date): ModelDefinition
702
+ ```
703
+
704
+ - `modelDef` — The model definition.
705
+ - `at` — The instant to project at (defaults to now).
706
+
707
+ **Returns:** The model with effective pricing and no `scheduledPricing`.
520
708
 
521
709
  ### Constants
522
710
 
@@ -525,8 +713,9 @@ function priceMultiplierAt(modelDef: ModelDefinition | undefined, at: Date): num
525
713
  Set of _selectable_ model IDs for fast validation.
526
714
 
527
715
  Excludes `disabled` models so a retired model (e.g. `grok-code-fast-1`) can
528
- never be chosen for a new chat, while {@link getModel} still resolves it for
529
- historical pricing.
716
+ never be chosen for a new chat, and `supersededBy` models so an older
717
+ generation of a family (e.g. `qwen3.7-max` next to `qwen3.8-max`) is never
718
+ offered — while {@link getModel} still resolves both for historical pricing.
530
719
 
531
720
  ```typescript
532
721
  const MODEL_IDS: ReadonlySet<string>
@@ -554,6 +743,18 @@ Effort is each model's OWN native value — there is no abstract scale (see
554
743
  control) carries `thinkingConfigurable: false` and OMITS both fields —
555
744
  there is nothing to tune.
556
745
 
746
+ ONE GENERATION PER FAMILY. When a provider ships a newer generation of a
747
+ model line, the older entry gets `supersededBy: '<newer id>'` and stops being
748
+ offered — the picker never shows both `qwen3.7-max` and `qwen3.8-max`. The
749
+ entry is NEVER deleted: `getModel()` still resolves it so saved selections and
750
+ historical usage stay priceable, and a persisted id resolves forward to the
751
+ successor. Supersede only within the same TIER: a cheaper or specialist model
752
+ with no newer equivalent (`gemini-3.1-pro-preview`, `qwen3-coder-plus`,
753
+ `kimi-k2.7-code`, `grok-build-0.1`) keeps at most `deprecatedAt`, so every
754
+ provider keeps a real choice. `__tests__/lookup.test.ts` fails on any two
755
+ selectable models of one family at different versions that aren't a
756
+ documented exception.
757
+
557
758
  Sources (verified 2026-07-28; OpenAI re-verified 2026-07-31 after the
558
759
  2026-07-30 GPT-5.6 repricing — cross-check prices against models.dev with
559
760
  `npm run check:model-freshness` from the workspace root):
@@ -576,21 +777,31 @@ Sources (verified 2026-07-28; OpenAI re-verified 2026-07-31 after the
576
777
  2026-07-21 $1.50/$7.50 supersedes 3.5-flash as the agentic flagship;
577
778
  gemini-3.1-pro-preview still the pro tier — "3.5 Pro" has NOT shipped as
578
779
  of 2026-07-28 despite the coming-soon badge; do not add until it has an id)
780
+ (re-verified 2026-08-13: gemini-3.7-flash "New Stable" — supersedes
781
+ 3.6-flash as the flash flagship at the SAME list price ($1.50/$7.50, cache
782
+ read $0.15), with a launch promo ($0.75/$3.75, cache read $0.075) through
783
+ 2026-12-31 billed here at list; specs from /docs/models/gemini-3.7-flash:
784
+ 1M ctx / 65,536 out, thinking low|medium|high (no minimal), vision, tools,
785
+ caching, search grounding, code execution, url context)
579
786
  - xAI: https://docs.x.ai/developers/models + /developers/grok-4-5
580
787
  (grok-4.5 flagship 2026-07-08: $2/$6, 500K ctx, ≥200K prompts bill 2× —
581
788
  not modeled; reasoning_effort low|medium|high default high, image input;
582
789
  grok-4.3 still served at $1.25/$2.50 with the bigger 1M window;
583
790
  grok-code-fast-1 no longer listed — retires 2026-08-15)
584
- - DeepSeek: https://api-docs.deepseek.com/quick_start/pricing (unchanged V4
585
- Pro/Flash pricing; legacy deepseek-chat/-reasoner ids fully retired
586
- 2026-07-24 — never in this catalog; the announced peak-hour 2× surcharge is
587
- still NOT active as of 2026-07-28, see the entries)
588
- - Moonshot: https://platform.kimi.ai/docs/models (kimi-k3 flagship 2026-07-16
791
+ - DeepSeek: https://api-docs.deepseek.com/quick_start/pricing (verified
792
+ 2026-08-14; legacy deepseek-chat/-reasoner ids fully retired 2026-07-24 —
793
+ never in this catalog. V4-Pro GA on 2026-08-13 came with a price RISE
794
+ effective 2026-08-16T16:00Z plus the long-announced peak-hour 2×: both
795
+ entries carry it as `scheduledPricing`, so today's rates bill until that
796
+ instant and the new ones after. Re-verify weekday-vs-daily peak windows and
797
+ the CN/US region default once it lands — see the entries.)
798
+ - Moonshot: https://platform.kimi.ai/docs/models + DeepInfra's model API for
799
+ the US re-host (kimi-k3 flagship 2026-07-16
589
800
  — 2.8T MoE, 1M ctx, $3/$15 — NOT added: thinking is forced-on with
590
801
  reasoning_content that must be replayed through tool loops, the same
591
- constraint that keeps kimi-k2.7-code out; add BOTH once the moonshot bond
592
- supports preserved thinking + reasoning_effort low|high|max. kimi-k2.6
593
- remains the newest model the bond can run correctly.)
802
+ constraint that kept kimi-k2.7-code out. BOTH are now in the catalog: the
803
+ moonshot bond gained preserved thinking (reasoning replayed through tool
804
+ loops), so kimi-k3 is the Moonshot pick.)
594
805
  - MiniMax: https://platform.minimax.io/docs/guides/pricing-paygo (unchanged;
595
806
  minimax-m3 $0.30/$1.20 is a "permanent 50% off" list rate)
596
807
  - Alibaba: https://www.alibabacloud.com/help/en/model-studio/deep-thinking
@@ -2,10 +2,18 @@
2
2
  * `GET /ai/models` — returns the catalog of available AI models.
3
3
  *
4
4
  * Filters the central `MODELS` list to only those whose provider is currently
5
- * bonded under the `'ai'` category AND are not `disabled` (a retired model is
6
- * never listed for selection, though `getModel` still prices it). No further
7
- * projection is applied — every `ModelDefinition` field is fine to expose to
8
- * authenticated clients today.
5
+ * bonded under the `'ai'` category AND are selectable — neither `disabled` (a
6
+ * model the provider retired) nor superseded by a newer generation of the same
7
+ * family, so the picker offers exactly one generation per family. Both kinds
8
+ * stay priceable via `getModel`.
9
+ *
10
+ * The one projection applied is `withEffectivePricing`: a model carrying a
11
+ * staged `scheduledPricing` change is served at whichever rates are billing at
12
+ * request time, with the schedule stripped. The server's clock decides when an
13
+ * announced price change lands, so a client never renders a rate that is not
14
+ * yet in force (nor keeps rendering one that has been superseded), and no
15
+ * client needs to know that scheduled pricing exists. Every other
16
+ * `ModelDefinition` field is fine to expose to authenticated clients today.
9
17
  *
10
18
  * Secure-by-default: this handler enforces authentication IN the handler
11
19
  * (`res.locals.session.userId`) and fails closed with `401` for an
@@ -1 +1 @@
1
- {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../src/handlers/list.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAK/E;;;;;;;;;;;GAWG;AACH,wBAAsB,IAAI,CAAC,IAAI,EAAE,eAAe,EAAE,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CActF"}
1
+ {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../src/handlers/list.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAM/E;;;;;;;;;;;GAWG;AACH,wBAAsB,IAAI,CAAC,IAAI,EAAE,eAAe,EAAE,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBtF"}
@@ -2,10 +2,18 @@
2
2
  * `GET /ai/models` — returns the catalog of available AI models.
3
3
  *
4
4
  * Filters the central `MODELS` list to only those whose provider is currently
5
- * bonded under the `'ai'` category AND are not `disabled` (a retired model is
6
- * never listed for selection, though `getModel` still prices it). No further
7
- * projection is applied — every `ModelDefinition` field is fine to expose to
8
- * authenticated clients today.
5
+ * bonded under the `'ai'` category AND are selectable — neither `disabled` (a
6
+ * model the provider retired) nor superseded by a newer generation of the same
7
+ * family, so the picker offers exactly one generation per family. Both kinds
8
+ * stay priceable via `getModel`.
9
+ *
10
+ * The one projection applied is `withEffectivePricing`: a model carrying a
11
+ * staged `scheduledPricing` change is served at whichever rates are billing at
12
+ * request time, with the schedule stripped. The server's clock decides when an
13
+ * announced price change lands, so a client never renders a rate that is not
14
+ * yet in force (nor keeps rendering one that has been superseded), and no
15
+ * client needs to know that scheduled pricing exists. Every other
16
+ * `ModelDefinition` field is fine to expose to authenticated clients today.
9
17
  *
10
18
  * Secure-by-default: this handler enforces authentication IN the handler
11
19
  * (`res.locals.session.userId`) and fails closed with `401` for an
@@ -19,6 +27,7 @@
19
27
  */
20
28
  import { getAll } from '@molecule/api-bond';
21
29
  import { t } from '@molecule/api-i18n';
30
+ import { isSelectableModel, withEffectivePricing } from '../lookup.js';
22
31
  import { MODELS } from '../models.js';
23
32
  /**
24
33
  * Returns models whose `provider` has a bond registered under the `'ai'`
@@ -42,7 +51,8 @@ export async function list(_req, res) {
42
51
  return;
43
52
  }
44
53
  const bondedProviders = new Set(getAll('ai').keys());
45
- const models = MODELS.filter((m) => bondedProviders.has(m.provider) && !m.disabled);
46
- const response = { models: [...models] };
54
+ const now = new Date();
55
+ const models = MODELS.filter((m) => bondedProviders.has(m.provider) && isSelectableModel(m)).map((m) => withEffectivePricing(m, now));
56
+ const response = { models };
47
57
  res.json(response);
48
58
  }
package/dist/lookup.d.ts CHANGED
@@ -4,21 +4,45 @@
4
4
  * @module
5
5
  */
6
6
  import type { AIProviderID, ModelDefinition } from './types.js';
7
+ /**
8
+ * Whether a model may be offered for selection: not `disabled` (retired
9
+ * upstream) and not `supersededBy` a newer generation of its own family. Both
10
+ * kinds stay in the catalog for pricing — this predicate is the single place
11
+ * that decides *exposure*, so every listing/validation surface agrees.
12
+ *
13
+ * @param model - The model definition (or the two flags from one).
14
+ * @returns True when the model may be listed and chosen.
15
+ */
16
+ export declare function isSelectableModel(model: Pick<ModelDefinition, 'disabled' | 'supersededBy'>): boolean;
17
+ /**
18
+ * Resolve a model id FORWARD to the selectable model that replaces it, following
19
+ * the {@link ModelDefinition.supersededBy} chain (a saved `qwen3.7-max` →
20
+ * `qwen3.8-max`). Lets a persisted selection keep the user's intent — the same
21
+ * tier from the same provider — instead of falling back to the platform default
22
+ * once the older generation stops being offered.
23
+ *
24
+ * @param id - The persisted model id.
25
+ * @returns The selectable successor's id, the id itself when it is already
26
+ * selectable, or `undefined` for an unknown or `disabled` model (nothing to
27
+ * forward to).
28
+ */
29
+ export declare function resolveSelectableModelId(id: string): string | undefined;
7
30
  /**
8
31
  * Set of *selectable* model IDs for fast validation.
9
32
  *
10
33
  * Excludes `disabled` models so a retired model (e.g. `grok-code-fast-1`) can
11
- * never be chosen for a new chat, while {@link getModel} still resolves it for
12
- * historical pricing.
34
+ * never be chosen for a new chat, and `supersededBy` models so an older
35
+ * generation of a family (e.g. `qwen3.7-max` next to `qwen3.8-max`) is never
36
+ * offered — while {@link getModel} still resolves both for historical pricing.
13
37
  */
14
38
  export declare const MODEL_IDS: ReadonlySet<string>;
15
39
  /**
16
40
  * Look up a model definition by ID.
17
41
  *
18
- * Returns `disabled` models too: a saved selection or a historical usage row
19
- * may reference a since-retired model, and it must stay priceable. Use
20
- * {@link MODEL_IDS} / {@link getAvailableModels} (which exclude disabled
21
- * models) to decide what is *selectable*.
42
+ * Returns `disabled` and `supersededBy` models too: a saved selection or a
43
+ * historical usage row may reference a since-retired or since-superseded model,
44
+ * and it must stay priceable. Use {@link MODEL_IDS} / {@link getAvailableModels}
45
+ * (or {@link isSelectableModel}) to decide what is *selectable*.
22
46
  *
23
47
  * @param id - The API model ID.
24
48
  * @returns The model definition, or `undefined` if not found.
@@ -35,25 +59,78 @@ export declare function getModelsByProvider(provider: AIProviderID): readonly Mo
35
59
  * Get models that are currently usable — filtered to only providers that are available.
36
60
  *
37
61
  * The caller passes in which provider IDs are active (i.e. have a bond wired).
38
- * `disabled` models are excluded — they are never offered for selection.
62
+ * Models that are not {@link isSelectableModel} — `disabled` or superseded by a
63
+ * newer generation — are excluded; they are never offered for selection.
39
64
  *
40
65
  * @param availableProviders - Set or array of provider IDs that have active bonds.
41
- * @returns Non-disabled models whose provider is in the available set.
66
+ * @returns Selectable models whose provider is in the available set.
42
67
  */
43
68
  export declare function getAvailableModels(availableProviders: ReadonlySet<AIProviderID> | readonly AIProviderID[]): readonly ModelDefinition[];
44
69
  /**
45
- * The price multiplier in effect for a model at a given instant.
70
+ * A model's BASE token rates in effect at a given instant — the staged
71
+ * {@link ModelDefinition.scheduledPricing} rates once their `effectiveFrom` has
72
+ * passed, else the base fields.
73
+ *
74
+ * These are the native provider's rates. A `regionPricing` override is a
75
+ * different host's rate card and is resolved separately by
76
+ * {@link modelRegionRates}.
77
+ *
78
+ * @param modelDef - The model definition.
79
+ * @param at - The instant to price at (defaults to now).
80
+ * @returns The base rates in effect at that instant.
81
+ */
82
+ export declare function effectiveBaseRates(modelDef: ModelDefinition, at?: Date): ModelTokenRates;
83
+ /**
84
+ * A model's peak-hour pricing in effect at a given instant: the staged
85
+ * {@link ModelDefinition.scheduledPricing} `peakPricing` once its
86
+ * `effectiveFrom` has passed (when that entry declares one — an omitted one
87
+ * leaves the existing windows in force), else the model's own `peakPricing`.
88
+ *
89
+ * @param modelDef - The model definition.
90
+ * @param at - The instant to evaluate (defaults to now).
91
+ * @returns The peak-pricing config in effect, or `undefined` when none is.
92
+ */
93
+ export declare function effectivePeakPricing(modelDef: ModelDefinition, at?: Date): ModelDefinition['peakPricing'];
94
+ /**
95
+ * A model projected onto the pricing in effect at a given instant: the staged
96
+ * {@link ModelDefinition.scheduledPricing} rates folded into the base fields
97
+ * (and its peak windows into `peakPricing`) once effective, with the staged
98
+ * entry stripped.
99
+ *
100
+ * This is what the `GET /ai/models` handler serves, so a client renders the
101
+ * rates that are actually billing right now without needing to resolve a
102
+ * schedule against its own clock — the server's clock is the only one that
103
+ * decides when a price change lands.
104
+ *
105
+ * @param modelDef - The model definition.
106
+ * @param at - The instant to project at (defaults to now).
107
+ * @returns The model with effective pricing and no `scheduledPricing`.
108
+ */
109
+ export declare function withEffectivePricing(modelDef: ModelDefinition, at?: Date): ModelDefinition;
110
+ /**
111
+ * The price multiplier in effect for a model at a given instant, in a region.
46
112
  *
47
113
  * Consults the model's {@link ModelDefinition.peakPricing} windows (UTC,
48
114
  * half-open, may wrap midnight). Metering MUST call this with each request's
49
115
  * own timestamp so peak-hour usage bills at the provider's real rate — pricing
50
116
  * everything at the flat rate silently under-meters peak traffic.
51
117
  *
118
+ * Peak windows belong to the NATIVE provider, so they apply only where the base
119
+ * rates do. A region with a {@link ModelDefinition.regionPricing} override is a
120
+ * different host billing its own complete rate card, including whether it has
121
+ * time-of-day pricing at all — and re-hosts generally do not. Applying the
122
+ * native provider's surcharge on top of a re-host's flat rates would over-bill
123
+ * every turn in its windows (DeepSeek's 2× Beijing-hours pricing charged
124
+ * against DeepInfra, which has no peak pricing).
125
+ *
52
126
  * @param modelDef - The model definition (or undefined).
53
127
  * @param at - The instant the request was made.
54
- * @returns The multiplier (`1` outside peak windows or when none are declared).
128
+ * @param region - The user's per-model region choice, if any (omitted → the
129
+ * model's default region).
130
+ * @returns The multiplier (`1` outside peak windows, when none are declared, or
131
+ * in a region that prices off its own override).
55
132
  */
56
- export declare function priceMultiplierAt(modelDef: ModelDefinition | undefined, at: Date): number;
133
+ export declare function priceMultiplierAt(modelDef: ModelDefinition | undefined, at: Date, region?: string): number;
57
134
  /**
58
135
  * Resolve a model's effective processing region: the requested region when the
59
136
  * model's {@link ModelDefinition.regions} list offers it, else the model's
@@ -80,14 +157,22 @@ export interface ModelTokenRates {
80
157
  /**
81
158
  * The token rates for a model in a given processing region: the model's
82
159
  * {@link ModelDefinition.regionPricing} override for the region when one
83
- * exists, else the base rates (the native provider's list prices). Omitted
84
- * cache fields in an override fall back to the override's input price (hosts
85
- * with no cache discount / no write premium). The region is resolved via
86
- * {@link effectiveModelRegion}, so callers may pass the raw user choice.
160
+ * exists, else the base rates in effect at `at` (the native provider's list
161
+ * prices, including any staged {@link ModelDefinition.scheduledPricing} change
162
+ * that has landed). Omitted cache fields in an override fall back to the
163
+ * override's input price (hosts with no cache discount / no write premium). The
164
+ * region is resolved via {@link effectiveModelRegion}, so callers may pass the
165
+ * raw user choice.
166
+ *
167
+ * `at` defaults to NOW rather than being required, so an existing caller cannot
168
+ * keep billing a superseded rate by omitting it — metering should still pass
169
+ * each request's own timestamp, the same way it must for
170
+ * {@link priceMultiplierAt}.
87
171
  *
88
172
  * @param modelDef - The model definition.
89
173
  * @param requested - The user's per-model region choice, if any.
174
+ * @param at - The instant to price at (defaults to now).
90
175
  * @returns The region-effective rates.
91
176
  */
92
- export declare function modelRegionRates(modelDef: ModelDefinition, requested?: string): ModelTokenRates;
177
+ export declare function modelRegionRates(modelDef: ModelDefinition, requested?: string, at?: Date): ModelTokenRates;
93
178
  //# sourceMappingURL=lookup.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"lookup.d.ts","sourceRoot":"","sources":["../src/lookup.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAE/D;;;;;;GAMG;AACH,eAAO,MAAM,SAAS,EAAE,WAAW,CAAC,MAAM,CAEzC,CAAA;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAEhE;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,YAAY,GAAG,SAAS,eAAe,EAAE,CAEtF;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,kBAAkB,EAAE,WAAW,CAAC,YAAY,CAAC,GAAG,SAAS,YAAY,EAAE,GACtE,SAAS,eAAe,EAAE,CAI5B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,eAAe,GAAG,SAAS,EAAE,EAAE,EAAE,IAAI,GAAG,MAAM,CAYzF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,eAAe,GAAG,SAAS,EACrC,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,CAGR;AAED,2DAA2D;AAC3D,MAAM,WAAW,eAAe;IAC9B,sDAAsD;IACtD,iBAAiB,EAAE,MAAM,CAAA;IACzB,8CAA8C;IAC9C,kBAAkB,EAAE,MAAM,CAAA;IAC1B,yDAAyD;IACzD,qBAAqB,EAAE,MAAM,CAAA;IAC7B,0DAA0D;IAC1D,sBAAsB,EAAE,MAAM,CAAA;CAC/B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,eAAe,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,eAAe,CAiB/F"}
1
+ {"version":3,"file":"lookup.d.ts","sourceRoot":"","sources":["../src/lookup.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAE/D;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,IAAI,CAAC,eAAe,EAAE,UAAU,GAAG,cAAc,CAAC,GACxD,OAAO,CAET;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CASvE;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS,EAAE,WAAW,CAAC,MAAM,CAEzC,CAAA;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAEhE;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,YAAY,GAAG,SAAS,eAAe,EAAE,CAEtF;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,kBAAkB,EAAE,WAAW,CAAC,YAAY,CAAC,GAAG,SAAS,YAAY,EAAE,GACtE,SAAS,eAAe,EAAE,CAI5B;AAoBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,eAAe,EACzB,EAAE,GAAE,IAAiB,GACpB,eAAe,CAgBjB;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,eAAe,EACzB,EAAE,GAAE,IAAiB,GACpB,eAAe,CAAC,aAAa,CAAC,CAMhC;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,eAAe,EACzB,EAAE,GAAE,IAAiB,GACpB,eAAe,CASjB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,eAAe,GAAG,SAAS,EACrC,EAAE,EAAE,IAAI,EACR,MAAM,CAAC,EAAE,MAAM,GACd,MAAM,CAcR;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,eAAe,GAAG,SAAS,EACrC,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,CAGR;AAED,2DAA2D;AAC3D,MAAM,WAAW,eAAe;IAC9B,sDAAsD;IACtD,iBAAiB,EAAE,MAAM,CAAA;IACzB,8CAA8C;IAC9C,kBAAkB,EAAE,MAAM,CAAA;IAC1B,yDAAyD;IACzD,qBAAqB,EAAE,MAAM,CAAA;IAC7B,0DAA0D;IAC1D,sBAAsB,EAAE,MAAM,CAAA;CAC/B;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,eAAe,EACzB,SAAS,CAAC,EAAE,MAAM,EAClB,EAAE,GAAE,IAAiB,GACpB,eAAe,CAYjB"}