@broberg/ai-sdk 0.39.0 → 0.40.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.
@@ -140,6 +140,93 @@ function getPrice(provider, model) {
140
140
  return void 0;
141
141
  }
142
142
 
143
+ // src/cost/media-pricing.ts
144
+ var MEDIA_PRICING_CHECKED_AT = "2026-09-05";
145
+ var VEO_PER_SEC = {
146
+ "veo-3.1-generate-preview": { usd: 0.4, source: "ai.google.dev/gemini-api/docs/pricing \u2014 video with audio, 720p/1080p (4K = 0.60)" },
147
+ "veo-3.1-fast-generate-preview": { usd: 0.1, source: "ai.google.dev/gemini-api/docs/pricing \u2014 720p (1080p = 0.12, 4K = 0.30)" },
148
+ "veo-3.1-lite-generate-preview": { usd: 0.05, source: "ai.google.dev/gemini-api/docs/pricing \u2014 720p (1080p = 0.08)" },
149
+ "veo-3.0-generate-001": { usd: 0.4, source: "ai.google.dev/gemini-api/docs/pricing" },
150
+ "veo-3.0-fast-generate-001": { usd: 0.1, source: "ai.google.dev/gemini-api/docs/pricing" }
151
+ };
152
+ var veoRows = (provider) => Object.fromEntries(
153
+ Object.entries(VEO_PER_SEC).map(([model, v]) => [
154
+ `${provider}:${model}`,
155
+ { unit: "per_sec", usd: v.usd, checkedAt: MEDIA_PRICING_CHECKED_AT, source: v.source }
156
+ ])
157
+ );
158
+ var MEDIA_PRICING = {
159
+ ...veoRows("gemini"),
160
+ ...veoRows("vertex"),
161
+ // fal video — fal's OFFICIAL published per-second rate, not a guess. Kling 2.5
162
+ // Turbo Pro i2v is the blessed FAL_KEY-only image→video route: $0.35 for the
163
+ // first 5 s + $0.07/additional s = a flat $0.07/s at 1080p.
164
+ "fal:fal-ai/kling-video/v2.5-turbo/pro/image-to-video": {
165
+ unit: "per_sec",
166
+ usd: 0.07,
167
+ checkedAt: MEDIA_PRICING_CHECKED_AT,
168
+ source: "fal.ai/models/fal-ai/kling-video/v2.5-turbo/pro/image-to-video"
169
+ },
170
+ // fal images — ESTIMATES. fal bills by megapixel and changes often; fal returns
171
+ // no price with the result, so a call costed from these carries
172
+ // `costBasis: "estimated"`. Override per call with config.pricePerImage.
173
+ "fal:fal-ai/flux/schnell": { unit: "per_image", usd: 3e-3, checkedAt: MEDIA_PRICING_CHECKED_AT, source: "fal.ai pricing \u2014 estimate, billed by megapixel" },
174
+ "fal:fal-ai/flux/dev": { unit: "per_image", usd: 0.025, checkedAt: MEDIA_PRICING_CHECKED_AT, source: "fal.ai pricing \u2014 estimate, billed by megapixel" },
175
+ "fal:fal-ai/flux-lora": { unit: "per_image", usd: 0.025, checkedAt: MEDIA_PRICING_CHECKED_AT, source: "fal.ai pricing \u2014 estimate, billed by megapixel" },
176
+ "fal:fal-ai/flux-pro": { unit: "per_image", usd: 0.05, checkedAt: MEDIA_PRICING_CHECKED_AT, source: "fal.ai pricing \u2014 estimate, billed by megapixel" },
177
+ "fal:fal-ai/flux-pro/v1.1": { unit: "per_image", usd: 0.04, checkedAt: MEDIA_PRICING_CHECKED_AT, source: "fal.ai pricing \u2014 estimate, billed by megapixel" },
178
+ // BFL — the fallback only. BFL RETURNS the real billed cost in credits, so a
179
+ // normal call is `costBasis: "reported"` and never reads this row.
180
+ "bfl:flux-pro-1.1-ultra-finetuned": {
181
+ unit: "per_image",
182
+ usd: 0.06,
183
+ checkedAt: MEDIA_PRICING_CHECKED_AT,
184
+ source: "bfl.ai/pricing \u2014 estimate; used only when BFL omits the credit cost"
185
+ },
186
+ // Gemini images — billed per image, not per token. Google's price RISES with
187
+ // resolution; these are the common 1K/1024px default.
188
+ "gemini:gemini-2.5-flash-image": { unit: "per_image", usd: 0.039, checkedAt: MEDIA_PRICING_CHECKED_AT, source: "ai.google.dev/gemini-api/docs/pricing \u2014 'nano-banana', 1024px = 1290 tok" },
189
+ "gemini:gemini-3.1-flash-image": { unit: "per_image", usd: 0.067, checkedAt: MEDIA_PRICING_CHECKED_AT, source: "ai.google.dev/gemini-api/docs/pricing \u2014 1K; 2K=0.101, 4K=0.151" },
190
+ "gemini:gemini-3.1-flash-image-preview": { unit: "per_image", usd: 0.067, checkedAt: MEDIA_PRICING_CHECKED_AT, source: "ai.google.dev/gemini-api/docs/pricing \u2014 1K; 2K=0.101, 4K=0.151" },
191
+ "gemini:gemini-3-pro-image": { unit: "per_image", usd: 0.134, checkedAt: MEDIA_PRICING_CHECKED_AT, source: "ai.google.dev/gemini-api/docs/pricing \u2014 premium; 1K/2K=0.134, 4K=0.24" },
192
+ "gemini:gemini-3-pro-image-preview": { unit: "per_image", usd: 0.134, checkedAt: MEDIA_PRICING_CHECKED_AT, source: "ai.google.dev/gemini-api/docs/pricing \u2014 premium; was mistakenly the flash price once" },
193
+ // OpenRouter images — the FALLBACK only. OpenRouter usually returns usage.cost.
194
+ "openrouter:recraft/recraft-v4.1": { unit: "per_image", usd: 0.035, checkedAt: MEDIA_PRICING_CHECKED_AT, source: "openrouter.ai/recraft/recraft-v4.1 \u2014 fallback when usage.cost is omitted" },
195
+ "openrouter:recraft/recraft-v4.1-vector": { unit: "per_image", usd: 0.08, checkedAt: MEDIA_PRICING_CHECKED_AT, source: "openrouter.ai/recraft/recraft-v4.1-vector \u2014 fallback when usage.cost is omitted" },
196
+ // Speech OUT — billed per 1000 characters of input text.
197
+ "azure:tts": { unit: "per_1k_chars", usd: 0.016, checkedAt: MEDIA_PRICING_CHECKED_AT, source: "azure.microsoft.com/pricing \u2014 neural standard, $16/1M chars" },
198
+ "elevenlabs:tts": { unit: "per_1k_chars", usd: 0.15, checkedAt: MEDIA_PRICING_CHECKED_AT, source: "elevenlabs.io/pricing \u2014 API overage approx $0.10-0.18/1k chars; ESTIMATE" },
199
+ // Translation — per 1000 characters. Free tier (":fx" key, within quota) is truly $0;
200
+ // this rate only bites past the Pro allowance.
201
+ "deepl:translate": { unit: "per_1k_chars", usd: 0.0217, checkedAt: MEDIA_PRICING_CHECKED_AT, source: "deepl.com/pro-api \u2014 ESTIMATE, conflicting tier reports; verify before a budget decision" },
202
+ // Speech IN — billed per audio-minute.
203
+ "azure:stt": { unit: "per_min", usd: 0.0167, checkedAt: MEDIA_PRICING_CHECKED_AT, source: "azure.microsoft.com/pricing \u2014 standard STT, $1/audio-hour" },
204
+ "openai:whisper-1": { unit: "per_min", usd: 6e-3, checkedAt: MEDIA_PRICING_CHECKED_AT, source: "openai.com/api/pricing \u2014 Whisper" },
205
+ "mistral:voxtral-mini-latest": { unit: "per_min", usd: 2e-3, checkedAt: MEDIA_PRICING_CHECKED_AT, source: "mistral.ai/pricing \u2014 Voxtral" },
206
+ "mistral:voxtral-mini-2507": { unit: "per_min", usd: 2e-3, checkedAt: MEDIA_PRICING_CHECKED_AT, source: "mistral.ai/pricing \u2014 Voxtral" },
207
+ "mistral:voxtral-mini-2602": { unit: "per_min", usd: 2e-3, checkedAt: MEDIA_PRICING_CHECKED_AT, source: "mistral.ai/pricing \u2014 Voxtral" },
208
+ // OCR — per page.
209
+ "mistral:ocr": { unit: "per_page", usd: 2e-3, checkedAt: MEDIA_PRICING_CHECKED_AT, source: "mistral.ai/pricing \u2014 OCR, $2/1000 pages" },
210
+ // A flat fee per training run, not per anything produced.
211
+ "fal:train": { unit: "per_training", usd: 2, checkedAt: MEDIA_PRICING_CHECKED_AT, source: "fal.ai/models/fal-ai/flux-lora-fast-training \u2014 approx $2, ESTIMATE" }
212
+ };
213
+ var DEFAULT_CLIP_SEC = 8;
214
+ function getMediaPrice(provider, model) {
215
+ return MEDIA_PRICING[`${provider}:${model}`];
216
+ }
217
+ function mediaUnitCounts() {
218
+ const counts = {
219
+ per_sec: 0,
220
+ per_image: 0,
221
+ per_1k_chars: 0,
222
+ per_min: 0,
223
+ per_page: 0,
224
+ per_training: 0
225
+ };
226
+ for (const p of Object.values(MEDIA_PRICING)) counts[p.unit]++;
227
+ return counts;
228
+ }
229
+
143
230
  // src/catalogue/pricing-data.ts
144
231
  var PRICING_GENERATED_AT = "2026-09-03T19:06:51.817Z";
145
232
  var PRICING_CHECKED_AT = "2026-09-03T19:06:58.096Z";
@@ -171,6 +258,34 @@ function regionForProvider(provider) {
171
258
  return "other";
172
259
  }
173
260
  }
261
+ var _media = null;
262
+ function ensureMedia() {
263
+ if (_media) return _media;
264
+ const m = /* @__PURE__ */ new Map();
265
+ for (const [key, p] of Object.entries(MEDIA_PRICING)) {
266
+ const ci = key.indexOf(":");
267
+ const provider = ci >= 0 ? key.slice(0, ci) : "";
268
+ const model = ci >= 0 ? key.slice(ci + 1) : key;
269
+ const row = {
270
+ provider,
271
+ model,
272
+ inputPer1M: 0,
273
+ outputPer1M: 0,
274
+ unit: p.unit,
275
+ ...p.unit === "per_sec" ? { perSec: p.usd } : { perImage: p.usd },
276
+ checkedAt: p.checkedAt,
277
+ region: regionForProvider(provider),
278
+ source: "curated"
279
+ };
280
+ if (!m.has(model.toLowerCase())) m.set(model.toLowerCase(), row);
281
+ m.set(key.toLowerCase(), row);
282
+ }
283
+ _media = m;
284
+ return m;
285
+ }
286
+ function listMediaPrices() {
287
+ return [...new Set(ensureMedia().values())];
288
+ }
174
289
  var _list = null;
175
290
  var _full = null;
176
291
  var _base = null;
@@ -225,7 +340,11 @@ function getModelPrice(modelId) {
225
340
  ensure();
226
341
  const s = modelId.trim().toLowerCase();
227
342
  const dated = stripDatedSuffix(s);
228
- return _full.get(s) ?? (s.includes(":") ? _full.get(s.slice(s.indexOf(":") + 1)) : void 0) ?? _base.get(basename(s)) ?? (dated !== s ? _full.get(dated) ?? _base.get(basename(dated)) : void 0);
343
+ const media = ensureMedia();
344
+ return _full.get(s) ?? (s.includes(":") ? _full.get(s.slice(s.indexOf(":") + 1)) : void 0) ?? _base.get(basename(s)) ?? (dated !== s ? _full.get(dated) ?? _base.get(basename(dated)) : void 0) ?? // Media LAST, so a token model never loses its own row to a media id collision.
345
+ // Before F050 this returned undefined for every video model, which a caller could
346
+ // not tell apart from "we have no price for that anywhere".
347
+ media.get(s) ?? (s.includes(":") ? media.get(s.slice(s.indexOf(":") + 1)) : void 0);
229
348
  }
230
349
  function listModelPrices() {
231
350
  ensure();
@@ -250,33 +369,68 @@ function pricingGeneratedAt() {
250
369
  return PRICING_GENERATED_AT;
251
370
  }
252
371
  var PRICING_STALE_AFTER_DAYS = 35;
253
- function computeFreshness(generatedAt, checkedAt, nowMs) {
254
- const t = checkedAt ? Date.parse(checkedAt) : NaN;
255
- const ageDays = Number.isFinite(t) ? Math.floor((nowMs - t) / 864e5) : null;
372
+ function computeFreshness(generatedAt, checkedAt, nowMs, units = []) {
373
+ const age = (iso) => {
374
+ const t = iso ? Date.parse(iso) : NaN;
375
+ return Number.isFinite(t) ? Math.max(0, Math.floor((nowMs - t) / 864e5)) : null;
376
+ };
377
+ const isStale = (d) => d === null || d > PRICING_STALE_AFTER_DAYS;
378
+ const ageDays = age(checkedAt);
379
+ const unitRows = units.map((u) => {
380
+ const a = age(u.checkedAt);
381
+ return { unit: u.unit, count: u.count, checkedAt: u.checkedAt, ageDays: a, stale: isStale(a) };
382
+ });
383
+ const caveats = unitRows.flatMap(
384
+ (u) => u.count === 0 ? [`${u.unit}: 0 rows \u2014 this table cannot price anything billed in ${u.unit}, so "stale: false" says nothing about it`] : u.stale ? [
385
+ `${u.unit}: ${u.count} row(s) last verified ${u.checkedAt || "never"}` + (u.ageDays === null ? "" : ` (${u.ageDays} days ago)`) + `, past the ${PRICING_STALE_AFTER_DAYS}d threshold`
386
+ ] : []
387
+ );
256
388
  return {
257
389
  generatedAt,
258
390
  checkedAt,
259
391
  ageDays,
260
- // No check date → stale. The absent case must not read as the healthy one; that is
261
- // the whole failure this feature exists to remove.
262
- stale: ageDays === null || ageDays > PRICING_STALE_AFTER_DAYS,
263
- thresholdDays: PRICING_STALE_AFTER_DAYS
392
+ stale: isStale(ageDays),
393
+ thresholdDays: PRICING_STALE_AFTER_DAYS,
394
+ units: unitRows,
395
+ caveats
264
396
  };
265
397
  }
266
398
  function pricingFreshness(nowMs = Date.now()) {
267
- return computeFreshness(PRICING_GENERATED_AT, PRICING_CHECKED_AT, nowMs);
399
+ return computeFreshness(PRICING_GENERATED_AT, PRICING_CHECKED_AT, nowMs, [
400
+ { unit: "per_1m_tokens", count: PRICING_DATA.length, checkedAt: PRICING_CHECKED_AT },
401
+ // Every media unit, derived from the table rather than listed here — a unit added
402
+ // to MEDIA_PRICING and forgotten here would be a unit the freshness API silently
403
+ // does not report, which is this whole feature's own bug wearing a new hat.
404
+ // Hand-maintained, and the monthly job cannot reach them, hence their own date:
405
+ // inheriting the token snapshot's would make an un-revised video price look freshly
406
+ // verified every time the token job ran.
407
+ ...Object.entries(mediaUnitCounts()).map(([unit, count]) => ({
408
+ unit,
409
+ count,
410
+ checkedAt: MEDIA_PRICING_CHECKED_AT
411
+ }))
412
+ ]);
268
413
  }
269
414
  var warned = false;
270
415
  function warnIfPricingStale(nowMs = Date.now()) {
271
416
  if (warned) return;
272
417
  if (globalThis.process?.env?.BROBERG_AI_SDK_SILENCE_PRICING_WARNING) return;
273
418
  const f = pricingFreshness(nowMs);
274
- if (!f.stale) return;
419
+ if (!f.stale && f.caveats.length === 0) return;
275
420
  warned = true;
276
- const age = f.ageDays === null ? "of unknown age" : `${f.ageDays} days old`;
277
- console.warn(
278
- `[@broberg/ai-sdk] price table is ${age} (last verified ${f.checkedAt || "never"}, threshold ${f.thresholdDays}d). Prices drift: one week has produced 34 changes before. Refresh with \`bun run scripts/build-inventory.ts\` in ai-sdk, or take a newer release.`
279
- );
421
+ const parts = [];
422
+ if (f.stale) {
423
+ const age = f.ageDays === null ? "of unknown age" : `${f.ageDays} days old`;
424
+ parts.push(
425
+ `price table is ${age} (last verified ${f.checkedAt || "never"}, threshold ${f.thresholdDays}d). Prices drift: one week has produced 34 changes before. Refresh with \`bun run scripts/build-inventory.ts\` in ai-sdk, or take a newer release.`
426
+ );
427
+ }
428
+ if (f.caveats.length > 0) {
429
+ parts.push(
430
+ `${f.caveats.length} pricing caveat(s): ${f.caveats.join("; ")}. Non-token prices are HAND-maintained (no vendor catalogue API serves them), so refreshing them means a human re-reading the vendor's pricing page and bumping MEDIA_PRICING_CHECKED_AT in src/cost/media-pricing.ts.`
431
+ );
432
+ }
433
+ console.warn(`[@broberg/ai-sdk] ${parts.join(" | ")}`);
280
434
  }
281
435
  function resetPricingWarningForTests() {
282
436
  warned = false;
@@ -284,6 +438,10 @@ function resetPricingWarningForTests() {
284
438
 
285
439
  export {
286
440
  getPrice,
441
+ MEDIA_PRICING_CHECKED_AT,
442
+ DEFAULT_CLIP_SEC,
443
+ getMediaPrice,
444
+ listMediaPrices,
287
445
  getModelPrice,
288
446
  listModelPrices,
289
447
  findModelPrices,
@@ -294,4 +452,4 @@ export {
294
452
  warnIfPricingStale,
295
453
  resetPricingWarningForTests
296
454
  };
297
- //# sourceMappingURL=chunk-QUOZWQ7G.js.map
455
+ //# sourceMappingURL=chunk-KXTCLLVF.js.map