@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/{chunk-QUOZWQ7G.js → chunk-B74RSBT5.js} +186 -19
- package/dist/chunk-B74RSBT5.js.map +1 -0
- package/dist/index.d.ts +41 -5
- package/dist/index.js +61 -85
- package/dist/index.js.map +1 -1
- package/dist/pricing.d.ts +127 -11
- package/dist/pricing.js +9 -1
- package/package.json +1 -1
- package/dist/chunk-QUOZWQ7G.js.map +0 -1
|
@@ -48,9 +48,13 @@ var PRICING = {
|
|
|
48
48
|
"openai:text-embedding-3-large": { inputPer1M: 0.13, outputPer1M: 0, version: V },
|
|
49
49
|
"openai:gpt-4o": { inputPer1M: 2.5, cacheReadPer1M: 1.25, outputPer1M: 10, version: "2026-08-27-developers.openai.com" },
|
|
50
50
|
"openai:gpt-4o-mini": { inputPer1M: 0.15, cacheReadPer1M: 0.075, outputPer1M: 0.6, version: "2026-08-27-developers.openai.com" },
|
|
51
|
-
// Whisper is priced per
|
|
52
|
-
//
|
|
53
|
-
"
|
|
51
|
+
// Whisper is priced per MINUTE — see MEDIA_PRICING in ./media-pricing.ts. It used to
|
|
52
|
+
// sit here as 0/0 "so token-based compute never charges it", and inwardly that worked.
|
|
53
|
+
// Outwardly it did not: getModelPrice("whisper-1") answered `{unit:"per_1m_tokens",
|
|
54
|
+
// inputPer1M: 0}` — i.e. "this model is free" — because media rows are consulted LAST
|
|
55
|
+
// and a token row was already there. The per-minute price was unreachable by its own
|
|
56
|
+
// id. computeCost still returns 0 for it (no entry) and transcribe still computes its
|
|
57
|
+
// own cost, so nothing about billing changed; only the public answer stopped lying.
|
|
54
58
|
// OpenRouter (meta-router — model slugs include the upstream vendor). Slugs use
|
|
55
59
|
// dots (claude-sonnet-4.6) to match OpenRouter's live ids; the dashed forms
|
|
56
60
|
// never matched a real call. Caught by the F014 catalogue research.
|
|
@@ -140,6 +144,93 @@ function getPrice(provider, model) {
|
|
|
140
144
|
return void 0;
|
|
141
145
|
}
|
|
142
146
|
|
|
147
|
+
// src/cost/media-pricing.ts
|
|
148
|
+
var MEDIA_PRICING_CHECKED_AT = "2026-09-05";
|
|
149
|
+
var VEO_PER_SEC = {
|
|
150
|
+
"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)" },
|
|
151
|
+
"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)" },
|
|
152
|
+
"veo-3.1-lite-generate-preview": { usd: 0.05, source: "ai.google.dev/gemini-api/docs/pricing \u2014 720p (1080p = 0.08)" },
|
|
153
|
+
"veo-3.0-generate-001": { usd: 0.4, source: "ai.google.dev/gemini-api/docs/pricing" },
|
|
154
|
+
"veo-3.0-fast-generate-001": { usd: 0.1, source: "ai.google.dev/gemini-api/docs/pricing" }
|
|
155
|
+
};
|
|
156
|
+
var veoRows = (provider) => Object.fromEntries(
|
|
157
|
+
Object.entries(VEO_PER_SEC).map(([model, v]) => [
|
|
158
|
+
`${provider}:${model}`,
|
|
159
|
+
{ unit: "per_sec", usd: v.usd, checkedAt: MEDIA_PRICING_CHECKED_AT, source: v.source }
|
|
160
|
+
])
|
|
161
|
+
);
|
|
162
|
+
var MEDIA_PRICING = {
|
|
163
|
+
...veoRows("gemini"),
|
|
164
|
+
...veoRows("vertex"),
|
|
165
|
+
// fal video — fal's OFFICIAL published per-second rate, not a guess. Kling 2.5
|
|
166
|
+
// Turbo Pro i2v is the blessed FAL_KEY-only image→video route: $0.35 for the
|
|
167
|
+
// first 5 s + $0.07/additional s = a flat $0.07/s at 1080p.
|
|
168
|
+
"fal:fal-ai/kling-video/v2.5-turbo/pro/image-to-video": {
|
|
169
|
+
unit: "per_sec",
|
|
170
|
+
usd: 0.07,
|
|
171
|
+
checkedAt: MEDIA_PRICING_CHECKED_AT,
|
|
172
|
+
source: "fal.ai/models/fal-ai/kling-video/v2.5-turbo/pro/image-to-video"
|
|
173
|
+
},
|
|
174
|
+
// fal images — ESTIMATES. fal bills by megapixel and changes often; fal returns
|
|
175
|
+
// no price with the result, so a call costed from these carries
|
|
176
|
+
// `costBasis: "estimated"`. Override per call with config.pricePerImage.
|
|
177
|
+
"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" },
|
|
178
|
+
"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" },
|
|
179
|
+
"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" },
|
|
180
|
+
"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" },
|
|
181
|
+
"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" },
|
|
182
|
+
// BFL — the fallback only. BFL RETURNS the real billed cost in credits, so a
|
|
183
|
+
// normal call is `costBasis: "reported"` and never reads this row.
|
|
184
|
+
"bfl:flux-pro-1.1-ultra-finetuned": {
|
|
185
|
+
unit: "per_image",
|
|
186
|
+
usd: 0.06,
|
|
187
|
+
checkedAt: MEDIA_PRICING_CHECKED_AT,
|
|
188
|
+
source: "bfl.ai/pricing \u2014 estimate; used only when BFL omits the credit cost"
|
|
189
|
+
},
|
|
190
|
+
// Gemini images — billed per image, not per token. Google's price RISES with
|
|
191
|
+
// resolution; these are the common 1K/1024px default.
|
|
192
|
+
"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" },
|
|
193
|
+
"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" },
|
|
194
|
+
"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" },
|
|
195
|
+
"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" },
|
|
196
|
+
"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" },
|
|
197
|
+
// OpenRouter images — the FALLBACK only. OpenRouter usually returns usage.cost.
|
|
198
|
+
"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" },
|
|
199
|
+
"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" },
|
|
200
|
+
// Speech OUT — billed per 1000 characters of input text.
|
|
201
|
+
"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" },
|
|
202
|
+
"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" },
|
|
203
|
+
// Translation — per 1000 characters. Free tier (":fx" key, within quota) is truly $0;
|
|
204
|
+
// this rate only bites past the Pro allowance.
|
|
205
|
+
"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" },
|
|
206
|
+
// Speech IN — billed per audio-minute.
|
|
207
|
+
"azure:stt": { unit: "per_min", usd: 0.0167, checkedAt: MEDIA_PRICING_CHECKED_AT, source: "azure.microsoft.com/pricing \u2014 standard STT, $1/audio-hour" },
|
|
208
|
+
"openai:whisper-1": { unit: "per_min", usd: 6e-3, checkedAt: MEDIA_PRICING_CHECKED_AT, source: "openai.com/api/pricing \u2014 Whisper" },
|
|
209
|
+
"mistral:voxtral-mini-latest": { unit: "per_min", usd: 2e-3, checkedAt: MEDIA_PRICING_CHECKED_AT, source: "mistral.ai/pricing \u2014 Voxtral" },
|
|
210
|
+
"mistral:voxtral-mini-2507": { unit: "per_min", usd: 2e-3, checkedAt: MEDIA_PRICING_CHECKED_AT, source: "mistral.ai/pricing \u2014 Voxtral" },
|
|
211
|
+
"mistral:voxtral-mini-2602": { unit: "per_min", usd: 2e-3, checkedAt: MEDIA_PRICING_CHECKED_AT, source: "mistral.ai/pricing \u2014 Voxtral" },
|
|
212
|
+
// OCR — per page.
|
|
213
|
+
"mistral:ocr": { unit: "per_page", usd: 2e-3, checkedAt: MEDIA_PRICING_CHECKED_AT, source: "mistral.ai/pricing \u2014 OCR, $2/1000 pages" },
|
|
214
|
+
// A flat fee per training run, not per anything produced.
|
|
215
|
+
"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" }
|
|
216
|
+
};
|
|
217
|
+
var DEFAULT_CLIP_SEC = 8;
|
|
218
|
+
function getMediaPrice(provider, model) {
|
|
219
|
+
return MEDIA_PRICING[`${provider}:${model}`];
|
|
220
|
+
}
|
|
221
|
+
function mediaUnitCounts() {
|
|
222
|
+
const counts = {
|
|
223
|
+
per_sec: 0,
|
|
224
|
+
per_image: 0,
|
|
225
|
+
per_1k_chars: 0,
|
|
226
|
+
per_min: 0,
|
|
227
|
+
per_page: 0,
|
|
228
|
+
per_training: 0
|
|
229
|
+
};
|
|
230
|
+
for (const p of Object.values(MEDIA_PRICING)) counts[p.unit]++;
|
|
231
|
+
return counts;
|
|
232
|
+
}
|
|
233
|
+
|
|
143
234
|
// src/catalogue/pricing-data.ts
|
|
144
235
|
var PRICING_GENERATED_AT = "2026-09-03T19:06:51.817Z";
|
|
145
236
|
var PRICING_CHECKED_AT = "2026-09-03T19:06:58.096Z";
|
|
@@ -171,6 +262,34 @@ function regionForProvider(provider) {
|
|
|
171
262
|
return "other";
|
|
172
263
|
}
|
|
173
264
|
}
|
|
265
|
+
var _media = null;
|
|
266
|
+
function ensureMedia() {
|
|
267
|
+
if (_media) return _media;
|
|
268
|
+
const m = /* @__PURE__ */ new Map();
|
|
269
|
+
for (const [key, p] of Object.entries(MEDIA_PRICING)) {
|
|
270
|
+
const ci = key.indexOf(":");
|
|
271
|
+
const provider = ci >= 0 ? key.slice(0, ci) : "";
|
|
272
|
+
const model = ci >= 0 ? key.slice(ci + 1) : key;
|
|
273
|
+
const row = {
|
|
274
|
+
provider,
|
|
275
|
+
model,
|
|
276
|
+
unit: p.unit,
|
|
277
|
+
usd: p.usd,
|
|
278
|
+
...p.unit === "per_sec" ? { perSec: p.usd } : {},
|
|
279
|
+
...p.unit === "per_image" ? { perImage: p.usd } : {},
|
|
280
|
+
checkedAt: p.checkedAt,
|
|
281
|
+
region: regionForProvider(provider),
|
|
282
|
+
source: "curated"
|
|
283
|
+
};
|
|
284
|
+
if (!m.has(model.toLowerCase())) m.set(model.toLowerCase(), row);
|
|
285
|
+
m.set(key.toLowerCase(), row);
|
|
286
|
+
}
|
|
287
|
+
_media = m;
|
|
288
|
+
return m;
|
|
289
|
+
}
|
|
290
|
+
function listMediaPrices() {
|
|
291
|
+
return [...new Set(ensureMedia().values())];
|
|
292
|
+
}
|
|
174
293
|
var _list = null;
|
|
175
294
|
var _full = null;
|
|
176
295
|
var _base = null;
|
|
@@ -186,7 +305,7 @@ function ensure() {
|
|
|
186
305
|
name: r.name,
|
|
187
306
|
inputPer1M: r.input,
|
|
188
307
|
outputPer1M: r.output,
|
|
189
|
-
unit:
|
|
308
|
+
unit: "per_1m_tokens",
|
|
190
309
|
region: ["eu", "us", "cn", "other"].includes(r.region) ? r.region : "other",
|
|
191
310
|
source: "inventory"
|
|
192
311
|
};
|
|
@@ -220,12 +339,21 @@ function ensure() {
|
|
|
220
339
|
_list = list;
|
|
221
340
|
_base = byBase;
|
|
222
341
|
_full = new Map(list.map((e) => [e.model.toLowerCase(), e]));
|
|
342
|
+
for (const [key, mp] of Object.entries(MEDIA_PRICING)) {
|
|
343
|
+
const model = key.slice(key.indexOf(":") + 1).toLowerCase();
|
|
344
|
+
const tok = _full.get(model) ?? byBase.get(basename(model));
|
|
345
|
+
if (tok) tok.alsoBilled = { unit: mp.unit, usd: mp.usd, checkedAt: mp.checkedAt };
|
|
346
|
+
}
|
|
223
347
|
}
|
|
224
348
|
function getModelPrice(modelId) {
|
|
225
349
|
ensure();
|
|
226
350
|
const s = modelId.trim().toLowerCase();
|
|
227
351
|
const dated = stripDatedSuffix(s);
|
|
228
|
-
|
|
352
|
+
const media = ensureMedia();
|
|
353
|
+
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.
|
|
354
|
+
// Before F050 this returned undefined for every video model, which a caller could
|
|
355
|
+
// not tell apart from "we have no price for that anywhere".
|
|
356
|
+
media.get(s) ?? (s.includes(":") ? media.get(s.slice(s.indexOf(":") + 1)) : void 0);
|
|
229
357
|
}
|
|
230
358
|
function listModelPrices() {
|
|
231
359
|
ensure();
|
|
@@ -250,33 +378,68 @@ function pricingGeneratedAt() {
|
|
|
250
378
|
return PRICING_GENERATED_AT;
|
|
251
379
|
}
|
|
252
380
|
var PRICING_STALE_AFTER_DAYS = 35;
|
|
253
|
-
function computeFreshness(generatedAt, checkedAt, nowMs) {
|
|
254
|
-
const
|
|
255
|
-
|
|
381
|
+
function computeFreshness(generatedAt, checkedAt, nowMs, units = []) {
|
|
382
|
+
const age = (iso) => {
|
|
383
|
+
const t = iso ? Date.parse(iso) : NaN;
|
|
384
|
+
return Number.isFinite(t) ? Math.max(0, Math.floor((nowMs - t) / 864e5)) : null;
|
|
385
|
+
};
|
|
386
|
+
const isStale = (d) => d === null || d > PRICING_STALE_AFTER_DAYS;
|
|
387
|
+
const ageDays = age(checkedAt);
|
|
388
|
+
const unitRows = units.map((u) => {
|
|
389
|
+
const a = age(u.checkedAt);
|
|
390
|
+
return { unit: u.unit, count: u.count, checkedAt: u.checkedAt, ageDays: a, stale: isStale(a) };
|
|
391
|
+
});
|
|
392
|
+
const caveats = unitRows.flatMap(
|
|
393
|
+
(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 ? [
|
|
394
|
+
`${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`
|
|
395
|
+
] : []
|
|
396
|
+
);
|
|
256
397
|
return {
|
|
257
398
|
generatedAt,
|
|
258
399
|
checkedAt,
|
|
259
400
|
ageDays,
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
401
|
+
stale: isStale(ageDays),
|
|
402
|
+
thresholdDays: PRICING_STALE_AFTER_DAYS,
|
|
403
|
+
units: unitRows,
|
|
404
|
+
caveats
|
|
264
405
|
};
|
|
265
406
|
}
|
|
266
407
|
function pricingFreshness(nowMs = Date.now()) {
|
|
267
|
-
return computeFreshness(PRICING_GENERATED_AT, PRICING_CHECKED_AT, nowMs
|
|
408
|
+
return computeFreshness(PRICING_GENERATED_AT, PRICING_CHECKED_AT, nowMs, [
|
|
409
|
+
{ unit: "per_1m_tokens", count: PRICING_DATA.length, checkedAt: PRICING_CHECKED_AT },
|
|
410
|
+
// Every media unit, derived from the table rather than listed here — a unit added
|
|
411
|
+
// to MEDIA_PRICING and forgotten here would be a unit the freshness API silently
|
|
412
|
+
// does not report, which is this whole feature's own bug wearing a new hat.
|
|
413
|
+
// Hand-maintained, and the monthly job cannot reach them, hence their own date:
|
|
414
|
+
// inheriting the token snapshot's would make an un-revised video price look freshly
|
|
415
|
+
// verified every time the token job ran.
|
|
416
|
+
...Object.entries(mediaUnitCounts()).map(([unit, count]) => ({
|
|
417
|
+
unit,
|
|
418
|
+
count,
|
|
419
|
+
checkedAt: MEDIA_PRICING_CHECKED_AT
|
|
420
|
+
}))
|
|
421
|
+
]);
|
|
268
422
|
}
|
|
269
423
|
var warned = false;
|
|
270
424
|
function warnIfPricingStale(nowMs = Date.now()) {
|
|
271
425
|
if (warned) return;
|
|
272
426
|
if (globalThis.process?.env?.BROBERG_AI_SDK_SILENCE_PRICING_WARNING) return;
|
|
273
427
|
const f = pricingFreshness(nowMs);
|
|
274
|
-
if (!f.stale) return;
|
|
428
|
+
if (!f.stale && f.caveats.length === 0) return;
|
|
275
429
|
warned = true;
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
430
|
+
const parts = [];
|
|
431
|
+
if (f.stale) {
|
|
432
|
+
const age = f.ageDays === null ? "of unknown age" : `${f.ageDays} days old`;
|
|
433
|
+
parts.push(
|
|
434
|
+
`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.`
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
if (f.caveats.length > 0) {
|
|
438
|
+
parts.push(
|
|
439
|
+
`${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.`
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
console.warn(`[@broberg/ai-sdk] ${parts.join(" | ")}`);
|
|
280
443
|
}
|
|
281
444
|
function resetPricingWarningForTests() {
|
|
282
445
|
warned = false;
|
|
@@ -284,6 +447,10 @@ function resetPricingWarningForTests() {
|
|
|
284
447
|
|
|
285
448
|
export {
|
|
286
449
|
getPrice,
|
|
450
|
+
MEDIA_PRICING_CHECKED_AT,
|
|
451
|
+
DEFAULT_CLIP_SEC,
|
|
452
|
+
getMediaPrice,
|
|
453
|
+
listMediaPrices,
|
|
287
454
|
getModelPrice,
|
|
288
455
|
listModelPrices,
|
|
289
456
|
findModelPrices,
|
|
@@ -294,4 +461,4 @@ export {
|
|
|
294
461
|
warnIfPricingStale,
|
|
295
462
|
resetPricingWarningForTests
|
|
296
463
|
};
|
|
297
|
-
//# sourceMappingURL=chunk-
|
|
464
|
+
//# sourceMappingURL=chunk-B74RSBT5.js.map
|