provider-mistral.test.ts 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. import { createMistral } from "@ai-sdk/mistral"
  2. import { expect, test } from "bun:test"
  3. test("Mistral sends promptCacheKey as prompt_cache_key", async () => {
  4. let body: Record<string, unknown> | undefined
  5. const mockFetch = Object.assign(
  6. async (_input: Parameters<typeof fetch>[0], init?: RequestInit) => {
  7. body = JSON.parse(String(init?.body))
  8. return Response.json({
  9. id: "response-1",
  10. created: 0,
  11. model: "mistral-large-latest",
  12. object: "chat.completion",
  13. choices: [{ index: 0, message: { role: "assistant", content: "Hello" }, finish_reason: "stop" }],
  14. usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
  15. })
  16. },
  17. { preconnect: fetch.preconnect },
  18. )
  19. const model = createMistral({ apiKey: "test", fetch: mockFetch })("mistral-large-latest")
  20. await model.doGenerate({
  21. prompt: [{ role: "user", content: [{ type: "text", text: "Hello" }] }],
  22. providerOptions: { mistral: { promptCacheKey: "session-123" } },
  23. })
  24. expect(body?.prompt_cache_key).toBe("session-123")
  25. })
  26. test("Mistral round-trips native reasoning in assistant history", async () => {
  27. let body: { messages?: unknown[] } | undefined
  28. const mockFetch = Object.assign(
  29. async (_input: Parameters<typeof fetch>[0], init?: RequestInit) => {
  30. body = JSON.parse(String(init?.body))
  31. return Response.json({
  32. id: "response-1",
  33. created: 0,
  34. model: "mistral-small-latest",
  35. object: "chat.completion",
  36. choices: [
  37. {
  38. index: 0,
  39. message: {
  40. role: "assistant",
  41. content: [
  42. {
  43. type: "thinking",
  44. thinking: [
  45. { type: "text", text: "The user is greeting me." },
  46. {
  47. type: "tool_reference",
  48. tool: "web_search",
  49. title: "Example result",
  50. url: "https://example.com/tool",
  51. favicon: "https://example.com/favicon.ico",
  52. description: "Example description",
  53. },
  54. { type: "reference", reference_ids: [1, "source-2"] },
  55. ],
  56. closed: true,
  57. signature: "sig-123",
  58. },
  59. { type: "text", text: "Hi" },
  60. ],
  61. },
  62. finish_reason: "stop",
  63. },
  64. ],
  65. usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
  66. })
  67. },
  68. { preconnect: fetch.preconnect },
  69. )
  70. const model = createMistral({ apiKey: "test", fetch: mockFetch })("mistral-small-latest")
  71. const first = await model.doGenerate({
  72. prompt: [{ role: "user", content: [{ type: "text", text: "Hello" }] }],
  73. })
  74. const reasoning = first.content.find((part) => part.type === "reasoning")
  75. const text = first.content.find((part) => part.type === "text")
  76. if (!reasoning || !text) throw new Error("expected reasoning and text")
  77. await model.doGenerate({
  78. prompt: [
  79. { role: "user", content: [{ type: "text", text: "Hello" }] },
  80. {
  81. role: "assistant",
  82. content: [{ ...reasoning, providerOptions: reasoning.providerMetadata }, text],
  83. },
  84. { role: "user", content: [{ type: "text", text: "Hello again" }] },
  85. ],
  86. })
  87. expect(body?.messages?.[1]).toEqual({
  88. role: "assistant",
  89. content: [
  90. {
  91. type: "thinking",
  92. thinking: [
  93. { type: "text", text: "The user is greeting me." },
  94. {
  95. type: "tool_reference",
  96. tool: "web_search",
  97. title: "Example result",
  98. url: "https://example.com/tool",
  99. favicon: "https://example.com/favicon.ico",
  100. description: "Example description",
  101. },
  102. { type: "reference", reference_ids: [1, "source-2"] },
  103. ],
  104. closed: true,
  105. signature: "sig-123",
  106. },
  107. { type: "text", text: "Hi" },
  108. ],
  109. })
  110. await model.doGenerate({
  111. prompt: [
  112. { role: "user", content: [{ type: "text", text: "Hello" }] },
  113. {
  114. role: "assistant",
  115. content: [
  116. { type: "reasoning", text: "thinking" },
  117. { type: "text", text: "Hi" },
  118. ],
  119. },
  120. { role: "user", content: [{ type: "text", text: "Hello again" }] },
  121. ],
  122. })
  123. expect(body?.messages?.[1]).toEqual({ role: "assistant", content: "thinkingHi" })
  124. })
  125. test("Mistral preserves native reasoning metadata while streaming", async () => {
  126. const chunks = [
  127. {
  128. id: "response-1",
  129. created: 0,
  130. model: "mistral-small-latest",
  131. choices: [
  132. {
  133. index: 0,
  134. delta: {
  135. role: "assistant",
  136. content: [
  137. {
  138. type: "thinking",
  139. thinking: [
  140. { type: "text", text: "thinking" },
  141. {
  142. type: "tool_reference",
  143. tool: "web_search",
  144. title: "Example result",
  145. url: "https://example.com/tool",
  146. favicon: "https://example.com/favicon.ico",
  147. description: "Example description",
  148. },
  149. ],
  150. },
  151. ],
  152. },
  153. },
  154. ],
  155. },
  156. {
  157. id: "response-1",
  158. created: 0,
  159. model: "mistral-small-latest",
  160. choices: [
  161. {
  162. index: 0,
  163. delta: {
  164. content: [
  165. {
  166. type: "thinking",
  167. thinking: [{ type: "reference", reference_ids: [1, "source-2"] }],
  168. closed: true,
  169. signature: "sig-123",
  170. },
  171. ],
  172. },
  173. },
  174. ],
  175. },
  176. {
  177. id: "response-1",
  178. created: 0,
  179. model: "mistral-small-latest",
  180. choices: [{ index: 0, delta: { content: [{ type: "text", text: "answer" }] } }],
  181. },
  182. {
  183. id: "response-1",
  184. created: 0,
  185. model: "mistral-small-latest",
  186. choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
  187. usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
  188. },
  189. ]
  190. const mockFetch = Object.assign(
  191. async () =>
  192. new Response(chunks.map((chunk) => `data: ${JSON.stringify(chunk)}\n\n`).join(""), {
  193. headers: { "Content-Type": "text/event-stream" },
  194. }),
  195. { preconnect: fetch.preconnect },
  196. )
  197. const model = createMistral({ apiKey: "test", fetch: mockFetch })("mistral-small-latest")
  198. const result = await model.doStream({
  199. prompt: [{ role: "user", content: [{ type: "text", text: "Hello" }] }],
  200. })
  201. const events = []
  202. for await (const event of result.stream) events.push(event)
  203. expect(events.find((event) => event.type === "reasoning-end")?.providerMetadata).toEqual({
  204. mistral: {
  205. thinking: {
  206. type: "thinking",
  207. thinking: [
  208. { type: "text", text: "thinking" },
  209. {
  210. type: "tool_reference",
  211. tool: "web_search",
  212. title: "Example result",
  213. url: "https://example.com/tool",
  214. favicon: "https://example.com/favicon.ico",
  215. description: "Example description",
  216. },
  217. { type: "reference", reference_ids: [1, "source-2"] },
  218. ],
  219. closed: true,
  220. signature: "sig-123",
  221. },
  222. },
  223. })
  224. expect(
  225. events
  226. .filter((event) => event.type === "reasoning-start" || event.type === "reasoning-delta")
  227. .every((event) => event.providerMetadata === undefined),
  228. ).toBe(true)
  229. })
  230. test("Mistral preserves metadata-only thinking chunks", async () => {
  231. const thinking = {
  232. type: "thinking" as const,
  233. thinking: [
  234. {
  235. type: "tool_reference",
  236. tool: "web_search",
  237. title: "Example result",
  238. url: "https://example.com/tool",
  239. favicon: "https://example.com/favicon.ico",
  240. description: "Example description",
  241. },
  242. { type: "reference", reference_ids: [1, "source-2"] },
  243. ],
  244. closed: true,
  245. signature: "sig-123",
  246. }
  247. const mockFetch = Object.assign(
  248. async () =>
  249. Response.json({
  250. id: "response-1",
  251. created: 0,
  252. model: "mistral-small-latest",
  253. object: "chat.completion",
  254. choices: [{ index: 0, message: { role: "assistant", content: [thinking] }, finish_reason: "stop" }],
  255. usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
  256. }),
  257. { preconnect: fetch.preconnect },
  258. )
  259. const model = createMistral({ apiKey: "test", fetch: mockFetch })("mistral-small-latest")
  260. const result = await model.doGenerate({
  261. prompt: [{ role: "user", content: [{ type: "text", text: "Hello" }] }],
  262. })
  263. expect(result.content).toEqual([
  264. {
  265. type: "reasoning",
  266. text: "",
  267. providerMetadata: { mistral: { thinking } },
  268. },
  269. ])
  270. })