@broberg/ai-sdk 0.52.0 → 0.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-XDBN6QJ4.js → chunk-PDGFAD66.js} +2 -1
- package/dist/chunk-TSNJ5N4F.js +480 -0
- package/dist/chunk-TSNJ5N4F.js.map +1 -0
- package/dist/index.d.ts +123 -61
- package/dist/index.js +314 -6
- package/dist/index.js.map +1 -1
- package/dist/pricing.js +1 -1
- package/dist/registry.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-O4LR6X4U.js +0 -478
- package/dist/chunk-O4LR6X4U.js.map +0 -1
- /package/dist/{chunk-XDBN6QJ4.js.map → chunk-PDGFAD66.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DEFAULT_TIER_MAP,
|
|
3
3
|
ModelUnavailableError,
|
|
4
|
+
allEntries,
|
|
4
5
|
assertOverrideProvider,
|
|
5
6
|
listModels,
|
|
6
7
|
providerIds,
|
|
@@ -8,17 +9,19 @@ import {
|
|
|
8
9
|
resolveModel,
|
|
9
10
|
resolveTier,
|
|
10
11
|
setAvailability
|
|
11
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-PDGFAD66.js";
|
|
12
13
|
import {
|
|
13
14
|
DEFAULT_CLIP_SEC,
|
|
15
|
+
MEDIA_PRICING,
|
|
14
16
|
MEDIA_PRICING_CHECKED_AT,
|
|
17
|
+
PRICING,
|
|
15
18
|
PRICING_STALE_AFTER_DAYS,
|
|
16
19
|
getMediaPrice,
|
|
17
20
|
getPrice,
|
|
18
21
|
listMediaPrices,
|
|
19
22
|
pricingFreshness,
|
|
20
23
|
pricingGeneratedAt
|
|
21
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-TSNJ5N4F.js";
|
|
22
25
|
|
|
23
26
|
// src/cost/default-hosts.ts
|
|
24
27
|
var DEFAULT_BASE_URLS = {
|
|
@@ -363,7 +366,7 @@ function regionOfProvider(provider) {
|
|
|
363
366
|
function computeCost(provider, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheCreationTokens = 0) {
|
|
364
367
|
const price = getPrice(provider, model);
|
|
365
368
|
if (!price) return 0;
|
|
366
|
-
const perToken = (
|
|
369
|
+
const perToken = (per1M2) => per1M2 / 1e6;
|
|
367
370
|
const inRate = perToken(price.inputPer1M);
|
|
368
371
|
const outRate = perToken(price.outputPer1M);
|
|
369
372
|
const cacheReadRate = price.cacheReadPer1M !== void 0 ? perToken(price.cacheReadPer1M) : inRate;
|
|
@@ -517,6 +520,7 @@ function anthropicAdapter(config = {}) {
|
|
|
517
520
|
cacheReadTokens: data.usage?.cache_read_input_tokens ?? 0,
|
|
518
521
|
cacheCreationTokens: data.usage?.cache_creation_input_tokens ?? 0
|
|
519
522
|
});
|
|
523
|
+
if (typeof data.model === "string" && data.model) usage.servedModel = data.model;
|
|
520
524
|
const result = { text, usage };
|
|
521
525
|
if (toolCalls.length > 0) result.toolCalls = toolCalls;
|
|
522
526
|
return result;
|
|
@@ -809,6 +813,7 @@ function makeOpenAICompatibleAdapter(config) {
|
|
|
809
813
|
if (config.costFromResponseField && typeof data.usage?.cost === "number") {
|
|
810
814
|
usage.costUsd = data.usage.cost;
|
|
811
815
|
}
|
|
816
|
+
if (typeof data.model === "string" && data.model) usage.servedModel = data.model;
|
|
812
817
|
const result = { text, usage };
|
|
813
818
|
if (toolCalls && toolCalls.length > 0) result.toolCalls = toolCalls;
|
|
814
819
|
return result;
|
|
@@ -876,6 +881,7 @@ function makeOpenAICompatibleAdapter(config) {
|
|
|
876
881
|
if (config.costFromResponseField && typeof chunk.usage.cost === "number") {
|
|
877
882
|
usage.costUsd = chunk.usage.cost;
|
|
878
883
|
}
|
|
884
|
+
if (typeof chunk.model === "string" && chunk.model) usage.servedModel = chunk.model;
|
|
879
885
|
yield { type: "usage", costUsd: usage.costUsd, model: usage.model, usage };
|
|
880
886
|
}
|
|
881
887
|
}
|
|
@@ -3520,8 +3526,8 @@ var aiConfigSchema = z.object({
|
|
|
3520
3526
|
import { randomUUID } from "crypto";
|
|
3521
3527
|
|
|
3522
3528
|
// src/version.ts
|
|
3523
|
-
var VERSION = "0.
|
|
3524
|
-
var SDK_TAG = "@broberg/ai-sdk@0.
|
|
3529
|
+
var VERSION = "0.54.0";
|
|
3530
|
+
var SDK_TAG = "@broberg/ai-sdk@0.54.0";
|
|
3525
3531
|
|
|
3526
3532
|
// src/cost/sinks/upmetrics.ts
|
|
3527
3533
|
function isRetryableStatus(status) {
|
|
@@ -3571,6 +3577,9 @@ function upmetricsSink(config) {
|
|
|
3571
3577
|
// capability/transport — no ingest-schema change, and without it the one
|
|
3572
3578
|
// field built for auditability existed only in memory.
|
|
3573
3579
|
region: usage.region,
|
|
3580
|
+
// F071.2 — the model that answered (contract with upmetrics F048). Omitted when
|
|
3581
|
+
// the provider did not say, rather than echoing the requested model as a guess.
|
|
3582
|
+
...usage.servedModel ? { served_model: usage.servedModel } : {},
|
|
3574
3583
|
// F050: HOW cost_usd was arrived at. upmetrics already distinguishes
|
|
3575
3584
|
// reported / computed / unpriced — we were sending an assumed number in
|
|
3576
3585
|
// the same field as a measured one, so their labels could not be right
|
|
@@ -3597,6 +3606,36 @@ function upmetricsSink(config) {
|
|
|
3597
3606
|
void config.complianceMode;
|
|
3598
3607
|
return JSON.stringify(body);
|
|
3599
3608
|
}
|
|
3609
|
+
function buildFailureBody(f) {
|
|
3610
|
+
const endedAt = new Date(new Date(f.ts).getTime()).toISOString();
|
|
3611
|
+
const startedAt = new Date(new Date(f.ts).getTime() - (f.latencyMs || 0)).toISOString();
|
|
3612
|
+
const body = {
|
|
3613
|
+
mode: "record",
|
|
3614
|
+
agent_kind: config.agentKind ?? (f.capability === "embedding" ? "embedding" : "chatbot"),
|
|
3615
|
+
agent_name: config.agentName,
|
|
3616
|
+
provider: f.provider,
|
|
3617
|
+
model: f.model,
|
|
3618
|
+
status: "error",
|
|
3619
|
+
input_tokens: 0,
|
|
3620
|
+
output_tokens: 0,
|
|
3621
|
+
cost_usd: 0,
|
|
3622
|
+
duration_ms: f.latencyMs,
|
|
3623
|
+
started_at: startedAt,
|
|
3624
|
+
ended_at: endedAt,
|
|
3625
|
+
tags: {
|
|
3626
|
+
...f.labels,
|
|
3627
|
+
capability: f.capability,
|
|
3628
|
+
transport: f.transport,
|
|
3629
|
+
error_code: f.errorCode,
|
|
3630
|
+
error_kind: f.errorKind,
|
|
3631
|
+
sdk: SDK_TAG,
|
|
3632
|
+
idempotencyKey: randomUUID()
|
|
3633
|
+
}
|
|
3634
|
+
};
|
|
3635
|
+
if (f.tier !== void 0) body.tier = f.tier;
|
|
3636
|
+
if (f.purpose !== void 0) body.purpose = f.purpose;
|
|
3637
|
+
return JSON.stringify(body);
|
|
3638
|
+
}
|
|
3600
3639
|
async function send(body) {
|
|
3601
3640
|
try {
|
|
3602
3641
|
const res = await doFetch(url, {
|
|
@@ -3690,6 +3729,30 @@ function upmetricsSink(config) {
|
|
|
3690
3729
|
config.onError?.(err);
|
|
3691
3730
|
}
|
|
3692
3731
|
},
|
|
3732
|
+
async recordFailure(failure) {
|
|
3733
|
+
try {
|
|
3734
|
+
const body = buildFailureBody(failure);
|
|
3735
|
+
const r = await send(body);
|
|
3736
|
+
if (r.kind === "ok") {
|
|
3737
|
+
counts.sent += 1;
|
|
3738
|
+
return;
|
|
3739
|
+
}
|
|
3740
|
+
if (r.kind === "reject") {
|
|
3741
|
+
counts.rejected += 1;
|
|
3742
|
+
config.onError?.(r.err);
|
|
3743
|
+
return;
|
|
3744
|
+
}
|
|
3745
|
+
if (!retry) {
|
|
3746
|
+
lose(r.err);
|
|
3747
|
+
return;
|
|
3748
|
+
}
|
|
3749
|
+
queue.push({ body, attempts: 1 });
|
|
3750
|
+
enforceCap();
|
|
3751
|
+
schedule();
|
|
3752
|
+
} catch (err) {
|
|
3753
|
+
config.onError?.(err);
|
|
3754
|
+
}
|
|
3755
|
+
},
|
|
3693
3756
|
async flush() {
|
|
3694
3757
|
if (timer) {
|
|
3695
3758
|
clearTimeout(timer);
|
|
@@ -3745,6 +3808,29 @@ function assertByokProviders(providers) {
|
|
|
3745
3808
|
}
|
|
3746
3809
|
}
|
|
3747
3810
|
|
|
3811
|
+
// src/cost/failure.ts
|
|
3812
|
+
function classifyFailure(err) {
|
|
3813
|
+
const e = err;
|
|
3814
|
+
const name = e?.name ?? "";
|
|
3815
|
+
const message = String(e?.message ?? "");
|
|
3816
|
+
if (name === "AbortError" || name === "TimeoutError" || /timed? ?out/i.test(message.split("\n")[0] ?? "")) {
|
|
3817
|
+
return { errorCode: "timeout", errorKind: "timeout" };
|
|
3818
|
+
}
|
|
3819
|
+
let status = typeof e?.status === "number" ? e.status : void 0;
|
|
3820
|
+
if (status === void 0) {
|
|
3821
|
+
const m = /(?:^|\s)([45]\d{2})(?=[:\s]|$)/.exec(message.split("\n")[0] ?? "");
|
|
3822
|
+
if (m) status = Number(m[1]);
|
|
3823
|
+
}
|
|
3824
|
+
if (status !== void 0) {
|
|
3825
|
+
const kind = status === 401 || status === 403 ? "auth" : status === 404 ? "not_found" : status === 429 ? "rate_limit" : status >= 500 ? "server" : "other";
|
|
3826
|
+
return { errorCode: String(status), errorKind: kind };
|
|
3827
|
+
}
|
|
3828
|
+
if (name === "TypeError" && /fetch failed|network|ECONN|ENOTFOUND/i.test(message + (e?.cause?.code ?? ""))) {
|
|
3829
|
+
return { errorCode: "network", errorKind: "network" };
|
|
3830
|
+
}
|
|
3831
|
+
return { errorCode: "unknown", errorKind: "other" };
|
|
3832
|
+
}
|
|
3833
|
+
|
|
3748
3834
|
// src/client.ts
|
|
3749
3835
|
var DEFAULT_IMAGE_SPEC = {
|
|
3750
3836
|
provider: "fal",
|
|
@@ -3788,6 +3874,21 @@ var DEFAULT_JUDGE_SPEC = { provider: "typesafe", model: "jev-latest", transport:
|
|
|
3788
3874
|
var DEFAULT_PODCAST_SPEC = { provider: "elevenlabs", model: "eleven_v3", transport: "http" };
|
|
3789
3875
|
var DEFAULT_TTS_SPEC = { provider: "elevenlabs", model: "eleven_multilingual_v2", transport: "http" };
|
|
3790
3876
|
var DEFAULT_BATCH_SPEC = { provider: "mistral", model: "mistral-small-latest", transport: "http" };
|
|
3877
|
+
var ROUTED_SPECS = [
|
|
3878
|
+
DEFAULT_IMAGE_SPEC,
|
|
3879
|
+
DEFAULT_LORA_IMAGE_SPEC,
|
|
3880
|
+
DEFAULT_TRAINSTYLE_SPEC,
|
|
3881
|
+
DEFAULT_ANIMATE_SPEC,
|
|
3882
|
+
DEFAULT_BFL_FINETUNE_SPEC,
|
|
3883
|
+
DEFAULT_BFL_REFERENCE_SPEC,
|
|
3884
|
+
DEFAULT_OCR_SPEC,
|
|
3885
|
+
DEFAULT_MODERATION_SPEC,
|
|
3886
|
+
DEFAULT_JUDGE_SPEC,
|
|
3887
|
+
DEFAULT_PODCAST_SPEC,
|
|
3888
|
+
DEFAULT_TTS_SPEC,
|
|
3889
|
+
DEFAULT_BATCH_SPEC,
|
|
3890
|
+
DEFAULT_TRANSCRIBE_SPEC
|
|
3891
|
+
];
|
|
3791
3892
|
function defaultCostSink() {
|
|
3792
3893
|
const apiKey = process.env.UPMETRICS_API_KEY;
|
|
3793
3894
|
if (!apiKey) return void 0;
|
|
@@ -3840,6 +3941,13 @@ function createAI(config = {}) {
|
|
|
3840
3941
|
if (!usage.ts) usage.ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
3841
3942
|
return usage;
|
|
3842
3943
|
}
|
|
3944
|
+
async function reportFailure(f, err) {
|
|
3945
|
+
if (!costSink?.recordFailure) return;
|
|
3946
|
+
try {
|
|
3947
|
+
await costSink.recordFailure({ ...f, ...classifyFailure(err), ts: (/* @__PURE__ */ new Date()).toISOString() });
|
|
3948
|
+
} catch {
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3843
3951
|
async function report(usage) {
|
|
3844
3952
|
if (!costSink) return;
|
|
3845
3953
|
try {
|
|
@@ -3875,8 +3983,8 @@ function createAI(config = {}) {
|
|
|
3875
3983
|
for (let i = 0; i < routes.length; i++) {
|
|
3876
3984
|
const spec = routes[i];
|
|
3877
3985
|
await preflight(spec, opts.estIn, opts.estOut);
|
|
3986
|
+
const t0 = performance.now();
|
|
3878
3987
|
try {
|
|
3879
|
-
const t0 = performance.now();
|
|
3880
3988
|
const res = await opts.invoke(spec);
|
|
3881
3989
|
enrich(res.usage, opts.capability, i === 0 ? opts.tier : void 0, opts.purpose, performance.now() - t0, opts.labels);
|
|
3882
3990
|
await settle(res.usage);
|
|
@@ -3884,6 +3992,20 @@ function createAI(config = {}) {
|
|
|
3884
3992
|
return res;
|
|
3885
3993
|
} catch (e) {
|
|
3886
3994
|
lastErr = e;
|
|
3995
|
+
const labels = { ...cfg.labels, ...opts.labels };
|
|
3996
|
+
await reportFailure(
|
|
3997
|
+
{
|
|
3998
|
+
provider: spec.provider,
|
|
3999
|
+
model: spec.model,
|
|
4000
|
+
transport: spec.transport,
|
|
4001
|
+
capability: opts.capability,
|
|
4002
|
+
...i === 0 && opts.tier ? { tier: opts.tier } : {},
|
|
4003
|
+
...opts.purpose ? { purpose: opts.purpose } : {},
|
|
4004
|
+
...Object.keys(labels).length ? { labels } : {},
|
|
4005
|
+
latencyMs: Math.round(performance.now() - t0)
|
|
4006
|
+
},
|
|
4007
|
+
e
|
|
4008
|
+
);
|
|
3887
4009
|
}
|
|
3888
4010
|
}
|
|
3889
4011
|
throw lastErr;
|
|
@@ -3953,6 +4075,19 @@ function createAI(config = {}) {
|
|
|
3953
4075
|
return;
|
|
3954
4076
|
} catch (e) {
|
|
3955
4077
|
lastErr = e;
|
|
4078
|
+
await reportFailure(
|
|
4079
|
+
{
|
|
4080
|
+
provider: spec.provider,
|
|
4081
|
+
model: spec.model,
|
|
4082
|
+
transport: spec.transport,
|
|
4083
|
+
capability: "chat",
|
|
4084
|
+
...i === 0 ? { tier } : {},
|
|
4085
|
+
...input.purpose ? { purpose: input.purpose } : {},
|
|
4086
|
+
...Object.keys({ ...cfg.labels, ...input.labels }).length ? { labels: { ...cfg.labels, ...input.labels } } : {},
|
|
4087
|
+
latencyMs: Math.round(performance.now() - t0)
|
|
4088
|
+
},
|
|
4089
|
+
e
|
|
4090
|
+
);
|
|
3956
4091
|
if (emitted || !eligibleForFallback(e)) {
|
|
3957
4092
|
yield errorEvent(e);
|
|
3958
4093
|
return;
|
|
@@ -4315,6 +4450,175 @@ function createAI(config = {}) {
|
|
|
4315
4450
|
return client;
|
|
4316
4451
|
}
|
|
4317
4452
|
|
|
4453
|
+
// src/catalogue/fetchers.ts
|
|
4454
|
+
async function getJson(f, url, headers) {
|
|
4455
|
+
const res = await f(url, { headers: { accept: "application/json", ...headers } });
|
|
4456
|
+
if (!res.ok) {
|
|
4457
|
+
const body = await res.text().catch(() => "");
|
|
4458
|
+
throw new Error(`${res.status} ${body.slice(0, 200)}`);
|
|
4459
|
+
}
|
|
4460
|
+
return res.json();
|
|
4461
|
+
}
|
|
4462
|
+
function per1M(v) {
|
|
4463
|
+
if (v == null) return void 0;
|
|
4464
|
+
const n = Number(v) * 1e6;
|
|
4465
|
+
return Number.isFinite(n) && n >= 0 ? n : void 0;
|
|
4466
|
+
}
|
|
4467
|
+
async function fetchOpenRouterCatalogue(opts = {}) {
|
|
4468
|
+
const f = opts.fetch ?? fetch;
|
|
4469
|
+
const base = opts.baseUrl ?? "https://openrouter.ai/api/v1";
|
|
4470
|
+
const json = await getJson(f, `${base}/models`, {});
|
|
4471
|
+
return (json.data ?? []).map((m) => {
|
|
4472
|
+
const inputPer1M = per1M(m.pricing?.prompt);
|
|
4473
|
+
const outputPer1M = per1M(m.pricing?.completion);
|
|
4474
|
+
return {
|
|
4475
|
+
provider: "openrouter",
|
|
4476
|
+
model: m.id,
|
|
4477
|
+
...inputPer1M !== void 0 ? { inputPer1M } : {},
|
|
4478
|
+
...outputPer1M !== void 0 ? { outputPer1M } : {},
|
|
4479
|
+
...m.context_length ? { contextLength: m.context_length } : {}
|
|
4480
|
+
};
|
|
4481
|
+
});
|
|
4482
|
+
}
|
|
4483
|
+
|
|
4484
|
+
// src/catalogue/types.ts
|
|
4485
|
+
function catalogueKey(m) {
|
|
4486
|
+
return `${m.provider}:${m.model}`;
|
|
4487
|
+
}
|
|
4488
|
+
|
|
4489
|
+
// src/catalogue/diff.ts
|
|
4490
|
+
function brandToken(model) {
|
|
4491
|
+
const tail = model.split("/").pop() ?? model;
|
|
4492
|
+
return tail.split("-")[0] ?? tail;
|
|
4493
|
+
}
|
|
4494
|
+
var DIRECT_PROVIDERS = /* @__PURE__ */ new Set(["openai", "anthropic", "gemini"]);
|
|
4495
|
+
var FULL_LIST_PROVIDERS = /* @__PURE__ */ new Set(["mistral", "deepseek", "elevenlabs"]);
|
|
4496
|
+
function undatedBase(id) {
|
|
4497
|
+
return id.replace(/-(\d{4}-\d{2}-\d{2}|\d{8}|\d{4})$/, "");
|
|
4498
|
+
}
|
|
4499
|
+
function knownKeys() {
|
|
4500
|
+
const known = /* @__PURE__ */ new Set([...Object.keys(PRICING), ...Object.keys(MEDIA_PRICING)]);
|
|
4501
|
+
for (const e of allEntries()) known.add(`${e.provider}:${e.id}`);
|
|
4502
|
+
for (const spec of [...Object.values(DEFAULT_TIER_MAP), ...ROUTED_SPECS]) known.add(`${spec.provider}:${spec.model}`);
|
|
4503
|
+
return known;
|
|
4504
|
+
}
|
|
4505
|
+
function diffCatalogue(fetched, opts = {}) {
|
|
4506
|
+
const threshold = opts.priceThreshold ?? 0.01;
|
|
4507
|
+
const fetchedProviders = new Set(opts.fetchedProviders ?? fetched.map((m) => m.provider));
|
|
4508
|
+
const fetchedByKey = /* @__PURE__ */ new Map();
|
|
4509
|
+
for (const m of fetched) {
|
|
4510
|
+
fetchedByKey.set(catalogueKey(m), m);
|
|
4511
|
+
for (const a of m.aliases ?? []) {
|
|
4512
|
+
const k = catalogueKey({ provider: m.provider, model: a });
|
|
4513
|
+
if (!fetchedByKey.has(k)) fetchedByKey.set(k, m);
|
|
4514
|
+
}
|
|
4515
|
+
}
|
|
4516
|
+
const known = knownKeys();
|
|
4517
|
+
const trackedBrands = /* @__PURE__ */ new Map();
|
|
4518
|
+
for (const key of Object.keys(PRICING)) {
|
|
4519
|
+
const [provider, model] = splitKey(key);
|
|
4520
|
+
if (!trackedBrands.has(provider)) trackedBrands.set(provider, /* @__PURE__ */ new Set());
|
|
4521
|
+
trackedBrands.get(provider).add(brandToken(model));
|
|
4522
|
+
}
|
|
4523
|
+
const added = [];
|
|
4524
|
+
const reportedGroups = /* @__PURE__ */ new Set();
|
|
4525
|
+
const listedByProvider = /* @__PURE__ */ new Map();
|
|
4526
|
+
for (const m of fetched) {
|
|
4527
|
+
if (!listedByProvider.has(m.provider)) listedByProvider.set(m.provider, /* @__PURE__ */ new Set());
|
|
4528
|
+
listedByProvider.get(m.provider).add(m.model);
|
|
4529
|
+
}
|
|
4530
|
+
const isSnapshotOfListed = (m) => {
|
|
4531
|
+
const base = undatedBase(m.model);
|
|
4532
|
+
if (base === m.model) return false;
|
|
4533
|
+
return listedByProvider.get(m.provider)?.has(base) === true || known.has(catalogueKey({ provider: m.provider, model: base }));
|
|
4534
|
+
};
|
|
4535
|
+
for (const m of fetched) {
|
|
4536
|
+
if (FULL_LIST_PROVIDERS.has(m.provider)) {
|
|
4537
|
+
if (m.deprecated) continue;
|
|
4538
|
+
const group = [m.model, ...m.aliases ?? []].map((id) => catalogueKey({ provider: m.provider, model: id }));
|
|
4539
|
+
if (group.some((k) => known.has(k) || reportedGroups.has(k))) continue;
|
|
4540
|
+
for (const k of group) reportedGroups.add(k);
|
|
4541
|
+
added.push(m);
|
|
4542
|
+
continue;
|
|
4543
|
+
}
|
|
4544
|
+
if (!DIRECT_PROVIDERS.has(m.provider)) continue;
|
|
4545
|
+
if (isSnapshotOfListed(m)) continue;
|
|
4546
|
+
const brands = trackedBrands.get(m.provider);
|
|
4547
|
+
if (!brands?.has(brandToken(m.model))) continue;
|
|
4548
|
+
if (PRICING[catalogueKey(m)]) continue;
|
|
4549
|
+
added.push(m);
|
|
4550
|
+
}
|
|
4551
|
+
const missingPrice = [];
|
|
4552
|
+
for (const spec of Object.values(DEFAULT_TIER_MAP)) {
|
|
4553
|
+
if (!getPrice(spec.provider, spec.model)) missingPrice.push(`${spec.provider}:${spec.model}`);
|
|
4554
|
+
}
|
|
4555
|
+
const priceChanged = [];
|
|
4556
|
+
const removedUpstream = [];
|
|
4557
|
+
for (const [key, entry] of Object.entries(PRICING)) {
|
|
4558
|
+
const [provider] = splitKey(key);
|
|
4559
|
+
if (!fetchedProviders.has(provider)) continue;
|
|
4560
|
+
const up = fetchedByKey.get(key);
|
|
4561
|
+
if (!up) {
|
|
4562
|
+
removedUpstream.push(key);
|
|
4563
|
+
continue;
|
|
4564
|
+
}
|
|
4565
|
+
if (up.inputPer1M === void 0 && up.outputPer1M === void 0) continue;
|
|
4566
|
+
const inDrift = relDrift(entry.inputPer1M, up.inputPer1M);
|
|
4567
|
+
const outDrift = relDrift(entry.outputPer1M, up.outputPer1M);
|
|
4568
|
+
if (inDrift > threshold || outDrift > threshold) {
|
|
4569
|
+
priceChanged.push({
|
|
4570
|
+
key,
|
|
4571
|
+
ourInputPer1M: entry.inputPer1M,
|
|
4572
|
+
ourOutputPer1M: entry.outputPer1M,
|
|
4573
|
+
...up.inputPer1M !== void 0 ? { upstreamInputPer1M: up.inputPer1M } : {},
|
|
4574
|
+
...up.outputPer1M !== void 0 ? { upstreamOutputPer1M: up.outputPer1M } : {}
|
|
4575
|
+
});
|
|
4576
|
+
}
|
|
4577
|
+
}
|
|
4578
|
+
return { added, missingPrice, priceChanged, removedUpstream };
|
|
4579
|
+
}
|
|
4580
|
+
function relDrift(ours, upstream) {
|
|
4581
|
+
if (upstream === void 0) return 0;
|
|
4582
|
+
if (upstream === 0) return ours === 0 ? 0 : Infinity;
|
|
4583
|
+
return Math.abs(ours - upstream) / upstream;
|
|
4584
|
+
}
|
|
4585
|
+
function splitKey(key) {
|
|
4586
|
+
const i = key.indexOf(":");
|
|
4587
|
+
return [key.slice(0, i), key.slice(i + 1)];
|
|
4588
|
+
}
|
|
4589
|
+
|
|
4590
|
+
// src/catalogue/price-drift.ts
|
|
4591
|
+
var COVERAGE = "OpenRouter catalogue only. Not covered: direct-provider prices (no price API) and speech/image/video/OCR (hand-checked, F050).";
|
|
4592
|
+
function pct(ours, up) {
|
|
4593
|
+
if (up === void 0) return void 0;
|
|
4594
|
+
if (ours === 0) return up === 0 ? 0 : Infinity;
|
|
4595
|
+
return Math.round((up - ours) / ours * 1e3) / 10;
|
|
4596
|
+
}
|
|
4597
|
+
async function checkPriceDrift(opts = {}) {
|
|
4598
|
+
const measuredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4599
|
+
try {
|
|
4600
|
+
const models = await fetchOpenRouterCatalogue({ fetch: opts.fetch });
|
|
4601
|
+
if (models.length === 0) return { ok: false, measuredAt, error: "OpenRouter returned an empty catalogue" };
|
|
4602
|
+
const diff = diffCatalogue(models, { fetchedProviders: ["openrouter"], priceThreshold: opts.threshold ?? 0.01 });
|
|
4603
|
+
const drift = diff.priceChanged.map((p) => {
|
|
4604
|
+
const i = p.key.indexOf(":");
|
|
4605
|
+
return {
|
|
4606
|
+
key: p.key,
|
|
4607
|
+
provider: p.key.slice(0, i),
|
|
4608
|
+
model: p.key.slice(i + 1),
|
|
4609
|
+
ours: { inputPer1M: p.ourInputPer1M, outputPer1M: p.ourOutputPer1M },
|
|
4610
|
+
upstream: { inputPer1M: p.upstreamInputPer1M, outputPer1M: p.upstreamOutputPer1M },
|
|
4611
|
+
inputChangePct: pct(p.ourInputPer1M, p.upstreamInputPer1M),
|
|
4612
|
+
outputChangePct: pct(p.ourOutputPer1M, p.upstreamOutputPer1M)
|
|
4613
|
+
};
|
|
4614
|
+
});
|
|
4615
|
+
const checked = models.filter((m) => m.inputPer1M !== void 0 || m.outputPer1M !== void 0).length;
|
|
4616
|
+
return { ok: true, measuredAt, source: "openrouter", checked, drift, coverage: COVERAGE };
|
|
4617
|
+
} catch (e) {
|
|
4618
|
+
return { ok: false, measuredAt, error: e instanceof Error ? e.message : String(e) };
|
|
4619
|
+
}
|
|
4620
|
+
}
|
|
4621
|
+
|
|
4318
4622
|
// src/providers/stub.ts
|
|
4319
4623
|
function stubUsage(provider, model, transport, capability) {
|
|
4320
4624
|
return {
|
|
@@ -4499,6 +4803,9 @@ function multiSink(sinks) {
|
|
|
4499
4803
|
return {
|
|
4500
4804
|
async record(usage) {
|
|
4501
4805
|
await Promise.allSettled(sinks.map(async (s) => s.record(usage)));
|
|
4806
|
+
},
|
|
4807
|
+
async recordFailure(failure) {
|
|
4808
|
+
await Promise.allSettled(sinks.map(async (s) => s.recordFailure?.(failure)));
|
|
4502
4809
|
}
|
|
4503
4810
|
};
|
|
4504
4811
|
}
|
|
@@ -4754,6 +5061,7 @@ export {
|
|
|
4754
5061
|
bflCredits,
|
|
4755
5062
|
byokAdapter,
|
|
4756
5063
|
chatInputSchema,
|
|
5064
|
+
checkPriceDrift,
|
|
4757
5065
|
checkVoice,
|
|
4758
5066
|
classifyRegionName,
|
|
4759
5067
|
computeCost,
|