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 +22 -0
- package/dist/index.cjs +93 -10
- package/dist/index.d.cts +37 -5
- package/dist/index.d.ts +37 -5
- package/dist/index.js +92 -10
- package/package.json +1 -1
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
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
* the
|
|
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
|
|
316
|
+
let baseline;
|
|
307
317
|
for (const p of this.opts.providers) {
|
|
308
318
|
if (!p.cost) continue;
|
|
309
|
-
|
|
310
|
-
if (max === void 0 || c > max) max = c;
|
|
319
|
+
baseline = costForUsage(p.cost, inputTokens, outputTokens, cacheReadTokens);
|
|
311
320
|
}
|
|
312
|
-
return
|
|
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 {
|
|
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
|
|
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
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* provider
|
|
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
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* provider
|
|
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
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
* the
|
|
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
|
|
274
|
+
let baseline;
|
|
266
275
|
for (const p of this.opts.providers) {
|
|
267
276
|
if (!p.cost) continue;
|
|
268
|
-
|
|
269
|
-
if (max === void 0 || c > max) max = c;
|
|
277
|
+
baseline = costForUsage(p.cost, inputTokens, outputTokens, cacheReadTokens);
|
|
270
278
|
}
|
|
271
|
-
return
|
|
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 {
|
|
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
|
|
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.
|
|
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",
|