@broberg/ai-sdk 0.27.0 → 0.29.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-IT7HNKLY.js → chunk-V2PD522L.js} +53 -13
- package/dist/chunk-V2PD522L.js.map +1 -0
- package/dist/index.d.ts +124 -29
- package/dist/index.js +163 -27
- package/dist/index.js.map +1 -1
- package/dist/registry.d.ts +13 -0
- package/dist/registry.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-IT7HNKLY.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,35 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DEFAULT_TIER_MAP,
|
|
2
3
|
ModelUnavailableError,
|
|
3
4
|
listModels,
|
|
4
5
|
providerIds,
|
|
5
6
|
resetRegistry,
|
|
6
7
|
resolveModel,
|
|
8
|
+
resolveTier,
|
|
7
9
|
setAvailability
|
|
8
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-V2PD522L.js";
|
|
9
11
|
import {
|
|
10
12
|
getPrice
|
|
11
13
|
} from "./chunk-LUPUAAHZ.js";
|
|
12
14
|
|
|
13
|
-
// src/routing/tier-map.ts
|
|
14
|
-
var DEFAULT_TIER_MAP = {
|
|
15
|
-
fast: { provider: "mistral", model: "mistral-small-latest", transport: "http" },
|
|
16
|
-
smart: { provider: "mistral", model: "mistral-large-latest", transport: "http" },
|
|
17
|
-
powerful: { provider: "mistral", model: "mistral-large-latest", transport: "http" },
|
|
18
|
-
cheap: { provider: "mistral", model: "mistral-small-latest", transport: "http" },
|
|
19
|
-
// Vision: small-latest (vision-capable, cheap EU) is the default; override to
|
|
20
|
-
// mistral-large-latest for demanding image/spatial/composition work.
|
|
21
|
-
vision: { provider: "mistral", model: "mistral-small-latest", transport: "http" },
|
|
22
|
-
// Native video understanding — Gemini leads; flash-lite is the cheap default (F019).
|
|
23
|
-
// NOT Anthropic → out of the F030 phase-out (its own EU epic if/when needed).
|
|
24
|
-
video: { provider: "gemini", model: "gemini-2.5-flash-lite", transport: "http" },
|
|
25
|
-
// NOT Anthropic → out of F030 (EU-embedding migration is its own future epic).
|
|
26
|
-
embedding: { provider: "openai", model: "text-embedding-3-small", transport: "http" }
|
|
27
|
-
};
|
|
28
|
-
function resolveTier(tier, override, configMap) {
|
|
29
|
-
const base = configMap?.[tier] ?? DEFAULT_TIER_MAP[tier];
|
|
30
|
-
return { ...base, ...override };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
15
|
// src/transport/http.ts
|
|
34
16
|
async function httpTransport(req) {
|
|
35
17
|
if (!req.http) {
|
|
@@ -1390,13 +1372,13 @@ function elevenlabsAdapter(config = {}) {
|
|
|
1390
1372
|
const audio = new Uint8Array(await res.arrayBuffer());
|
|
1391
1373
|
return { audio, mimeType: "audio/mpeg", usage: priceFor(req.text.length, model) };
|
|
1392
1374
|
}
|
|
1393
|
-
async function
|
|
1375
|
+
async function listVoices2() {
|
|
1394
1376
|
const res = await fetchImpl(`${baseUrl}/voices`, { headers: { "xi-api-key": key() } });
|
|
1395
1377
|
if (!res.ok) throw new Error(`elevenlabs voices ${res.status}`);
|
|
1396
1378
|
const data = await res.json();
|
|
1397
1379
|
return (data.voices ?? []).map((v) => ({ voiceId: v.voice_id, name: v.name, language: v.labels?.language }));
|
|
1398
1380
|
}
|
|
1399
|
-
return { name: "elevenlabs", dialogue, tts, listVoices };
|
|
1381
|
+
return { name: "elevenlabs", dialogue, tts, listVoices: listVoices2 };
|
|
1400
1382
|
}
|
|
1401
1383
|
|
|
1402
1384
|
// src/providers/azure.ts
|
|
@@ -2449,6 +2431,143 @@ ${JSON.stringify(input.items)}`,
|
|
|
2449
2431
|
};
|
|
2450
2432
|
}
|
|
2451
2433
|
|
|
2434
|
+
// src/voices/registry.ts
|
|
2435
|
+
var CHECKED_AT = {
|
|
2436
|
+
// All 5 curated ids confirmed by SYNTHESIS — POST /v1/text-to-speech/{id} returned
|
|
2437
|
+
// 200 with distinct audio per voice (5 distinct sha256), and a fabricated id
|
|
2438
|
+
// returned 404, so a 200 means something.
|
|
2439
|
+
//
|
|
2440
|
+
// NB: GET /v1/voices/{id} is NOT a liveness test and must not be used as one. It
|
|
2441
|
+
// answers "is this voice saved in our account", and returns voice_not_found for a
|
|
2442
|
+
// public/shared voice that synthesizes perfectly well. Using it cost us a false
|
|
2443
|
+
// retirement (see the note on `mads` below) — the only honest liveness check for
|
|
2444
|
+
// ElevenLabs is a real synthesis call, which is exactly why v1 does no live check.
|
|
2445
|
+
elevenlabs: "2026-08-11",
|
|
2446
|
+
// F026 (6ca38c4) verified the 6 curated names against Azure's voices/list. Not
|
|
2447
|
+
// re-probed since: this machine has no Azure Speech key, and inventing a fresher
|
|
2448
|
+
// date than the last real check is exactly the lie checkedAt exists to prevent.
|
|
2449
|
+
azure: "2026-06-23"
|
|
2450
|
+
};
|
|
2451
|
+
var RETIRED_DEFAULT = {};
|
|
2452
|
+
var RETIRED = RETIRED_DEFAULT;
|
|
2453
|
+
var ELEVENLABS_GENDER = {
|
|
2454
|
+
soren: "male",
|
|
2455
|
+
jesper: "male",
|
|
2456
|
+
noam: "male",
|
|
2457
|
+
camilla: "female"
|
|
2458
|
+
};
|
|
2459
|
+
var NOTES = {
|
|
2460
|
+
mads: "usable, but not saved to the ElevenLabs account, so the voices endpoint publishes no metadata for it (verified by synthesis 2026-08-11)"
|
|
2461
|
+
};
|
|
2462
|
+
function build() {
|
|
2463
|
+
const rows = [];
|
|
2464
|
+
for (const [name, id] of Object.entries(ELEVENLABS_DANISH_VOICES)) {
|
|
2465
|
+
const retired = RETIRED[name];
|
|
2466
|
+
rows.push({
|
|
2467
|
+
id,
|
|
2468
|
+
name,
|
|
2469
|
+
provider: "elevenlabs",
|
|
2470
|
+
// The curated ElevenLabs roster IS the Danish one (see the constant's name
|
|
2471
|
+
// + F020); the multilingual model speaks it as da-DK.
|
|
2472
|
+
locale: "da-DK",
|
|
2473
|
+
gender: ELEVENLABS_GENDER[name],
|
|
2474
|
+
available: retired === void 0,
|
|
2475
|
+
status: retired === void 0 ? "available" : "retired",
|
|
2476
|
+
note: retired ?? NOTES[name],
|
|
2477
|
+
checkedAt: CHECKED_AT.elevenlabs
|
|
2478
|
+
});
|
|
2479
|
+
}
|
|
2480
|
+
for (const v of AZURE_DANISH_VOICE_LIST) {
|
|
2481
|
+
const retired = RETIRED[v.name];
|
|
2482
|
+
rows.push({
|
|
2483
|
+
id: v.voiceId,
|
|
2484
|
+
name: v.name,
|
|
2485
|
+
provider: "azure",
|
|
2486
|
+
locale: localeOf(v.voiceId),
|
|
2487
|
+
gender: v.gender,
|
|
2488
|
+
available: retired === void 0,
|
|
2489
|
+
status: retired === void 0 ? "available" : "retired",
|
|
2490
|
+
note: retired ?? NOTES[v.name],
|
|
2491
|
+
checkedAt: CHECKED_AT.azure
|
|
2492
|
+
});
|
|
2493
|
+
}
|
|
2494
|
+
return rows;
|
|
2495
|
+
}
|
|
2496
|
+
var ROWS = build();
|
|
2497
|
+
function allVoices(provider) {
|
|
2498
|
+
return provider ? ROWS.filter((v) => v.provider === provider) : [...ROWS];
|
|
2499
|
+
}
|
|
2500
|
+
function getVoice(nameOrId) {
|
|
2501
|
+
return ROWS.find((v) => v.name === nameOrId || v.id === nameOrId);
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
// src/voices/types.ts
|
|
2505
|
+
var VoiceUnavailableError = class extends Error {
|
|
2506
|
+
code = "voice_unavailable";
|
|
2507
|
+
requested;
|
|
2508
|
+
provider;
|
|
2509
|
+
note;
|
|
2510
|
+
constructor(requested, note, provider) {
|
|
2511
|
+
super(`voice "${requested}" is unavailable${note ? ` (${note})` : ""}`);
|
|
2512
|
+
this.name = "VoiceUnavailableError";
|
|
2513
|
+
this.requested = requested;
|
|
2514
|
+
this.note = note;
|
|
2515
|
+
this.provider = provider;
|
|
2516
|
+
}
|
|
2517
|
+
};
|
|
2518
|
+
|
|
2519
|
+
// src/voices/resolve.ts
|
|
2520
|
+
function listVoices(opts = {}) {
|
|
2521
|
+
return allVoices(opts.provider);
|
|
2522
|
+
}
|
|
2523
|
+
function usable(nameOrId) {
|
|
2524
|
+
const v = getVoice(nameOrId);
|
|
2525
|
+
return v ? v.available : true;
|
|
2526
|
+
}
|
|
2527
|
+
function idFor(nameOrId) {
|
|
2528
|
+
return getVoice(nameOrId)?.id ?? nameOrId;
|
|
2529
|
+
}
|
|
2530
|
+
function checkVoice(requested, opts = {}) {
|
|
2531
|
+
const entry = getVoice(requested);
|
|
2532
|
+
const provider = entry?.provider;
|
|
2533
|
+
if (usable(requested)) {
|
|
2534
|
+
return {
|
|
2535
|
+
ok: true,
|
|
2536
|
+
voiceId: idFor(requested),
|
|
2537
|
+
requested,
|
|
2538
|
+
provider,
|
|
2539
|
+
fellBack: false,
|
|
2540
|
+
status: entry?.status ?? "unknown"
|
|
2541
|
+
};
|
|
2542
|
+
}
|
|
2543
|
+
const chain = opts.fallback === void 0 ? [] : Array.isArray(opts.fallback) ? opts.fallback : [opts.fallback];
|
|
2544
|
+
for (const fb of chain) {
|
|
2545
|
+
if (usable(fb)) {
|
|
2546
|
+
return {
|
|
2547
|
+
ok: false,
|
|
2548
|
+
voiceId: idFor(fb),
|
|
2549
|
+
requested,
|
|
2550
|
+
provider: getVoice(fb)?.provider ?? provider,
|
|
2551
|
+
fellBack: true,
|
|
2552
|
+
status: entry?.status ?? "retired",
|
|
2553
|
+
reason: entry?.note ?? `${requested} is unavailable`
|
|
2554
|
+
};
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
if (opts.throwIfUnavailable) {
|
|
2558
|
+
throw new VoiceUnavailableError(requested, entry?.note, provider);
|
|
2559
|
+
}
|
|
2560
|
+
return {
|
|
2561
|
+
ok: false,
|
|
2562
|
+
voiceId: idFor(requested),
|
|
2563
|
+
requested,
|
|
2564
|
+
provider,
|
|
2565
|
+
fellBack: false,
|
|
2566
|
+
status: entry?.status ?? "retired",
|
|
2567
|
+
reason: entry?.note ?? `${requested} is unavailable`
|
|
2568
|
+
};
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2452
2571
|
// src/schema/inputs.ts
|
|
2453
2572
|
import { z } from "zod";
|
|
2454
2573
|
var transportSchema = z.enum(["http", "subprocess"]);
|
|
@@ -2623,6 +2742,17 @@ var podcastInputSchema = z.object({
|
|
|
2623
2742
|
var ttsInputSchema = z.object({
|
|
2624
2743
|
text: z.string(),
|
|
2625
2744
|
voice: z.string(),
|
|
2745
|
+
/** F037: voice to use if `voice` is one we know the provider has retired. Without
|
|
2746
|
+
* it a retired voice throws VoiceUnavailableError rather than reaching the API.
|
|
2747
|
+
*
|
|
2748
|
+
* THINK BEFORE SETTING THIS. It is right for batch work where any acceptable voice
|
|
2749
|
+
* will do (a generated podcast, a bulk render). It is WRONG where the voice is an
|
|
2750
|
+
* identity a human recognises — a brand voice, a house narrator: falling back means
|
|
2751
|
+
* your product suddenly speaks with a stranger's voice and says nothing about why.
|
|
2752
|
+
* There, leave it unset and let the throw alarm you. Silence WITH a message beats
|
|
2753
|
+
* the wrong audio WITHOUT one. (torrent-search-api's call on their own house voice,
|
|
2754
|
+
* 2026-08-11 — they took the alarm over the fallback, deliberately.) */
|
|
2755
|
+
voiceFallback: z.string().optional(),
|
|
2626
2756
|
lang: z.string().optional(),
|
|
2627
2757
|
format: z.string().optional(),
|
|
2628
2758
|
rate: z.number().positive().optional(),
|
|
@@ -2650,8 +2780,8 @@ var aiConfigSchema = z.object({
|
|
|
2650
2780
|
});
|
|
2651
2781
|
|
|
2652
2782
|
// src/version.ts
|
|
2653
|
-
var VERSION = "0.
|
|
2654
|
-
var SDK_TAG = "@broberg/ai-sdk@0.
|
|
2783
|
+
var VERSION = "0.29.0";
|
|
2784
|
+
var SDK_TAG = "@broberg/ai-sdk@0.29.0";
|
|
2655
2785
|
|
|
2656
2786
|
// src/cost/sinks/upmetrics.ts
|
|
2657
2787
|
function upmetricsSink(config) {
|
|
@@ -3133,7 +3263,9 @@ function createAI(config = {}) {
|
|
|
3133
3263
|
const inputs = input.script.map((turn) => {
|
|
3134
3264
|
const mapped = input.voices[turn.speaker];
|
|
3135
3265
|
if (!mapped) throw new Error(`ai.podcast: no voice mapped for speaker "${turn.speaker}"`);
|
|
3136
|
-
|
|
3266
|
+
const res = checkVoice(mapped);
|
|
3267
|
+
if (!res.ok) throw new VoiceUnavailableError(mapped, `${res.reason} \u2014 speaker "${turn.speaker}"`, res.provider);
|
|
3268
|
+
return { text: turn.text, voiceId: res.voiceId };
|
|
3137
3269
|
});
|
|
3138
3270
|
const chars = input.script.reduce((n, t) => n + t.text.length, 0);
|
|
3139
3271
|
return runCapability({
|
|
@@ -3154,6 +3286,7 @@ function createAI(config = {}) {
|
|
|
3154
3286
|
},
|
|
3155
3287
|
async tts(input) {
|
|
3156
3288
|
input = ttsInputSchema.parse(input);
|
|
3289
|
+
const { voiceId } = checkVoice(input.voice, { fallback: input.voiceFallback, throwIfUnavailable: true });
|
|
3157
3290
|
return runCapability({
|
|
3158
3291
|
primary: { ...DEFAULT_TTS_SPEC, ...input.override },
|
|
3159
3292
|
fallback: input.fallback,
|
|
@@ -3166,7 +3299,7 @@ function createAI(config = {}) {
|
|
|
3166
3299
|
invoke: async (spec) => {
|
|
3167
3300
|
const adapter = pickProvider(spec.provider);
|
|
3168
3301
|
if (!adapter.tts) throw new Error(`createAI: provider "${spec.provider}" does not support tts`);
|
|
3169
|
-
return adapter.tts({ text: input.text, voiceId
|
|
3302
|
+
return adapter.tts({ text: input.text, voiceId, lang: input.lang, format: input.format, rate: input.rate, spec });
|
|
3170
3303
|
}
|
|
3171
3304
|
});
|
|
3172
3305
|
},
|
|
@@ -3614,6 +3747,7 @@ export {
|
|
|
3614
3747
|
StreamHttpError,
|
|
3615
3748
|
UpmetricsCostError,
|
|
3616
3749
|
VERSION,
|
|
3750
|
+
VoiceUnavailableError,
|
|
3617
3751
|
aiConfigSchema,
|
|
3618
3752
|
anthropicAdapter,
|
|
3619
3753
|
anthropicApiAdapter,
|
|
@@ -3622,6 +3756,7 @@ export {
|
|
|
3622
3756
|
bflAdapter,
|
|
3623
3757
|
bflCredits,
|
|
3624
3758
|
chatInputSchema,
|
|
3759
|
+
checkVoice,
|
|
3625
3760
|
computeCost,
|
|
3626
3761
|
createAI,
|
|
3627
3762
|
deepinfraAdapter,
|
|
@@ -3642,6 +3777,7 @@ export {
|
|
|
3642
3777
|
imageInputSchema,
|
|
3643
3778
|
listAzureDanishVoices,
|
|
3644
3779
|
listModels,
|
|
3780
|
+
listVoices,
|
|
3645
3781
|
makeContracts,
|
|
3646
3782
|
makeOpenAICompatibleAdapter,
|
|
3647
3783
|
messageSchema,
|