@broberg/ai-sdk 0.7.0 → 0.9.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
@@ -280,7 +280,10 @@ var PRICING = {
280
280
  "mistral:codestral-latest": { inputPer1M: 0.3, outputPer1M: 0.9, version: MS },
281
281
  "mistral:open-mistral-nemo": { inputPer1M: 0.15, outputPer1M: 0.15, version: MS },
282
282
  // Moderation (F016.4) — per input token; output 0. (OCR is per-page in the adapter.)
283
- "mistral:mistral-moderation-latest": { inputPer1M: 0.1, outputPer1M: 0, version: MS }
283
+ "mistral:mistral-moderation-latest": { inputPer1M: 0.1, outputPer1M: 0, version: MS },
284
+ // Embeddings (F016.5) — per input token.
285
+ "mistral:mistral-embed": { inputPer1M: 0.1, outputPer1M: 0, version: MS },
286
+ "mistral:codestral-embed": { inputPer1M: 0.15, outputPer1M: 0, version: MS }
284
287
  };
285
288
  function getPrice(provider, model) {
286
289
  const exact = PRICING[`${provider}:${model}`];
@@ -1028,6 +1031,11 @@ function openrouterAdapter(config = {}) {
1028
1031
 
1029
1032
  // src/providers/mistral.ts
1030
1033
  var MISTRAL_OCR_PRICE_PER_PAGE = 2e-3;
1034
+ var VOXTRAL_PRICE_PER_MIN = {
1035
+ "voxtral-mini-latest": 2e-3,
1036
+ "voxtral-mini-2507": 2e-3,
1037
+ "voxtral-mini-2602": 2e-3
1038
+ };
1031
1039
  function mistralAdapter(config = {}) {
1032
1040
  const baseUrl = config.baseUrl ?? "https://api.mistral.ai/v1";
1033
1041
  const base = makeOpenAICompatibleAdapter({ name: "mistral", baseUrl, apiKey: config.apiKey });
@@ -1097,7 +1105,175 @@ function mistralAdapter(config = {}) {
1097
1105
  });
1098
1106
  return { results, usage };
1099
1107
  }
1100
- return { ...base, ocr, moderate };
1108
+ async function embedding(req) {
1109
+ const res = await fetchImpl(`${baseUrl}/embeddings`, {
1110
+ method: "POST",
1111
+ headers: { "content-type": "application/json", authorization: `Bearer ${key()}` },
1112
+ body: JSON.stringify({ model: req.spec.model, input: req.input })
1113
+ });
1114
+ if (!res.ok) {
1115
+ const body = await res.text().catch(() => "");
1116
+ throw new Error(`mistral embeddings ${res.status}: ${body.slice(0, 300)}`);
1117
+ }
1118
+ const data = await res.json();
1119
+ const vectors = (data.data ?? []).map((d) => d.embedding);
1120
+ const usage = freshUsage({
1121
+ provider: "mistral",
1122
+ model: req.spec.model,
1123
+ transport: "http",
1124
+ capability: "embedding",
1125
+ inputTokens: data.usage?.prompt_tokens ?? data.usage?.total_tokens ?? 0,
1126
+ outputTokens: 0
1127
+ });
1128
+ return { vectors, usage };
1129
+ }
1130
+ async function transcribe(req) {
1131
+ const form = new FormData();
1132
+ form.append("file", new Blob([req.audio]), "audio");
1133
+ form.append("model", req.spec.model);
1134
+ if (req.language) form.append("language", req.language);
1135
+ const res = await fetchImpl(`${baseUrl}/audio/transcriptions`, {
1136
+ method: "POST",
1137
+ headers: { authorization: `Bearer ${key()}` },
1138
+ body: form
1139
+ });
1140
+ if (!res.ok) {
1141
+ const body = await res.text().catch(() => "");
1142
+ throw new Error(`mistral transcribe ${res.status}: ${body.slice(0, 300)}`);
1143
+ }
1144
+ const data = await res.json();
1145
+ const usage = freshUsage({
1146
+ provider: "mistral",
1147
+ model: req.spec.model,
1148
+ transport: "http",
1149
+ capability: "transcribe",
1150
+ inputTokens: 0,
1151
+ outputTokens: 0
1152
+ });
1153
+ if (req.durationSec !== void 0) {
1154
+ usage.costUsd = req.durationSec / 60 * (VOXTRAL_PRICE_PER_MIN[req.spec.model] ?? 0);
1155
+ }
1156
+ return { text: data.text ?? "", usage };
1157
+ }
1158
+ async function batchSubmit(req) {
1159
+ const jsonl = req.items.map(
1160
+ (it) => JSON.stringify({
1161
+ custom_id: it.customId,
1162
+ body: { model: req.spec.model, messages: [{ role: "user", content: it.prompt }] }
1163
+ })
1164
+ ).join("\n");
1165
+ const form = new FormData();
1166
+ form.append("purpose", "batch");
1167
+ form.append("file", new Blob([jsonl], { type: "application/jsonl" }), "batch.jsonl");
1168
+ const up = await fetchImpl(`${baseUrl}/files`, {
1169
+ method: "POST",
1170
+ headers: { authorization: `Bearer ${key()}` },
1171
+ body: form
1172
+ });
1173
+ if (!up.ok) throw new Error(`mistral batch upload ${up.status}: ${(await up.text().catch(() => "")).slice(0, 200)}`);
1174
+ const fileId = (await up.json()).id;
1175
+ const job = await fetchImpl(`${baseUrl}/batch/jobs`, {
1176
+ method: "POST",
1177
+ headers: { "content-type": "application/json", authorization: `Bearer ${key()}` },
1178
+ body: JSON.stringify({ input_files: [fileId], model: req.spec.model, endpoint: "/v1/chat/completions" })
1179
+ });
1180
+ if (!job.ok) throw new Error(`mistral batch job ${job.status}: ${(await job.text().catch(() => "")).slice(0, 200)}`);
1181
+ const data = await job.json();
1182
+ return { jobId: data.id ?? "", status: data.status ?? "queued", total: data.total_requests };
1183
+ }
1184
+ async function batchStatus(req) {
1185
+ const res = await fetchImpl(`${baseUrl}/batch/jobs/${req.jobId}`, { headers: { authorization: `Bearer ${key()}` } });
1186
+ if (!res.ok) throw new Error(`mistral batch status ${res.status}`);
1187
+ const d = await res.json();
1188
+ return { jobId: d.id ?? req.jobId, status: d.status ?? "unknown", total: d.total_requests, completed: d.succeeded_requests };
1189
+ }
1190
+ async function batchResults(req) {
1191
+ const job = await fetchImpl(`${baseUrl}/batch/jobs/${req.jobId}`, { headers: { authorization: `Bearer ${key()}` } });
1192
+ if (!job.ok) throw new Error(`mistral batch results ${job.status}`);
1193
+ const outputFile = (await job.json()).output_file;
1194
+ if (!outputFile) throw new Error("mistral batch: job has no output_file yet (not finished)");
1195
+ const content = await fetchImpl(`${baseUrl}/files/${outputFile}/content`, { headers: { authorization: `Bearer ${key()}` } });
1196
+ if (!content.ok) throw new Error(`mistral batch download ${content.status}`);
1197
+ const lines = (await content.text()).trim().split("\n").filter(Boolean);
1198
+ return lines.map((line) => {
1199
+ const row = JSON.parse(line);
1200
+ return { customId: row.custom_id ?? "", text: row.response?.body?.choices?.[0]?.message?.content ?? "" };
1201
+ });
1202
+ }
1203
+ return { ...base, ocr, moderate, embedding, transcribe, batchSubmit, batchStatus, batchResults };
1204
+ }
1205
+
1206
+ // src/providers/elevenlabs.ts
1207
+ var ELEVENLABS_PRICE_PER_1K_CHARS = 0.15;
1208
+ var ELEVENLABS_DANISH_VOICES = {
1209
+ soren: "xj6X4BCUsv9oxohm1E8o",
1210
+ jesper: "Bl1YwS3uJac5zEOSNESn",
1211
+ mads: "BIWC0507fYMfhPcAEIRP",
1212
+ noam: "V34B5u5UbLdNJVEkcgXp",
1213
+ camilla: "4RklGmuxoAskAbGXplXN"
1214
+ };
1215
+ function resolveVoice(nameOrId) {
1216
+ return ELEVENLABS_DANISH_VOICES[nameOrId] ?? nameOrId;
1217
+ }
1218
+ function elevenlabsAdapter(config = {}) {
1219
+ const baseUrl = config.baseUrl ?? "https://api.elevenlabs.io/v1";
1220
+ const fetchImpl = config.fetch ?? fetch;
1221
+ function key() {
1222
+ const k = config.apiKey ?? process.env.ELEVENLABS_API_KEY;
1223
+ if (!k) throw new Error("elevenlabs adapter: API key not set (env ELEVENLABS_API_KEY)");
1224
+ return k;
1225
+ }
1226
+ function priceFor(chars, model) {
1227
+ const usage = freshUsage({
1228
+ provider: "elevenlabs",
1229
+ model,
1230
+ transport: "http",
1231
+ capability: "podcast",
1232
+ inputTokens: 0,
1233
+ outputTokens: 0
1234
+ });
1235
+ usage.costUsd = chars / 1e3 * (config.pricePer1kChars ?? ELEVENLABS_PRICE_PER_1K_CHARS);
1236
+ return usage;
1237
+ }
1238
+ async function dialogue(req) {
1239
+ const res = await fetchImpl(`${baseUrl}/text-to-dialogue`, {
1240
+ method: "POST",
1241
+ headers: { "xi-api-key": key(), "content-type": "application/json", accept: "audio/mpeg" },
1242
+ body: JSON.stringify({
1243
+ model_id: req.spec.model,
1244
+ inputs: req.inputs.map((t) => ({ text: t.text, voice_id: t.voiceId })),
1245
+ ...req.format ? { output_format: req.format } : {}
1246
+ })
1247
+ });
1248
+ if (!res.ok) {
1249
+ const body = await res.text().catch(() => "");
1250
+ throw new Error(`elevenlabs dialogue ${res.status}: ${body.slice(0, 300)}`);
1251
+ }
1252
+ const audio = new Uint8Array(await res.arrayBuffer());
1253
+ const chars = req.inputs.reduce((n, t) => n + t.text.length, 0);
1254
+ return { audio, mimeType: "audio/mpeg", usage: priceFor(chars, req.spec.model) };
1255
+ }
1256
+ async function tts(req) {
1257
+ const model = req.spec.model;
1258
+ const res = await fetchImpl(`${baseUrl}/text-to-speech/${req.voiceId}`, {
1259
+ method: "POST",
1260
+ headers: { "xi-api-key": key(), "content-type": "application/json", accept: "audio/mpeg" },
1261
+ body: JSON.stringify({ text: req.text, model_id: model })
1262
+ });
1263
+ if (!res.ok) {
1264
+ const body = await res.text().catch(() => "");
1265
+ throw new Error(`elevenlabs tts ${res.status}: ${body.slice(0, 300)}`);
1266
+ }
1267
+ const audio = new Uint8Array(await res.arrayBuffer());
1268
+ return { audio, mimeType: "audio/mpeg", usage: priceFor(req.text.length, model) };
1269
+ }
1270
+ async function listVoices() {
1271
+ const res = await fetchImpl(`${baseUrl}/voices`, { headers: { "xi-api-key": key() } });
1272
+ if (!res.ok) throw new Error(`elevenlabs voices ${res.status}`);
1273
+ const data = await res.json();
1274
+ return (data.voices ?? []).map((v) => ({ voiceId: v.voice_id, name: v.name, language: v.labels?.language }));
1275
+ }
1276
+ return { name: "elevenlabs", dialogue, tts, listVoices };
1101
1277
  }
1102
1278
 
1103
1279
  // src/providers/fal.ts
@@ -1188,6 +1364,7 @@ var defaultProviders = {
1188
1364
  deepinfra: deepinfraAdapter(),
1189
1365
  openrouter: openrouterAdapter(),
1190
1366
  mistral: mistralAdapter(),
1367
+ elevenlabs: elevenlabsAdapter(),
1191
1368
  fal: falAdapter()
1192
1369
  };
1193
1370
 
@@ -1510,6 +1687,17 @@ var moderationInputSchema = z.object({
1510
1687
  input: z.union([z.string(), z.array(z.string())]),
1511
1688
  ...callOptions
1512
1689
  });
1690
+ var podcastInputSchema = z.object({
1691
+ script: z.array(z.object({ speaker: z.string(), text: z.string() })).min(1),
1692
+ voices: z.record(z.string(), z.string()),
1693
+ format: z.string().optional(),
1694
+ ...callOptions
1695
+ });
1696
+ var ttsInputSchema = z.object({
1697
+ text: z.string(),
1698
+ voice: z.string(),
1699
+ ...callOptions
1700
+ });
1513
1701
  var budgetSchema = z.object({
1514
1702
  perCallUsd: z.number().positive().optional(),
1515
1703
  rollingUsd: z.number().positive().optional()
@@ -1531,6 +1719,9 @@ var DEFAULT_IMAGE_SPEC = {
1531
1719
  };
1532
1720
  var DEFAULT_OCR_SPEC = { provider: "mistral", model: "mistral-ocr-latest", transport: "http" };
1533
1721
  var DEFAULT_MODERATION_SPEC = { provider: "mistral", model: "mistral-moderation-latest", transport: "http" };
1722
+ var DEFAULT_PODCAST_SPEC = { provider: "elevenlabs", model: "eleven_v3", transport: "http" };
1723
+ var DEFAULT_TTS_SPEC = { provider: "elevenlabs", model: "eleven_multilingual_v2", transport: "http" };
1724
+ var DEFAULT_BATCH_SPEC = { provider: "mistral", model: "mistral-small-latest", transport: "http" };
1534
1725
  function createAI(config = {}) {
1535
1726
  const cfg = aiConfigSchema.parse(config);
1536
1727
  const providers = cfg.providers ?? defaultProviders;
@@ -1810,6 +2001,48 @@ function createAI(config = {}) {
1810
2001
  }
1811
2002
  });
1812
2003
  },
2004
+ async podcast(input) {
2005
+ input = podcastInputSchema.parse(input);
2006
+ const inputs = input.script.map((turn) => {
2007
+ const mapped = input.voices[turn.speaker];
2008
+ if (!mapped) throw new Error(`ai.podcast: no voice mapped for speaker "${turn.speaker}"`);
2009
+ return { text: turn.text, voiceId: resolveVoice(mapped) };
2010
+ });
2011
+ const chars = input.script.reduce((n, t) => n + t.text.length, 0);
2012
+ return runCapability({
2013
+ primary: { ...DEFAULT_PODCAST_SPEC, ...input.override },
2014
+ fallback: input.fallback,
2015
+ capability: "podcast",
2016
+ purpose: input.purpose,
2017
+ labels: input.labels,
2018
+ estIn: chars,
2019
+ // per-character cost (not token-based)
2020
+ estOut: 0,
2021
+ invoke: async (spec) => {
2022
+ const adapter = pickProvider(spec.provider);
2023
+ if (!adapter.dialogue) throw new Error(`createAI: provider "${spec.provider}" does not support podcast/dialogue`);
2024
+ return adapter.dialogue({ inputs, format: input.format, spec });
2025
+ }
2026
+ });
2027
+ },
2028
+ async tts(input) {
2029
+ input = ttsInputSchema.parse(input);
2030
+ return runCapability({
2031
+ primary: { ...DEFAULT_TTS_SPEC, ...input.override },
2032
+ fallback: input.fallback,
2033
+ capability: "tts",
2034
+ purpose: input.purpose,
2035
+ labels: input.labels,
2036
+ estIn: input.text.length,
2037
+ // per-character cost
2038
+ estOut: 0,
2039
+ invoke: async (spec) => {
2040
+ const adapter = pickProvider(spec.provider);
2041
+ if (!adapter.tts) throw new Error(`createAI: provider "${spec.provider}" does not support tts`);
2042
+ return adapter.tts({ text: input.text, voiceId: resolveVoice(input.voice), spec });
2043
+ }
2044
+ });
2045
+ },
1813
2046
  async embedding(input) {
1814
2047
  input = embeddingInputSchema.parse(input);
1815
2048
  const tier = input.tier ?? EMBEDDING_DEFAULT_TIER;
@@ -1848,6 +2081,26 @@ function createAI(config = {}) {
1848
2081
  }
1849
2082
  });
1850
2083
  },
2084
+ batch: {
2085
+ async submit(input) {
2086
+ const spec = { ...DEFAULT_BATCH_SPEC, ...input.override };
2087
+ const adapter = pickProvider(spec.provider);
2088
+ if (!adapter.batchSubmit) throw new Error(`createAI: provider "${spec.provider}" does not support batch`);
2089
+ return adapter.batchSubmit({ items: input.requests, spec });
2090
+ },
2091
+ async status(jobId, override) {
2092
+ const spec = { ...DEFAULT_BATCH_SPEC, ...override };
2093
+ const adapter = pickProvider(spec.provider);
2094
+ if (!adapter.batchStatus) throw new Error(`createAI: provider "${spec.provider}" does not support batch`);
2095
+ return adapter.batchStatus({ jobId, spec });
2096
+ },
2097
+ async results(jobId, override) {
2098
+ const spec = { ...DEFAULT_BATCH_SPEC, ...override };
2099
+ const adapter = pickProvider(spec.provider);
2100
+ if (!adapter.batchResults) throw new Error(`createAI: provider "${spec.provider}" does not support batch`);
2101
+ return adapter.batchResults({ jobId, spec });
2102
+ }
2103
+ },
1851
2104
  // Replaced below with the real prompt-contracts (needs the client itself).
1852
2105
  contracts: void 0
1853
2106
  };
@@ -1936,8 +2189,8 @@ var stubProviders = {
1936
2189
  };
1937
2190
 
1938
2191
  // src/version.ts
1939
- var VERSION = "0.7.0";
1940
- var SDK_TAG = "@broberg/ai-sdk@0.7.0";
2192
+ var VERSION = "0.9.0";
2193
+ var SDK_TAG = "@broberg/ai-sdk@0.9.0";
1941
2194
 
1942
2195
  // src/cost/budget-store.ts
1943
2196
  function sqliteBudgetStore(config) {
@@ -2169,6 +2422,7 @@ export {
2169
2422
  BudgetExceededError,
2170
2423
  BudgetGuard,
2171
2424
  DEFAULT_TIER_MAP,
2425
+ ELEVENLABS_DANISH_VOICES,
2172
2426
  SDK_TAG,
2173
2427
  StreamHttpError,
2174
2428
  VERSION,
@@ -2182,6 +2436,7 @@ export {
2182
2436
  deepinfraAdapter,
2183
2437
  defaultProviders,
2184
2438
  discordSink,
2439
+ elevenlabsAdapter,
2185
2440
  embeddingInputSchema,
2186
2441
  falAdapter,
2187
2442
  falStubAdapter,
@@ -2204,6 +2459,7 @@ export {
2204
2459
  parseClaudeCliJson,
2205
2460
  parseJsonLoose,
2206
2461
  resolveTier,
2462
+ resolveVoice,
2207
2463
  sqliteBudgetStore,
2208
2464
  sqliteSink,
2209
2465
  streamTransport,