@broberg/ai-sdk 0.38.0 → 0.39.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.d.ts CHANGED
@@ -39,6 +39,18 @@ declare function classifyRegionName(name: string | undefined): Region;
39
39
  * is it?" — a narrower question than "where will my call go?". */
40
40
  declare function regionOfProvider(provider: string): Region;
41
41
 
42
+ /** One dictionary entry. `alias` says it differently; `ipa` says it precisely. */
43
+ interface Pronunciation {
44
+ /** The word as it appears in the text. Matched whole-word, case-insensitively. */
45
+ word: string;
46
+ /** Say this instead. Azure `<sub alias>`; ElevenLabs plain substitution. */
47
+ alias?: string;
48
+ /** IPA phonemes. Azure `<phoneme alphabet="ipa" ph>`. ElevenLabs cannot do this. */
49
+ ipa?: string;
50
+ /** Reserved for a future per-entry language switch; carried but not yet emitted. */
51
+ lang?: string;
52
+ }
53
+
42
54
  /** How a call reaches the model. `http` = provider REST API; `subprocess` = local
43
55
  * `claude -p` CLI (Max plan, costUsd 0). */
44
56
  type Transport = "http" | "subprocess";
@@ -423,6 +435,7 @@ interface PodcastResult {
423
435
  mimeType: string;
424
436
  usage: Usage;
425
437
  }
438
+
426
439
  interface TtsRequest {
427
440
  text: string;
428
441
  voiceId: string;
@@ -432,6 +445,14 @@ interface TtsRequest {
432
445
  format?: string;
433
446
  /** Speaking-rate multiplier (Azure): 1 = normal, 0.9 = 10% slower, 1.1 = faster. ElevenLabs ignores it. */
434
447
  rate?: number;
448
+ /** F051 — pronunciation dictionary. A Danish voice says English jargon wrongly:
449
+ * measured on da-DK, "AI" comes out as the word "aj", "native" as "nativ".
450
+ *
451
+ * Azure renders these as SSML (`<sub alias>` / `<phoneme alphabet="ipa" ph>`);
452
+ * ElevenLabs has no SSML and can only apply `alias`. **This is the CONTROLLED DOOR
453
+ * into SSML** — the substitution happens adapter-side AFTER the text is escaped, so
454
+ * `text` can never inject markup, and `alias`/`ipa` are escaped too. */
455
+ pronunciations?: Pronunciation[];
435
456
  spec: TierSpec;
436
457
  }
437
458
  interface BatchRequestItem {
@@ -1807,6 +1828,24 @@ declare const ttsInputSchema: z.ZodObject<{
1807
1828
  labels: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
1808
1829
  text: z.ZodString;
1809
1830
  voice: z.ZodString;
1831
+ /** F051 — see TtsRequest.pronunciations. The alias/ipa exclusivity is enforced in
1832
+ * the adapter, not here: the message must name the provider that cannot do it. */
1833
+ pronunciations: z.ZodOptional<z.ZodArray<z.ZodObject<{
1834
+ word: z.ZodString;
1835
+ alias: z.ZodOptional<z.ZodString>;
1836
+ ipa: z.ZodOptional<z.ZodString>;
1837
+ lang: z.ZodOptional<z.ZodString>;
1838
+ }, "strip", z.ZodTypeAny, {
1839
+ word: string;
1840
+ alias?: string | undefined;
1841
+ ipa?: string | undefined;
1842
+ lang?: string | undefined;
1843
+ }, {
1844
+ word: string;
1845
+ alias?: string | undefined;
1846
+ ipa?: string | undefined;
1847
+ lang?: string | undefined;
1848
+ }>, "many">>;
1810
1849
  /** F037: voice to use if `voice` is one we know the provider has retired. Without
1811
1850
  * it a retired voice throws VoiceUnavailableError rather than reaching the API.
1812
1851
  *
@@ -1838,8 +1877,14 @@ declare const ttsInputSchema: z.ZodObject<{
1838
1877
  })[] | undefined;
1839
1878
  labels?: Record<string, string> | undefined;
1840
1879
  format?: string | undefined;
1841
- voiceFallback?: string | undefined;
1842
1880
  lang?: string | undefined;
1881
+ pronunciations?: {
1882
+ word: string;
1883
+ alias?: string | undefined;
1884
+ ipa?: string | undefined;
1885
+ lang?: string | undefined;
1886
+ }[] | undefined;
1887
+ voiceFallback?: string | undefined;
1843
1888
  rate?: number | undefined;
1844
1889
  }, {
1845
1890
  text: string;
@@ -1858,8 +1903,14 @@ declare const ttsInputSchema: z.ZodObject<{
1858
1903
  })[] | undefined;
1859
1904
  labels?: Record<string, string> | undefined;
1860
1905
  format?: string | undefined;
1861
- voiceFallback?: string | undefined;
1862
1906
  lang?: string | undefined;
1907
+ pronunciations?: {
1908
+ word: string;
1909
+ alias?: string | undefined;
1910
+ ipa?: string | undefined;
1911
+ lang?: string | undefined;
1912
+ }[] | undefined;
1913
+ voiceFallback?: string | undefined;
1863
1914
  rate?: number | undefined;
1864
1915
  }>;
1865
1916
  declare const aiConfigSchema: z.ZodObject<{
@@ -2251,8 +2302,8 @@ declare const falStubAdapter: ProviderAdapter;
2251
2302
  * wires the live adapters. */
2252
2303
  declare const stubProviders: Record<string, ProviderAdapter>;
2253
2304
 
2254
- declare const VERSION: "0.38.0";
2255
- declare const SDK_TAG: "@broberg/ai-sdk@0.38.0";
2305
+ declare const VERSION: "0.39.0";
2306
+ declare const SDK_TAG: "@broberg/ai-sdk@0.39.0";
2256
2307
 
2257
2308
  /** Built-in defaults. Every entry is overridable via AiConfig.defaults or a
2258
2309
  * per-call override.
package/dist/index.js CHANGED
@@ -1547,6 +1547,47 @@ function mistralAdapter(config = {}) {
1547
1547
  return { ...base, ocr, moderate, embedding, transcribe, batchSubmit, batchStatus, batchResults };
1548
1548
  }
1549
1549
 
1550
+ // src/providers/pronunciation.ts
1551
+ function xmlEscape(s) {
1552
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1553
+ }
1554
+ function escapeRegex(s) {
1555
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1556
+ }
1557
+ function assertPronunciations(list, provider) {
1558
+ for (const p of list ?? []) {
1559
+ if (!p.word.trim()) {
1560
+ throw new Error(`${provider} adapter: a pronunciation entry has an empty "word".`);
1561
+ }
1562
+ if (p.alias !== void 0 && p.ipa !== void 0) {
1563
+ throw new Error(
1564
+ `${provider} adapter: pronunciation "${p.word}" sets BOTH alias and ipa. They are different instructions \u2014 alias says it differently, ipa says it precisely. Pick one.`
1565
+ );
1566
+ }
1567
+ if (p.alias === void 0 && p.ipa === void 0) {
1568
+ throw new Error(
1569
+ `${provider} adapter: pronunciation "${p.word}" sets neither alias nor ipa, so there is nothing to say instead.`
1570
+ );
1571
+ }
1572
+ }
1573
+ }
1574
+ function applyPronunciations(haystack, list, render, escape = (s) => s) {
1575
+ const entries = (list ?? []).filter((p) => p.word.trim().length > 0);
1576
+ if (entries.length === 0) return haystack;
1577
+ const sorted = [...entries].sort((a, b) => escape(b.word).length - escape(a.word).length);
1578
+ const byLower = /* @__PURE__ */ new Map();
1579
+ for (const p of sorted) {
1580
+ const k = escape(p.word).toLowerCase();
1581
+ if (!byLower.has(k)) byLower.set(k, p);
1582
+ }
1583
+ const alternation = sorted.map((p) => escapeRegex(escape(p.word))).join("|");
1584
+ const re = new RegExp(`(?<![\\w-])(${alternation})(?![\\w-])`, "gi");
1585
+ return haystack.replace(re, (matched) => {
1586
+ const entry = byLower.get(matched.toLowerCase());
1587
+ return entry ? render(entry, matched) : matched;
1588
+ });
1589
+ }
1590
+
1550
1591
  // src/providers/elevenlabs.ts
1551
1592
  var ELEVENLABS_PRICE_PER_1K_CHARS = 0.15;
1552
1593
  var ELEVENLABS_DANISH_VOICES = {
@@ -1559,6 +1600,17 @@ var ELEVENLABS_DANISH_VOICES = {
1559
1600
  function resolveVoice(nameOrId) {
1560
1601
  return ELEVENLABS_DANISH_VOICES[nameOrId] ?? nameOrId;
1561
1602
  }
1603
+ function ttsText(req) {
1604
+ assertPronunciations(req.pronunciations, "elevenlabs");
1605
+ for (const p of req.pronunciations ?? []) {
1606
+ if (p.ipa !== void 0) {
1607
+ throw new Error(
1608
+ `elevenlabs adapter: pronunciation "${p.word}" uses ipa, which needs SSML \u2014 ElevenLabs has none. Use { alias } here, or route this call to azure.`
1609
+ );
1610
+ }
1611
+ }
1612
+ return applyPronunciations(req.text, req.pronunciations, (p) => p.alias);
1613
+ }
1562
1614
  function elevenlabsAdapter(config = {}) {
1563
1615
  const baseUrl = config.baseUrl ?? "https://api.elevenlabs.io/v1";
1564
1616
  const fetchImpl = config.fetch ?? fetch;
@@ -1602,7 +1654,7 @@ function elevenlabsAdapter(config = {}) {
1602
1654
  const res = await fetchImpl(`${baseUrl}/text-to-speech/${req.voiceId}`, {
1603
1655
  method: "POST",
1604
1656
  headers: { "xi-api-key": key(), "content-type": "application/json", accept: "audio/mpeg" },
1605
- body: JSON.stringify({ text: req.text, model_id: model })
1657
+ body: JSON.stringify({ text: ttsText(req), model_id: model })
1606
1658
  });
1607
1659
  if (!res.ok) {
1608
1660
  const body = await res.text().catch(() => "");
@@ -1662,9 +1714,6 @@ function listAzureDanishVoices() {
1662
1714
  function resolveAzureVoice(nameOrVoice) {
1663
1715
  return AZURE_DANISH_VOICES[nameOrVoice] ?? nameOrVoice;
1664
1716
  }
1665
- function xmlEscape(s) {
1666
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1667
- }
1668
1717
  function localeOf(voice) {
1669
1718
  const parts = voice.split("-");
1670
1719
  return parts.length >= 2 ? `${parts[0]}-${parts[1]}` : "en-US";
@@ -1710,7 +1759,13 @@ function azureAdapter(config = {}) {
1710
1759
  const voice = resolveAzureVoice(req.voiceId);
1711
1760
  const lang = req.lang ?? localeOf(voice);
1712
1761
  const format = req.format ?? DEFAULT_FORMAT;
1713
- const escaped = xmlEscape(req.text);
1762
+ assertPronunciations(req.pronunciations, "azure");
1763
+ const escaped = applyPronunciations(
1764
+ xmlEscape(req.text),
1765
+ req.pronunciations,
1766
+ (p, matched) => p.ipa !== void 0 ? `<phoneme alphabet='ipa' ph='${xmlEscape(p.ipa)}'>${matched}</phoneme>` : `<sub alias='${xmlEscape(p.alias)}'>${matched}</sub>`,
1767
+ xmlEscape
1768
+ );
1714
1769
  const effRate = req.rate ?? AZURE_DANISH_VOICE_LIST.find((v) => v.voiceId === voice)?.defaultRate;
1715
1770
  const inner = effRate != null && effRate !== 1 ? `<prosody rate='${effRate}'>${escaped}</prosody>` : escaped;
1716
1771
  const ssml = `<speak version='1.0' xml:lang='${lang}'><voice name='${voice}'>${inner}</voice></speak>`;
@@ -3023,9 +3078,18 @@ var podcastInputSchema = z.object({
3023
3078
  format: z.string().optional(),
3024
3079
  ...callOptions
3025
3080
  });
3081
+ var pronunciationSchema = z.object({
3082
+ word: z.string(),
3083
+ alias: z.string().optional(),
3084
+ ipa: z.string().optional(),
3085
+ lang: z.string().optional()
3086
+ });
3026
3087
  var ttsInputSchema = z.object({
3027
3088
  text: z.string(),
3028
3089
  voice: z.string(),
3090
+ /** F051 — see TtsRequest.pronunciations. The alias/ipa exclusivity is enforced in
3091
+ * the adapter, not here: the message must name the provider that cannot do it. */
3092
+ pronunciations: z.array(pronunciationSchema).optional(),
3029
3093
  /** F037: voice to use if `voice` is one we know the provider has retired. Without
3030
3094
  * it a retired voice throws VoiceUnavailableError rather than reaching the API.
3031
3095
  *
@@ -3067,8 +3131,8 @@ var aiConfigSchema = z.object({
3067
3131
  });
3068
3132
 
3069
3133
  // src/version.ts
3070
- var VERSION = "0.38.0";
3071
- var SDK_TAG = "@broberg/ai-sdk@0.38.0";
3134
+ var VERSION = "0.39.0";
3135
+ var SDK_TAG = "@broberg/ai-sdk@0.39.0";
3072
3136
 
3073
3137
  // src/cost/sinks/upmetrics.ts
3074
3138
  function upmetricsSink(config) {
@@ -3611,7 +3675,15 @@ function createAI(config = {}) {
3611
3675
  invoke: async (spec) => {
3612
3676
  const adapter = pickProvider(spec.provider);
3613
3677
  if (!adapter.tts) throw new Error(`createAI: provider "${spec.provider}" does not support tts`);
3614
- return adapter.tts({ text: input.text, voiceId, lang: input.lang, format: input.format, rate: input.rate, spec });
3678
+ return adapter.tts({
3679
+ text: input.text,
3680
+ voiceId,
3681
+ lang: input.lang,
3682
+ format: input.format,
3683
+ rate: input.rate,
3684
+ pronunciations: input.pronunciations,
3685
+ spec
3686
+ });
3615
3687
  }
3616
3688
  });
3617
3689
  },