@broberg/ai-sdk 0.39.0 → 0.41.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.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  export { AvailabilitySource, AvailabilityStatus, ModelStatus, ModelUnavailableError, ResolveOptions, ResolveResult, listModels, resolveModel } from './registry.js';
3
- export { PRICING_STALE_AFTER_DAYS, PricingFreshness, pricingFreshness, pricingGeneratedAt } from './pricing.js';
3
+ export { DEFAULT_CLIP_SEC, MEDIA_PRICING_CHECKED_AT, MediaPrice, MediaUnit, PRICING_STALE_AFTER_DAYS, PricingFreshness, UnitFreshness, getMediaPrice, listMediaPrices, pricingFreshness, pricingGeneratedAt } from './pricing.js';
4
4
 
5
5
  /** Coarse data-residency of the endpoint a call went to.
6
6
  *
@@ -39,13 +39,31 @@ declare function classifyRegionName(name: string | undefined): Region;
39
39
  * is it?" — a narrower question than "where will my call go?". */
40
40
  declare function regionOfProvider(provider: string): Region;
41
41
 
42
- /** One dictionary entry. `alias` says it differently; `ipa` says it precisely. */
42
+ /** One dictionary entry. `alias` says it differently; `ipa` says it in phonemes.
43
+ *
44
+ * **`ipa` is NOT the "more precise" option for a FOREIGN word — measured 2026-09-04.**
45
+ * Christian listened to `workflow` as `ˈwɜːkfloʊ` through the da-DK voices jeppe and
46
+ * christel and reported: *"ikke korrekt engelsk med et sjovt mix"*. A Danish neural
47
+ * voice renders English phonemes through Danish phonology, so foreign IPA comes out a
48
+ * hybrid rather than English. The symbols are honoured; the accent is not.
49
+ *
50
+ * So pick by what the word IS, not by which field sounds more exact:
51
+ * - a DANISH word the voice mispronounces → `ipa` does what you want.
52
+ * - an ENGLISH word inside Danish text → neither field is right yet. `alias` (spell it
53
+ * the way a Dane would say it) is the honest workaround today; a real engine switch
54
+ * is F051.2, and it is blocked on a listening test, not on code.
55
+ *
56
+ * Dated on purpose. This is a measurement of two specific voices on one date, not a
57
+ * permanent property of IPA — Azure ships new voices, and the next reader deserves to
58
+ * see that it may have moved. */
43
59
  interface Pronunciation {
44
60
  /** The word as it appears in the text. Matched whole-word, case-insensitively. */
45
61
  word: string;
46
62
  /** Say this instead. Azure `<sub alias>`; ElevenLabs plain substitution. */
47
63
  alias?: string;
48
- /** IPA phonemes. Azure `<phoneme alphabet="ipa" ph>`. ElevenLabs cannot do this. */
64
+ /** IPA phonemes. Azure `<phoneme alphabet="ipa" ph>`. ElevenLabs cannot do this
65
+ * (it THROWS rather than silently skipping). See the type doc above before reaching
66
+ * for this on a foreign word. */
49
67
  ipa?: string;
50
68
  /** Reserved for a future per-entry language switch; carried but not yet emitted. */
51
69
  lang?: string;
@@ -159,6 +177,24 @@ interface Usage {
159
177
  cacheReadTokens: number;
160
178
  cacheCreationTokens: number;
161
179
  costUsd: number;
180
+ /** How `costUsd` was arrived at (F050). Absent means the same as `"computed"`;
181
+ * it is optional only so that adding it could not break a consumer building a
182
+ * Usage by hand.
183
+ *
184
+ * - `"reported"` — the PROVIDER told us what the call cost (BFL returns credits).
185
+ * - `"computed"` — a price table rate × a quantity we actually measured (tokens).
186
+ * - `"estimated"` — a quantity or a rate was ASSUMED. A video call with no
187
+ * `durationSec` is billed at {@link DEFAULT_CLIP_SEC} seconds regardless of the
188
+ * clip that came back, and fal's per-image rates are fal's own estimates.
189
+ * - `"unpriced"` — we have NO price for this model, so `costUsd` is 0 because we
190
+ * could not answer, not because the call was free. Those two were the same
191
+ * number, and the free one is much rarer than the unknown one.
192
+ *
193
+ * This is a DATA field on purpose. upmetrics distinguishes reported / computed /
194
+ * unpriced / untokened, and it reads fields — a JSDoc note saying "this one is a
195
+ * guess" never reaches it, so an assumed number arrived there indistinguishable
196
+ * from a measured one. super did exactly that and passed it on as measured. */
197
+ costBasis?: "reported" | "computed" | "estimated" | "unpriced";
162
198
  toolCalls?: {
163
199
  name: string;
164
200
  count: number;
@@ -2302,8 +2338,8 @@ declare const falStubAdapter: ProviderAdapter;
2302
2338
  * wires the live adapters. */
2303
2339
  declare const stubProviders: Record<string, ProviderAdapter>;
2304
2340
 
2305
- declare const VERSION: "0.39.0";
2306
- declare const SDK_TAG: "@broberg/ai-sdk@0.39.0";
2341
+ declare const VERSION: "0.41.0";
2342
+ declare const SDK_TAG: "@broberg/ai-sdk@0.41.0";
2307
2343
 
2308
2344
  /** Built-in defaults. Every entry is overridable via AiConfig.defaults or a
2309
2345
  * per-call override.
package/dist/index.js CHANGED
@@ -10,11 +10,15 @@ import {
10
10
  setAvailability
11
11
  } from "./chunk-ZFWSLSE7.js";
12
12
  import {
13
+ DEFAULT_CLIP_SEC,
14
+ MEDIA_PRICING_CHECKED_AT,
13
15
  PRICING_STALE_AFTER_DAYS,
16
+ getMediaPrice,
14
17
  getPrice,
18
+ listMediaPrices,
15
19
  pricingFreshness,
16
20
  pricingGeneratedAt
17
- } from "./chunk-QUOZWQ7G.js";
21
+ } from "./chunk-B74RSBT5.js";
18
22
 
19
23
  // src/transport/http.ts
20
24
  async function httpTransport(req) {
@@ -355,6 +359,11 @@ function freshUsage(args) {
355
359
  cacheReadTokens,
356
360
  cacheCreationTokens,
357
361
  costUsd,
362
+ // F050 — say HOW we got the number. A $0 because the model is not in the price
363
+ // table and a $0 because the call was genuinely free were the same value, and the
364
+ // free case is much rarer than the unknown one. Adapters that bill a non-token
365
+ // unit overwrite this with "estimated"/"reported" right after.
366
+ costBasis: args.subprocess ? "computed" : getPrice(args.provider, args.model) ? "computed" : "unpriced",
358
367
  latencyMs: 0,
359
368
  capability: args.capability,
360
369
  ts: ""
@@ -882,9 +891,6 @@ function mapFinishReason(reason) {
882
891
  }
883
892
 
884
893
  // src/providers/openai.ts
885
- var WHISPER_PRICE_PER_MIN = {
886
- "whisper-1": 6e-3
887
- };
888
894
  function openaiAdapter(config = {}) {
889
895
  const baseUrl = config.baseUrl ?? "https://api.openai.com/v1";
890
896
  const base = makeOpenAICompatibleAdapter({ name: "openai", baseUrl, apiKey: config.apiKey });
@@ -945,7 +951,7 @@ function openaiAdapter(config = {}) {
945
951
  // Whisper is per-minute, not token-priced.
946
952
  });
947
953
  if (req.durationSec !== void 0) {
948
- const perMinute = WHISPER_PRICE_PER_MIN[req.spec.model] ?? 0;
954
+ const perMinute = getMediaPrice("openai", req.spec.model)?.usd ?? 0;
949
955
  usage.costUsd = req.durationSec / 60 * perMinute;
950
956
  }
951
957
  const result = { text: data.text ?? "", usage };
@@ -985,17 +991,6 @@ function sniffMime(b) {
985
991
  }
986
992
 
987
993
  // src/providers/gemini.ts
988
- var GEMINI_IMAGE_PRICE_PER_IMAGE = {
989
- "gemini-2.5-flash-image": 0.039,
990
- // "nano-banana" — 1024px = 1290 tok
991
- "gemini-3.1-flash-image": 0.067,
992
- // 1K=$0.067, 2K=$0.101, 4K=$0.151
993
- "gemini-3.1-flash-image-preview": 0.067,
994
- "gemini-3-pro-image": 0.134,
995
- // premium — 1K/2K=$0.134, 4K=$0.24
996
- "gemini-3-pro-image-preview": 0.134
997
- // was $0.039 — wrong (that's the flash price); pro is $0.134
998
- };
999
994
  function splitCached(meta) {
1000
995
  const prompt = meta?.promptTokenCount ?? 0;
1001
996
  const cached = meta?.cachedContentTokenCount;
@@ -1014,16 +1009,6 @@ function partsFrom(content) {
1014
1009
  return { inlineData: { mimeType: p.mimeType ?? "image/png", data } };
1015
1010
  });
1016
1011
  }
1017
- var VEO_PRICE_PER_SEC = {
1018
- "veo-3.1-generate-preview": 0.4,
1019
- // standard; 4K = 0.60
1020
- "veo-3.1-fast-generate-preview": 0.1,
1021
- // 720p; 1080p = 0.12, 4K = 0.30
1022
- "veo-3.1-lite-generate-preview": 0.05,
1023
- // 720p; 1080p = 0.08
1024
- "veo-3.0-generate-001": 0.4,
1025
- "veo-3.0-fast-generate-001": 0.1
1026
- };
1027
1012
  function geminiAdapter(config = {}) {
1028
1013
  const baseUrl = config.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta";
1029
1014
  function resolveKey() {
@@ -1180,7 +1165,7 @@ function geminiAdapter(config = {}) {
1180
1165
  ...splitCached(data.usageMetadata),
1181
1166
  outputTokens: data.usageMetadata?.candidatesTokenCount ?? 0
1182
1167
  });
1183
- usage.costUsd = config.pricePerImage ?? GEMINI_IMAGE_PRICE_PER_IMAGE[req.spec.model] ?? 0;
1168
+ usage.costUsd = config.pricePerImage ?? getMediaPrice("gemini", req.spec.model)?.usd ?? 0;
1184
1169
  return { url, usage };
1185
1170
  }
1186
1171
  async function animate(req) {
@@ -1232,8 +1217,9 @@ function geminiAdapter(config = {}) {
1232
1217
  inputTokens: 0,
1233
1218
  outputTokens: 0
1234
1219
  });
1235
- const perSec = config.pricePerSecond ?? VEO_PRICE_PER_SEC[req.spec.model] ?? 0;
1236
- usage.costUsd = perSec * (req.durationSec ?? 8);
1220
+ const perSec = config.pricePerSecond ?? getMediaPrice("gemini", req.spec.model)?.usd ?? 0;
1221
+ usage.costUsd = perSec * (req.durationSec ?? DEFAULT_CLIP_SEC);
1222
+ usage.costBasis = req.durationSec === void 0 ? "estimated" : "computed";
1237
1223
  return { url: videoUri, bytes, mimeType: "video/mp4", usage };
1238
1224
  }
1239
1225
  return { name: "gemini", chat, chatStream, image, animate, vision: chat };
@@ -1259,10 +1245,6 @@ function deepinfraAdapter(config = {}) {
1259
1245
  }
1260
1246
 
1261
1247
  // src/providers/openrouter.ts
1262
- var OPENROUTER_IMAGE_PRICE_ESTIMATE = {
1263
- "recraft/recraft-v4.1": 0.035,
1264
- "recraft/recraft-v4.1-vector": 0.08
1265
- };
1266
1248
  function openrouterAdapter(config = {}) {
1267
1249
  const baseUrl = config.baseUrl ?? "https://openrouter.ai/api/v1";
1268
1250
  const headers = {
@@ -1317,7 +1299,7 @@ function openrouterAdapter(config = {}) {
1317
1299
  inputTokens: 0,
1318
1300
  outputTokens: 0
1319
1301
  });
1320
- usage.costUsd = data.usage?.cost ?? config.pricePerImage ?? OPENROUTER_IMAGE_PRICE_ESTIMATE[req.spec.model] ?? 0;
1302
+ usage.costUsd = data.usage?.cost ?? config.pricePerImage ?? getMediaPrice("openrouter", req.spec.model)?.usd ?? 0;
1321
1303
  return { url: `data:${first.media_type ?? "image/png"};base64,${first.b64_json}`, usage };
1322
1304
  }
1323
1305
  return { ...base, image };
@@ -1354,12 +1336,6 @@ function deepseekAdapter(config = {}) {
1354
1336
  }
1355
1337
 
1356
1338
  // src/providers/mistral.ts
1357
- var MISTRAL_OCR_PRICE_PER_PAGE = 2e-3;
1358
- var VOXTRAL_PRICE_PER_MIN = {
1359
- "voxtral-mini-latest": 2e-3,
1360
- "voxtral-mini-2507": 2e-3,
1361
- "voxtral-mini-2602": 2e-3
1362
- };
1363
1339
  function mistralAdapter(config = {}) {
1364
1340
  const baseUrl = config.baseUrl ?? "https://api.mistral.ai/v1";
1365
1341
  const base = makeOpenAICompatibleAdapter({ name: "mistral", baseUrl, apiKey: config.apiKey, supportsPromptCacheKey: true, supportsPrefix: true });
@@ -1401,7 +1377,7 @@ function mistralAdapter(config = {}) {
1401
1377
  inputTokens: 0,
1402
1378
  outputTokens: 0
1403
1379
  });
1404
- usage.costUsd = pagesProcessed * (config.pricePerPage ?? MISTRAL_OCR_PRICE_PER_PAGE);
1380
+ usage.costUsd = pagesProcessed * (config.pricePerPage ?? getMediaPrice("mistral", "ocr")?.usd ?? 0);
1405
1381
  return { pages, usage };
1406
1382
  }
1407
1383
  async function moderate(req) {
@@ -1495,7 +1471,7 @@ function mistralAdapter(config = {}) {
1495
1471
  outputTokens: 0
1496
1472
  });
1497
1473
  if (req.durationSec !== void 0) {
1498
- usage.costUsd = req.durationSec / 60 * (VOXTRAL_PRICE_PER_MIN[req.spec.model] ?? 0);
1474
+ usage.costUsd = req.durationSec / 60 * (getMediaPrice("mistral", req.spec.model)?.usd ?? 0);
1499
1475
  }
1500
1476
  return { text: data.text ?? "", usage };
1501
1477
  }
@@ -1561,7 +1537,7 @@ function assertPronunciations(list, provider) {
1561
1537
  }
1562
1538
  if (p.alias !== void 0 && p.ipa !== void 0) {
1563
1539
  throw new Error(
1564
- `${provider} adapter: pronunciation "${p.word}" sets BOTH alias and ipa. They are different instructions \u2014 alias says it differently, ipa says it precisely. Pick one.`
1540
+ `${provider} adapter: pronunciation "${p.word}" sets BOTH alias and ipa. They are different instructions \u2014 alias says it differently, ipa says it in phonemes. Pick one.`
1565
1541
  );
1566
1542
  }
1567
1543
  if (p.alias === void 0 && p.ipa === void 0) {
@@ -1589,7 +1565,6 @@ function applyPronunciations(haystack, list, render, escape = (s) => s) {
1589
1565
  }
1590
1566
 
1591
1567
  // src/providers/elevenlabs.ts
1592
- var ELEVENLABS_PRICE_PER_1K_CHARS = 0.15;
1593
1568
  var ELEVENLABS_DANISH_VOICES = {
1594
1569
  soren: "xj6X4BCUsv9oxohm1E8o",
1595
1570
  jesper: "Bl1YwS3uJac5zEOSNESn",
@@ -1628,7 +1603,8 @@ function elevenlabsAdapter(config = {}) {
1628
1603
  inputTokens: 0,
1629
1604
  outputTokens: 0
1630
1605
  });
1631
- usage.costUsd = chars / 1e3 * (config.pricePer1kChars ?? ELEVENLABS_PRICE_PER_1K_CHARS);
1606
+ usage.costUsd = chars / 1e3 * (config.pricePer1kChars ?? getMediaPrice("elevenlabs", "tts")?.usd ?? 0);
1607
+ usage.costBasis = config.pricePer1kChars !== void 0 ? "computed" : "estimated";
1632
1608
  return usage;
1633
1609
  }
1634
1610
  async function dialogue(req) {
@@ -1673,8 +1649,6 @@ function elevenlabsAdapter(config = {}) {
1673
1649
  }
1674
1650
 
1675
1651
  // src/providers/azure.ts
1676
- var AZURE_TTS_PRICE_PER_1K_CHARS = 0.016;
1677
- var AZURE_STT_PRICE_PER_MIN = 0.0167;
1678
1652
  var DEFAULT_STT_API_VERSION = "2025-10-15";
1679
1653
  var DEFAULT_REGION = "westeurope";
1680
1654
  var DEFAULT_FORMAT = "audio-24khz-48kbitrate-mono-mp3";
@@ -1752,7 +1726,7 @@ function azureAdapter(config = {}) {
1752
1726
  inputTokens: 0,
1753
1727
  outputTokens: 0
1754
1728
  });
1755
- usage.costUsd = chars / 1e3 * (config.pricePer1kChars ?? AZURE_TTS_PRICE_PER_1K_CHARS);
1729
+ usage.costUsd = chars / 1e3 * (config.pricePer1kChars ?? getMediaPrice("azure", "tts")?.usd ?? 0);
1756
1730
  return usage;
1757
1731
  }
1758
1732
  async function tts(req) {
@@ -1819,7 +1793,7 @@ function azureAdapter(config = {}) {
1819
1793
  inputTokens: 0,
1820
1794
  outputTokens: 0
1821
1795
  });
1822
- usage.costUsd = minutes * (config.sttPricePerMin ?? AZURE_STT_PRICE_PER_MIN);
1796
+ usage.costUsd = minutes * (config.sttPricePerMin ?? getMediaPrice("azure", "stt")?.usd ?? 0);
1823
1797
  const result = { text, usage };
1824
1798
  if (req.timestamps && req.timestamps.length > 0 && data.phrases) {
1825
1799
  const toSec = (ms) => (ms ?? 0) / 1e3;
@@ -1850,13 +1824,6 @@ import { readFileSync } from "fs";
1850
1824
  var TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
1851
1825
  var DEFAULT_REGION2 = "europe-west1";
1852
1826
  var CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform";
1853
- var VERTEX_VEO_PRICE_PER_SEC = {
1854
- "veo-3.1-generate-preview": 0.4,
1855
- "veo-3.1-fast-generate-preview": 0.1,
1856
- "veo-3.1-lite-generate-preview": 0.05,
1857
- "veo-3.0-generate-001": 0.4,
1858
- "veo-3.0-fast-generate-001": 0.1
1859
- };
1860
1827
  function base64url(input) {
1861
1828
  const buf = typeof input === "string" ? Buffer.from(input) : input;
1862
1829
  return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
@@ -1996,8 +1963,9 @@ function vertexAdapter(config = {}) {
1996
1963
  inputTokens: 0,
1997
1964
  outputTokens: 0
1998
1965
  });
1999
- const perSec = config.pricePerSecond ?? VERTEX_VEO_PRICE_PER_SEC[req.spec.model] ?? 0;
2000
- usage.costUsd = perSec * (req.durationSec ?? 8);
1966
+ const perSec = config.pricePerSecond ?? getMediaPrice("vertex", req.spec.model)?.usd ?? 0;
1967
+ usage.costUsd = perSec * (req.durationSec ?? DEFAULT_CLIP_SEC);
1968
+ usage.costBasis = req.durationSec === void 0 ? "estimated" : "computed";
2001
1969
  return { url: `vertex://${op.name}`, bytes: Buffer.from(videoB64, "base64"), mimeType: videoMime, usage };
2002
1970
  }
2003
1971
  async function vision(req) {
@@ -2049,7 +2017,6 @@ function vertexAdapter(config = {}) {
2049
2017
  }
2050
2018
 
2051
2019
  // src/providers/deepl.ts
2052
- var DEEPL_PRICE_PER_1K_CHARS_ESTIMATE = 0.0217;
2053
2020
  function deeplAdapter(config = {}) {
2054
2021
  const fetchImpl = config.fetch ?? fetch;
2055
2022
  function key() {
@@ -2085,7 +2052,8 @@ function deeplAdapter(config = {}) {
2085
2052
  inputTokens: 0,
2086
2053
  outputTokens: 0
2087
2054
  });
2088
- usage.costUsd = req.text.length / 1e3 * (config.pricePer1kChars ?? DEEPL_PRICE_PER_1K_CHARS_ESTIMATE);
2055
+ usage.costUsd = req.text.length / 1e3 * (config.pricePer1kChars ?? getMediaPrice("deepl", "translate")?.usd ?? 0);
2056
+ usage.costBasis = config.pricePer1kChars !== void 0 ? "computed" : "estimated";
2089
2057
  return { text, usage };
2090
2058
  }
2091
2059
  return { name: "deepl", translate };
@@ -2093,21 +2061,6 @@ function deeplAdapter(config = {}) {
2093
2061
 
2094
2062
  // src/providers/fal.ts
2095
2063
  import { deflateRawSync, crc32 } from "zlib";
2096
- var FAL_IMAGE_PRICE_ESTIMATE = {
2097
- "fal-ai/flux/schnell": 3e-3,
2098
- "fal-ai/flux/dev": 0.025,
2099
- "fal-ai/flux-lora": 0.025,
2100
- "fal-ai/flux-pro": 0.05,
2101
- "fal-ai/flux-pro/v1.1": 0.04
2102
- };
2103
- var FAL_TRAIN_PRICE_ESTIMATE = 2;
2104
- var FAL_VIDEO_PRICE_PER_SEC = {
2105
- // Kling 2.5 Turbo Pro i2v — the blessed FAL_KEY-only image→video route (F014
2106
- // contentpush spike). $0.35 for the first 5 s + $0.07/additional s = a flat
2107
- // $0.07/s (1080p); per-second-billed, commercial-use, not deprecated.
2108
- "fal-ai/kling-video/v2.5-turbo/pro/image-to-video": 0.07
2109
- };
2110
- var FAL_VIDEO_DEFAULT_SEC = 8;
2111
2064
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
2112
2065
  function falAdapter(config = {}) {
2113
2066
  const doFetch = config.fetch ?? fetch;
@@ -2147,7 +2100,9 @@ function falAdapter(config = {}) {
2147
2100
  inputTokens: 0,
2148
2101
  outputTokens: 0
2149
2102
  });
2150
- usage.costUsd = (config.pricePerImage ?? FAL_IMAGE_PRICE_ESTIMATE[req.spec.model] ?? 0) * calls;
2103
+ const perImage = config.pricePerImage ?? getMediaPrice("fal", req.spec.model)?.usd;
2104
+ usage.costUsd = (perImage ?? 0) * calls;
2105
+ usage.costBasis = config.pricePerImage !== void 0 ? "computed" : "estimated";
2151
2106
  return { url, usage };
2152
2107
  }
2153
2108
  async function animate(req) {
@@ -2176,8 +2131,9 @@ function falAdapter(config = {}) {
2176
2131
  inputTokens: 0,
2177
2132
  outputTokens: 0
2178
2133
  });
2179
- const perSec = config.pricePerSecond ?? FAL_VIDEO_PRICE_PER_SEC[req.spec.model] ?? 0;
2180
- usage.costUsd = perSec * (req.durationSec ?? FAL_VIDEO_DEFAULT_SEC);
2134
+ const perSec = config.pricePerSecond ?? getMediaPrice("fal", req.spec.model)?.usd ?? 0;
2135
+ usage.costUsd = perSec * (req.durationSec ?? DEFAULT_CLIP_SEC);
2136
+ usage.costBasis = req.durationSec === void 0 ? "estimated" : "computed";
2181
2137
  return { url, usage };
2182
2138
  }
2183
2139
  async function trainStyle(req) {
@@ -2213,7 +2169,8 @@ function falAdapter(config = {}) {
2213
2169
  inputTokens: 0,
2214
2170
  outputTokens: 0
2215
2171
  });
2216
- usage.costUsd = config.pricePerTraining ?? FAL_TRAIN_PRICE_ESTIMATE;
2172
+ usage.costUsd = config.pricePerTraining ?? getMediaPrice("fal", "train")?.usd ?? 0;
2173
+ usage.costBasis = config.pricePerTraining !== void 0 ? "computed" : "estimated";
2217
2174
  return { loraUrl, configUrl: configUrl ?? "", usage };
2218
2175
  }
2219
2176
  async function resolveImagesUrl(images, apiKey) {
@@ -2411,7 +2368,6 @@ function buildZip(files) {
2411
2368
  // src/providers/bfl.ts
2412
2369
  var EU_BASE2 = "https://api.eu.bfl.ai";
2413
2370
  var BFL_CREDIT_USD = 0.01;
2414
- var BFL_IMAGE_PRICE = 0.06;
2415
2371
  var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
2416
2372
  function gcd(a, b) {
2417
2373
  return b === 0 ? a : gcd(b, a % b);
@@ -2487,7 +2443,13 @@ function bflAdapter(config = {}) {
2487
2443
  inputTokens: 0,
2488
2444
  outputTokens: 0
2489
2445
  });
2490
- usage.costUsd = typeof submit.cost === "number" ? submit.cost * BFL_CREDIT_USD : config.pricePerImage ?? BFL_IMAGE_PRICE;
2446
+ if (typeof submit.cost === "number") {
2447
+ usage.costUsd = submit.cost * BFL_CREDIT_USD;
2448
+ usage.costBasis = "reported";
2449
+ } else {
2450
+ usage.costUsd = config.pricePerImage ?? getMediaPrice("bfl", "flux-pro-1.1-ultra-finetuned")?.usd ?? 0;
2451
+ usage.costBasis = config.pricePerImage !== void 0 ? "computed" : "estimated";
2452
+ }
2491
2453
  return { url: sample, usage };
2492
2454
  }
2493
2455
  function toBflImage(img) {
@@ -3131,8 +3093,8 @@ var aiConfigSchema = z.object({
3131
3093
  });
3132
3094
 
3133
3095
  // src/version.ts
3134
- var VERSION = "0.39.0";
3135
- var SDK_TAG = "@broberg/ai-sdk@0.39.0";
3096
+ var VERSION = "0.41.0";
3097
+ var SDK_TAG = "@broberg/ai-sdk@0.41.0";
3136
3098
 
3137
3099
  // src/cost/sinks/upmetrics.ts
3138
3100
  function upmetricsSink(config) {
@@ -3172,6 +3134,11 @@ function upmetricsSink(config) {
3172
3134
  // capability/transport — no ingest-schema change, and without it the one
3173
3135
  // field built for auditability existed only in memory.
3174
3136
  region: usage.region,
3137
+ // F050: HOW cost_usd was arrived at. upmetrics already distinguishes
3138
+ // reported / computed / unpriced — we were sending an assumed number in
3139
+ // the same field as a measured one, so their labels could not be right
3140
+ // about our rows however carefully they were applied.
3141
+ cost_basis: usage.costBasis ?? "computed",
3175
3142
  sdk: SDK_TAG
3176
3143
  }
3177
3144
  };
@@ -3998,6 +3965,7 @@ CREATE TABLE IF NOT EXISTS ai_usage (
3998
3965
  cache_read_tokens INTEGER NOT NULL,
3999
3966
  cache_creation_tokens INTEGER NOT NULL,
4000
3967
  cost_usd REAL NOT NULL,
3968
+ cost_basis TEXT NOT NULL DEFAULT 'unknown',
4001
3969
  latency_ms INTEGER NOT NULL,
4002
3970
  subprocess INTEGER NOT NULL DEFAULT 0
4003
3971
  )`;
@@ -4014,13 +3982,16 @@ function sqliteSink(config) {
4014
3982
  if (!cols.some((c) => c.name === "region")) {
4015
3983
  db.run(`ALTER TABLE ai_usage ADD COLUMN region TEXT NOT NULL DEFAULT 'unknown'`);
4016
3984
  }
3985
+ if (!cols.some((c) => c.name === "cost_basis")) {
3986
+ db.run(`ALTER TABLE ai_usage ADD COLUMN cost_basis TEXT NOT NULL DEFAULT 'unknown'`);
3987
+ }
4017
3988
  const insert = db.prepare(
4018
3989
  `INSERT INTO ai_usage
4019
3990
  (ts, provider, model, tier, transport, capability, region, purpose,
4020
3991
  input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens,
4021
- cost_usd, latency_ms, subprocess)
3992
+ cost_usd, cost_basis, latency_ms, subprocess)
4022
3993
  VALUES ($ts, $provider, $model, $tier, $transport, $capability, $region, $purpose,
4023
- $input, $output, $cacheRead, $cacheCreation, $cost, $latency, $subprocess)`
3994
+ $input, $output, $cacheRead, $cacheCreation, $cost, $costBasis, $latency, $subprocess)`
4024
3995
  );
4025
3996
  return insert;
4026
3997
  };
@@ -4041,6 +4012,7 @@ function sqliteSink(config) {
4041
4012
  $cacheRead: usage.cacheReadTokens,
4042
4013
  $cacheCreation: usage.cacheCreationTokens,
4043
4014
  $cost: usage.costUsd,
4015
+ $costBasis: usage.costBasis ?? "computed",
4044
4016
  $latency: usage.latencyMs,
4045
4017
  $subprocess: usage.subprocess ? 1 : 0
4046
4018
  });
@@ -4133,8 +4105,10 @@ export {
4133
4105
  AZURE_DANISH_VOICE_LIST,
4134
4106
  BudgetExceededError,
4135
4107
  BudgetGuard,
4108
+ DEFAULT_CLIP_SEC,
4136
4109
  DEFAULT_TIER_MAP,
4137
4110
  ELEVENLABS_DANISH_VOICES,
4111
+ MEDIA_PRICING_CHECKED_AT,
4138
4112
  ModelUnavailableError,
4139
4113
  PRICING_STALE_AFTER_DAYS,
4140
4114
  SDK_TAG,
@@ -4167,10 +4141,12 @@ export {
4167
4141
  fromProviderToolCall,
4168
4142
  geminiAdapter,
4169
4143
  getCostSummary,
4144
+ getMediaPrice,
4170
4145
  getPrice,
4171
4146
  httpTransport,
4172
4147
  imageInputSchema,
4173
4148
  listAzureDanishVoices,
4149
+ listMediaPrices,
4174
4150
  listModels,
4175
4151
  listVoices,
4176
4152
  makeContracts,