@broberg/ai-sdk 0.37.3 → 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";
@@ -75,6 +87,24 @@ interface Message {
75
87
  toolCalls?: ToolCallLike[];
76
88
  /** Set on `tool` role messages — which call this result answers. */
77
89
  toolCallId?: string;
90
+ /** F049 — Mistral's `prefix`: start the model's reply with this text instead of
91
+ * asking it to. Only valid on the LAST message, and only when that message is an
92
+ * `assistant` one; anything else throws rather than being sent, because Mistral
93
+ * documents no behaviour for it elsewhere.
94
+ *
95
+ * Mistral's own first-listed use case is Language Adherence, and it pairs with a
96
+ * system instruction rather than replacing one — they warn that a prefix alone
97
+ * gives "noisy and unpredictable answers". Measured on this repo's own data: an
98
+ * explicit language rule in the system prompt already took 5/15 wrong answers to
99
+ * 0/15, and every leak began in the FIRST words ("Tak for din henvendelse"), which
100
+ * is exactly what a prefix pins.
101
+ *
102
+ * **The SDK strips the prefix back off the response**, in `chat` and `chatStream`
103
+ * alike, so a caller never has to. Mistral's own example does
104
+ * `content[len(prefix):]` by hand; forgetting it puts "Here is the answer in
105
+ * Norwegian:" at the top of a real customer's email — a defect that reads as
106
+ * formatting rather than as a bug. */
107
+ prefix?: boolean;
78
108
  }
79
109
  /** SDK-level tool definition. Adapters convert this to each provider's format
80
110
  * (F4.5). `parameters` is a JSON Schema object. */
@@ -405,6 +435,7 @@ interface PodcastResult {
405
435
  mimeType: string;
406
436
  usage: Usage;
407
437
  }
438
+
408
439
  interface TtsRequest {
409
440
  text: string;
410
441
  voiceId: string;
@@ -414,6 +445,14 @@ interface TtsRequest {
414
445
  format?: string;
415
446
  /** Speaking-rate multiplier (Azure): 1 = normal, 0.9 = 10% slower, 1.1 = faster. ElevenLabs ignores it. */
416
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[];
417
456
  spec: TierSpec;
418
457
  }
419
458
  interface BatchRequestItem {
@@ -635,6 +674,10 @@ declare const messageSchema: z.ZodObject<{
635
674
  arguments?: Record<string, unknown> | undefined;
636
675
  }>, "many">>;
637
676
  toolCallId: z.ZodOptional<z.ZodString>;
677
+ /** F049 — see Message.prefix. Position/role validity is enforced in the adapter,
678
+ * not here: the rule is about a message's place in the ARRAY, which a per-message
679
+ * schema cannot see. */
680
+ prefix: z.ZodOptional<z.ZodBoolean>;
638
681
  }, "strip", z.ZodTypeAny, {
639
682
  content: string | ({
640
683
  text: string;
@@ -651,6 +694,7 @@ declare const messageSchema: z.ZodObject<{
651
694
  args?: Record<string, unknown> | undefined;
652
695
  arguments?: Record<string, unknown> | undefined;
653
696
  }[] | undefined;
697
+ prefix?: boolean | undefined;
654
698
  toolCallId?: string | undefined;
655
699
  }, {
656
700
  content: string | ({
@@ -668,6 +712,7 @@ declare const messageSchema: z.ZodObject<{
668
712
  args?: Record<string, unknown> | undefined;
669
713
  arguments?: Record<string, unknown> | undefined;
670
714
  }[] | undefined;
715
+ prefix?: boolean | undefined;
671
716
  toolCallId?: string | undefined;
672
717
  }>;
673
718
  declare const chatInputSchema: z.ZodObject<{
@@ -760,6 +805,10 @@ declare const chatInputSchema: z.ZodObject<{
760
805
  arguments?: Record<string, unknown> | undefined;
761
806
  }>, "many">>;
762
807
  toolCallId: z.ZodOptional<z.ZodString>;
808
+ /** F049 — see Message.prefix. Position/role validity is enforced in the adapter,
809
+ * not here: the rule is about a message's place in the ARRAY, which a per-message
810
+ * schema cannot see. */
811
+ prefix: z.ZodOptional<z.ZodBoolean>;
763
812
  }, "strip", z.ZodTypeAny, {
764
813
  content: string | ({
765
814
  text: string;
@@ -776,6 +825,7 @@ declare const chatInputSchema: z.ZodObject<{
776
825
  args?: Record<string, unknown> | undefined;
777
826
  arguments?: Record<string, unknown> | undefined;
778
827
  }[] | undefined;
828
+ prefix?: boolean | undefined;
779
829
  toolCallId?: string | undefined;
780
830
  }, {
781
831
  content: string | ({
@@ -793,6 +843,7 @@ declare const chatInputSchema: z.ZodObject<{
793
843
  args?: Record<string, unknown> | undefined;
794
844
  arguments?: Record<string, unknown> | undefined;
795
845
  }[] | undefined;
846
+ prefix?: boolean | undefined;
796
847
  toolCallId?: string | undefined;
797
848
  }>, "many">>;
798
849
  system: z.ZodOptional<z.ZodString>;
@@ -833,6 +884,7 @@ declare const chatInputSchema: z.ZodObject<{
833
884
  args?: Record<string, unknown> | undefined;
834
885
  arguments?: Record<string, unknown> | undefined;
835
886
  }[] | undefined;
887
+ prefix?: boolean | undefined;
836
888
  toolCallId?: string | undefined;
837
889
  }[] | undefined;
838
890
  tools?: {
@@ -877,6 +929,7 @@ declare const chatInputSchema: z.ZodObject<{
877
929
  args?: Record<string, unknown> | undefined;
878
930
  arguments?: Record<string, unknown> | undefined;
879
931
  }[] | undefined;
932
+ prefix?: boolean | undefined;
880
933
  toolCallId?: string | undefined;
881
934
  }[] | undefined;
882
935
  tools?: {
@@ -1775,6 +1828,24 @@ declare const ttsInputSchema: z.ZodObject<{
1775
1828
  labels: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
1776
1829
  text: z.ZodString;
1777
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">>;
1778
1849
  /** F037: voice to use if `voice` is one we know the provider has retired. Without
1779
1850
  * it a retired voice throws VoiceUnavailableError rather than reaching the API.
1780
1851
  *
@@ -1806,8 +1877,14 @@ declare const ttsInputSchema: z.ZodObject<{
1806
1877
  })[] | undefined;
1807
1878
  labels?: Record<string, string> | undefined;
1808
1879
  format?: string | undefined;
1809
- voiceFallback?: string | undefined;
1810
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;
1811
1888
  rate?: number | undefined;
1812
1889
  }, {
1813
1890
  text: string;
@@ -1826,8 +1903,14 @@ declare const ttsInputSchema: z.ZodObject<{
1826
1903
  })[] | undefined;
1827
1904
  labels?: Record<string, string> | undefined;
1828
1905
  format?: string | undefined;
1829
- voiceFallback?: string | undefined;
1830
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;
1831
1914
  rate?: number | undefined;
1832
1915
  }>;
1833
1916
  declare const aiConfigSchema: z.ZodObject<{
@@ -2182,6 +2265,11 @@ interface OpenAICompatibleConfig {
2182
2265
  extraHeaders?: Record<string, string>;
2183
2266
  /** Injectable fetch for the streaming path (tests). */
2184
2267
  fetch?: typeof fetch;
2268
+ /** F049 — the provider understands `prefix: true` on a trailing assistant message.
2269
+ * Mistral only. A prefix sent anywhere else is REFUSED rather than dropped: a
2270
+ * silently ignored flag means the call succeeds, the language is not pinned, and
2271
+ * the caller believes it is. */
2272
+ supportsPrefix?: boolean;
2185
2273
  /** OpenRouter ground-truth cost (F010): send `usage:{include:true}` and use the
2186
2274
  * response's `usage.cost` (USD) as costUsd, falling back to the pricing table.
2187
2275
  * Only OpenRouter returns this field — openai/deepinfra leave it false. */
@@ -2214,8 +2302,8 @@ declare const falStubAdapter: ProviderAdapter;
2214
2302
  * wires the live adapters. */
2215
2303
  declare const stubProviders: Record<string, ProviderAdapter>;
2216
2304
 
2217
- declare const VERSION: "0.37.3";
2218
- declare const SDK_TAG: "@broberg/ai-sdk@0.37.3";
2305
+ declare const VERSION: "0.39.0";
2306
+ declare const SDK_TAG: "@broberg/ai-sdk@0.39.0";
2219
2307
 
2220
2308
  /** Built-in defaults. Every entry is overridable via AiConfig.defaults or a
2221
2309
  * per-call override.
package/dist/index.js CHANGED
@@ -619,6 +619,7 @@ function autoCacheKey(messages) {
619
619
  function toOpenAIMessage(m) {
620
620
  if (typeof m.content === "string") {
621
621
  const base = { role: m.role, content: m.content };
622
+ if (m.prefix) base.prefix = true;
622
623
  if (m.toolCallId) base.tool_call_id = m.toolCallId;
623
624
  if (m.toolCalls && m.toolCalls.length > 0) {
624
625
  base.tool_calls = m.toolCalls.map((tc) => ({
@@ -640,7 +641,70 @@ function toOpenAIMessage(m) {
640
641
  });
641
642
  return { role: m.role, content };
642
643
  }
644
+ function makeStreamPrefixStripper(prefix) {
645
+ if (!prefix) return (d) => d;
646
+ let seen = "";
647
+ let done = false;
648
+ let trimNext = false;
649
+ return (delta) => {
650
+ if (done) {
651
+ if (!trimNext) return delta;
652
+ const t = delta.trimStart();
653
+ if (t.length === 0) return "";
654
+ trimNext = false;
655
+ return t;
656
+ }
657
+ seen += delta;
658
+ if (seen.length < prefix.length) {
659
+ if (!prefix.startsWith(seen)) {
660
+ done = true;
661
+ return seen;
662
+ }
663
+ return "";
664
+ }
665
+ done = true;
666
+ const rest = seen.startsWith(prefix) ? seen.slice(prefix.length) : seen;
667
+ if (rest.length > 0) return rest.trimStart();
668
+ trimNext = true;
669
+ return "";
670
+ };
671
+ }
672
+ function stripPrefix(text, prefix) {
673
+ if (!prefix) return text;
674
+ return text.startsWith(prefix) ? text.slice(prefix.length).trimStart() : text;
675
+ }
676
+ function prefixText(messages) {
677
+ const last = messages[messages.length - 1];
678
+ if (!last?.prefix) return void 0;
679
+ return typeof last.content === "string" ? last.content : void 0;
680
+ }
681
+ function assertPrefixUsage(messages, config) {
682
+ const at = messages.findIndex((m) => m.prefix === true);
683
+ if (at === -1) return;
684
+ if (!config.supportsPrefix) {
685
+ throw new Error(
686
+ `${config.name} adapter: message.prefix is not supported by "${config.name}" \u2014 only mistral implements it. Remove the flag, or route this call with override:{provider:"mistral", model:"<a mistral model>"}.`
687
+ );
688
+ }
689
+ if (at !== messages.length - 1) {
690
+ throw new Error(
691
+ `${config.name} adapter: message.prefix is only valid on the LAST message (found at index ${at} of ${messages.length}). The provider documents no behaviour for it elsewhere.`
692
+ );
693
+ }
694
+ const last = messages[at];
695
+ if (last.role !== "assistant") {
696
+ throw new Error(
697
+ `${config.name} adapter: message.prefix is only valid on an "assistant" message (found on "${last.role}"). The prefix IS the start of the assistant's reply.`
698
+ );
699
+ }
700
+ if (typeof last.content !== "string") {
701
+ throw new Error(
702
+ `${config.name} adapter: a prefix message's content must be a plain string (got content blocks). The prefix is text the reply continues from.`
703
+ );
704
+ }
705
+ }
643
706
  function buildChatBody(req, config) {
707
+ assertPrefixUsage(req.messages, config);
644
708
  const body = {
645
709
  model: req.spec.model,
646
710
  messages: req.messages.map(toOpenAIMessage)
@@ -680,7 +744,7 @@ function makeOpenAICompatibleAdapter(config) {
680
744
  }
681
745
  const data = res.json;
682
746
  const msg = data.choices?.[0]?.message;
683
- const text = contentToText(msg?.content);
747
+ const text = stripPrefix(contentToText(msg?.content), prefixText(req.messages));
684
748
  const toolCalls = msg?.tool_calls?.map(
685
749
  (tc) => fromProviderToolCall(tc, "openai")
686
750
  );
@@ -713,6 +777,7 @@ function makeOpenAICompatibleAdapter(config) {
713
777
  if (!apiKey) {
714
778
  throw new Error(`${config.name} adapter: API key not set (env ${config.name.toUpperCase()}_API_KEY)`);
715
779
  }
780
+ const stripStreamPrefix = makeStreamPrefixStripper(prefixText(req.messages));
716
781
  const body = {
717
782
  ...buildChatBody(req, config),
718
783
  stream: true,
@@ -744,7 +809,8 @@ function makeOpenAICompatibleAdapter(config) {
744
809
  if (choice) {
745
810
  const delta = choice.delta ?? {};
746
811
  if (typeof delta.content === "string" && delta.content.length > 0) {
747
- yield { type: "text", delta: delta.content };
812
+ const out = stripStreamPrefix(delta.content);
813
+ if (out.length > 0) yield { type: "text", delta: out };
748
814
  }
749
815
  for (const tc of delta.tool_calls ?? []) {
750
816
  const idx = tc.index ?? 0;
@@ -1296,7 +1362,7 @@ var VOXTRAL_PRICE_PER_MIN = {
1296
1362
  };
1297
1363
  function mistralAdapter(config = {}) {
1298
1364
  const baseUrl = config.baseUrl ?? "https://api.mistral.ai/v1";
1299
- const base = makeOpenAICompatibleAdapter({ name: "mistral", baseUrl, apiKey: config.apiKey, supportsPromptCacheKey: true });
1365
+ const base = makeOpenAICompatibleAdapter({ name: "mistral", baseUrl, apiKey: config.apiKey, supportsPromptCacheKey: true, supportsPrefix: true });
1300
1366
  function key() {
1301
1367
  const k = config.apiKey ?? process.env.MISTRAL_API_KEY;
1302
1368
  if (!k) throw new Error("mistral adapter: API key not set (env MISTRAL_API_KEY)");
@@ -1481,6 +1547,47 @@ function mistralAdapter(config = {}) {
1481
1547
  return { ...base, ocr, moderate, embedding, transcribe, batchSubmit, batchStatus, batchResults };
1482
1548
  }
1483
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
+
1484
1591
  // src/providers/elevenlabs.ts
1485
1592
  var ELEVENLABS_PRICE_PER_1K_CHARS = 0.15;
1486
1593
  var ELEVENLABS_DANISH_VOICES = {
@@ -1493,6 +1600,17 @@ var ELEVENLABS_DANISH_VOICES = {
1493
1600
  function resolveVoice(nameOrId) {
1494
1601
  return ELEVENLABS_DANISH_VOICES[nameOrId] ?? nameOrId;
1495
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
+ }
1496
1614
  function elevenlabsAdapter(config = {}) {
1497
1615
  const baseUrl = config.baseUrl ?? "https://api.elevenlabs.io/v1";
1498
1616
  const fetchImpl = config.fetch ?? fetch;
@@ -1536,7 +1654,7 @@ function elevenlabsAdapter(config = {}) {
1536
1654
  const res = await fetchImpl(`${baseUrl}/text-to-speech/${req.voiceId}`, {
1537
1655
  method: "POST",
1538
1656
  headers: { "xi-api-key": key(), "content-type": "application/json", accept: "audio/mpeg" },
1539
- body: JSON.stringify({ text: req.text, model_id: model })
1657
+ body: JSON.stringify({ text: ttsText(req), model_id: model })
1540
1658
  });
1541
1659
  if (!res.ok) {
1542
1660
  const body = await res.text().catch(() => "");
@@ -1596,9 +1714,6 @@ function listAzureDanishVoices() {
1596
1714
  function resolveAzureVoice(nameOrVoice) {
1597
1715
  return AZURE_DANISH_VOICES[nameOrVoice] ?? nameOrVoice;
1598
1716
  }
1599
- function xmlEscape(s) {
1600
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1601
- }
1602
1717
  function localeOf(voice) {
1603
1718
  const parts = voice.split("-");
1604
1719
  return parts.length >= 2 ? `${parts[0]}-${parts[1]}` : "en-US";
@@ -1644,7 +1759,13 @@ function azureAdapter(config = {}) {
1644
1759
  const voice = resolveAzureVoice(req.voiceId);
1645
1760
  const lang = req.lang ?? localeOf(voice);
1646
1761
  const format = req.format ?? DEFAULT_FORMAT;
1647
- 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
+ );
1648
1769
  const effRate = req.rate ?? AZURE_DANISH_VOICE_LIST.find((v) => v.voiceId === voice)?.defaultRate;
1649
1770
  const inner = effRate != null && effRate !== 1 ? `<prosody rate='${effRate}'>${escaped}</prosody>` : escaped;
1650
1771
  const ssml = `<speak version='1.0' xml:lang='${lang}'><voice name='${voice}'>${inner}</voice></speak>`;
@@ -2812,7 +2933,11 @@ var messageSchema = z.object({
2812
2933
  role: z.enum(["system", "user", "assistant", "tool"]),
2813
2934
  content: z.union([z.string(), z.array(contentPartSchema)]),
2814
2935
  toolCalls: z.array(toolCallSchema).optional(),
2815
- toolCallId: z.string().optional()
2936
+ toolCallId: z.string().optional(),
2937
+ /** F049 — see Message.prefix. Position/role validity is enforced in the adapter,
2938
+ * not here: the rule is about a message's place in the ARRAY, which a per-message
2939
+ * schema cannot see. */
2940
+ prefix: z.boolean().optional()
2816
2941
  });
2817
2942
  var callOptions = {
2818
2943
  tier: tierSchema.optional(),
@@ -2953,9 +3078,18 @@ var podcastInputSchema = z.object({
2953
3078
  format: z.string().optional(),
2954
3079
  ...callOptions
2955
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
+ });
2956
3087
  var ttsInputSchema = z.object({
2957
3088
  text: z.string(),
2958
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(),
2959
3093
  /** F037: voice to use if `voice` is one we know the provider has retired. Without
2960
3094
  * it a retired voice throws VoiceUnavailableError rather than reaching the API.
2961
3095
  *
@@ -2997,8 +3131,8 @@ var aiConfigSchema = z.object({
2997
3131
  });
2998
3132
 
2999
3133
  // src/version.ts
3000
- var VERSION = "0.37.3";
3001
- var SDK_TAG = "@broberg/ai-sdk@0.37.3";
3134
+ var VERSION = "0.39.0";
3135
+ var SDK_TAG = "@broberg/ai-sdk@0.39.0";
3002
3136
 
3003
3137
  // src/cost/sinks/upmetrics.ts
3004
3138
  function upmetricsSink(config) {
@@ -3541,7 +3675,15 @@ function createAI(config = {}) {
3541
3675
  invoke: async (spec) => {
3542
3676
  const adapter = pickProvider(spec.provider);
3543
3677
  if (!adapter.tts) throw new Error(`createAI: provider "${spec.provider}" does not support tts`);
3544
- 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
+ });
3545
3687
  }
3546
3688
  });
3547
3689
  },