@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/dist/lookup.js CHANGED
@@ -4,21 +4,57 @@
4
4
  * @module
5
5
  */
6
6
  import { MODELS } from './models.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 function isSelectableModel(model) {
17
+ return !model.disabled && !model.supersededBy;
18
+ }
19
+ /**
20
+ * Resolve a model id FORWARD to the selectable model that replaces it, following
21
+ * the {@link ModelDefinition.supersededBy} chain (a saved `qwen3.7-max` →
22
+ * `qwen3.8-max`). Lets a persisted selection keep the user's intent — the same
23
+ * tier from the same provider — instead of falling back to the platform default
24
+ * once the older generation stops being offered.
25
+ *
26
+ * @param id - The persisted model id.
27
+ * @returns The selectable successor's id, the id itself when it is already
28
+ * selectable, or `undefined` for an unknown or `disabled` model (nothing to
29
+ * forward to).
30
+ */
31
+ export function resolveSelectableModelId(id) {
32
+ let model = getModel(id);
33
+ // Bounded by the catalog size: a supersession cycle would otherwise spin here,
34
+ // and the invariant that forbids one is a test, not a runtime guarantee.
35
+ for (let hops = 0; model && !isSelectableModel(model) && hops <= MODELS.length; hops++) {
36
+ if (!model.supersededBy)
37
+ return undefined; // disabled — no successor declared
38
+ model = getModel(model.supersededBy);
39
+ }
40
+ return model && isSelectableModel(model) ? model.id : undefined;
41
+ }
7
42
  /**
8
43
  * Set of *selectable* model IDs for fast validation.
9
44
  *
10
45
  * 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.
46
+ * never be chosen for a new chat, and `supersededBy` models so an older
47
+ * generation of a family (e.g. `qwen3.7-max` next to `qwen3.8-max`) is never
48
+ * offered — while {@link getModel} still resolves both for historical pricing.
13
49
  */
14
- export const MODEL_IDS = new Set(MODELS.filter((m) => !m.disabled).map((m) => m.id));
50
+ export const MODEL_IDS = new Set(MODELS.filter(isSelectableModel).map((m) => m.id));
15
51
  /**
16
52
  * Look up a model definition by ID.
17
53
  *
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*.
54
+ * Returns `disabled` and `supersededBy` models too: a saved selection or a
55
+ * historical usage row may reference a since-retired or since-superseded model,
56
+ * and it must stay priceable. Use {@link MODEL_IDS} / {@link getAvailableModels}
57
+ * (or {@link isSelectableModel}) to decide what is *selectable*.
22
58
  *
23
59
  * @param id - The API model ID.
24
60
  * @returns The model definition, or `undefined` if not found.
@@ -39,29 +75,137 @@ export function getModelsByProvider(provider) {
39
75
  * Get models that are currently usable — filtered to only providers that are available.
40
76
  *
41
77
  * The caller passes in which provider IDs are active (i.e. have a bond wired).
42
- * `disabled` models are excluded — they are never offered for selection.
78
+ * Models that are not {@link isSelectableModel} — `disabled` or superseded by a
79
+ * newer generation — are excluded; they are never offered for selection.
43
80
  *
44
81
  * @param availableProviders - Set or array of provider IDs that have active bonds.
45
- * @returns Non-disabled models whose provider is in the available set.
82
+ * @returns Selectable models whose provider is in the available set.
46
83
  */
47
84
  export function getAvailableModels(availableProviders) {
48
85
  const providerSet = availableProviders instanceof Set ? availableProviders : new Set(availableProviders);
49
- return MODELS.filter((m) => providerSet.has(m.provider) && !m.disabled);
86
+ return MODELS.filter((m) => providerSet.has(m.provider) && isSelectableModel(m));
50
87
  }
51
88
  /**
52
- * The price multiplier in effect for a model at a given instant.
89
+ * Whether a model's staged {@link ModelDefinition.scheduledPricing} change has
90
+ * taken effect at a given instant.
91
+ *
92
+ * @param modelDef - The model definition.
93
+ * @param at - The instant to evaluate.
94
+ * @returns `true` once `at` is at or past the scheduled `effectiveFrom`.
95
+ */
96
+ function scheduledPricingApplies(modelDef, at) {
97
+ const scheduled = modelDef.scheduledPricing;
98
+ if (!scheduled)
99
+ return false;
100
+ const effectiveFrom = Date.parse(scheduled.effectiveFrom);
101
+ // An unparseable date must never silently reprice a model. Ignoring the
102
+ // staged entry keeps the current, verified rates in force.
103
+ if (Number.isNaN(effectiveFrom))
104
+ return false;
105
+ return at.getTime() >= effectiveFrom;
106
+ }
107
+ /**
108
+ * A model's BASE token rates in effect at a given instant — the staged
109
+ * {@link ModelDefinition.scheduledPricing} rates once their `effectiveFrom` has
110
+ * passed, else the base fields.
111
+ *
112
+ * These are the native provider's rates. A `regionPricing` override is a
113
+ * different host's rate card and is resolved separately by
114
+ * {@link modelRegionRates}.
115
+ *
116
+ * @param modelDef - The model definition.
117
+ * @param at - The instant to price at (defaults to now).
118
+ * @returns The base rates in effect at that instant.
119
+ */
120
+ export function effectiveBaseRates(modelDef, at = new Date()) {
121
+ const scheduled = modelDef.scheduledPricing;
122
+ if (scheduled && scheduledPricingApplies(modelDef, at)) {
123
+ return {
124
+ inputPricePerMTok: scheduled.inputPricePerMTok,
125
+ outputPricePerMTok: scheduled.outputPricePerMTok,
126
+ cacheReadPricePerMTok: scheduled.cacheReadPricePerMTok,
127
+ cacheWritePricePerMTok: scheduled.cacheWritePricePerMTok,
128
+ };
129
+ }
130
+ return {
131
+ inputPricePerMTok: modelDef.inputPricePerMTok,
132
+ outputPricePerMTok: modelDef.outputPricePerMTok,
133
+ cacheReadPricePerMTok: modelDef.cacheReadPricePerMTok,
134
+ cacheWritePricePerMTok: modelDef.cacheWritePricePerMTok,
135
+ };
136
+ }
137
+ /**
138
+ * A model's peak-hour pricing in effect at a given instant: the staged
139
+ * {@link ModelDefinition.scheduledPricing} `peakPricing` once its
140
+ * `effectiveFrom` has passed (when that entry declares one — an omitted one
141
+ * leaves the existing windows in force), else the model's own `peakPricing`.
142
+ *
143
+ * @param modelDef - The model definition.
144
+ * @param at - The instant to evaluate (defaults to now).
145
+ * @returns The peak-pricing config in effect, or `undefined` when none is.
146
+ */
147
+ export function effectivePeakPricing(modelDef, at = new Date()) {
148
+ const scheduled = modelDef.scheduledPricing;
149
+ if (scheduled?.peakPricing && scheduledPricingApplies(modelDef, at)) {
150
+ return scheduled.peakPricing;
151
+ }
152
+ return modelDef.peakPricing;
153
+ }
154
+ /**
155
+ * A model projected onto the pricing in effect at a given instant: the staged
156
+ * {@link ModelDefinition.scheduledPricing} rates folded into the base fields
157
+ * (and its peak windows into `peakPricing`) once effective, with the staged
158
+ * entry stripped.
159
+ *
160
+ * This is what the `GET /ai/models` handler serves, so a client renders the
161
+ * rates that are actually billing right now without needing to resolve a
162
+ * schedule against its own clock — the server's clock is the only one that
163
+ * decides when a price change lands.
164
+ *
165
+ * @param modelDef - The model definition.
166
+ * @param at - The instant to project at (defaults to now).
167
+ * @returns The model with effective pricing and no `scheduledPricing`.
168
+ */
169
+ export function withEffectivePricing(modelDef, at = new Date()) {
170
+ if (!modelDef.scheduledPricing)
171
+ return modelDef;
172
+ const { scheduledPricing: _scheduledPricing, ...rest } = modelDef;
173
+ const peakPricing = effectivePeakPricing(modelDef, at);
174
+ return {
175
+ ...rest,
176
+ ...effectiveBaseRates(modelDef, at),
177
+ ...(peakPricing ? { peakPricing } : {}),
178
+ };
179
+ }
180
+ /**
181
+ * The price multiplier in effect for a model at a given instant, in a region.
53
182
  *
54
183
  * Consults the model's {@link ModelDefinition.peakPricing} windows (UTC,
55
184
  * half-open, may wrap midnight). Metering MUST call this with each request's
56
185
  * own timestamp so peak-hour usage bills at the provider's real rate — pricing
57
186
  * everything at the flat rate silently under-meters peak traffic.
58
187
  *
188
+ * Peak windows belong to the NATIVE provider, so they apply only where the base
189
+ * rates do. A region with a {@link ModelDefinition.regionPricing} override is a
190
+ * different host billing its own complete rate card, including whether it has
191
+ * time-of-day pricing at all — and re-hosts generally do not. Applying the
192
+ * native provider's surcharge on top of a re-host's flat rates would over-bill
193
+ * every turn in its windows (DeepSeek's 2× Beijing-hours pricing charged
194
+ * against DeepInfra, which has no peak pricing).
195
+ *
59
196
  * @param modelDef - The model definition (or undefined).
60
197
  * @param at - The instant the request was made.
61
- * @returns The multiplier (`1` outside peak windows or when none are declared).
198
+ * @param region - The user's per-model region choice, if any (omitted → the
199
+ * model's default region).
200
+ * @returns The multiplier (`1` outside peak windows, when none are declared, or
201
+ * in a region that prices off its own override).
62
202
  */
63
- export function priceMultiplierAt(modelDef, at) {
64
- const peak = modelDef?.peakPricing;
203
+ export function priceMultiplierAt(modelDef, at, region) {
204
+ if (!modelDef)
205
+ return 1;
206
+ if (modelDef.regionPricing?.[effectiveModelRegion(modelDef, region)])
207
+ return 1;
208
+ const peak = effectivePeakPricing(modelDef, at);
65
209
  if (!peak || peak.windows.length === 0)
66
210
  return 1;
67
211
  const minute = at.getUTCHours() * 60 + at.getUTCMinutes();
@@ -92,25 +236,28 @@ export function effectiveModelRegion(modelDef, requested) {
92
236
  /**
93
237
  * The token rates for a model in a given processing region: the model's
94
238
  * {@link ModelDefinition.regionPricing} override for the region when one
95
- * exists, else the base rates (the native provider's list prices). Omitted
96
- * cache fields in an override fall back to the override's input price (hosts
97
- * with no cache discount / no write premium). The region is resolved via
98
- * {@link effectiveModelRegion}, so callers may pass the raw user choice.
239
+ * exists, else the base rates in effect at `at` (the native provider's list
240
+ * prices, including any staged {@link ModelDefinition.scheduledPricing} change
241
+ * that has landed). Omitted cache fields in an override fall back to the
242
+ * override's input price (hosts with no cache discount / no write premium). The
243
+ * region is resolved via {@link effectiveModelRegion}, so callers may pass the
244
+ * raw user choice.
245
+ *
246
+ * `at` defaults to NOW rather than being required, so an existing caller cannot
247
+ * keep billing a superseded rate by omitting it — metering should still pass
248
+ * each request's own timestamp, the same way it must for
249
+ * {@link priceMultiplierAt}.
99
250
  *
100
251
  * @param modelDef - The model definition.
101
252
  * @param requested - The user's per-model region choice, if any.
253
+ * @param at - The instant to price at (defaults to now).
102
254
  * @returns The region-effective rates.
103
255
  */
104
- export function modelRegionRates(modelDef, requested) {
256
+ export function modelRegionRates(modelDef, requested, at = new Date()) {
105
257
  const region = effectiveModelRegion(modelDef, requested);
106
258
  const override = modelDef.regionPricing?.[region];
107
259
  if (!override) {
108
- return {
109
- inputPricePerMTok: modelDef.inputPricePerMTok,
110
- outputPricePerMTok: modelDef.outputPricePerMTok,
111
- cacheReadPricePerMTok: modelDef.cacheReadPricePerMTok,
112
- cacheWritePricePerMTok: modelDef.cacheWritePricePerMTok,
113
- };
260
+ return effectiveBaseRates(modelDef, at);
114
261
  }
115
262
  return {
116
263
  inputPricePerMTok: override.inputPricePerMTok,
package/dist/models.d.ts CHANGED
@@ -28,6 +28,18 @@ import type { ModelDefinition } from './types.js';
28
28
  * control) carries `thinkingConfigurable: false` and OMITS both fields —
29
29
  * there is nothing to tune.
30
30
  *
31
+ * ONE GENERATION PER FAMILY. When a provider ships a newer generation of a
32
+ * model line, the older entry gets `supersededBy: '<newer id>'` and stops being
33
+ * offered — the picker never shows both `qwen3.7-max` and `qwen3.8-max`. The
34
+ * entry is NEVER deleted: `getModel()` still resolves it so saved selections and
35
+ * historical usage stay priceable, and a persisted id resolves forward to the
36
+ * successor. Supersede only within the same TIER: a cheaper or specialist model
37
+ * with no newer equivalent (`gemini-3.1-pro-preview`, `qwen3-coder-plus`,
38
+ * `kimi-k2.7-code`, `grok-build-0.1`) keeps at most `deprecatedAt`, so every
39
+ * provider keeps a real choice. `__tests__/lookup.test.ts` fails on any two
40
+ * selectable models of one family at different versions that aren't a
41
+ * documented exception.
42
+ *
31
43
  * Sources (verified 2026-07-28; OpenAI re-verified 2026-07-31 after the
32
44
  * 2026-07-30 GPT-5.6 repricing — cross-check prices against models.dev with
33
45
  * `npm run check:model-freshness` from the workspace root):
@@ -49,21 +61,31 @@ import type { ModelDefinition } from './types.js';
49
61
  * 2026-07-21 $1.50/$7.50 supersedes 3.5-flash as the agentic flagship;
50
62
  * gemini-3.1-pro-preview still the pro tier — "3.5 Pro" has NOT shipped as
51
63
  * of 2026-07-28 despite the coming-soon badge; do not add until it has an id)
64
+ * (re-verified 2026-08-13: gemini-3.7-flash "New Stable" — supersedes
65
+ * 3.6-flash as the flash flagship at the SAME list price ($1.50/$7.50, cache
66
+ * read $0.15), with a launch promo ($0.75/$3.75, cache read $0.075) through
67
+ * 2026-12-31 billed here at list; specs from /docs/models/gemini-3.7-flash:
68
+ * 1M ctx / 65,536 out, thinking low|medium|high (no minimal), vision, tools,
69
+ * caching, search grounding, code execution, url context)
52
70
  * - xAI: https://docs.x.ai/developers/models + /developers/grok-4-5
53
71
  * (grok-4.5 flagship 2026-07-08: $2/$6, 500K ctx, ≥200K prompts bill 2× —
54
72
  * not modeled; reasoning_effort low|medium|high default high, image input;
55
73
  * grok-4.3 still served at $1.25/$2.50 with the bigger 1M window;
56
74
  * grok-code-fast-1 no longer listed — retires 2026-08-15)
57
- * - DeepSeek: https://api-docs.deepseek.com/quick_start/pricing (unchanged V4
58
- * Pro/Flash pricing; legacy deepseek-chat/-reasoner ids fully retired
59
- * 2026-07-24 — never in this catalog; the announced peak-hour 2× surcharge is
60
- * still NOT active as of 2026-07-28, see the entries)
61
- * - Moonshot: https://platform.kimi.ai/docs/models (kimi-k3 flagship 2026-07-16
75
+ * - DeepSeek: https://api-docs.deepseek.com/quick_start/pricing (verified
76
+ * 2026-08-14; legacy deepseek-chat/-reasoner ids fully retired 2026-07-24 —
77
+ * never in this catalog. V4-Pro GA on 2026-08-13 came with a price RISE
78
+ * effective 2026-08-16T16:00Z plus the long-announced peak-hour 2×: both
79
+ * entries carry it as `scheduledPricing`, so today's rates bill until that
80
+ * instant and the new ones after. Re-verify weekday-vs-daily peak windows and
81
+ * the CN/US region default once it lands — see the entries.)
82
+ * - Moonshot: https://platform.kimi.ai/docs/models + DeepInfra's model API for
83
+ * the US re-host (kimi-k3 flagship 2026-07-16
62
84
  * — 2.8T MoE, 1M ctx, $3/$15 — NOT added: thinking is forced-on with
63
85
  * reasoning_content that must be replayed through tool loops, the same
64
- * constraint that keeps kimi-k2.7-code out; add BOTH once the moonshot bond
65
- * supports preserved thinking + reasoning_effort low|high|max. kimi-k2.6
66
- * remains the newest model the bond can run correctly.)
86
+ * constraint that kept kimi-k2.7-code out. BOTH are now in the catalog: the
87
+ * moonshot bond gained preserved thinking (reasoning replayed through tool
88
+ * loops), so kimi-k3 is the Moonshot pick.)
67
89
  * - MiniMax: https://platform.minimax.io/docs/guides/pricing-paygo (unchanged;
68
90
  * minimax-m3 $0.30/$1.20 is a "permanent 50% off" list rate)
69
91
  * - Alibaba: https://www.alibabacloud.com/help/en/model-studio/deep-thinking
@@ -1 +1 @@
1
- {"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAEjD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6EG;AACH,eAAO,MAAM,MAAM,EAAE,SAAS,eAAe,EAmtCnC,CAAA"}
1
+ {"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAEjD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmGG;AACH,eAAO,MAAM,MAAM,EAAE,SAAS,eAAe,EA+5CnC,CAAA"}