ai-lcr 0.4.0 → 0.5.1

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/CHANGELOG.md CHANGED
@@ -4,6 +4,28 @@ All notable changes to `ai-lcr` are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/), and the project adheres to
5
5
  [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.5.0] — 2026-06-02
8
+
9
+ All additions are optional and backward compatible.
10
+
11
+ ### Added
12
+
13
+ - **Official-price savings baseline for media.** A media model's savings baseline
14
+ is now the model-maker's first-party list price — what a user pays going
15
+ *direct*, bypassing the cheaper providers we route to — instead of the priciest
16
+ provider we happen to route between. For the common case of a model served by a
17
+ single aggregator (Runware, fal, …), the old baseline equalled the actual cost,
18
+ so savings showed as `$0`; the official price surfaces the real saving.
19
+ - `MediaModelDef.official?: MediaPricing` — an inline first-party price on a
20
+ model def. When set, it wins.
21
+ - `MediaLCRConfig.officialPrices?: Record<string, MediaPricing>` — a modelId →
22
+ price map so a downstream registry gets correct baselines without inlining
23
+ prices. Defaults to the bundled **`OFFICIAL_PRICES`** (now exported), lifted
24
+ from the cross-provider price table by `scripts/gen-media-official.mjs`.
25
+ - When no official price is known (e.g. open-weight models served only by
26
+ aggregators), the baseline falls back to the priciest configured route — or
27
+ none if there's a single route — exactly as before.
28
+
7
29
  ## [0.4.0] — 2026-06-02
8
30
 
9
31
  All additions are optional and backward compatible.
package/dist/index.cjs CHANGED
@@ -22,6 +22,7 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  DEFAULT_REFERENCE: () => DEFAULT_REFERENCE,
24
24
  MEDIA_PRICING: () => MEDIA_PRICING,
25
+ OFFICIAL_PRICES: () => OFFICIAL_PRICES,
25
26
  cheapestRoute: () => cheapestRoute,
26
27
  classifyError: () => classifyError,
27
28
  classifyErrorKind: () => classifyErrorKind,
@@ -193,6 +194,11 @@ function costForUsage(cost, inputTokens, outputTokens, cacheReadTokens) {
193
194
  const cachedRate = cost.cacheRead ?? cost.input;
194
195
  return fullInput / 1e6 * cost.input + cached / 1e6 * cachedRate + outputTokens / 1e6 * cost.output;
195
196
  }
197
+ function cacheSavingForUsage(cost, inputTokens, cacheReadTokens) {
198
+ if (cost.cacheRead === void 0) return 0;
199
+ const cached = Math.min(Math.max(cacheReadTokens, 0), inputTokens);
200
+ return cached / 1e6 * (cost.input - cost.cacheRead);
201
+ }
196
202
  function requestIdFrom(options) {
197
203
  const raw = options.providerOptions?.lcr?.requestId;
198
204
  return typeof raw === "string" && raw.length > 0 ? raw : void 0;
@@ -297,19 +303,22 @@ var LcrFallbackModel = class {
297
303
  });
298
304
  }
299
305
  /**
300
- * Baseline = what this same usage would have cost on the most expensive
301
- * *priced* provider in the chain (typically the OpenRouter fallback leg). The
302
- * winner's savings is `baselineUsd - costUsd`. Undefined when no provider in
303
- * the chain carries a price (nothing to compare against).
306
+ * Baseline = what this same usage would have cost on the always-on fallback:
307
+ * the LAST priced leg of the chain (by convention the list-price provider you'd
308
+ * use without routing e.g. OpenRouter, always last). The winner's saving is
309
+ * `baselineUsd - costUsd`. We take the last priced leg, NOT the most expensive
310
+ * one: prompt caching can make a sticker-cheaper provider (no `cacheRead`) cost
311
+ * MORE on a cache-heavy call, and a max-of-chain baseline would then fabricate a
312
+ * "saving" even on calls the fallback itself served. Undefined when no provider
313
+ * in the chain carries a price (nothing to compare against).
304
314
  */
305
315
  baselineUsd(inputTokens, outputTokens, cacheReadTokens) {
306
- let max;
316
+ let baseline;
307
317
  for (const p of this.opts.providers) {
308
318
  if (!p.cost) continue;
309
- const c = costForUsage(p.cost, inputTokens, outputTokens, cacheReadTokens);
310
- if (max === void 0 || c > max) max = c;
319
+ baseline = costForUsage(p.cost, inputTokens, outputTokens, cacheReadTokens);
311
320
  }
312
- return max;
321
+ return baseline;
313
322
  }
314
323
  /** Winner settled: record the attempt, fire `onCost` (compat) + `onCall`. */
315
324
  finalizeOk(ctx, provider, attemptStart, usage, ttftMs) {
@@ -318,6 +327,7 @@ var LcrFallbackModel = class {
318
327
  const outputTokens = usage?.outputTokens?.total ?? 0;
319
328
  const cacheReadTokens = usage?.inputTokens?.cacheRead ?? 0;
320
329
  const costUsd = provider.cost ? costForUsage(provider.cost, inputTokens, outputTokens, cacheReadTokens) : 0;
330
+ const cachedSavingUsd = provider.cost ? cacheSavingForUsage(provider.cost, inputTokens, cacheReadTokens) : 0;
321
331
  const usageMissing = inputTokens === 0 && outputTokens === 0;
322
332
  this.emitCost({
323
333
  model: this.opts.modelName,
@@ -340,6 +350,7 @@ var LcrFallbackModel = class {
340
350
  ...cacheReadTokens > 0 ? { cachedInputTokens: cacheReadTokens } : {},
341
351
  costUsd,
342
352
  baselineUsd: this.baselineUsd(inputTokens, outputTokens, cacheReadTokens),
353
+ ...cachedSavingUsd > 0 ? { cachedSavingUsd } : {},
343
354
  ...ctx.requestId ? { requestId: ctx.requestId } : {},
344
355
  ...usageMissing ? { usageMissing: true } : {}
345
356
  });
@@ -569,6 +580,68 @@ function createHttpSink(options) {
569
580
  };
570
581
  }
571
582
 
583
+ // src/media-official.ts
584
+ var OFFICIAL_PRICES = {
585
+ "alibaba/qwen-image": { unit: "image", cents: 3.5 },
586
+ "alibaba/qwen-image-edit": { unit: "image", cents: 4.5 },
587
+ "alibaba/wan-2-2-i2v": { unit: "call", cents: 50 },
588
+ "alibaba/wan-2-2-t2v": { unit: "call", cents: 50 },
589
+ "alibaba/wan-2-5-i2v": { unit: "call", cents: 75 },
590
+ "alibaba/wan-2-5-t2v": { unit: "call", cents: 75 },
591
+ "alibaba/wan-2-7-image-pro": { unit: "image", cents: 7.5 },
592
+ "alibaba/wan-2-7-t2v": { unit: "call", cents: 75 },
593
+ "alibaba/z-image-turbo": { unit: "image", cents: 1.5 },
594
+ "bfl/flux-1.1-pro": { unit: "image", cents: 4 },
595
+ "bfl/flux-2-flex": { unit: "image", cents: 6 },
596
+ "bfl/flux-2-klein-4b": { unit: "image", cents: 1.4 },
597
+ "bfl/flux-2-klein-9b": { unit: "image", cents: 1.5 },
598
+ "bfl/flux-2-pro": { unit: "image", cents: 3 },
599
+ "bfl/flux-kontext-max": { unit: "image", cents: 8 },
600
+ "bfl/flux-kontext-pro": { unit: "image", cents: 4 },
601
+ "bria/rmbg-2": { unit: "image", cents: 1.8 },
602
+ "bytedance/seedance-2-0": { unit: "call", cents: 187 },
603
+ "bytedance/seedance-2-0-fast": { unit: "call", cents: 60 },
604
+ "bytedance/seedance-2-0-fast-i2v": { unit: "call", cents: 60 },
605
+ "bytedance/seedance-v1-pro": { unit: "call", cents: 61 },
606
+ "bytedance/seedance-v1-pro-i2v": { unit: "call", cents: 61 },
607
+ "bytedance/seedream-4-5": { unit: "image", cents: 3 },
608
+ "bytedance/seedream-5-lite": { unit: "image", cents: 3.5 },
609
+ "google/imagen-4-ultra": { unit: "image", cents: 6 },
610
+ "google/nano-banana": { unit: "image", cents: 3.9 },
611
+ "google/nano-banana-2": { unit: "image", cents: 6.7 },
612
+ "google/nano-banana-pro": { unit: "image", cents: 13.4 },
613
+ "google/veo-3-1": { unit: "call", cents: 200 },
614
+ "google/veo-3-1-lite": { unit: "call", cents: 40 },
615
+ "google/veo-3-quality": { unit: "call", cents: 200 },
616
+ "ideogram/v3-balanced": { unit: "image", cents: 6 },
617
+ "kuaishou/kling-image-3": { unit: "image", cents: 2.8 },
618
+ "kuaishou/kling-image-o3": { unit: "image", cents: 2.8 },
619
+ "kuaishou/kling-motion-control": { unit: "call", cents: 56 },
620
+ "kuaishou/kling-v21-master": { unit: "call", cents: 56 },
621
+ "kuaishou/kling-v21-master-i2v": { unit: "call", cents: 56 },
622
+ "kuaishou/kling-v26-pro-i2v": { unit: "call", cents: 56 },
623
+ "kuaishou/kling-v3-pro": { unit: "call", cents: 56 },
624
+ "kuaishou/kling-v3-pro-i2v": { unit: "call", cents: 56 },
625
+ "lightricks/ltx-2": { unit: "call", cents: 30 },
626
+ "lightricks/ltx-2-i2v": { unit: "call", cents: 30 },
627
+ "minimax/hailuo-02-pro": { unit: "call", cents: 53 },
628
+ "minimax/hailuo-02-standard": { unit: "call", cents: 27 },
629
+ "minimax/hailuo-2-3-pro": { unit: "call", cents: 53 },
630
+ "openai/gpt-image-1-5": { unit: "image", cents: 3.4 },
631
+ "openai/gpt-image-2": { unit: "image", cents: 5.3 },
632
+ "openai/gpt-image-2-high": { unit: "image", cents: 21.1 },
633
+ "openai/sora-2": { unit: "call", cents: 50 },
634
+ "openai/sora-2-i2v": { unit: "call", cents: 50 },
635
+ "pixverse/v5-5-i2v": { unit: "call", cents: 60 },
636
+ "pixverse/v6": { unit: "call", cents: 45 },
637
+ "recraft/v3": { unit: "image", cents: 4 },
638
+ "recraft/v4-1": { unit: "image", cents: 25 },
639
+ "runway/gen-4-image": { unit: "image", cents: 8 },
640
+ "stability/fast-sdxl": { unit: "image", cents: 0.9 },
641
+ "stability/stable-diffusion-3": { unit: "image", cents: 6.5 },
642
+ "xai/grok-image-quality": { unit: "image", cents: 7 }
643
+ };
644
+
572
645
  // src/media.ts
573
646
  var DEFAULT_REFERENCE = {
574
647
  image: { width: 1920, height: 1080 },
@@ -620,7 +693,15 @@ function newMediaCallId() {
620
693
  return c?.randomUUID ? c.randomUUID() : `lcr_${Date.now().toString(36)}`;
621
694
  }
622
695
  function createMediaLCR(config) {
623
- const { registry, adapters, reference = DEFAULT_REFERENCE, onError, onCost, onCall } = config;
696
+ const {
697
+ registry,
698
+ adapters,
699
+ reference = DEFAULT_REFERENCE,
700
+ officialPrices = OFFICIAL_PRICES,
701
+ onError,
702
+ onCost,
703
+ onCall
704
+ } = config;
624
705
  const safeError = (error, provider) => {
625
706
  try {
626
707
  onError?.(error, provider);
@@ -645,7 +726,8 @@ function createMediaLCR(config) {
645
726
  throw new Error(`ai-lcr: unknown media model "${modelId}" \u2014 add it to the registry`);
646
727
  }
647
728
  const ranked = rankRoutes(def, reference);
648
- const baselineUsd = ranked.length > 0 ? Math.max(...ranked.map((r) => r.refCents)) / 100 : 0;
729
+ const official = def.official ?? officialPrices[modelId];
730
+ const baselineUsd = official !== void 0 ? normalizedCents(official, reference) / 100 : ranked.length > 0 ? Math.max(...ranked.map((r) => r.refCents)) / 100 : 0;
649
731
  const startedAt = Date.now();
650
732
  const attempts = [];
651
733
  let lastErr;
@@ -1130,6 +1212,7 @@ function createLCR(config) {
1130
1212
  0 && (module.exports = {
1131
1213
  DEFAULT_REFERENCE,
1132
1214
  MEDIA_PRICING,
1215
+ OFFICIAL_PRICES,
1133
1216
  cheapestRoute,
1134
1217
  classifyError,
1135
1218
  classifyErrorKind,
package/dist/index.d.cts CHANGED
@@ -109,12 +109,25 @@ interface CallRecord {
109
109
  /** Computed from the winner's `cost`; 0 if no price was given or the call failed. */
110
110
  costUsd: number;
111
111
  /**
112
- * What the same request would have cost on the most expensive *priced*
113
- * provider in the chain, on identical token usage the savings baseline
114
- * (`baselineUsd - costUsd`). Set by both routers whenever at least one
115
- * provider carries a `cost`; undefined only when no provider was priced.
112
+ * What this same usage would have cost on the savings baseline, so
113
+ * `baselineUsd - costUsd` is what routing actually saved. Text router: the
114
+ * always-on fallback leg the LAST priced provider in the chain, i.e. the
115
+ * list-price provider you'd fall back to without routing (e.g. OpenRouter).
116
+ * Media router: the model-maker's official direct price. NOT the most
117
+ * expensive leg of the chain: prompt caching can make a sticker-cheaper
118
+ * provider cost more on a cache-heavy call, and a max-of-chain baseline would
119
+ * fabricate a "saving" on calls the fallback itself served. Undefined only
120
+ * when no provider was priced.
116
121
  */
117
122
  baselineUsd?: number;
123
+ /**
124
+ * The slice of `costUsd` that prompt-cache reads saved versus paying the full
125
+ * input rate for those same tokens (`cachedTokens × (input − cacheRead)`).
126
+ * Present only when > 0. This is the serving provider's own caching benefit —
127
+ * it happens with or without routing — so it is NOT a routing saving and must
128
+ * be surfaced separately, never folded into `baselineUsd - costUsd`.
129
+ */
130
+ cachedSavingUsd?: number;
118
131
  /**
119
132
  * Caller-supplied correlation id, read from `providerOptions.lcr.requestId`
120
133
  * on the call. Multi-step tool loops emit one record per `doStream`/
@@ -264,6 +277,15 @@ interface MediaModelDef {
264
277
  modality: MediaModality;
265
278
  /** Providers that serve this model. Order is irrelevant — routing sorts by cost. */
266
279
  routes: MediaRoute[];
280
+ /**
281
+ * The model-maker's first-party list price — what a user pays going DIRECT,
282
+ * bypassing the cheaper providers we route to. When set, it's the savings
283
+ * baseline (savings = official − actual cost). Omit for open-weight models
284
+ * with no first-party API price; those fall back to the priciest configured
285
+ * route, or no baseline if there's only one. Can also be supplied out-of-band
286
+ * via {@link MediaLCRConfig.officialPrices} so a registry needn't carry it inline.
287
+ */
288
+ official?: MediaPricing;
267
289
  }
268
290
  type MediaRegistry = Record<string, MediaModelDef>;
269
291
  /**
@@ -347,6 +369,14 @@ interface MediaLCRConfig {
347
369
  /** Adapters keyed by provider. A route with no adapter is skipped. */
348
370
  adapters: Record<string, MediaAdapter>;
349
371
  reference?: ReferenceSpec;
372
+ /**
373
+ * Model-maker first-party list prices keyed by modelId — the savings baseline
374
+ * for a model whose registry def carries no inline `official` price. Lets a
375
+ * downstream registry (e.g. ai-art's) get correct baselines without inlining
376
+ * prices. Defaults to the bundled {@link OFFICIAL_PRICES} (lifted from the
377
+ * cross-provider price table). A def's inline `official` wins over this.
378
+ */
379
+ officialPrices?: Record<string, MediaPricing>;
350
380
  onError?: (error: Error, provider: string) => void;
351
381
  onCost?: (event: MediaCostEvent) => void;
352
382
  /**
@@ -387,6 +417,8 @@ declare function createMediaLCR(config: MediaLCRConfig): (modelId: string, input
387
417
 
388
418
  declare const MEDIA_PRICING: MediaRegistry;
389
419
 
420
+ declare const OFFICIAL_PRICES: Record<string, MediaPricing>;
421
+
390
422
  /**
391
423
  * Kunavo media adapter — image (sync) + video (async poll).
392
424
  *
@@ -545,4 +577,4 @@ type LCRRouter = (modelName: string) => LanguageModelV3;
545
577
  */
546
578
  declare function createLCR(config: LCRConfig): LCRRouter;
547
579
 
548
- export { type CallRecord, type CostEvent, DEFAULT_REFERENCE, type ErrorKind, type FormatOptions, type HttpSinkOptions, type LCRConfig, type LCRRouter, MEDIA_PRICING, type MediaAdapter, type MediaCostEvent, type MediaGenerateRequest, type MediaGenerateResult, type MediaLCRConfig, type MediaModality, type MediaModelDef, type MediaOutput, type MediaPricing, type MediaRegistry, type MediaRoute, type MediaRunResult, type MediaUnit, type PriceComparisonRow, type ProviderCost, type ProviderEntry, type RankedRoute, type ReferenceSpec, type RouteAttempt, cheapestRoute, classifyError, classifyErrorKind, comparePrices, createFalMediaAdapter, createHttpSink, createKunavoMediaAdapter, createLCR, createMediaLCR, createRunwareMediaAdapter, formatCallRecord, normalizedCents, rankRoutes, referenceMegapixels };
580
+ export { type CallRecord, type CostEvent, DEFAULT_REFERENCE, type ErrorKind, type FormatOptions, type HttpSinkOptions, type LCRConfig, type LCRRouter, MEDIA_PRICING, type MediaAdapter, type MediaCostEvent, type MediaGenerateRequest, type MediaGenerateResult, type MediaLCRConfig, type MediaModality, type MediaModelDef, type MediaOutput, type MediaPricing, type MediaRegistry, type MediaRoute, type MediaRunResult, type MediaUnit, OFFICIAL_PRICES, type PriceComparisonRow, type ProviderCost, type ProviderEntry, type RankedRoute, type ReferenceSpec, type RouteAttempt, cheapestRoute, classifyError, classifyErrorKind, comparePrices, createFalMediaAdapter, createHttpSink, createKunavoMediaAdapter, createLCR, createMediaLCR, createRunwareMediaAdapter, formatCallRecord, normalizedCents, rankRoutes, referenceMegapixels };
package/dist/index.d.ts CHANGED
@@ -109,12 +109,25 @@ interface CallRecord {
109
109
  /** Computed from the winner's `cost`; 0 if no price was given or the call failed. */
110
110
  costUsd: number;
111
111
  /**
112
- * What the same request would have cost on the most expensive *priced*
113
- * provider in the chain, on identical token usage the savings baseline
114
- * (`baselineUsd - costUsd`). Set by both routers whenever at least one
115
- * provider carries a `cost`; undefined only when no provider was priced.
112
+ * What this same usage would have cost on the savings baseline, so
113
+ * `baselineUsd - costUsd` is what routing actually saved. Text router: the
114
+ * always-on fallback leg the LAST priced provider in the chain, i.e. the
115
+ * list-price provider you'd fall back to without routing (e.g. OpenRouter).
116
+ * Media router: the model-maker's official direct price. NOT the most
117
+ * expensive leg of the chain: prompt caching can make a sticker-cheaper
118
+ * provider cost more on a cache-heavy call, and a max-of-chain baseline would
119
+ * fabricate a "saving" on calls the fallback itself served. Undefined only
120
+ * when no provider was priced.
116
121
  */
117
122
  baselineUsd?: number;
123
+ /**
124
+ * The slice of `costUsd` that prompt-cache reads saved versus paying the full
125
+ * input rate for those same tokens (`cachedTokens × (input − cacheRead)`).
126
+ * Present only when > 0. This is the serving provider's own caching benefit —
127
+ * it happens with or without routing — so it is NOT a routing saving and must
128
+ * be surfaced separately, never folded into `baselineUsd - costUsd`.
129
+ */
130
+ cachedSavingUsd?: number;
118
131
  /**
119
132
  * Caller-supplied correlation id, read from `providerOptions.lcr.requestId`
120
133
  * on the call. Multi-step tool loops emit one record per `doStream`/
@@ -264,6 +277,15 @@ interface MediaModelDef {
264
277
  modality: MediaModality;
265
278
  /** Providers that serve this model. Order is irrelevant — routing sorts by cost. */
266
279
  routes: MediaRoute[];
280
+ /**
281
+ * The model-maker's first-party list price — what a user pays going DIRECT,
282
+ * bypassing the cheaper providers we route to. When set, it's the savings
283
+ * baseline (savings = official − actual cost). Omit for open-weight models
284
+ * with no first-party API price; those fall back to the priciest configured
285
+ * route, or no baseline if there's only one. Can also be supplied out-of-band
286
+ * via {@link MediaLCRConfig.officialPrices} so a registry needn't carry it inline.
287
+ */
288
+ official?: MediaPricing;
267
289
  }
268
290
  type MediaRegistry = Record<string, MediaModelDef>;
269
291
  /**
@@ -347,6 +369,14 @@ interface MediaLCRConfig {
347
369
  /** Adapters keyed by provider. A route with no adapter is skipped. */
348
370
  adapters: Record<string, MediaAdapter>;
349
371
  reference?: ReferenceSpec;
372
+ /**
373
+ * Model-maker first-party list prices keyed by modelId — the savings baseline
374
+ * for a model whose registry def carries no inline `official` price. Lets a
375
+ * downstream registry (e.g. ai-art's) get correct baselines without inlining
376
+ * prices. Defaults to the bundled {@link OFFICIAL_PRICES} (lifted from the
377
+ * cross-provider price table). A def's inline `official` wins over this.
378
+ */
379
+ officialPrices?: Record<string, MediaPricing>;
350
380
  onError?: (error: Error, provider: string) => void;
351
381
  onCost?: (event: MediaCostEvent) => void;
352
382
  /**
@@ -387,6 +417,8 @@ declare function createMediaLCR(config: MediaLCRConfig): (modelId: string, input
387
417
 
388
418
  declare const MEDIA_PRICING: MediaRegistry;
389
419
 
420
+ declare const OFFICIAL_PRICES: Record<string, MediaPricing>;
421
+
390
422
  /**
391
423
  * Kunavo media adapter — image (sync) + video (async poll).
392
424
  *
@@ -545,4 +577,4 @@ type LCRRouter = (modelName: string) => LanguageModelV3;
545
577
  */
546
578
  declare function createLCR(config: LCRConfig): LCRRouter;
547
579
 
548
- export { type CallRecord, type CostEvent, DEFAULT_REFERENCE, type ErrorKind, type FormatOptions, type HttpSinkOptions, type LCRConfig, type LCRRouter, MEDIA_PRICING, type MediaAdapter, type MediaCostEvent, type MediaGenerateRequest, type MediaGenerateResult, type MediaLCRConfig, type MediaModality, type MediaModelDef, type MediaOutput, type MediaPricing, type MediaRegistry, type MediaRoute, type MediaRunResult, type MediaUnit, type PriceComparisonRow, type ProviderCost, type ProviderEntry, type RankedRoute, type ReferenceSpec, type RouteAttempt, cheapestRoute, classifyError, classifyErrorKind, comparePrices, createFalMediaAdapter, createHttpSink, createKunavoMediaAdapter, createLCR, createMediaLCR, createRunwareMediaAdapter, formatCallRecord, normalizedCents, rankRoutes, referenceMegapixels };
580
+ export { type CallRecord, type CostEvent, DEFAULT_REFERENCE, type ErrorKind, type FormatOptions, type HttpSinkOptions, type LCRConfig, type LCRRouter, MEDIA_PRICING, type MediaAdapter, type MediaCostEvent, type MediaGenerateRequest, type MediaGenerateResult, type MediaLCRConfig, type MediaModality, type MediaModelDef, type MediaOutput, type MediaPricing, type MediaRegistry, type MediaRoute, type MediaRunResult, type MediaUnit, OFFICIAL_PRICES, type PriceComparisonRow, type ProviderCost, type ProviderEntry, type RankedRoute, type ReferenceSpec, type RouteAttempt, cheapestRoute, classifyError, classifyErrorKind, comparePrices, createFalMediaAdapter, createHttpSink, createKunavoMediaAdapter, createLCR, createMediaLCR, createRunwareMediaAdapter, formatCallRecord, normalizedCents, rankRoutes, referenceMegapixels };
package/dist/index.js CHANGED
@@ -152,6 +152,11 @@ function costForUsage(cost, inputTokens, outputTokens, cacheReadTokens) {
152
152
  const cachedRate = cost.cacheRead ?? cost.input;
153
153
  return fullInput / 1e6 * cost.input + cached / 1e6 * cachedRate + outputTokens / 1e6 * cost.output;
154
154
  }
155
+ function cacheSavingForUsage(cost, inputTokens, cacheReadTokens) {
156
+ if (cost.cacheRead === void 0) return 0;
157
+ const cached = Math.min(Math.max(cacheReadTokens, 0), inputTokens);
158
+ return cached / 1e6 * (cost.input - cost.cacheRead);
159
+ }
155
160
  function requestIdFrom(options) {
156
161
  const raw = options.providerOptions?.lcr?.requestId;
157
162
  return typeof raw === "string" && raw.length > 0 ? raw : void 0;
@@ -256,19 +261,22 @@ var LcrFallbackModel = class {
256
261
  });
257
262
  }
258
263
  /**
259
- * Baseline = what this same usage would have cost on the most expensive
260
- * *priced* provider in the chain (typically the OpenRouter fallback leg). The
261
- * winner's savings is `baselineUsd - costUsd`. Undefined when no provider in
262
- * the chain carries a price (nothing to compare against).
264
+ * Baseline = what this same usage would have cost on the always-on fallback:
265
+ * the LAST priced leg of the chain (by convention the list-price provider you'd
266
+ * use without routing e.g. OpenRouter, always last). The winner's saving is
267
+ * `baselineUsd - costUsd`. We take the last priced leg, NOT the most expensive
268
+ * one: prompt caching can make a sticker-cheaper provider (no `cacheRead`) cost
269
+ * MORE on a cache-heavy call, and a max-of-chain baseline would then fabricate a
270
+ * "saving" even on calls the fallback itself served. Undefined when no provider
271
+ * in the chain carries a price (nothing to compare against).
263
272
  */
264
273
  baselineUsd(inputTokens, outputTokens, cacheReadTokens) {
265
- let max;
274
+ let baseline;
266
275
  for (const p of this.opts.providers) {
267
276
  if (!p.cost) continue;
268
- const c = costForUsage(p.cost, inputTokens, outputTokens, cacheReadTokens);
269
- if (max === void 0 || c > max) max = c;
277
+ baseline = costForUsage(p.cost, inputTokens, outputTokens, cacheReadTokens);
270
278
  }
271
- return max;
279
+ return baseline;
272
280
  }
273
281
  /** Winner settled: record the attempt, fire `onCost` (compat) + `onCall`. */
274
282
  finalizeOk(ctx, provider, attemptStart, usage, ttftMs) {
@@ -277,6 +285,7 @@ var LcrFallbackModel = class {
277
285
  const outputTokens = usage?.outputTokens?.total ?? 0;
278
286
  const cacheReadTokens = usage?.inputTokens?.cacheRead ?? 0;
279
287
  const costUsd = provider.cost ? costForUsage(provider.cost, inputTokens, outputTokens, cacheReadTokens) : 0;
288
+ const cachedSavingUsd = provider.cost ? cacheSavingForUsage(provider.cost, inputTokens, cacheReadTokens) : 0;
280
289
  const usageMissing = inputTokens === 0 && outputTokens === 0;
281
290
  this.emitCost({
282
291
  model: this.opts.modelName,
@@ -299,6 +308,7 @@ var LcrFallbackModel = class {
299
308
  ...cacheReadTokens > 0 ? { cachedInputTokens: cacheReadTokens } : {},
300
309
  costUsd,
301
310
  baselineUsd: this.baselineUsd(inputTokens, outputTokens, cacheReadTokens),
311
+ ...cachedSavingUsd > 0 ? { cachedSavingUsd } : {},
302
312
  ...ctx.requestId ? { requestId: ctx.requestId } : {},
303
313
  ...usageMissing ? { usageMissing: true } : {}
304
314
  });
@@ -528,6 +538,68 @@ function createHttpSink(options) {
528
538
  };
529
539
  }
530
540
 
541
+ // src/media-official.ts
542
+ var OFFICIAL_PRICES = {
543
+ "alibaba/qwen-image": { unit: "image", cents: 3.5 },
544
+ "alibaba/qwen-image-edit": { unit: "image", cents: 4.5 },
545
+ "alibaba/wan-2-2-i2v": { unit: "call", cents: 50 },
546
+ "alibaba/wan-2-2-t2v": { unit: "call", cents: 50 },
547
+ "alibaba/wan-2-5-i2v": { unit: "call", cents: 75 },
548
+ "alibaba/wan-2-5-t2v": { unit: "call", cents: 75 },
549
+ "alibaba/wan-2-7-image-pro": { unit: "image", cents: 7.5 },
550
+ "alibaba/wan-2-7-t2v": { unit: "call", cents: 75 },
551
+ "alibaba/z-image-turbo": { unit: "image", cents: 1.5 },
552
+ "bfl/flux-1.1-pro": { unit: "image", cents: 4 },
553
+ "bfl/flux-2-flex": { unit: "image", cents: 6 },
554
+ "bfl/flux-2-klein-4b": { unit: "image", cents: 1.4 },
555
+ "bfl/flux-2-klein-9b": { unit: "image", cents: 1.5 },
556
+ "bfl/flux-2-pro": { unit: "image", cents: 3 },
557
+ "bfl/flux-kontext-max": { unit: "image", cents: 8 },
558
+ "bfl/flux-kontext-pro": { unit: "image", cents: 4 },
559
+ "bria/rmbg-2": { unit: "image", cents: 1.8 },
560
+ "bytedance/seedance-2-0": { unit: "call", cents: 187 },
561
+ "bytedance/seedance-2-0-fast": { unit: "call", cents: 60 },
562
+ "bytedance/seedance-2-0-fast-i2v": { unit: "call", cents: 60 },
563
+ "bytedance/seedance-v1-pro": { unit: "call", cents: 61 },
564
+ "bytedance/seedance-v1-pro-i2v": { unit: "call", cents: 61 },
565
+ "bytedance/seedream-4-5": { unit: "image", cents: 3 },
566
+ "bytedance/seedream-5-lite": { unit: "image", cents: 3.5 },
567
+ "google/imagen-4-ultra": { unit: "image", cents: 6 },
568
+ "google/nano-banana": { unit: "image", cents: 3.9 },
569
+ "google/nano-banana-2": { unit: "image", cents: 6.7 },
570
+ "google/nano-banana-pro": { unit: "image", cents: 13.4 },
571
+ "google/veo-3-1": { unit: "call", cents: 200 },
572
+ "google/veo-3-1-lite": { unit: "call", cents: 40 },
573
+ "google/veo-3-quality": { unit: "call", cents: 200 },
574
+ "ideogram/v3-balanced": { unit: "image", cents: 6 },
575
+ "kuaishou/kling-image-3": { unit: "image", cents: 2.8 },
576
+ "kuaishou/kling-image-o3": { unit: "image", cents: 2.8 },
577
+ "kuaishou/kling-motion-control": { unit: "call", cents: 56 },
578
+ "kuaishou/kling-v21-master": { unit: "call", cents: 56 },
579
+ "kuaishou/kling-v21-master-i2v": { unit: "call", cents: 56 },
580
+ "kuaishou/kling-v26-pro-i2v": { unit: "call", cents: 56 },
581
+ "kuaishou/kling-v3-pro": { unit: "call", cents: 56 },
582
+ "kuaishou/kling-v3-pro-i2v": { unit: "call", cents: 56 },
583
+ "lightricks/ltx-2": { unit: "call", cents: 30 },
584
+ "lightricks/ltx-2-i2v": { unit: "call", cents: 30 },
585
+ "minimax/hailuo-02-pro": { unit: "call", cents: 53 },
586
+ "minimax/hailuo-02-standard": { unit: "call", cents: 27 },
587
+ "minimax/hailuo-2-3-pro": { unit: "call", cents: 53 },
588
+ "openai/gpt-image-1-5": { unit: "image", cents: 3.4 },
589
+ "openai/gpt-image-2": { unit: "image", cents: 5.3 },
590
+ "openai/gpt-image-2-high": { unit: "image", cents: 21.1 },
591
+ "openai/sora-2": { unit: "call", cents: 50 },
592
+ "openai/sora-2-i2v": { unit: "call", cents: 50 },
593
+ "pixverse/v5-5-i2v": { unit: "call", cents: 60 },
594
+ "pixverse/v6": { unit: "call", cents: 45 },
595
+ "recraft/v3": { unit: "image", cents: 4 },
596
+ "recraft/v4-1": { unit: "image", cents: 25 },
597
+ "runway/gen-4-image": { unit: "image", cents: 8 },
598
+ "stability/fast-sdxl": { unit: "image", cents: 0.9 },
599
+ "stability/stable-diffusion-3": { unit: "image", cents: 6.5 },
600
+ "xai/grok-image-quality": { unit: "image", cents: 7 }
601
+ };
602
+
531
603
  // src/media.ts
532
604
  var DEFAULT_REFERENCE = {
533
605
  image: { width: 1920, height: 1080 },
@@ -579,7 +651,15 @@ function newMediaCallId() {
579
651
  return c?.randomUUID ? c.randomUUID() : `lcr_${Date.now().toString(36)}`;
580
652
  }
581
653
  function createMediaLCR(config) {
582
- const { registry, adapters, reference = DEFAULT_REFERENCE, onError, onCost, onCall } = config;
654
+ const {
655
+ registry,
656
+ adapters,
657
+ reference = DEFAULT_REFERENCE,
658
+ officialPrices = OFFICIAL_PRICES,
659
+ onError,
660
+ onCost,
661
+ onCall
662
+ } = config;
583
663
  const safeError = (error, provider) => {
584
664
  try {
585
665
  onError?.(error, provider);
@@ -604,7 +684,8 @@ function createMediaLCR(config) {
604
684
  throw new Error(`ai-lcr: unknown media model "${modelId}" \u2014 add it to the registry`);
605
685
  }
606
686
  const ranked = rankRoutes(def, reference);
607
- const baselineUsd = ranked.length > 0 ? Math.max(...ranked.map((r) => r.refCents)) / 100 : 0;
687
+ const official = def.official ?? officialPrices[modelId];
688
+ const baselineUsd = official !== void 0 ? normalizedCents(official, reference) / 100 : ranked.length > 0 ? Math.max(...ranked.map((r) => r.refCents)) / 100 : 0;
608
689
  const startedAt = Date.now();
609
690
  const attempts = [];
610
691
  let lastErr;
@@ -1088,6 +1169,7 @@ function createLCR(config) {
1088
1169
  export {
1089
1170
  DEFAULT_REFERENCE,
1090
1171
  MEDIA_PRICING,
1172
+ OFFICIAL_PRICES,
1091
1173
  cheapestRoute,
1092
1174
  classifyError,
1093
1175
  classifyErrorKind,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-lcr",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Least Cost Routing for LLMs — route every model call to the cheapest available provider, fall back automatically, and track real cost. Built for the Vercel AI SDK.",
5
5
  "keywords": [
6
6
  "ai",