ai-lcr 0.5.6 → 0.6.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/CHANGELOG.md +44 -0
- package/README.md +3 -1
- package/dist/index.cjs +195 -52
- package/dist/index.d.cts +125 -4
- package/dist/index.d.ts +125 -4
- package/dist/index.js +192 -52
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,50 @@ 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.6.0] — 2026-06-10
|
|
8
|
+
|
|
9
|
+
Media billing contract v2: **rank by the reference, bill by actual usage.**
|
|
10
|
+
The 0.5 media router used one number for both jobs — the price normalized to a
|
|
11
|
+
reference output (1080p image / 5-second clip) ranked routes *and* estimated
|
|
12
|
+
costs, multiplied by an untyped `units` count. That mispriced off-reference
|
|
13
|
+
outputs (an 8s clip billed as 5s) and made the baseline duration-blind, and the
|
|
14
|
+
bare `units` invited a seconds-as-count 8× overcharge. 0.6 separates the two.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **Typed usage (`MediaUsage`).** Adapter results (`MediaGenerateResult`,
|
|
19
|
+
`MediaStatusResult`) carry `usage: { seconds?, outputs?, megapixels? }` —
|
|
20
|
+
explicitly named dimensions that cannot be confused. The bundled adapters
|
|
21
|
+
report it (Kunavo video now safely reports the real `duration_seconds`).
|
|
22
|
+
The legacy bare `units` field is still honored as an output count.
|
|
23
|
+
- **Settle-time billing.** Cost estimates price the route's actual unit on
|
|
24
|
+
actual usage: per-second SKUs bill `usage.seconds` → `input.duration`
|
|
25
|
+
(numbers or `"8s"`-style strings) → the reference (last resort); per-image /
|
|
26
|
+
per-call SKUs bill output count; per-megapixel SKUs bill measured megapixels.
|
|
27
|
+
New public helpers: `billableUnits`, `priceCents`, `durationFromInput`.
|
|
28
|
+
- **Usage-aware savings baseline.** `baselineUsd` is now priced at settle time
|
|
29
|
+
against the same usage as the cost — an 8-second clip is baselined at 8
|
|
30
|
+
seconds of the official rate, not the 5-second reference. Off-reference calls
|
|
31
|
+
can no longer produce negative or understated savings.
|
|
32
|
+
- **`CallRecord` provenance fields** (all optional, backward compatible):
|
|
33
|
+
`modality` ("image" | "video"), `usage`, `baselineKind`
|
|
34
|
+
("official" | "priciest-route" | "last-leg" — the text router now stamps
|
|
35
|
+
"last-leg"), `officialUsd` (the official price for this call's usage), and
|
|
36
|
+
`estCostUsd` (the price-table prediction; `costUsd − estCostUsd` on
|
|
37
|
+
provider-reported rows is price-table drift).
|
|
38
|
+
- **Cost-outlier guard.** A provider-reported cost ≥25× off the table
|
|
39
|
+
prediction (the classic USD-vs-cents slip is exactly 100×) raises `onError`
|
|
40
|
+
with both numbers; the reported bill still stands.
|
|
41
|
+
- `MediaRunResult` and the terminal `MediaPollResult` expose the `usage` that
|
|
42
|
+
backed the bill.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- `MediaJobHandle` now carries the serving route's `pricing` and the resolved
|
|
47
|
+
savings `baseline` so settle-time billing works across processes. Handles
|
|
48
|
+
serialized by 0.5.x still poll fine: they settle with the legacy
|
|
49
|
+
reference-price estimate and the submit-time baseline.
|
|
50
|
+
|
|
7
51
|
## [0.5.6] — 2026-06-07
|
|
8
52
|
|
|
9
53
|
All additions are optional and backward compatible. The sync `createMediaLCR`
|
package/README.md
CHANGED
|
@@ -293,7 +293,9 @@ USD per second, as of 2026-05 — verify current rates. Video billing differs by
|
|
|
293
293
|
|
|
294
294
|
## Image & video routing (`createMediaLCR`)
|
|
295
295
|
|
|
296
|
-
Image and video are a separate, self-contained side of `ai-lcr` (file outputs, mixed pricing units, async jobs) — see [`src/media.ts`](src/media.ts). You give it a registry (each model's provider routes + per-unit price) and a set of adapters; it routes cheapest-first, fails over, and reports real
|
|
296
|
+
Image and video are a separate, self-contained side of `ai-lcr` (file outputs, mixed pricing units, async jobs) — see [`src/media.ts`](src/media.ts). You give it a registry (each model's provider routes + per-unit price) and a set of adapters; it routes cheapest-first, fails over, and reports real cost through the same `onCall` sink as text.
|
|
297
|
+
|
|
298
|
+
Two prices, two jobs: routes are **ranked** by their price normalized to one reference output (a 1080p image / a 5-second clip) so mixed units are comparable, but each settled call is **billed** on its actual usage — an 8-second clip on a per-second SKU costs 8 × the per-second rate, and its savings baseline is the official price for those same 8 seconds. Adapters report typed usage (`usage: { seconds, outputs, megapixels }`); when a provider returns its own bill, that wins, and a bill wildly off the price table (the classic USD-vs-cents slip is exactly 100×) raises `onError` so the table gets fixed.
|
|
297
299
|
|
|
298
300
|
```ts
|
|
299
301
|
import { createMediaLCR, createKunavoMediaAdapter, createFalMediaAdapter } from 'ai-lcr'
|
package/dist/index.cjs
CHANGED
|
@@ -23,6 +23,7 @@ __export(index_exports, {
|
|
|
23
23
|
DEFAULT_REFERENCE: () => DEFAULT_REFERENCE,
|
|
24
24
|
MEDIA_PRICING: () => MEDIA_PRICING,
|
|
25
25
|
OFFICIAL_PRICES: () => OFFICIAL_PRICES,
|
|
26
|
+
billableUnits: () => billableUnits,
|
|
26
27
|
cheapestRoute: () => cheapestRoute,
|
|
27
28
|
classifyError: () => classifyError,
|
|
28
29
|
classifyErrorKind: () => classifyErrorKind,
|
|
@@ -33,11 +34,13 @@ __export(index_exports, {
|
|
|
33
34
|
createLCR: () => createLCR,
|
|
34
35
|
createMediaLCR: () => createMediaLCR,
|
|
35
36
|
createRunwareMediaAdapter: () => createRunwareMediaAdapter,
|
|
37
|
+
durationFromInput: () => durationFromInput,
|
|
36
38
|
formatCallRecord: () => formatCallRecord,
|
|
37
39
|
isAbortError: () => isAbortError,
|
|
38
40
|
isNetworkError: () => isNetworkError,
|
|
39
41
|
isRetryableError: () => isRetryableError,
|
|
40
42
|
normalizedCents: () => normalizedCents,
|
|
43
|
+
priceCents: () => priceCents,
|
|
41
44
|
rankRoutes: () => rankRoutes,
|
|
42
45
|
referenceMegapixels: () => referenceMegapixels,
|
|
43
46
|
shouldFailover: () => shouldFailover
|
|
@@ -364,6 +367,7 @@ var LcrFallbackModel = class {
|
|
|
364
367
|
const cachedSavingUsd = provider.cost ? cacheSavingForUsage(provider.cost, inputTokens, cacheReadTokens) : 0;
|
|
365
368
|
const usageMissing = inputTokens === 0 && outputTokens === 0;
|
|
366
369
|
const emptyCompletion = inputTokens > 0 && outputTokens === 0;
|
|
370
|
+
const baselineUsd = this.baselineUsd(inputTokens, outputTokens, cacheReadTokens);
|
|
367
371
|
this.emitCost({
|
|
368
372
|
model: this.opts.modelName,
|
|
369
373
|
provider: provider.label,
|
|
@@ -384,7 +388,7 @@ var LcrFallbackModel = class {
|
|
|
384
388
|
outputTokens,
|
|
385
389
|
...cacheReadTokens > 0 ? { cachedInputTokens: cacheReadTokens } : {},
|
|
386
390
|
costUsd,
|
|
387
|
-
baselineUsd
|
|
391
|
+
...baselineUsd !== void 0 ? { baselineUsd, baselineKind: "last-leg" } : {},
|
|
388
392
|
...cachedSavingUsd > 0 ? { cachedSavingUsd } : {},
|
|
389
393
|
...ctx.requestId ? { requestId: ctx.requestId } : {},
|
|
390
394
|
...usageMissing ? { usageMissing: true } : {},
|
|
@@ -715,6 +719,40 @@ function normalizedCents(pricing, ref = DEFAULT_REFERENCE) {
|
|
|
715
719
|
return pricing.cents * ref.videoSeconds;
|
|
716
720
|
}
|
|
717
721
|
}
|
|
722
|
+
function durationFromInput(input) {
|
|
723
|
+
const raw = input?.["duration"];
|
|
724
|
+
if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) return raw;
|
|
725
|
+
if (typeof raw === "string") {
|
|
726
|
+
const m = /^(\d+(?:\.\d+)?)\s*s?$/i.exec(raw.trim());
|
|
727
|
+
if (m) {
|
|
728
|
+
const n = Number(m[1]);
|
|
729
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return void 0;
|
|
733
|
+
}
|
|
734
|
+
function billableUnits(unit, ctx) {
|
|
735
|
+
const ref = ctx.reference ?? DEFAULT_REFERENCE;
|
|
736
|
+
const positive = (n) => typeof n === "number" && Number.isFinite(n) && n > 0 ? n : void 0;
|
|
737
|
+
switch (unit) {
|
|
738
|
+
case "second": {
|
|
739
|
+
const measured = positive(ctx.usage?.seconds) ?? durationFromInput(ctx.input);
|
|
740
|
+
return measured !== void 0 ? { value: measured, assumed: false } : { value: ref.videoSeconds, assumed: true };
|
|
741
|
+
}
|
|
742
|
+
case "megapixel": {
|
|
743
|
+
const measured = positive(ctx.usage?.megapixels);
|
|
744
|
+
return measured !== void 0 ? { value: measured, assumed: false } : { value: referenceMegapixels(ref), assumed: true };
|
|
745
|
+
}
|
|
746
|
+
case "image":
|
|
747
|
+
case "call": {
|
|
748
|
+
const measured = positive(ctx.usage?.outputs) ?? positive(ctx.outputCount) ?? positive(ctx.units);
|
|
749
|
+
return measured !== void 0 ? { value: measured, assumed: false } : { value: 1, assumed: true };
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
function priceCents(pricing, ctx) {
|
|
754
|
+
return pricing.cents * billableUnits(pricing.unit, ctx).value;
|
|
755
|
+
}
|
|
718
756
|
function rankRoutes(def, ref = DEFAULT_REFERENCE) {
|
|
719
757
|
return def.routes.map((r) => ({ ...r, refCents: normalizedCents(r.pricing, ref) })).sort((a, b) => a.refCents - b.refCents);
|
|
720
758
|
}
|
|
@@ -780,38 +818,91 @@ function createMediaLCR(config) {
|
|
|
780
818
|
}
|
|
781
819
|
const ranked = rankRoutes(def, reference);
|
|
782
820
|
const official = def.official ?? officialPrices[modelId];
|
|
783
|
-
const
|
|
784
|
-
return { ranked,
|
|
821
|
+
const baseline = official !== void 0 ? { pricing: official, kind: "official" } : ranked.length > 0 ? { pricing: ranked[ranked.length - 1].pricing, kind: "priciest-route" } : void 0;
|
|
822
|
+
return { def, ranked, baseline };
|
|
785
823
|
}
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
824
|
+
const refBaselineUsd = (baseline) => baseline ? normalizedCents(baseline.pricing, reference) / 100 : 0;
|
|
825
|
+
function settle(pricing, refCents, result, input) {
|
|
826
|
+
const ctx = {
|
|
827
|
+
...result.usage ? { usage: result.usage } : {},
|
|
828
|
+
...result.units !== void 0 ? { units: result.units } : {},
|
|
829
|
+
...result.outputs ? { outputCount: result.outputs.length } : {},
|
|
830
|
+
...input ? { input } : {},
|
|
831
|
+
reference
|
|
832
|
+
};
|
|
833
|
+
const estCents = pricing ? priceCents(pricing, ctx) : refCents * billableUnits("call", ctx).value;
|
|
834
|
+
const estimated = result.costCents === void 0;
|
|
835
|
+
const costCents = result.costCents ?? estCents;
|
|
836
|
+
const usage = { ...result.usage ?? {} };
|
|
837
|
+
if (usage.outputs === void 0 && (result.outputs?.length ?? 0) > 0) {
|
|
838
|
+
usage.outputs = result.outputs.length;
|
|
839
|
+
}
|
|
840
|
+
if (usage.seconds === void 0 && pricing?.unit === "second") {
|
|
841
|
+
const s = billableUnits("second", ctx);
|
|
842
|
+
if (!s.assumed) usage.seconds = s.value;
|
|
843
|
+
}
|
|
844
|
+
return {
|
|
845
|
+
costCents,
|
|
846
|
+
estCents,
|
|
847
|
+
estimated,
|
|
848
|
+
...Object.keys(usage).length > 0 ? { usage } : {},
|
|
849
|
+
ctx
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
const DRIFT_RATIO = 25;
|
|
853
|
+
const warnDrift = (modelId, provider, s) => {
|
|
854
|
+
if (s.estimated || s.estCents <= 0 || s.costCents <= 0) return;
|
|
855
|
+
const ratio = s.costCents / s.estCents;
|
|
856
|
+
if (ratio >= DRIFT_RATIO || ratio <= 1 / DRIFT_RATIO) {
|
|
857
|
+
safeError(
|
|
858
|
+
new Error(
|
|
859
|
+
`ai-lcr: ${provider} reported ${s.costCents}\xA2 for "${modelId}" but the price table predicts ${s.estCents}\xA2 (${ratio.toFixed(1)}\xD7) \u2014 check the route's pricing units (USD vs cents) or refresh the registry price`
|
|
860
|
+
),
|
|
861
|
+
provider
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
const emitFail = (modelId, attempts, baselineUsd, startedAt) => {
|
|
866
|
+
const modality = registry[modelId]?.modality;
|
|
867
|
+
safeCall({
|
|
868
|
+
id: newMediaCallId(),
|
|
869
|
+
model: modelId,
|
|
870
|
+
attempts,
|
|
871
|
+
winner: void 0,
|
|
872
|
+
ok: false,
|
|
873
|
+
failedOver: attempts.length > 1,
|
|
874
|
+
latencyMs: Date.now() - startedAt,
|
|
875
|
+
inputTokens: 0,
|
|
876
|
+
outputTokens: 0,
|
|
877
|
+
costUsd: 0,
|
|
878
|
+
baselineUsd,
|
|
879
|
+
...modality ? { modality } : {}
|
|
880
|
+
});
|
|
881
|
+
};
|
|
882
|
+
const emitOk = (args) => {
|
|
883
|
+
const { settled, baseline } = args;
|
|
884
|
+
const baselineUsd = baseline ? priceCents(baseline.pricing, settled.ctx) / 100 : args.legacyBaselineUsd ?? 0;
|
|
885
|
+
safeCall({
|
|
886
|
+
id: newMediaCallId(),
|
|
887
|
+
model: args.modelId,
|
|
888
|
+
attempts: args.attempts,
|
|
889
|
+
winner: args.provider,
|
|
890
|
+
ok: true,
|
|
891
|
+
failedOver: args.attempts.length > 1,
|
|
892
|
+
latencyMs: Date.now() - args.startedAt,
|
|
893
|
+
inputTokens: 0,
|
|
894
|
+
outputTokens: 0,
|
|
895
|
+
costUsd: settled.costCents / 100,
|
|
896
|
+
baselineUsd,
|
|
897
|
+
...baseline ? { baselineKind: baseline.kind } : {},
|
|
898
|
+
...baseline?.kind === "official" ? { officialUsd: baselineUsd } : {},
|
|
899
|
+
modality: args.modality,
|
|
900
|
+
...settled.usage ? { usage: settled.usage } : {},
|
|
901
|
+
estCostUsd: settled.estCents / 100
|
|
902
|
+
});
|
|
903
|
+
};
|
|
813
904
|
const generate = async function generate2(modelId, input) {
|
|
814
|
-
const { ranked,
|
|
905
|
+
const { def, ranked, baseline } = resolve(modelId);
|
|
815
906
|
const startedAt = Date.now();
|
|
816
907
|
const attempts = [];
|
|
817
908
|
let lastErr;
|
|
@@ -821,12 +912,31 @@ function createMediaLCR(config) {
|
|
|
821
912
|
const attemptStart = Date.now();
|
|
822
913
|
try {
|
|
823
914
|
const result = await adapter.run({ externalId: route.externalId, input });
|
|
824
|
-
const
|
|
825
|
-
const costCents = costFor(route.refCents, result);
|
|
915
|
+
const settled = settle(route.pricing, route.refCents, result, input);
|
|
826
916
|
attempts.push({ provider: route.provider, ok: true, latencyMs: Date.now() - attemptStart });
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
917
|
+
warnDrift(modelId, route.provider, settled);
|
|
918
|
+
safeCost({
|
|
919
|
+
modelId,
|
|
920
|
+
provider: route.provider,
|
|
921
|
+
costCents: settled.costCents,
|
|
922
|
+
estimated: settled.estimated
|
|
923
|
+
});
|
|
924
|
+
emitOk({
|
|
925
|
+
modelId,
|
|
926
|
+
modality: def.modality,
|
|
927
|
+
provider: route.provider,
|
|
928
|
+
attempts,
|
|
929
|
+
settled,
|
|
930
|
+
baseline,
|
|
931
|
+
startedAt
|
|
932
|
+
});
|
|
933
|
+
return {
|
|
934
|
+
outputs: result.outputs,
|
|
935
|
+
provider: route.provider,
|
|
936
|
+
costCents: settled.costCents,
|
|
937
|
+
estimated: settled.estimated,
|
|
938
|
+
...settled.usage ? { usage: settled.usage } : {}
|
|
939
|
+
};
|
|
830
940
|
} catch (err) {
|
|
831
941
|
lastErr = err;
|
|
832
942
|
attempts.push({
|
|
@@ -837,19 +947,19 @@ function createMediaLCR(config) {
|
|
|
837
947
|
});
|
|
838
948
|
safeError(err, route.provider);
|
|
839
949
|
if (!isRetryableError(err)) {
|
|
840
|
-
emitFail(modelId, attempts,
|
|
950
|
+
emitFail(modelId, attempts, refBaselineUsd(baseline), startedAt);
|
|
841
951
|
throw err;
|
|
842
952
|
}
|
|
843
953
|
}
|
|
844
954
|
}
|
|
845
|
-
emitFail(modelId, attempts,
|
|
955
|
+
emitFail(modelId, attempts, refBaselineUsd(baseline), startedAt);
|
|
846
956
|
throw lastErr instanceof Error ? lastErr : new Error(`ai-lcr: no provider could serve media model "${modelId}"`);
|
|
847
957
|
};
|
|
848
958
|
const asyncRanked = (modelId) => {
|
|
849
959
|
const { ranked } = resolve(modelId);
|
|
850
960
|
return ranked.filter((r) => typeof adapters[r.provider]?.submit === "function");
|
|
851
961
|
};
|
|
852
|
-
async function submitFrom(modelId, routes, input, metadata,
|
|
962
|
+
async function submitFrom(modelId, routes, input, metadata, baseline, startedAt, attempts) {
|
|
853
963
|
let lastErr;
|
|
854
964
|
for (let i = 0; i < routes.length; i++) {
|
|
855
965
|
const route = routes[i];
|
|
@@ -864,10 +974,12 @@ function createMediaLCR(config) {
|
|
|
864
974
|
externalId: route.externalId,
|
|
865
975
|
requestId,
|
|
866
976
|
refCents: route.refCents,
|
|
977
|
+
pricing: route.pricing,
|
|
978
|
+
...baseline ? { baseline } : {},
|
|
867
979
|
fallbacks: routes.slice(i + 1).map((r) => ({ provider: r.provider, externalId: r.externalId, refCents: r.refCents })),
|
|
868
980
|
input,
|
|
869
981
|
...metadata ? { metadata } : {},
|
|
870
|
-
baselineUsd,
|
|
982
|
+
baselineUsd: refBaselineUsd(baseline),
|
|
871
983
|
startedAt,
|
|
872
984
|
attemptStart,
|
|
873
985
|
attempts
|
|
@@ -884,18 +996,18 @@ function createMediaLCR(config) {
|
|
|
884
996
|
if (!isRetryableError(err)) break;
|
|
885
997
|
}
|
|
886
998
|
}
|
|
887
|
-
emitFail(modelId, attempts,
|
|
999
|
+
emitFail(modelId, attempts, refBaselineUsd(baseline), startedAt);
|
|
888
1000
|
throw lastErr instanceof Error ? lastErr : new Error(`ai-lcr: no async provider could submit media model "${modelId}"`);
|
|
889
1001
|
}
|
|
890
1002
|
generate.submit = async function submit(modelId, input, options) {
|
|
891
|
-
const { ranked,
|
|
1003
|
+
const { ranked, baseline } = resolve(modelId);
|
|
892
1004
|
const usable = ranked.filter((r) => typeof adapters[r.provider]?.submit === "function");
|
|
893
1005
|
if (usable.length === 0) {
|
|
894
1006
|
throw new Error(
|
|
895
1007
|
`ai-lcr: no provider for media model "${modelId}" supports async submit (need an adapter with submit/checkStatus)`
|
|
896
1008
|
);
|
|
897
1009
|
}
|
|
898
|
-
return submitFrom(modelId, usable, input, options?.metadata,
|
|
1010
|
+
return submitFrom(modelId, usable, input, options?.metadata, baseline, Date.now(), []);
|
|
899
1011
|
};
|
|
900
1012
|
generate.poll = async function poll(handle) {
|
|
901
1013
|
const adapter = adapters[handle.provider];
|
|
@@ -905,6 +1017,7 @@ function createMediaLCR(config) {
|
|
|
905
1017
|
);
|
|
906
1018
|
}
|
|
907
1019
|
const failover = async (attempts) => {
|
|
1020
|
+
const { baseline } = resolve(handle.modelId);
|
|
908
1021
|
const next = asyncRanked(handle.modelId).filter(
|
|
909
1022
|
(r) => handle.fallbacks.some((f) => f.provider === r.provider && f.externalId === r.externalId)
|
|
910
1023
|
);
|
|
@@ -913,7 +1026,7 @@ function createMediaLCR(config) {
|
|
|
913
1026
|
next,
|
|
914
1027
|
handle.input,
|
|
915
1028
|
handle.metadata,
|
|
916
|
-
handle.
|
|
1029
|
+
handle.baseline ?? baseline,
|
|
917
1030
|
handle.startedAt,
|
|
918
1031
|
attempts
|
|
919
1032
|
);
|
|
@@ -953,15 +1066,37 @@ function createMediaLCR(config) {
|
|
|
953
1066
|
true
|
|
954
1067
|
);
|
|
955
1068
|
}
|
|
956
|
-
const
|
|
957
|
-
const costCents = costFor(handle.refCents, status);
|
|
1069
|
+
const settled = settle(handle.pricing, handle.refCents, { ...status, outputs }, handle.input);
|
|
958
1070
|
const attempts = [
|
|
959
1071
|
...handle.attempts,
|
|
960
1072
|
{ provider: handle.provider, ok: true, latencyMs: Date.now() - handle.attemptStart }
|
|
961
1073
|
];
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1074
|
+
warnDrift(handle.modelId, handle.provider, settled);
|
|
1075
|
+
safeCost({
|
|
1076
|
+
modelId: handle.modelId,
|
|
1077
|
+
provider: handle.provider,
|
|
1078
|
+
costCents: settled.costCents,
|
|
1079
|
+
estimated: settled.estimated
|
|
1080
|
+
});
|
|
1081
|
+
emitOk({
|
|
1082
|
+
modelId: handle.modelId,
|
|
1083
|
+
modality: registry[handle.modelId]?.modality ?? outputs[0].type,
|
|
1084
|
+
provider: handle.provider,
|
|
1085
|
+
attempts,
|
|
1086
|
+
settled,
|
|
1087
|
+
baseline: handle.baseline,
|
|
1088
|
+
legacyBaselineUsd: handle.baselineUsd,
|
|
1089
|
+
startedAt: handle.startedAt
|
|
1090
|
+
});
|
|
1091
|
+
return {
|
|
1092
|
+
done: true,
|
|
1093
|
+
status: "done",
|
|
1094
|
+
outputs,
|
|
1095
|
+
provider: handle.provider,
|
|
1096
|
+
costCents: settled.costCents,
|
|
1097
|
+
estimated: settled.estimated,
|
|
1098
|
+
...settled.usage ? { usage: settled.usage } : {}
|
|
1099
|
+
};
|
|
965
1100
|
}
|
|
966
1101
|
return onLegFailure(new Error(status.error ?? `ai-lcr: ${handle.provider} job failed`), true);
|
|
967
1102
|
};
|
|
@@ -1152,9 +1287,12 @@ function createKunavoMediaAdapter(config) {
|
|
|
1152
1287
|
if (urls.length === 0) {
|
|
1153
1288
|
return { status: "error", error: `Kunavo video job ${req.requestId} completed with no URL` };
|
|
1154
1289
|
}
|
|
1290
|
+
const durationSeconds = body.output?.duration_seconds;
|
|
1291
|
+
const seconds = typeof durationSeconds === "number" && Number.isFinite(durationSeconds) && durationSeconds > 0 ? durationSeconds : void 0;
|
|
1155
1292
|
return {
|
|
1156
1293
|
status: "done",
|
|
1157
|
-
outputs: urls.map((url) => ({ url, type: "video" }))
|
|
1294
|
+
outputs: urls.map((url) => ({ url, type: "video" })),
|
|
1295
|
+
usage: { outputs: urls.length, ...seconds !== void 0 ? { seconds } : {} }
|
|
1158
1296
|
};
|
|
1159
1297
|
}
|
|
1160
1298
|
if (status === "failed" || status === "error") {
|
|
@@ -1312,6 +1450,7 @@ function createRunwareMediaAdapter(config) {
|
|
|
1312
1450
|
return {
|
|
1313
1451
|
outputs,
|
|
1314
1452
|
units: images.length,
|
|
1453
|
+
usage: { outputs: images.length },
|
|
1315
1454
|
...cents !== void 0 ? { costCents: cents } : {}
|
|
1316
1455
|
};
|
|
1317
1456
|
},
|
|
@@ -1342,6 +1481,7 @@ function createRunwareMediaAdapter(config) {
|
|
|
1342
1481
|
return {
|
|
1343
1482
|
status: "done",
|
|
1344
1483
|
outputs: [{ url, type: "video" }],
|
|
1484
|
+
usage: { outputs: 1 },
|
|
1345
1485
|
...cents !== void 0 ? { costCents: cents } : {}
|
|
1346
1486
|
};
|
|
1347
1487
|
}
|
|
@@ -1433,7 +1573,7 @@ function createFalMediaAdapter(config) {
|
|
|
1433
1573
|
if (outputs.length === 0) {
|
|
1434
1574
|
return { status: "error", error: `fal job ${req.requestId} completed with no media URL` };
|
|
1435
1575
|
}
|
|
1436
|
-
return { status: "done", outputs, units: outputs.length };
|
|
1576
|
+
return { status: "done", outputs, units: outputs.length, usage: { outputs: outputs.length } };
|
|
1437
1577
|
}
|
|
1438
1578
|
return {
|
|
1439
1579
|
provider: "fal",
|
|
@@ -1485,7 +1625,7 @@ function createFalMediaAdapter(config) {
|
|
|
1485
1625
|
if (outputs.length === 0) {
|
|
1486
1626
|
throw new Error(`ai-lcr: fal returned no media URL for "${req.externalId}"`);
|
|
1487
1627
|
}
|
|
1488
|
-
return { outputs, units: outputs.length };
|
|
1628
|
+
return { outputs, units: outputs.length, usage: { outputs: outputs.length } };
|
|
1489
1629
|
}
|
|
1490
1630
|
};
|
|
1491
1631
|
}
|
|
@@ -1571,6 +1711,7 @@ function createLCR(config) {
|
|
|
1571
1711
|
DEFAULT_REFERENCE,
|
|
1572
1712
|
MEDIA_PRICING,
|
|
1573
1713
|
OFFICIAL_PRICES,
|
|
1714
|
+
billableUnits,
|
|
1574
1715
|
cheapestRoute,
|
|
1575
1716
|
classifyError,
|
|
1576
1717
|
classifyErrorKind,
|
|
@@ -1581,11 +1722,13 @@ function createLCR(config) {
|
|
|
1581
1722
|
createLCR,
|
|
1582
1723
|
createMediaLCR,
|
|
1583
1724
|
createRunwareMediaAdapter,
|
|
1725
|
+
durationFromInput,
|
|
1584
1726
|
formatCallRecord,
|
|
1585
1727
|
isAbortError,
|
|
1586
1728
|
isNetworkError,
|
|
1587
1729
|
isRetryableError,
|
|
1588
1730
|
normalizedCents,
|
|
1731
|
+
priceCents,
|
|
1589
1732
|
rankRoutes,
|
|
1590
1733
|
referenceMegapixels,
|
|
1591
1734
|
shouldFailover
|
package/dist/index.d.cts
CHANGED
|
@@ -127,6 +127,47 @@ interface CallRecord {
|
|
|
127
127
|
* when no provider was priced.
|
|
128
128
|
*/
|
|
129
129
|
baselineUsd?: number;
|
|
130
|
+
/**
|
|
131
|
+
* How `baselineUsd` was derived, so a dashboard can qualify the savings
|
|
132
|
+
* number instead of treating every baseline as equally authoritative:
|
|
133
|
+
* - "last-leg": text router — the always-on fallback leg's list price.
|
|
134
|
+
* - "official": media router — the model maker's first-party price.
|
|
135
|
+
* - "priciest-route": media router with no official price — the most
|
|
136
|
+
* expensive configured route (self-referential; honest
|
|
137
|
+
* about cross-provider spread, but not a market price).
|
|
138
|
+
* Undefined when `baselineUsd` is undefined.
|
|
139
|
+
*/
|
|
140
|
+
baselineKind?: "last-leg" | "official" | "priciest-route";
|
|
141
|
+
/**
|
|
142
|
+
* Media only: "image" | "video". Lets the dashboard split media traffic from
|
|
143
|
+
* token-billed text (whose records leave this unset) without inferring from
|
|
144
|
+
* zero token counts.
|
|
145
|
+
*/
|
|
146
|
+
modality?: "image" | "video";
|
|
147
|
+
/**
|
|
148
|
+
* Media only: the actual billable quantity behind `costUsd` — seconds of
|
|
149
|
+
* video, output count, or megapixels — so per-unit economics ($/second,
|
|
150
|
+
* $/image) are derivable downstream. Absent when nothing was measured.
|
|
151
|
+
*/
|
|
152
|
+
usage?: {
|
|
153
|
+
seconds?: number;
|
|
154
|
+
outputs?: number;
|
|
155
|
+
megapixels?: number;
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Media only: the model maker's official first-party price for THIS call's
|
|
159
|
+
* usage (USD). Present only when an official price is known; equals
|
|
160
|
+
* `baselineUsd` when `baselineKind` is "official".
|
|
161
|
+
*/
|
|
162
|
+
officialUsd?: number;
|
|
163
|
+
/**
|
|
164
|
+
* What the configured price table PREDICTED this call would cost (USD), on
|
|
165
|
+
* the same usage. When the provider reports an actual cost, `costUsd −
|
|
166
|
+
* estCostUsd` is the price-table drift — the signal that a registry price is
|
|
167
|
+
* stale or mis-entered. When no provider cost is reported the two are equal
|
|
168
|
+
* (the estimate IS the cost), so drift is only meaningful on reported rows.
|
|
169
|
+
*/
|
|
170
|
+
estCostUsd?: number;
|
|
130
171
|
/**
|
|
131
172
|
* The slice of `costUsd` that prompt-cache reads saved versus paying the full
|
|
132
173
|
* input rate for those same tokens (`cachedTokens × (input − cacheRead)`).
|
|
@@ -366,6 +407,41 @@ declare function referenceMegapixels(ref: ReferenceSpec): number;
|
|
|
366
407
|
* This is the single normalization that makes providers comparable.
|
|
367
408
|
*/
|
|
368
409
|
declare function normalizedCents(pricing: MediaPricing, ref?: ReferenceSpec): number;
|
|
410
|
+
/**
|
|
411
|
+
* Parse a duration in seconds out of a canonical media input. Accepts a number
|
|
412
|
+
* or a numeric-ish string (Veo-style `"8s"`, `"8"`). Returns undefined when the
|
|
413
|
+
* input carries no usable duration.
|
|
414
|
+
*/
|
|
415
|
+
declare function durationFromInput(input: Record<string, unknown> | undefined): number | undefined;
|
|
416
|
+
/** What `billableUnits` can draw on to resolve the actual billed quantity. */
|
|
417
|
+
interface BillableContext {
|
|
418
|
+
/** Typed usage the adapter measured on the settled result. Highest priority. */
|
|
419
|
+
usage?: MediaUsage;
|
|
420
|
+
/** Legacy bare count from older adapters — OUTPUT COUNT only. */
|
|
421
|
+
units?: number;
|
|
422
|
+
/** Settled output count (`outputs.length`), when available. */
|
|
423
|
+
outputCount?: number;
|
|
424
|
+
/** The canonical request input — `duration` is read for per-second pricing. */
|
|
425
|
+
input?: Record<string, unknown>;
|
|
426
|
+
/** Reference spec, the last-resort assumption when nothing was measured. */
|
|
427
|
+
reference?: ReferenceSpec;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Resolve the actual billable quantity for a pricing unit, with provenance:
|
|
431
|
+
* `assumed: true` means nothing was measured and the reference spec filled in
|
|
432
|
+
* (so the resulting cost is a guess of last resort, not a measurement).
|
|
433
|
+
*
|
|
434
|
+
* Resolution order per unit:
|
|
435
|
+
* - "second": usage.seconds → input.duration → reference.videoSeconds (assumed)
|
|
436
|
+
* - "megapixel": usage.megapixels → reference image MP (assumed)
|
|
437
|
+
* - "image"/"call": usage.outputs → outputCount → legacy units → 1 (assumed)
|
|
438
|
+
*/
|
|
439
|
+
declare function billableUnits(unit: MediaUnit, ctx: BillableContext): {
|
|
440
|
+
value: number;
|
|
441
|
+
assumed: boolean;
|
|
442
|
+
};
|
|
443
|
+
/** Price a settled generation on `pricing`, from its actual usage (cents). */
|
|
444
|
+
declare function priceCents(pricing: MediaPricing, ctx: BillableContext): number;
|
|
369
445
|
interface RankedRoute extends MediaRoute {
|
|
370
446
|
/** Normalized cost (cents per reference output) used for ordering. */
|
|
371
447
|
refCents: number;
|
|
@@ -398,11 +474,31 @@ interface MediaOutput {
|
|
|
398
474
|
url: string;
|
|
399
475
|
type: MediaModality;
|
|
400
476
|
}
|
|
477
|
+
/**
|
|
478
|
+
* Actual measured usage for one settled generation — the typed billing
|
|
479
|
+
* quantities. Adapters report whichever dimensions they can observe:
|
|
480
|
+
* - `seconds`: video length actually produced (per-second pricing bills this)
|
|
481
|
+
* - `outputs`: output count — images or clips (per-image / per-call pricing)
|
|
482
|
+
* - `megapixels`: total output megapixels (per-megapixel pricing)
|
|
483
|
+
* Dimensions are EXPLICITLY named so a seconds value can never be mistaken for
|
|
484
|
+
* an output count (the bug class the old bare `units` number invited: a flat
|
|
485
|
+
* per-call price × 8 "units" of an 8-second clip = an 8× overcharge).
|
|
486
|
+
*/
|
|
487
|
+
interface MediaUsage {
|
|
488
|
+
seconds?: number;
|
|
489
|
+
outputs?: number;
|
|
490
|
+
megapixels?: number;
|
|
491
|
+
}
|
|
401
492
|
interface MediaGenerateResult {
|
|
402
493
|
outputs: MediaOutput[];
|
|
403
494
|
/** Provider-reported actual cost in cents, when the API returns it. */
|
|
404
495
|
costCents?: number;
|
|
405
|
-
/**
|
|
496
|
+
/** Typed actual usage (seconds / outputs / megapixels) — drives cost estimation. */
|
|
497
|
+
usage?: MediaUsage;
|
|
498
|
+
/**
|
|
499
|
+
* @deprecated Legacy bare count, meaning OUTPUT COUNT only (never seconds).
|
|
500
|
+
* Prefer `usage: { outputs }`. Still honored for backward compatibility.
|
|
501
|
+
*/
|
|
406
502
|
units?: number;
|
|
407
503
|
}
|
|
408
504
|
interface MediaSubmitRequest {
|
|
@@ -429,7 +525,12 @@ interface MediaStatusResult {
|
|
|
429
525
|
outputs?: MediaOutput[];
|
|
430
526
|
/** Provider-reported actual cost in cents, when the API returns it (`done`). */
|
|
431
527
|
costCents?: number;
|
|
432
|
-
/**
|
|
528
|
+
/** Typed actual usage (seconds / outputs / megapixels) — drives cost estimation. */
|
|
529
|
+
usage?: MediaUsage;
|
|
530
|
+
/**
|
|
531
|
+
* @deprecated Legacy bare count, meaning OUTPUT COUNT only (never seconds).
|
|
532
|
+
* Prefer `usage: { outputs }` (or `usage: { seconds }` for per-second SKUs).
|
|
533
|
+
*/
|
|
433
534
|
units?: number;
|
|
434
535
|
/** Human-readable reason on `error`. */
|
|
435
536
|
error?: string;
|
|
@@ -486,6 +587,8 @@ interface MediaRunResult {
|
|
|
486
587
|
provider: string;
|
|
487
588
|
costCents: number;
|
|
488
589
|
estimated: boolean;
|
|
590
|
+
/** Actual usage behind `costCents` (seconds / outputs / megapixels), when known. */
|
|
591
|
+
usage?: MediaUsage;
|
|
489
592
|
}
|
|
490
593
|
interface MediaSubmitOptions {
|
|
491
594
|
/** Opaque caller metadata forwarded to the provider's `submit`. */
|
|
@@ -514,8 +617,24 @@ interface MediaJobHandle {
|
|
|
514
617
|
externalId: string;
|
|
515
618
|
/** Provider-native job id from `submit`. */
|
|
516
619
|
requestId: string;
|
|
517
|
-
/** Normalized cost (cents/ref output) of the serving route —
|
|
620
|
+
/** Normalized cost (cents/ref output) of the serving route — ranking metadata. */
|
|
518
621
|
refCents: number;
|
|
622
|
+
/**
|
|
623
|
+
* The serving route's pricing — settle-time billing prices ACTUAL usage on
|
|
624
|
+
* this (`priceCents`), not the reference estimate. Optional only so handles
|
|
625
|
+
* serialized by pre-0.6 versions still poll; those fall back to `refCents`.
|
|
626
|
+
*/
|
|
627
|
+
pricing?: MediaPricing;
|
|
628
|
+
/**
|
|
629
|
+
* Savings baseline carried across processes: the official first-party price
|
|
630
|
+
* when known, else the priciest configured route. Priced against the SAME
|
|
631
|
+
* actual usage as the cost at settle time. Absent on pre-0.6 handles (those
|
|
632
|
+
* settle with the submit-time `baselineUsd` estimate below).
|
|
633
|
+
*/
|
|
634
|
+
baseline?: {
|
|
635
|
+
pricing: MediaPricing;
|
|
636
|
+
kind: "official" | "priciest-route";
|
|
637
|
+
};
|
|
519
638
|
/** Not-yet-tried routes, cheapest-first, for poll-time re-submit failover. */
|
|
520
639
|
fallbacks: {
|
|
521
640
|
provider: string;
|
|
@@ -551,6 +670,8 @@ type MediaPollResult = {
|
|
|
551
670
|
provider: string;
|
|
552
671
|
costCents: number;
|
|
553
672
|
estimated: boolean;
|
|
673
|
+
/** Actual usage behind `costCents` (seconds / outputs / megapixels), when known. */
|
|
674
|
+
usage?: MediaUsage;
|
|
554
675
|
};
|
|
555
676
|
/**
|
|
556
677
|
* The router returned by {@link createMediaLCR}: a callable (the sync `run`
|
|
@@ -791,4 +912,4 @@ type LCRRouter = (modelName: string) => LanguageModelV3;
|
|
|
791
912
|
*/
|
|
792
913
|
declare function createLCR(config: LCRConfig): LCRRouter;
|
|
793
914
|
|
|
794
|
-
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 MediaJobHandle, type MediaJobStatus, type MediaLCR, type MediaLCRConfig, type MediaModality, type MediaModelDef, type MediaOutput, type MediaPollResult, type MediaPricing, type MediaRegistry, type MediaRoute, type MediaRunResult, type MediaStatusRequest, type MediaStatusResult, type MediaSubmitOptions, type MediaSubmitRequest, type MediaSubmitResult, 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, isAbortError, isNetworkError, isRetryableError, normalizedCents, rankRoutes, referenceMegapixels, shouldFailover };
|
|
915
|
+
export { type BillableContext, 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 MediaJobHandle, type MediaJobStatus, type MediaLCR, type MediaLCRConfig, type MediaModality, type MediaModelDef, type MediaOutput, type MediaPollResult, type MediaPricing, type MediaRegistry, type MediaRoute, type MediaRunResult, type MediaStatusRequest, type MediaStatusResult, type MediaSubmitOptions, type MediaSubmitRequest, type MediaSubmitResult, type MediaUnit, type MediaUsage, OFFICIAL_PRICES, type PriceComparisonRow, type ProviderCost, type ProviderEntry, type RankedRoute, type ReferenceSpec, type RouteAttempt, billableUnits, cheapestRoute, classifyError, classifyErrorKind, comparePrices, createFalMediaAdapter, createHttpSink, createKunavoMediaAdapter, createLCR, createMediaLCR, createRunwareMediaAdapter, durationFromInput, formatCallRecord, isAbortError, isNetworkError, isRetryableError, normalizedCents, priceCents, rankRoutes, referenceMegapixels, shouldFailover };
|
package/dist/index.d.ts
CHANGED
|
@@ -127,6 +127,47 @@ interface CallRecord {
|
|
|
127
127
|
* when no provider was priced.
|
|
128
128
|
*/
|
|
129
129
|
baselineUsd?: number;
|
|
130
|
+
/**
|
|
131
|
+
* How `baselineUsd` was derived, so a dashboard can qualify the savings
|
|
132
|
+
* number instead of treating every baseline as equally authoritative:
|
|
133
|
+
* - "last-leg": text router — the always-on fallback leg's list price.
|
|
134
|
+
* - "official": media router — the model maker's first-party price.
|
|
135
|
+
* - "priciest-route": media router with no official price — the most
|
|
136
|
+
* expensive configured route (self-referential; honest
|
|
137
|
+
* about cross-provider spread, but not a market price).
|
|
138
|
+
* Undefined when `baselineUsd` is undefined.
|
|
139
|
+
*/
|
|
140
|
+
baselineKind?: "last-leg" | "official" | "priciest-route";
|
|
141
|
+
/**
|
|
142
|
+
* Media only: "image" | "video". Lets the dashboard split media traffic from
|
|
143
|
+
* token-billed text (whose records leave this unset) without inferring from
|
|
144
|
+
* zero token counts.
|
|
145
|
+
*/
|
|
146
|
+
modality?: "image" | "video";
|
|
147
|
+
/**
|
|
148
|
+
* Media only: the actual billable quantity behind `costUsd` — seconds of
|
|
149
|
+
* video, output count, or megapixels — so per-unit economics ($/second,
|
|
150
|
+
* $/image) are derivable downstream. Absent when nothing was measured.
|
|
151
|
+
*/
|
|
152
|
+
usage?: {
|
|
153
|
+
seconds?: number;
|
|
154
|
+
outputs?: number;
|
|
155
|
+
megapixels?: number;
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Media only: the model maker's official first-party price for THIS call's
|
|
159
|
+
* usage (USD). Present only when an official price is known; equals
|
|
160
|
+
* `baselineUsd` when `baselineKind` is "official".
|
|
161
|
+
*/
|
|
162
|
+
officialUsd?: number;
|
|
163
|
+
/**
|
|
164
|
+
* What the configured price table PREDICTED this call would cost (USD), on
|
|
165
|
+
* the same usage. When the provider reports an actual cost, `costUsd −
|
|
166
|
+
* estCostUsd` is the price-table drift — the signal that a registry price is
|
|
167
|
+
* stale or mis-entered. When no provider cost is reported the two are equal
|
|
168
|
+
* (the estimate IS the cost), so drift is only meaningful on reported rows.
|
|
169
|
+
*/
|
|
170
|
+
estCostUsd?: number;
|
|
130
171
|
/**
|
|
131
172
|
* The slice of `costUsd` that prompt-cache reads saved versus paying the full
|
|
132
173
|
* input rate for those same tokens (`cachedTokens × (input − cacheRead)`).
|
|
@@ -366,6 +407,41 @@ declare function referenceMegapixels(ref: ReferenceSpec): number;
|
|
|
366
407
|
* This is the single normalization that makes providers comparable.
|
|
367
408
|
*/
|
|
368
409
|
declare function normalizedCents(pricing: MediaPricing, ref?: ReferenceSpec): number;
|
|
410
|
+
/**
|
|
411
|
+
* Parse a duration in seconds out of a canonical media input. Accepts a number
|
|
412
|
+
* or a numeric-ish string (Veo-style `"8s"`, `"8"`). Returns undefined when the
|
|
413
|
+
* input carries no usable duration.
|
|
414
|
+
*/
|
|
415
|
+
declare function durationFromInput(input: Record<string, unknown> | undefined): number | undefined;
|
|
416
|
+
/** What `billableUnits` can draw on to resolve the actual billed quantity. */
|
|
417
|
+
interface BillableContext {
|
|
418
|
+
/** Typed usage the adapter measured on the settled result. Highest priority. */
|
|
419
|
+
usage?: MediaUsage;
|
|
420
|
+
/** Legacy bare count from older adapters — OUTPUT COUNT only. */
|
|
421
|
+
units?: number;
|
|
422
|
+
/** Settled output count (`outputs.length`), when available. */
|
|
423
|
+
outputCount?: number;
|
|
424
|
+
/** The canonical request input — `duration` is read for per-second pricing. */
|
|
425
|
+
input?: Record<string, unknown>;
|
|
426
|
+
/** Reference spec, the last-resort assumption when nothing was measured. */
|
|
427
|
+
reference?: ReferenceSpec;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Resolve the actual billable quantity for a pricing unit, with provenance:
|
|
431
|
+
* `assumed: true` means nothing was measured and the reference spec filled in
|
|
432
|
+
* (so the resulting cost is a guess of last resort, not a measurement).
|
|
433
|
+
*
|
|
434
|
+
* Resolution order per unit:
|
|
435
|
+
* - "second": usage.seconds → input.duration → reference.videoSeconds (assumed)
|
|
436
|
+
* - "megapixel": usage.megapixels → reference image MP (assumed)
|
|
437
|
+
* - "image"/"call": usage.outputs → outputCount → legacy units → 1 (assumed)
|
|
438
|
+
*/
|
|
439
|
+
declare function billableUnits(unit: MediaUnit, ctx: BillableContext): {
|
|
440
|
+
value: number;
|
|
441
|
+
assumed: boolean;
|
|
442
|
+
};
|
|
443
|
+
/** Price a settled generation on `pricing`, from its actual usage (cents). */
|
|
444
|
+
declare function priceCents(pricing: MediaPricing, ctx: BillableContext): number;
|
|
369
445
|
interface RankedRoute extends MediaRoute {
|
|
370
446
|
/** Normalized cost (cents per reference output) used for ordering. */
|
|
371
447
|
refCents: number;
|
|
@@ -398,11 +474,31 @@ interface MediaOutput {
|
|
|
398
474
|
url: string;
|
|
399
475
|
type: MediaModality;
|
|
400
476
|
}
|
|
477
|
+
/**
|
|
478
|
+
* Actual measured usage for one settled generation — the typed billing
|
|
479
|
+
* quantities. Adapters report whichever dimensions they can observe:
|
|
480
|
+
* - `seconds`: video length actually produced (per-second pricing bills this)
|
|
481
|
+
* - `outputs`: output count — images or clips (per-image / per-call pricing)
|
|
482
|
+
* - `megapixels`: total output megapixels (per-megapixel pricing)
|
|
483
|
+
* Dimensions are EXPLICITLY named so a seconds value can never be mistaken for
|
|
484
|
+
* an output count (the bug class the old bare `units` number invited: a flat
|
|
485
|
+
* per-call price × 8 "units" of an 8-second clip = an 8× overcharge).
|
|
486
|
+
*/
|
|
487
|
+
interface MediaUsage {
|
|
488
|
+
seconds?: number;
|
|
489
|
+
outputs?: number;
|
|
490
|
+
megapixels?: number;
|
|
491
|
+
}
|
|
401
492
|
interface MediaGenerateResult {
|
|
402
493
|
outputs: MediaOutput[];
|
|
403
494
|
/** Provider-reported actual cost in cents, when the API returns it. */
|
|
404
495
|
costCents?: number;
|
|
405
|
-
/**
|
|
496
|
+
/** Typed actual usage (seconds / outputs / megapixels) — drives cost estimation. */
|
|
497
|
+
usage?: MediaUsage;
|
|
498
|
+
/**
|
|
499
|
+
* @deprecated Legacy bare count, meaning OUTPUT COUNT only (never seconds).
|
|
500
|
+
* Prefer `usage: { outputs }`. Still honored for backward compatibility.
|
|
501
|
+
*/
|
|
406
502
|
units?: number;
|
|
407
503
|
}
|
|
408
504
|
interface MediaSubmitRequest {
|
|
@@ -429,7 +525,12 @@ interface MediaStatusResult {
|
|
|
429
525
|
outputs?: MediaOutput[];
|
|
430
526
|
/** Provider-reported actual cost in cents, when the API returns it (`done`). */
|
|
431
527
|
costCents?: number;
|
|
432
|
-
/**
|
|
528
|
+
/** Typed actual usage (seconds / outputs / megapixels) — drives cost estimation. */
|
|
529
|
+
usage?: MediaUsage;
|
|
530
|
+
/**
|
|
531
|
+
* @deprecated Legacy bare count, meaning OUTPUT COUNT only (never seconds).
|
|
532
|
+
* Prefer `usage: { outputs }` (or `usage: { seconds }` for per-second SKUs).
|
|
533
|
+
*/
|
|
433
534
|
units?: number;
|
|
434
535
|
/** Human-readable reason on `error`. */
|
|
435
536
|
error?: string;
|
|
@@ -486,6 +587,8 @@ interface MediaRunResult {
|
|
|
486
587
|
provider: string;
|
|
487
588
|
costCents: number;
|
|
488
589
|
estimated: boolean;
|
|
590
|
+
/** Actual usage behind `costCents` (seconds / outputs / megapixels), when known. */
|
|
591
|
+
usage?: MediaUsage;
|
|
489
592
|
}
|
|
490
593
|
interface MediaSubmitOptions {
|
|
491
594
|
/** Opaque caller metadata forwarded to the provider's `submit`. */
|
|
@@ -514,8 +617,24 @@ interface MediaJobHandle {
|
|
|
514
617
|
externalId: string;
|
|
515
618
|
/** Provider-native job id from `submit`. */
|
|
516
619
|
requestId: string;
|
|
517
|
-
/** Normalized cost (cents/ref output) of the serving route —
|
|
620
|
+
/** Normalized cost (cents/ref output) of the serving route — ranking metadata. */
|
|
518
621
|
refCents: number;
|
|
622
|
+
/**
|
|
623
|
+
* The serving route's pricing — settle-time billing prices ACTUAL usage on
|
|
624
|
+
* this (`priceCents`), not the reference estimate. Optional only so handles
|
|
625
|
+
* serialized by pre-0.6 versions still poll; those fall back to `refCents`.
|
|
626
|
+
*/
|
|
627
|
+
pricing?: MediaPricing;
|
|
628
|
+
/**
|
|
629
|
+
* Savings baseline carried across processes: the official first-party price
|
|
630
|
+
* when known, else the priciest configured route. Priced against the SAME
|
|
631
|
+
* actual usage as the cost at settle time. Absent on pre-0.6 handles (those
|
|
632
|
+
* settle with the submit-time `baselineUsd` estimate below).
|
|
633
|
+
*/
|
|
634
|
+
baseline?: {
|
|
635
|
+
pricing: MediaPricing;
|
|
636
|
+
kind: "official" | "priciest-route";
|
|
637
|
+
};
|
|
519
638
|
/** Not-yet-tried routes, cheapest-first, for poll-time re-submit failover. */
|
|
520
639
|
fallbacks: {
|
|
521
640
|
provider: string;
|
|
@@ -551,6 +670,8 @@ type MediaPollResult = {
|
|
|
551
670
|
provider: string;
|
|
552
671
|
costCents: number;
|
|
553
672
|
estimated: boolean;
|
|
673
|
+
/** Actual usage behind `costCents` (seconds / outputs / megapixels), when known. */
|
|
674
|
+
usage?: MediaUsage;
|
|
554
675
|
};
|
|
555
676
|
/**
|
|
556
677
|
* The router returned by {@link createMediaLCR}: a callable (the sync `run`
|
|
@@ -791,4 +912,4 @@ type LCRRouter = (modelName: string) => LanguageModelV3;
|
|
|
791
912
|
*/
|
|
792
913
|
declare function createLCR(config: LCRConfig): LCRRouter;
|
|
793
914
|
|
|
794
|
-
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 MediaJobHandle, type MediaJobStatus, type MediaLCR, type MediaLCRConfig, type MediaModality, type MediaModelDef, type MediaOutput, type MediaPollResult, type MediaPricing, type MediaRegistry, type MediaRoute, type MediaRunResult, type MediaStatusRequest, type MediaStatusResult, type MediaSubmitOptions, type MediaSubmitRequest, type MediaSubmitResult, 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, isAbortError, isNetworkError, isRetryableError, normalizedCents, rankRoutes, referenceMegapixels, shouldFailover };
|
|
915
|
+
export { type BillableContext, 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 MediaJobHandle, type MediaJobStatus, type MediaLCR, type MediaLCRConfig, type MediaModality, type MediaModelDef, type MediaOutput, type MediaPollResult, type MediaPricing, type MediaRegistry, type MediaRoute, type MediaRunResult, type MediaStatusRequest, type MediaStatusResult, type MediaSubmitOptions, type MediaSubmitRequest, type MediaSubmitResult, type MediaUnit, type MediaUsage, OFFICIAL_PRICES, type PriceComparisonRow, type ProviderCost, type ProviderEntry, type RankedRoute, type ReferenceSpec, type RouteAttempt, billableUnits, cheapestRoute, classifyError, classifyErrorKind, comparePrices, createFalMediaAdapter, createHttpSink, createKunavoMediaAdapter, createLCR, createMediaLCR, createRunwareMediaAdapter, durationFromInput, formatCallRecord, isAbortError, isNetworkError, isRetryableError, normalizedCents, priceCents, rankRoutes, referenceMegapixels, shouldFailover };
|
package/dist/index.js
CHANGED
|
@@ -318,6 +318,7 @@ var LcrFallbackModel = class {
|
|
|
318
318
|
const cachedSavingUsd = provider.cost ? cacheSavingForUsage(provider.cost, inputTokens, cacheReadTokens) : 0;
|
|
319
319
|
const usageMissing = inputTokens === 0 && outputTokens === 0;
|
|
320
320
|
const emptyCompletion = inputTokens > 0 && outputTokens === 0;
|
|
321
|
+
const baselineUsd = this.baselineUsd(inputTokens, outputTokens, cacheReadTokens);
|
|
321
322
|
this.emitCost({
|
|
322
323
|
model: this.opts.modelName,
|
|
323
324
|
provider: provider.label,
|
|
@@ -338,7 +339,7 @@ var LcrFallbackModel = class {
|
|
|
338
339
|
outputTokens,
|
|
339
340
|
...cacheReadTokens > 0 ? { cachedInputTokens: cacheReadTokens } : {},
|
|
340
341
|
costUsd,
|
|
341
|
-
baselineUsd
|
|
342
|
+
...baselineUsd !== void 0 ? { baselineUsd, baselineKind: "last-leg" } : {},
|
|
342
343
|
...cachedSavingUsd > 0 ? { cachedSavingUsd } : {},
|
|
343
344
|
...ctx.requestId ? { requestId: ctx.requestId } : {},
|
|
344
345
|
...usageMissing ? { usageMissing: true } : {},
|
|
@@ -669,6 +670,40 @@ function normalizedCents(pricing, ref = DEFAULT_REFERENCE) {
|
|
|
669
670
|
return pricing.cents * ref.videoSeconds;
|
|
670
671
|
}
|
|
671
672
|
}
|
|
673
|
+
function durationFromInput(input) {
|
|
674
|
+
const raw = input?.["duration"];
|
|
675
|
+
if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) return raw;
|
|
676
|
+
if (typeof raw === "string") {
|
|
677
|
+
const m = /^(\d+(?:\.\d+)?)\s*s?$/i.exec(raw.trim());
|
|
678
|
+
if (m) {
|
|
679
|
+
const n = Number(m[1]);
|
|
680
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return void 0;
|
|
684
|
+
}
|
|
685
|
+
function billableUnits(unit, ctx) {
|
|
686
|
+
const ref = ctx.reference ?? DEFAULT_REFERENCE;
|
|
687
|
+
const positive = (n) => typeof n === "number" && Number.isFinite(n) && n > 0 ? n : void 0;
|
|
688
|
+
switch (unit) {
|
|
689
|
+
case "second": {
|
|
690
|
+
const measured = positive(ctx.usage?.seconds) ?? durationFromInput(ctx.input);
|
|
691
|
+
return measured !== void 0 ? { value: measured, assumed: false } : { value: ref.videoSeconds, assumed: true };
|
|
692
|
+
}
|
|
693
|
+
case "megapixel": {
|
|
694
|
+
const measured = positive(ctx.usage?.megapixels);
|
|
695
|
+
return measured !== void 0 ? { value: measured, assumed: false } : { value: referenceMegapixels(ref), assumed: true };
|
|
696
|
+
}
|
|
697
|
+
case "image":
|
|
698
|
+
case "call": {
|
|
699
|
+
const measured = positive(ctx.usage?.outputs) ?? positive(ctx.outputCount) ?? positive(ctx.units);
|
|
700
|
+
return measured !== void 0 ? { value: measured, assumed: false } : { value: 1, assumed: true };
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
function priceCents(pricing, ctx) {
|
|
705
|
+
return pricing.cents * billableUnits(pricing.unit, ctx).value;
|
|
706
|
+
}
|
|
672
707
|
function rankRoutes(def, ref = DEFAULT_REFERENCE) {
|
|
673
708
|
return def.routes.map((r) => ({ ...r, refCents: normalizedCents(r.pricing, ref) })).sort((a, b) => a.refCents - b.refCents);
|
|
674
709
|
}
|
|
@@ -734,38 +769,91 @@ function createMediaLCR(config) {
|
|
|
734
769
|
}
|
|
735
770
|
const ranked = rankRoutes(def, reference);
|
|
736
771
|
const official = def.official ?? officialPrices[modelId];
|
|
737
|
-
const
|
|
738
|
-
return { ranked,
|
|
772
|
+
const baseline = official !== void 0 ? { pricing: official, kind: "official" } : ranked.length > 0 ? { pricing: ranked[ranked.length - 1].pricing, kind: "priciest-route" } : void 0;
|
|
773
|
+
return { def, ranked, baseline };
|
|
739
774
|
}
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
775
|
+
const refBaselineUsd = (baseline) => baseline ? normalizedCents(baseline.pricing, reference) / 100 : 0;
|
|
776
|
+
function settle(pricing, refCents, result, input) {
|
|
777
|
+
const ctx = {
|
|
778
|
+
...result.usage ? { usage: result.usage } : {},
|
|
779
|
+
...result.units !== void 0 ? { units: result.units } : {},
|
|
780
|
+
...result.outputs ? { outputCount: result.outputs.length } : {},
|
|
781
|
+
...input ? { input } : {},
|
|
782
|
+
reference
|
|
783
|
+
};
|
|
784
|
+
const estCents = pricing ? priceCents(pricing, ctx) : refCents * billableUnits("call", ctx).value;
|
|
785
|
+
const estimated = result.costCents === void 0;
|
|
786
|
+
const costCents = result.costCents ?? estCents;
|
|
787
|
+
const usage = { ...result.usage ?? {} };
|
|
788
|
+
if (usage.outputs === void 0 && (result.outputs?.length ?? 0) > 0) {
|
|
789
|
+
usage.outputs = result.outputs.length;
|
|
790
|
+
}
|
|
791
|
+
if (usage.seconds === void 0 && pricing?.unit === "second") {
|
|
792
|
+
const s = billableUnits("second", ctx);
|
|
793
|
+
if (!s.assumed) usage.seconds = s.value;
|
|
794
|
+
}
|
|
795
|
+
return {
|
|
796
|
+
costCents,
|
|
797
|
+
estCents,
|
|
798
|
+
estimated,
|
|
799
|
+
...Object.keys(usage).length > 0 ? { usage } : {},
|
|
800
|
+
ctx
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
const DRIFT_RATIO = 25;
|
|
804
|
+
const warnDrift = (modelId, provider, s) => {
|
|
805
|
+
if (s.estimated || s.estCents <= 0 || s.costCents <= 0) return;
|
|
806
|
+
const ratio = s.costCents / s.estCents;
|
|
807
|
+
if (ratio >= DRIFT_RATIO || ratio <= 1 / DRIFT_RATIO) {
|
|
808
|
+
safeError(
|
|
809
|
+
new Error(
|
|
810
|
+
`ai-lcr: ${provider} reported ${s.costCents}\xA2 for "${modelId}" but the price table predicts ${s.estCents}\xA2 (${ratio.toFixed(1)}\xD7) \u2014 check the route's pricing units (USD vs cents) or refresh the registry price`
|
|
811
|
+
),
|
|
812
|
+
provider
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
const emitFail = (modelId, attempts, baselineUsd, startedAt) => {
|
|
817
|
+
const modality = registry[modelId]?.modality;
|
|
818
|
+
safeCall({
|
|
819
|
+
id: newMediaCallId(),
|
|
820
|
+
model: modelId,
|
|
821
|
+
attempts,
|
|
822
|
+
winner: void 0,
|
|
823
|
+
ok: false,
|
|
824
|
+
failedOver: attempts.length > 1,
|
|
825
|
+
latencyMs: Date.now() - startedAt,
|
|
826
|
+
inputTokens: 0,
|
|
827
|
+
outputTokens: 0,
|
|
828
|
+
costUsd: 0,
|
|
829
|
+
baselineUsd,
|
|
830
|
+
...modality ? { modality } : {}
|
|
831
|
+
});
|
|
832
|
+
};
|
|
833
|
+
const emitOk = (args) => {
|
|
834
|
+
const { settled, baseline } = args;
|
|
835
|
+
const baselineUsd = baseline ? priceCents(baseline.pricing, settled.ctx) / 100 : args.legacyBaselineUsd ?? 0;
|
|
836
|
+
safeCall({
|
|
837
|
+
id: newMediaCallId(),
|
|
838
|
+
model: args.modelId,
|
|
839
|
+
attempts: args.attempts,
|
|
840
|
+
winner: args.provider,
|
|
841
|
+
ok: true,
|
|
842
|
+
failedOver: args.attempts.length > 1,
|
|
843
|
+
latencyMs: Date.now() - args.startedAt,
|
|
844
|
+
inputTokens: 0,
|
|
845
|
+
outputTokens: 0,
|
|
846
|
+
costUsd: settled.costCents / 100,
|
|
847
|
+
baselineUsd,
|
|
848
|
+
...baseline ? { baselineKind: baseline.kind } : {},
|
|
849
|
+
...baseline?.kind === "official" ? { officialUsd: baselineUsd } : {},
|
|
850
|
+
modality: args.modality,
|
|
851
|
+
...settled.usage ? { usage: settled.usage } : {},
|
|
852
|
+
estCostUsd: settled.estCents / 100
|
|
853
|
+
});
|
|
854
|
+
};
|
|
767
855
|
const generate = async function generate2(modelId, input) {
|
|
768
|
-
const { ranked,
|
|
856
|
+
const { def, ranked, baseline } = resolve(modelId);
|
|
769
857
|
const startedAt = Date.now();
|
|
770
858
|
const attempts = [];
|
|
771
859
|
let lastErr;
|
|
@@ -775,12 +863,31 @@ function createMediaLCR(config) {
|
|
|
775
863
|
const attemptStart = Date.now();
|
|
776
864
|
try {
|
|
777
865
|
const result = await adapter.run({ externalId: route.externalId, input });
|
|
778
|
-
const
|
|
779
|
-
const costCents = costFor(route.refCents, result);
|
|
866
|
+
const settled = settle(route.pricing, route.refCents, result, input);
|
|
780
867
|
attempts.push({ provider: route.provider, ok: true, latencyMs: Date.now() - attemptStart });
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
868
|
+
warnDrift(modelId, route.provider, settled);
|
|
869
|
+
safeCost({
|
|
870
|
+
modelId,
|
|
871
|
+
provider: route.provider,
|
|
872
|
+
costCents: settled.costCents,
|
|
873
|
+
estimated: settled.estimated
|
|
874
|
+
});
|
|
875
|
+
emitOk({
|
|
876
|
+
modelId,
|
|
877
|
+
modality: def.modality,
|
|
878
|
+
provider: route.provider,
|
|
879
|
+
attempts,
|
|
880
|
+
settled,
|
|
881
|
+
baseline,
|
|
882
|
+
startedAt
|
|
883
|
+
});
|
|
884
|
+
return {
|
|
885
|
+
outputs: result.outputs,
|
|
886
|
+
provider: route.provider,
|
|
887
|
+
costCents: settled.costCents,
|
|
888
|
+
estimated: settled.estimated,
|
|
889
|
+
...settled.usage ? { usage: settled.usage } : {}
|
|
890
|
+
};
|
|
784
891
|
} catch (err) {
|
|
785
892
|
lastErr = err;
|
|
786
893
|
attempts.push({
|
|
@@ -791,19 +898,19 @@ function createMediaLCR(config) {
|
|
|
791
898
|
});
|
|
792
899
|
safeError(err, route.provider);
|
|
793
900
|
if (!isRetryableError(err)) {
|
|
794
|
-
emitFail(modelId, attempts,
|
|
901
|
+
emitFail(modelId, attempts, refBaselineUsd(baseline), startedAt);
|
|
795
902
|
throw err;
|
|
796
903
|
}
|
|
797
904
|
}
|
|
798
905
|
}
|
|
799
|
-
emitFail(modelId, attempts,
|
|
906
|
+
emitFail(modelId, attempts, refBaselineUsd(baseline), startedAt);
|
|
800
907
|
throw lastErr instanceof Error ? lastErr : new Error(`ai-lcr: no provider could serve media model "${modelId}"`);
|
|
801
908
|
};
|
|
802
909
|
const asyncRanked = (modelId) => {
|
|
803
910
|
const { ranked } = resolve(modelId);
|
|
804
911
|
return ranked.filter((r) => typeof adapters[r.provider]?.submit === "function");
|
|
805
912
|
};
|
|
806
|
-
async function submitFrom(modelId, routes, input, metadata,
|
|
913
|
+
async function submitFrom(modelId, routes, input, metadata, baseline, startedAt, attempts) {
|
|
807
914
|
let lastErr;
|
|
808
915
|
for (let i = 0; i < routes.length; i++) {
|
|
809
916
|
const route = routes[i];
|
|
@@ -818,10 +925,12 @@ function createMediaLCR(config) {
|
|
|
818
925
|
externalId: route.externalId,
|
|
819
926
|
requestId,
|
|
820
927
|
refCents: route.refCents,
|
|
928
|
+
pricing: route.pricing,
|
|
929
|
+
...baseline ? { baseline } : {},
|
|
821
930
|
fallbacks: routes.slice(i + 1).map((r) => ({ provider: r.provider, externalId: r.externalId, refCents: r.refCents })),
|
|
822
931
|
input,
|
|
823
932
|
...metadata ? { metadata } : {},
|
|
824
|
-
baselineUsd,
|
|
933
|
+
baselineUsd: refBaselineUsd(baseline),
|
|
825
934
|
startedAt,
|
|
826
935
|
attemptStart,
|
|
827
936
|
attempts
|
|
@@ -838,18 +947,18 @@ function createMediaLCR(config) {
|
|
|
838
947
|
if (!isRetryableError(err)) break;
|
|
839
948
|
}
|
|
840
949
|
}
|
|
841
|
-
emitFail(modelId, attempts,
|
|
950
|
+
emitFail(modelId, attempts, refBaselineUsd(baseline), startedAt);
|
|
842
951
|
throw lastErr instanceof Error ? lastErr : new Error(`ai-lcr: no async provider could submit media model "${modelId}"`);
|
|
843
952
|
}
|
|
844
953
|
generate.submit = async function submit(modelId, input, options) {
|
|
845
|
-
const { ranked,
|
|
954
|
+
const { ranked, baseline } = resolve(modelId);
|
|
846
955
|
const usable = ranked.filter((r) => typeof adapters[r.provider]?.submit === "function");
|
|
847
956
|
if (usable.length === 0) {
|
|
848
957
|
throw new Error(
|
|
849
958
|
`ai-lcr: no provider for media model "${modelId}" supports async submit (need an adapter with submit/checkStatus)`
|
|
850
959
|
);
|
|
851
960
|
}
|
|
852
|
-
return submitFrom(modelId, usable, input, options?.metadata,
|
|
961
|
+
return submitFrom(modelId, usable, input, options?.metadata, baseline, Date.now(), []);
|
|
853
962
|
};
|
|
854
963
|
generate.poll = async function poll(handle) {
|
|
855
964
|
const adapter = adapters[handle.provider];
|
|
@@ -859,6 +968,7 @@ function createMediaLCR(config) {
|
|
|
859
968
|
);
|
|
860
969
|
}
|
|
861
970
|
const failover = async (attempts) => {
|
|
971
|
+
const { baseline } = resolve(handle.modelId);
|
|
862
972
|
const next = asyncRanked(handle.modelId).filter(
|
|
863
973
|
(r) => handle.fallbacks.some((f) => f.provider === r.provider && f.externalId === r.externalId)
|
|
864
974
|
);
|
|
@@ -867,7 +977,7 @@ function createMediaLCR(config) {
|
|
|
867
977
|
next,
|
|
868
978
|
handle.input,
|
|
869
979
|
handle.metadata,
|
|
870
|
-
handle.
|
|
980
|
+
handle.baseline ?? baseline,
|
|
871
981
|
handle.startedAt,
|
|
872
982
|
attempts
|
|
873
983
|
);
|
|
@@ -907,15 +1017,37 @@ function createMediaLCR(config) {
|
|
|
907
1017
|
true
|
|
908
1018
|
);
|
|
909
1019
|
}
|
|
910
|
-
const
|
|
911
|
-
const costCents = costFor(handle.refCents, status);
|
|
1020
|
+
const settled = settle(handle.pricing, handle.refCents, { ...status, outputs }, handle.input);
|
|
912
1021
|
const attempts = [
|
|
913
1022
|
...handle.attempts,
|
|
914
1023
|
{ provider: handle.provider, ok: true, latencyMs: Date.now() - handle.attemptStart }
|
|
915
1024
|
];
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1025
|
+
warnDrift(handle.modelId, handle.provider, settled);
|
|
1026
|
+
safeCost({
|
|
1027
|
+
modelId: handle.modelId,
|
|
1028
|
+
provider: handle.provider,
|
|
1029
|
+
costCents: settled.costCents,
|
|
1030
|
+
estimated: settled.estimated
|
|
1031
|
+
});
|
|
1032
|
+
emitOk({
|
|
1033
|
+
modelId: handle.modelId,
|
|
1034
|
+
modality: registry[handle.modelId]?.modality ?? outputs[0].type,
|
|
1035
|
+
provider: handle.provider,
|
|
1036
|
+
attempts,
|
|
1037
|
+
settled,
|
|
1038
|
+
baseline: handle.baseline,
|
|
1039
|
+
legacyBaselineUsd: handle.baselineUsd,
|
|
1040
|
+
startedAt: handle.startedAt
|
|
1041
|
+
});
|
|
1042
|
+
return {
|
|
1043
|
+
done: true,
|
|
1044
|
+
status: "done",
|
|
1045
|
+
outputs,
|
|
1046
|
+
provider: handle.provider,
|
|
1047
|
+
costCents: settled.costCents,
|
|
1048
|
+
estimated: settled.estimated,
|
|
1049
|
+
...settled.usage ? { usage: settled.usage } : {}
|
|
1050
|
+
};
|
|
919
1051
|
}
|
|
920
1052
|
return onLegFailure(new Error(status.error ?? `ai-lcr: ${handle.provider} job failed`), true);
|
|
921
1053
|
};
|
|
@@ -1106,9 +1238,12 @@ function createKunavoMediaAdapter(config) {
|
|
|
1106
1238
|
if (urls.length === 0) {
|
|
1107
1239
|
return { status: "error", error: `Kunavo video job ${req.requestId} completed with no URL` };
|
|
1108
1240
|
}
|
|
1241
|
+
const durationSeconds = body.output?.duration_seconds;
|
|
1242
|
+
const seconds = typeof durationSeconds === "number" && Number.isFinite(durationSeconds) && durationSeconds > 0 ? durationSeconds : void 0;
|
|
1109
1243
|
return {
|
|
1110
1244
|
status: "done",
|
|
1111
|
-
outputs: urls.map((url) => ({ url, type: "video" }))
|
|
1245
|
+
outputs: urls.map((url) => ({ url, type: "video" })),
|
|
1246
|
+
usage: { outputs: urls.length, ...seconds !== void 0 ? { seconds } : {} }
|
|
1112
1247
|
};
|
|
1113
1248
|
}
|
|
1114
1249
|
if (status === "failed" || status === "error") {
|
|
@@ -1266,6 +1401,7 @@ function createRunwareMediaAdapter(config) {
|
|
|
1266
1401
|
return {
|
|
1267
1402
|
outputs,
|
|
1268
1403
|
units: images.length,
|
|
1404
|
+
usage: { outputs: images.length },
|
|
1269
1405
|
...cents !== void 0 ? { costCents: cents } : {}
|
|
1270
1406
|
};
|
|
1271
1407
|
},
|
|
@@ -1296,6 +1432,7 @@ function createRunwareMediaAdapter(config) {
|
|
|
1296
1432
|
return {
|
|
1297
1433
|
status: "done",
|
|
1298
1434
|
outputs: [{ url, type: "video" }],
|
|
1435
|
+
usage: { outputs: 1 },
|
|
1299
1436
|
...cents !== void 0 ? { costCents: cents } : {}
|
|
1300
1437
|
};
|
|
1301
1438
|
}
|
|
@@ -1387,7 +1524,7 @@ function createFalMediaAdapter(config) {
|
|
|
1387
1524
|
if (outputs.length === 0) {
|
|
1388
1525
|
return { status: "error", error: `fal job ${req.requestId} completed with no media URL` };
|
|
1389
1526
|
}
|
|
1390
|
-
return { status: "done", outputs, units: outputs.length };
|
|
1527
|
+
return { status: "done", outputs, units: outputs.length, usage: { outputs: outputs.length } };
|
|
1391
1528
|
}
|
|
1392
1529
|
return {
|
|
1393
1530
|
provider: "fal",
|
|
@@ -1439,7 +1576,7 @@ function createFalMediaAdapter(config) {
|
|
|
1439
1576
|
if (outputs.length === 0) {
|
|
1440
1577
|
throw new Error(`ai-lcr: fal returned no media URL for "${req.externalId}"`);
|
|
1441
1578
|
}
|
|
1442
|
-
return { outputs, units: outputs.length };
|
|
1579
|
+
return { outputs, units: outputs.length, usage: { outputs: outputs.length } };
|
|
1443
1580
|
}
|
|
1444
1581
|
};
|
|
1445
1582
|
}
|
|
@@ -1524,6 +1661,7 @@ export {
|
|
|
1524
1661
|
DEFAULT_REFERENCE,
|
|
1525
1662
|
MEDIA_PRICING,
|
|
1526
1663
|
OFFICIAL_PRICES,
|
|
1664
|
+
billableUnits,
|
|
1527
1665
|
cheapestRoute,
|
|
1528
1666
|
classifyError,
|
|
1529
1667
|
classifyErrorKind,
|
|
@@ -1534,11 +1672,13 @@ export {
|
|
|
1534
1672
|
createLCR,
|
|
1535
1673
|
createMediaLCR,
|
|
1536
1674
|
createRunwareMediaAdapter,
|
|
1675
|
+
durationFromInput,
|
|
1537
1676
|
formatCallRecord,
|
|
1538
1677
|
isAbortError,
|
|
1539
1678
|
isNetworkError,
|
|
1540
1679
|
isRetryableError,
|
|
1541
1680
|
normalizedCents,
|
|
1681
|
+
priceCents,
|
|
1542
1682
|
rankRoutes,
|
|
1543
1683
|
referenceMegapixels,
|
|
1544
1684
|
shouldFailover
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-lcr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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",
|
|
@@ -19,11 +19,11 @@
|
|
|
19
19
|
"author": "Victor",
|
|
20
20
|
"repository": {
|
|
21
21
|
"type": "git",
|
|
22
|
-
"url": "git+https://github.com/
|
|
22
|
+
"url": "git+https://github.com/ai-lcr/ai-lcr.git"
|
|
23
23
|
},
|
|
24
|
-
"homepage": "https://github.com/
|
|
24
|
+
"homepage": "https://github.com/ai-lcr/ai-lcr#readme",
|
|
25
25
|
"bugs": {
|
|
26
|
-
"url": "https://github.com/
|
|
26
|
+
"url": "https://github.com/ai-lcr/ai-lcr/issues"
|
|
27
27
|
},
|
|
28
28
|
"type": "module",
|
|
29
29
|
"main": "./dist/index.cjs",
|