@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.
- package/dist/{chunk-QUOZWQ7G.js → chunk-KXTCLLVF.js} +173 -15
- package/dist/chunk-KXTCLLVF.js.map +1 -0
- package/dist/index.d.ts +21 -3
- package/dist/index.js +60 -84
- package/dist/index.js.map +1 -1
- package/dist/pricing.d.ts +84 -4
- package/dist/pricing.js +9 -1
- package/package.json +2 -2
- package/dist/chunk-QUOZWQ7G.js.map +0 -1
|
@@ -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
|
-
|
|
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
|
|
255
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
|
277
|
-
|
|
278
|
-
|
|
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-
|
|
455
|
+
//# sourceMappingURL=chunk-KXTCLLVF.js.map
|