@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 +92 -4
- package/dist/index.js +154 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
2218
|
-
declare const SDK_TAG: "@broberg/ai-sdk@0.
|
|
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
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|
-
|
|
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.
|
|
3001
|
-
var SDK_TAG = "@broberg/ai-sdk@0.
|
|
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({
|
|
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
|
},
|