@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/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-XDBN6QJ4.js";
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-O4LR6X4U.js";
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 = (per1M) => per1M / 1e6;
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.52.0";
3524
- var SDK_TAG = "@broberg/ai-sdk@0.52.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,