@broberg/ai-sdk 0.51.0 → 0.53.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
@@ -18,7 +18,7 @@ import {
18
18
  listMediaPrices,
19
19
  pricingFreshness,
20
20
  pricingGeneratedAt
21
- } from "./chunk-O4LR6X4U.js";
21
+ } from "./chunk-ZSLRCOYM.js";
22
22
 
23
23
  // src/cost/default-hosts.ts
24
24
  var DEFAULT_BASE_URLS = {
@@ -517,6 +517,7 @@ function anthropicAdapter(config = {}) {
517
517
  cacheReadTokens: data.usage?.cache_read_input_tokens ?? 0,
518
518
  cacheCreationTokens: data.usage?.cache_creation_input_tokens ?? 0
519
519
  });
520
+ if (typeof data.model === "string" && data.model) usage.servedModel = data.model;
520
521
  const result = { text, usage };
521
522
  if (toolCalls.length > 0) result.toolCalls = toolCalls;
522
523
  return result;
@@ -809,6 +810,7 @@ function makeOpenAICompatibleAdapter(config) {
809
810
  if (config.costFromResponseField && typeof data.usage?.cost === "number") {
810
811
  usage.costUsd = data.usage.cost;
811
812
  }
813
+ if (typeof data.model === "string" && data.model) usage.servedModel = data.model;
812
814
  const result = { text, usage };
813
815
  if (toolCalls && toolCalls.length > 0) result.toolCalls = toolCalls;
814
816
  return result;
@@ -876,6 +878,7 @@ function makeOpenAICompatibleAdapter(config) {
876
878
  if (config.costFromResponseField && typeof chunk.usage.cost === "number") {
877
879
  usage.costUsd = chunk.usage.cost;
878
880
  }
881
+ if (typeof chunk.model === "string" && chunk.model) usage.servedModel = chunk.model;
879
882
  yield { type: "usage", costUsd: usage.costUsd, model: usage.model, usage };
880
883
  }
881
884
  }
@@ -3498,6 +3501,9 @@ var aiConfigSchema = z.object({
3498
3501
  /** F069.1 — customer-key mode. Every provider must be built with byokAdapter(), so no
3499
3502
  * call can fall back to the fleet's env keys; createAI throws at setup otherwise. */
3500
3503
  byok: z.boolean().optional(),
3504
+ /** F069.2 — attribution labels stamped on EVERY call's usage (and so on every cost-sink
3505
+ * row), e.g. { byok: "true", tenantId }. A call's own `labels` win on the same key. */
3506
+ labels: z.record(z.string(), z.string()).optional(),
3501
3507
  /** F039.2 — client-wide opt-out from prompt caching (default: on where the
3502
3508
  * provider supports it). A per-call `promptCache` wins over this. */
3503
3509
  promptCache: z.boolean().optional(),
@@ -3517,8 +3523,8 @@ var aiConfigSchema = z.object({
3517
3523
  import { randomUUID } from "crypto";
3518
3524
 
3519
3525
  // src/version.ts
3520
- var VERSION = "0.51.0";
3521
- var SDK_TAG = "@broberg/ai-sdk@0.51.0";
3526
+ var VERSION = "0.53.0";
3527
+ var SDK_TAG = "@broberg/ai-sdk@0.53.0";
3522
3528
 
3523
3529
  // src/cost/sinks/upmetrics.ts
3524
3530
  function isRetryableStatus(status) {
@@ -3568,6 +3574,9 @@ function upmetricsSink(config) {
3568
3574
  // capability/transport — no ingest-schema change, and without it the one
3569
3575
  // field built for auditability existed only in memory.
3570
3576
  region: usage.region,
3577
+ // F071.2 — the model that answered (contract with upmetrics F048). Omitted when
3578
+ // the provider did not say, rather than echoing the requested model as a guess.
3579
+ ...usage.servedModel ? { served_model: usage.servedModel } : {},
3571
3580
  // F050: HOW cost_usd was arrived at. upmetrics already distinguishes
3572
3581
  // reported / computed / unpriced — we were sending an assumed number in
3573
3582
  // the same field as a measured one, so their labels could not be right
@@ -3594,6 +3603,36 @@ function upmetricsSink(config) {
3594
3603
  void config.complianceMode;
3595
3604
  return JSON.stringify(body);
3596
3605
  }
3606
+ function buildFailureBody(f) {
3607
+ const endedAt = new Date(new Date(f.ts).getTime()).toISOString();
3608
+ const startedAt = new Date(new Date(f.ts).getTime() - (f.latencyMs || 0)).toISOString();
3609
+ const body = {
3610
+ mode: "record",
3611
+ agent_kind: config.agentKind ?? (f.capability === "embedding" ? "embedding" : "chatbot"),
3612
+ agent_name: config.agentName,
3613
+ provider: f.provider,
3614
+ model: f.model,
3615
+ status: "error",
3616
+ input_tokens: 0,
3617
+ output_tokens: 0,
3618
+ cost_usd: 0,
3619
+ duration_ms: f.latencyMs,
3620
+ started_at: startedAt,
3621
+ ended_at: endedAt,
3622
+ tags: {
3623
+ ...f.labels,
3624
+ capability: f.capability,
3625
+ transport: f.transport,
3626
+ error_code: f.errorCode,
3627
+ error_kind: f.errorKind,
3628
+ sdk: SDK_TAG,
3629
+ idempotencyKey: randomUUID()
3630
+ }
3631
+ };
3632
+ if (f.tier !== void 0) body.tier = f.tier;
3633
+ if (f.purpose !== void 0) body.purpose = f.purpose;
3634
+ return JSON.stringify(body);
3635
+ }
3597
3636
  async function send(body) {
3598
3637
  try {
3599
3638
  const res = await doFetch(url, {
@@ -3687,6 +3726,30 @@ function upmetricsSink(config) {
3687
3726
  config.onError?.(err);
3688
3727
  }
3689
3728
  },
3729
+ async recordFailure(failure) {
3730
+ try {
3731
+ const body = buildFailureBody(failure);
3732
+ const r = await send(body);
3733
+ if (r.kind === "ok") {
3734
+ counts.sent += 1;
3735
+ return;
3736
+ }
3737
+ if (r.kind === "reject") {
3738
+ counts.rejected += 1;
3739
+ config.onError?.(r.err);
3740
+ return;
3741
+ }
3742
+ if (!retry) {
3743
+ lose(r.err);
3744
+ return;
3745
+ }
3746
+ queue.push({ body, attempts: 1 });
3747
+ enforceCap();
3748
+ schedule();
3749
+ } catch (err) {
3750
+ config.onError?.(err);
3751
+ }
3752
+ },
3690
3753
  async flush() {
3691
3754
  if (timer) {
3692
3755
  clearTimeout(timer);
@@ -3742,6 +3805,29 @@ function assertByokProviders(providers) {
3742
3805
  }
3743
3806
  }
3744
3807
 
3808
+ // src/cost/failure.ts
3809
+ function classifyFailure(err) {
3810
+ const e = err;
3811
+ const name = e?.name ?? "";
3812
+ const message = String(e?.message ?? "");
3813
+ if (name === "AbortError" || name === "TimeoutError" || /timed? ?out/i.test(message.split("\n")[0] ?? "")) {
3814
+ return { errorCode: "timeout", errorKind: "timeout" };
3815
+ }
3816
+ let status = typeof e?.status === "number" ? e.status : void 0;
3817
+ if (status === void 0) {
3818
+ const m = /(?:^|\s)([45]\d{2})(?=[:\s]|$)/.exec(message.split("\n")[0] ?? "");
3819
+ if (m) status = Number(m[1]);
3820
+ }
3821
+ if (status !== void 0) {
3822
+ const kind = status === 401 || status === 403 ? "auth" : status === 404 ? "not_found" : status === 429 ? "rate_limit" : status >= 500 ? "server" : "other";
3823
+ return { errorCode: String(status), errorKind: kind };
3824
+ }
3825
+ if (name === "TypeError" && /fetch failed|network|ECONN|ENOTFOUND/i.test(message + (e?.cause?.code ?? ""))) {
3826
+ return { errorCode: "network", errorKind: "network" };
3827
+ }
3828
+ return { errorCode: "unknown", errorKind: "other" };
3829
+ }
3830
+
3745
3831
  // src/client.ts
3746
3832
  var DEFAULT_IMAGE_SPEC = {
3747
3833
  provider: "fal",
@@ -3831,11 +3917,19 @@ function createAI(config = {}) {
3831
3917
  usage.capability = capability;
3832
3918
  if (tier) usage.tier = tier;
3833
3919
  if (purpose) usage.purpose = purpose;
3834
- if (labels && Object.keys(labels).length > 0) usage.labels = labels;
3920
+ const merged = { ...cfg.labels, ...labels };
3921
+ if (Object.keys(merged).length > 0) usage.labels = merged;
3835
3922
  usage.latencyMs = Math.round(latencyMs);
3836
3923
  if (!usage.ts) usage.ts = (/* @__PURE__ */ new Date()).toISOString();
3837
3924
  return usage;
3838
3925
  }
3926
+ async function reportFailure(f, err) {
3927
+ if (!costSink?.recordFailure) return;
3928
+ try {
3929
+ await costSink.recordFailure({ ...f, ...classifyFailure(err), ts: (/* @__PURE__ */ new Date()).toISOString() });
3930
+ } catch {
3931
+ }
3932
+ }
3839
3933
  async function report(usage) {
3840
3934
  if (!costSink) return;
3841
3935
  try {
@@ -3871,8 +3965,8 @@ function createAI(config = {}) {
3871
3965
  for (let i = 0; i < routes.length; i++) {
3872
3966
  const spec = routes[i];
3873
3967
  await preflight(spec, opts.estIn, opts.estOut);
3968
+ const t0 = performance.now();
3874
3969
  try {
3875
- const t0 = performance.now();
3876
3970
  const res = await opts.invoke(spec);
3877
3971
  enrich(res.usage, opts.capability, i === 0 ? opts.tier : void 0, opts.purpose, performance.now() - t0, opts.labels);
3878
3972
  await settle(res.usage);
@@ -3880,6 +3974,20 @@ function createAI(config = {}) {
3880
3974
  return res;
3881
3975
  } catch (e) {
3882
3976
  lastErr = e;
3977
+ const labels = { ...cfg.labels, ...opts.labels };
3978
+ await reportFailure(
3979
+ {
3980
+ provider: spec.provider,
3981
+ model: spec.model,
3982
+ transport: spec.transport,
3983
+ capability: opts.capability,
3984
+ ...i === 0 && opts.tier ? { tier: opts.tier } : {},
3985
+ ...opts.purpose ? { purpose: opts.purpose } : {},
3986
+ ...Object.keys(labels).length ? { labels } : {},
3987
+ latencyMs: Math.round(performance.now() - t0)
3988
+ },
3989
+ e
3990
+ );
3883
3991
  }
3884
3992
  }
3885
3993
  throw lastErr;
@@ -3949,6 +4057,19 @@ function createAI(config = {}) {
3949
4057
  return;
3950
4058
  } catch (e) {
3951
4059
  lastErr = e;
4060
+ await reportFailure(
4061
+ {
4062
+ provider: spec.provider,
4063
+ model: spec.model,
4064
+ transport: spec.transport,
4065
+ capability: "chat",
4066
+ ...i === 0 ? { tier } : {},
4067
+ ...input.purpose ? { purpose: input.purpose } : {},
4068
+ ...Object.keys({ ...cfg.labels, ...input.labels }).length ? { labels: { ...cfg.labels, ...input.labels } } : {},
4069
+ latencyMs: Math.round(performance.now() - t0)
4070
+ },
4071
+ e
4072
+ );
3952
4073
  if (emitted || !eligibleForFallback(e)) {
3953
4074
  yield errorEvent(e);
3954
4075
  return;
@@ -4495,6 +4616,9 @@ function multiSink(sinks) {
4495
4616
  return {
4496
4617
  async record(usage) {
4497
4618
  await Promise.allSettled(sinks.map(async (s) => s.record(usage)));
4619
+ },
4620
+ async recordFailure(failure) {
4621
+ await Promise.allSettled(sinks.map(async (s) => s.recordFailure?.(failure)));
4498
4622
  }
4499
4623
  };
4500
4624
  }