@broberg/ai-sdk 0.37.3 → 0.38.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
@@ -75,6 +75,24 @@ interface Message {
75
75
  toolCalls?: ToolCallLike[];
76
76
  /** Set on `tool` role messages — which call this result answers. */
77
77
  toolCallId?: string;
78
+ /** F049 — Mistral's `prefix`: start the model's reply with this text instead of
79
+ * asking it to. Only valid on the LAST message, and only when that message is an
80
+ * `assistant` one; anything else throws rather than being sent, because Mistral
81
+ * documents no behaviour for it elsewhere.
82
+ *
83
+ * Mistral's own first-listed use case is Language Adherence, and it pairs with a
84
+ * system instruction rather than replacing one — they warn that a prefix alone
85
+ * gives "noisy and unpredictable answers". Measured on this repo's own data: an
86
+ * explicit language rule in the system prompt already took 5/15 wrong answers to
87
+ * 0/15, and every leak began in the FIRST words ("Tak for din henvendelse"), which
88
+ * is exactly what a prefix pins.
89
+ *
90
+ * **The SDK strips the prefix back off the response**, in `chat` and `chatStream`
91
+ * alike, so a caller never has to. Mistral's own example does
92
+ * `content[len(prefix):]` by hand; forgetting it puts "Here is the answer in
93
+ * Norwegian:" at the top of a real customer's email — a defect that reads as
94
+ * formatting rather than as a bug. */
95
+ prefix?: boolean;
78
96
  }
79
97
  /** SDK-level tool definition. Adapters convert this to each provider's format
80
98
  * (F4.5). `parameters` is a JSON Schema object. */
@@ -635,6 +653,10 @@ declare const messageSchema: z.ZodObject<{
635
653
  arguments?: Record<string, unknown> | undefined;
636
654
  }>, "many">>;
637
655
  toolCallId: z.ZodOptional<z.ZodString>;
656
+ /** F049 — see Message.prefix. Position/role validity is enforced in the adapter,
657
+ * not here: the rule is about a message's place in the ARRAY, which a per-message
658
+ * schema cannot see. */
659
+ prefix: z.ZodOptional<z.ZodBoolean>;
638
660
  }, "strip", z.ZodTypeAny, {
639
661
  content: string | ({
640
662
  text: string;
@@ -651,6 +673,7 @@ declare const messageSchema: z.ZodObject<{
651
673
  args?: Record<string, unknown> | undefined;
652
674
  arguments?: Record<string, unknown> | undefined;
653
675
  }[] | undefined;
676
+ prefix?: boolean | undefined;
654
677
  toolCallId?: string | undefined;
655
678
  }, {
656
679
  content: string | ({
@@ -668,6 +691,7 @@ declare const messageSchema: z.ZodObject<{
668
691
  args?: Record<string, unknown> | undefined;
669
692
  arguments?: Record<string, unknown> | undefined;
670
693
  }[] | undefined;
694
+ prefix?: boolean | undefined;
671
695
  toolCallId?: string | undefined;
672
696
  }>;
673
697
  declare const chatInputSchema: z.ZodObject<{
@@ -760,6 +784,10 @@ declare const chatInputSchema: z.ZodObject<{
760
784
  arguments?: Record<string, unknown> | undefined;
761
785
  }>, "many">>;
762
786
  toolCallId: z.ZodOptional<z.ZodString>;
787
+ /** F049 — see Message.prefix. Position/role validity is enforced in the adapter,
788
+ * not here: the rule is about a message's place in the ARRAY, which a per-message
789
+ * schema cannot see. */
790
+ prefix: z.ZodOptional<z.ZodBoolean>;
763
791
  }, "strip", z.ZodTypeAny, {
764
792
  content: string | ({
765
793
  text: string;
@@ -776,6 +804,7 @@ declare const chatInputSchema: z.ZodObject<{
776
804
  args?: Record<string, unknown> | undefined;
777
805
  arguments?: Record<string, unknown> | undefined;
778
806
  }[] | undefined;
807
+ prefix?: boolean | undefined;
779
808
  toolCallId?: string | undefined;
780
809
  }, {
781
810
  content: string | ({
@@ -793,6 +822,7 @@ declare const chatInputSchema: z.ZodObject<{
793
822
  args?: Record<string, unknown> | undefined;
794
823
  arguments?: Record<string, unknown> | undefined;
795
824
  }[] | undefined;
825
+ prefix?: boolean | undefined;
796
826
  toolCallId?: string | undefined;
797
827
  }>, "many">>;
798
828
  system: z.ZodOptional<z.ZodString>;
@@ -833,6 +863,7 @@ declare const chatInputSchema: z.ZodObject<{
833
863
  args?: Record<string, unknown> | undefined;
834
864
  arguments?: Record<string, unknown> | undefined;
835
865
  }[] | undefined;
866
+ prefix?: boolean | undefined;
836
867
  toolCallId?: string | undefined;
837
868
  }[] | undefined;
838
869
  tools?: {
@@ -877,6 +908,7 @@ declare const chatInputSchema: z.ZodObject<{
877
908
  args?: Record<string, unknown> | undefined;
878
909
  arguments?: Record<string, unknown> | undefined;
879
910
  }[] | undefined;
911
+ prefix?: boolean | undefined;
880
912
  toolCallId?: string | undefined;
881
913
  }[] | undefined;
882
914
  tools?: {
@@ -2182,6 +2214,11 @@ interface OpenAICompatibleConfig {
2182
2214
  extraHeaders?: Record<string, string>;
2183
2215
  /** Injectable fetch for the streaming path (tests). */
2184
2216
  fetch?: typeof fetch;
2217
+ /** F049 — the provider understands `prefix: true` on a trailing assistant message.
2218
+ * Mistral only. A prefix sent anywhere else is REFUSED rather than dropped: a
2219
+ * silently ignored flag means the call succeeds, the language is not pinned, and
2220
+ * the caller believes it is. */
2221
+ supportsPrefix?: boolean;
2185
2222
  /** OpenRouter ground-truth cost (F010): send `usage:{include:true}` and use the
2186
2223
  * response's `usage.cost` (USD) as costUsd, falling back to the pricing table.
2187
2224
  * Only OpenRouter returns this field — openai/deepinfra leave it false. */
@@ -2214,8 +2251,8 @@ declare const falStubAdapter: ProviderAdapter;
2214
2251
  * wires the live adapters. */
2215
2252
  declare const stubProviders: Record<string, ProviderAdapter>;
2216
2253
 
2217
- declare const VERSION: "0.37.3";
2218
- declare const SDK_TAG: "@broberg/ai-sdk@0.37.3";
2254
+ declare const VERSION: "0.38.0";
2255
+ declare const SDK_TAG: "@broberg/ai-sdk@0.38.0";
2219
2256
 
2220
2257
  /** Built-in defaults. Every entry is overridable via AiConfig.defaults or a
2221
2258
  * 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)");
@@ -2812,7 +2878,11 @@ var messageSchema = z.object({
2812
2878
  role: z.enum(["system", "user", "assistant", "tool"]),
2813
2879
  content: z.union([z.string(), z.array(contentPartSchema)]),
2814
2880
  toolCalls: z.array(toolCallSchema).optional(),
2815
- toolCallId: z.string().optional()
2881
+ toolCallId: z.string().optional(),
2882
+ /** F049 — see Message.prefix. Position/role validity is enforced in the adapter,
2883
+ * not here: the rule is about a message's place in the ARRAY, which a per-message
2884
+ * schema cannot see. */
2885
+ prefix: z.boolean().optional()
2816
2886
  });
2817
2887
  var callOptions = {
2818
2888
  tier: tierSchema.optional(),
@@ -2997,8 +3067,8 @@ var aiConfigSchema = z.object({
2997
3067
  });
2998
3068
 
2999
3069
  // src/version.ts
3000
- var VERSION = "0.37.3";
3001
- var SDK_TAG = "@broberg/ai-sdk@0.37.3";
3070
+ var VERSION = "0.38.0";
3071
+ var SDK_TAG = "@broberg/ai-sdk@0.38.0";
3002
3072
 
3003
3073
  // src/cost/sinks/upmetrics.ts
3004
3074
  function upmetricsSink(config) {