소스 검색

fix(provider): preserve Mistral reasoning history (#38453)

Aiden Cline 3 주 전
부모
커밋
20589d66d5

+ 7 - 7
bun.lock

@@ -303,7 +303,7 @@
         "@ai-sdk/google": "3.0.73",
         "@ai-sdk/google-vertex": "4.0.128",
         "@ai-sdk/groq": "3.0.31",
-        "@ai-sdk/mistral": "3.0.34",
+        "@ai-sdk/mistral": "3.0.51",
         "@ai-sdk/openai": "3.0.84",
         "@ai-sdk/openai-compatible": "2.0.41",
         "@ai-sdk/perplexity": "3.0.26",
@@ -578,7 +578,7 @@
         "@ai-sdk/google": "3.0.73",
         "@ai-sdk/google-vertex": "4.0.128",
         "@ai-sdk/groq": "3.0.31",
-        "@ai-sdk/mistral": "3.0.34",
+        "@ai-sdk/mistral": "3.0.51",
         "@ai-sdk/openai": "3.0.84",
         "@ai-sdk/openai-compatible": "2.0.41",
         "@ai-sdk/perplexity": "3.0.26",
@@ -1076,12 +1076,12 @@
     "gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch",
     "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
     "effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch",
-    "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
+    "@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch",
     "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
+    "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
     "solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
     "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
     "pacote@21.5.0": "patches/pacote@21.5.0.patch",
-    "@ai-sdk/mistral@3.0.34": "patches/@ai-sdk%2Fmistral@3.0.34.patch",
   },
   "overrides": {
     "@opentui/core": "catalog:",
@@ -1202,7 +1202,7 @@
 
     "@ai-sdk/groq": ["@ai-sdk/groq@3.0.31", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XbbugpnFmXGu2TlXiq8KUJskP6/VVbuFcnFIGDzDIB/Chg6XHsNnqrTF80Zxkh0Pd3+NvbM+2Uqrtsndk6bDAg=="],
 
-    "@ai-sdk/mistral": ["@ai-sdk/mistral@3.0.34", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.26" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HpK28sWGdIfg1vTSScJNtzVdvNRfA4mfCmPmPR+j/MGJ0oAuEJMqxWkL96ZnGPdhZt5KdW09aKovdIe+q2zQ7A=="],
+    "@ai-sdk/mistral": ["@ai-sdk/mistral@3.0.51", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.40" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-83eXY6p0lUFhSuMvNDmTKDuMciK5XDAWDlNh5c0L80tKjmtCFRItA1MZHp4IKe1r7eK8Rb5nN7qtxqMLUFRIRw=="],
 
     "@ai-sdk/openai": ["@ai-sdk/openai@3.0.48", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ALmj/53EXpcRqMbGpPJPP4UOSWw0q4VGpnDo7YctvsynjkrKDmoneDG/1a7VQnSPYHnJp6tTRMf5ZdxZ5whulg=="],
 
@@ -5702,9 +5702,9 @@
 
     "@ai-sdk/groq/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
 
-    "@ai-sdk/mistral/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
+    "@ai-sdk/mistral/@ai-sdk/provider": ["@ai-sdk/provider@3.0.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA=="],
 
-    "@ai-sdk/mistral/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.26", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CsKNLKsOpvPujRlIYvoz+Ybw+kGn7J4/fIZa/58+R7iWLLfwn6ifE2G6Yq8K9XvH/I/3bzaDAJ3NhRwEMsLBKQ=="],
+    "@ai-sdk/mistral/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.40", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OL5IrpUm9Y8Dwy+w/vvFwPotS6m52O9W0op2oXgXdCROMJIBalBI0oro6OIBYkPxvm5Xg02GSkoQN25RlR0bnw=="],
 
     "@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
 

+ 1 - 1
package.json

@@ -149,7 +149,7 @@
     "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
     "solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
     "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch",
-    "@ai-sdk/mistral@3.0.34": "patches/@ai-sdk%2Fmistral@3.0.34.patch",
+    "@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch",
     "gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch",
     "pacote@21.5.0": "patches/pacote@21.5.0.patch",
     "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",

+ 1 - 1
packages/core/package.json

@@ -72,7 +72,7 @@
     "@ai-sdk/google": "3.0.73",
     "@ai-sdk/google-vertex": "4.0.128",
     "@ai-sdk/groq": "3.0.31",
-    "@ai-sdk/mistral": "3.0.34",
+    "@ai-sdk/mistral": "3.0.51",
     "@ai-sdk/openai": "3.0.84",
     "@ai-sdk/openai-compatible": "2.0.41",
     "@ai-sdk/perplexity": "3.0.26",

+ 254 - 0
packages/core/test/provider-mistral.test.ts

@@ -26,3 +26,257 @@ test("Mistral sends promptCacheKey as prompt_cache_key", async () => {
 
   expect(body?.prompt_cache_key).toBe("session-123")
 })
+
+test("Mistral round-trips native reasoning in assistant history", async () => {
+  let body: { messages?: unknown[] } | undefined
+  const mockFetch = Object.assign(
+    async (_input: Parameters<typeof fetch>[0], init?: RequestInit) => {
+      body = JSON.parse(String(init?.body))
+      return Response.json({
+        id: "response-1",
+        created: 0,
+        model: "mistral-small-latest",
+        object: "chat.completion",
+        choices: [
+          {
+            index: 0,
+            message: {
+              role: "assistant",
+              content: [
+                {
+                  type: "thinking",
+                  thinking: [
+                    { type: "text", text: "The user is greeting me." },
+                    {
+                      type: "tool_reference",
+                      tool: "web_search",
+                      title: "Example result",
+                      url: "https://example.com/tool",
+                      favicon: "https://example.com/favicon.ico",
+                      description: "Example description",
+                    },
+                    { type: "reference", reference_ids: [1, "source-2"] },
+                  ],
+                  closed: true,
+                  signature: "sig-123",
+                },
+                { type: "text", text: "Hi" },
+              ],
+            },
+            finish_reason: "stop",
+          },
+        ],
+        usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
+      })
+    },
+    { preconnect: fetch.preconnect },
+  )
+  const model = createMistral({ apiKey: "test", fetch: mockFetch })("mistral-small-latest")
+
+  const first = await model.doGenerate({
+    prompt: [{ role: "user", content: [{ type: "text", text: "Hello" }] }],
+  })
+  const reasoning = first.content.find((part) => part.type === "reasoning")
+  const text = first.content.find((part) => part.type === "text")
+  if (!reasoning || !text) throw new Error("expected reasoning and text")
+
+  await model.doGenerate({
+    prompt: [
+      { role: "user", content: [{ type: "text", text: "Hello" }] },
+      {
+        role: "assistant",
+        content: [{ ...reasoning, providerOptions: reasoning.providerMetadata }, text],
+      },
+      { role: "user", content: [{ type: "text", text: "Hello again" }] },
+    ],
+  })
+
+  expect(body?.messages?.[1]).toEqual({
+    role: "assistant",
+    content: [
+      {
+        type: "thinking",
+        thinking: [
+          { type: "text", text: "The user is greeting me." },
+          {
+            type: "tool_reference",
+            tool: "web_search",
+            title: "Example result",
+            url: "https://example.com/tool",
+            favicon: "https://example.com/favicon.ico",
+            description: "Example description",
+          },
+          { type: "reference", reference_ids: [1, "source-2"] },
+        ],
+        closed: true,
+        signature: "sig-123",
+      },
+      { type: "text", text: "Hi" },
+    ],
+  })
+
+  await model.doGenerate({
+    prompt: [
+      { role: "user", content: [{ type: "text", text: "Hello" }] },
+      {
+        role: "assistant",
+        content: [
+          { type: "reasoning", text: "thinking" },
+          { type: "text", text: "Hi" },
+        ],
+      },
+      { role: "user", content: [{ type: "text", text: "Hello again" }] },
+    ],
+  })
+  expect(body?.messages?.[1]).toEqual({ role: "assistant", content: "thinkingHi" })
+})
+
+test("Mistral preserves native reasoning metadata while streaming", async () => {
+  const chunks = [
+    {
+      id: "response-1",
+      created: 0,
+      model: "mistral-small-latest",
+      choices: [
+        {
+          index: 0,
+          delta: {
+            role: "assistant",
+            content: [
+              {
+                type: "thinking",
+                thinking: [
+                  { type: "text", text: "thinking" },
+                  {
+                    type: "tool_reference",
+                    tool: "web_search",
+                    title: "Example result",
+                    url: "https://example.com/tool",
+                    favicon: "https://example.com/favicon.ico",
+                    description: "Example description",
+                  },
+                ],
+              },
+            ],
+          },
+        },
+      ],
+    },
+    {
+      id: "response-1",
+      created: 0,
+      model: "mistral-small-latest",
+      choices: [
+        {
+          index: 0,
+          delta: {
+            content: [
+              {
+                type: "thinking",
+                thinking: [{ type: "reference", reference_ids: [1, "source-2"] }],
+                closed: true,
+                signature: "sig-123",
+              },
+            ],
+          },
+        },
+      ],
+    },
+    {
+      id: "response-1",
+      created: 0,
+      model: "mistral-small-latest",
+      choices: [{ index: 0, delta: { content: [{ type: "text", text: "answer" }] } }],
+    },
+    {
+      id: "response-1",
+      created: 0,
+      model: "mistral-small-latest",
+      choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
+      usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
+    },
+  ]
+  const mockFetch = Object.assign(
+    async () =>
+      new Response(chunks.map((chunk) => `data: ${JSON.stringify(chunk)}\n\n`).join(""), {
+        headers: { "Content-Type": "text/event-stream" },
+      }),
+    { preconnect: fetch.preconnect },
+  )
+  const model = createMistral({ apiKey: "test", fetch: mockFetch })("mistral-small-latest")
+  const result = await model.doStream({
+    prompt: [{ role: "user", content: [{ type: "text", text: "Hello" }] }],
+  })
+  const events = []
+  for await (const event of result.stream) events.push(event)
+
+  expect(events.find((event) => event.type === "reasoning-end")?.providerMetadata).toEqual({
+    mistral: {
+      thinking: {
+        type: "thinking",
+        thinking: [
+          { type: "text", text: "thinking" },
+          {
+            type: "tool_reference",
+            tool: "web_search",
+            title: "Example result",
+            url: "https://example.com/tool",
+            favicon: "https://example.com/favicon.ico",
+            description: "Example description",
+          },
+          { type: "reference", reference_ids: [1, "source-2"] },
+        ],
+        closed: true,
+        signature: "sig-123",
+      },
+    },
+  })
+  expect(
+    events
+      .filter((event) => event.type === "reasoning-start" || event.type === "reasoning-delta")
+      .every((event) => event.providerMetadata === undefined),
+  ).toBe(true)
+})
+
+test("Mistral preserves metadata-only thinking chunks", async () => {
+  const thinking = {
+    type: "thinking" as const,
+    thinking: [
+      {
+        type: "tool_reference",
+        tool: "web_search",
+        title: "Example result",
+        url: "https://example.com/tool",
+        favicon: "https://example.com/favicon.ico",
+        description: "Example description",
+      },
+      { type: "reference", reference_ids: [1, "source-2"] },
+    ],
+    closed: true,
+    signature: "sig-123",
+  }
+  const mockFetch = Object.assign(
+    async () =>
+      Response.json({
+        id: "response-1",
+        created: 0,
+        model: "mistral-small-latest",
+        object: "chat.completion",
+        choices: [{ index: 0, message: { role: "assistant", content: [thinking] }, finish_reason: "stop" }],
+        usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
+      }),
+    { preconnect: fetch.preconnect },
+  )
+  const model = createMistral({ apiKey: "test", fetch: mockFetch })("mistral-small-latest")
+  const result = await model.doGenerate({
+    prompt: [{ role: "user", content: [{ type: "text", text: "Hello" }] }],
+  })
+
+  expect(result.content).toEqual([
+    {
+      type: "reasoning",
+      text: "",
+      providerMetadata: { mistral: { thinking } },
+    },
+  ])
+})

+ 1 - 1
packages/opencode/package.json

@@ -66,7 +66,7 @@
     "@ai-sdk/google": "3.0.73",
     "@ai-sdk/google-vertex": "4.0.128",
     "@ai-sdk/groq": "3.0.31",
-    "@ai-sdk/mistral": "3.0.34",
+    "@ai-sdk/mistral": "3.0.51",
     "@ai-sdk/openai": "3.0.84",
     "@ai-sdk/openai-compatible": "2.0.41",
     "@ai-sdk/perplexity": "3.0.26",

+ 111 - 0
packages/opencode/test/session/llm.test.ts

@@ -906,6 +906,117 @@ describe("session.llm.stream", () => {
     },
   )
 
+  const mistralFixture = { providerID: "mistral", modelID: "mistral-small-latest" }
+  it.instance(
+    "replays native Mistral reasoning from chat history",
+    () =>
+      Effect.gen(function* () {
+        const fixture = loadFixture(mistralFixture.providerID, mistralFixture.modelID)
+        const request = waitRequest(
+          "/chat/completions",
+          createEventResponse(
+            [
+              {
+                id: "chatcmpl-mistral",
+                object: "chat.completion.chunk",
+                created: 0,
+                model: fixture.model.id,
+                choices: [{ index: 0, delta: { role: "assistant", content: "Hello" } }],
+              },
+              {
+                id: "chatcmpl-mistral",
+                object: "chat.completion.chunk",
+                created: 0,
+                model: fixture.model.id,
+                choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
+              },
+            ],
+            true,
+          ),
+        )
+
+        const resolved = yield* Provider.use.getModel(
+          ProviderV2.ID.make(mistralFixture.providerID),
+          ModelV2.ID.make(fixture.model.id),
+        )
+        const sessionID = SessionID.make("session-test-mistral-reasoning")
+        const agent = {
+          name: "test",
+          mode: "primary",
+          options: {},
+          permission: [{ permission: "*", pattern: "*", action: "allow" }],
+        } satisfies Agent.Info
+
+        const user = {
+          id: MessageID.make("msg_user-mistral-reasoning"),
+          sessionID,
+          role: "user",
+          time: { created: Date.now() },
+          agent: agent.name,
+          model: { providerID: ProviderV2.ID.make(mistralFixture.providerID), modelID: resolved.id },
+        } satisfies SessionV1.User
+
+        const thinking = {
+          type: "thinking",
+          thinking: [
+            { type: "text", text: "thinking" },
+            {
+              type: "tool_reference",
+              tool: "web_search",
+              title: "Example result",
+              url: "https://example.com/tool",
+              favicon: "https://example.com/favicon.ico",
+              description: "Example description",
+            },
+            { type: "reference", reference_ids: [1, "source-2"] },
+          ],
+          closed: true,
+          signature: "sig-123",
+        }
+
+        yield* drain({
+          user,
+          sessionID,
+          model: resolved,
+          agent,
+          system: ["You are a helpful assistant."],
+          messages: [
+            { role: "user", content: "Hello" },
+            {
+              role: "assistant",
+              content: [
+                {
+                  type: "reasoning",
+                  text: "thinking",
+                  providerOptions: { mistral: { thinking } },
+                },
+                { type: "text", text: "Previous answer" },
+              ],
+            },
+            { role: "user", content: "Continue" },
+          ] satisfies ModelMessage[],
+          tools: {},
+        })
+
+        const capture = yield* Effect.promise(() => request)
+        const messages = capture.body.messages as Array<Record<string, unknown>>
+        expect(messages.find((message) => message.role === "assistant")).toEqual({
+          role: "assistant",
+          content: [thinking, { type: "text", text: "Previous answer" }],
+        })
+      }),
+    {
+      config: () => ({
+        enabled_providers: [mistralFixture.providerID],
+        provider: {
+          [mistralFixture.providerID]: {
+            options: { apiKey: "test-key", baseURL: `${state.server!.url.origin}/v1` },
+          },
+        },
+      }),
+    },
+  )
+
   const alibabaQwenFixture = { providerID: "alibaba", modelID: "qwen-plus" }
   it.instance(
     "service stream cancellation cancels provider response body promptly",

+ 0 - 84
patches/@ai-sdk%2Fmistral@3.0.34.patch

@@ -1,84 +0,0 @@
-diff --git a/dist/index.d.ts b/dist/index.d.ts
-index 1ca9113bed2728a616db773a8e08d8d6957447d7..15408ec429dc210b5fa43589d81b69c93bf27b2d 100644
---- a/dist/index.d.ts
-+++ b/dist/index.d.ts
-@@ -14,6 +14,7 @@ declare const mistralLanguageModelOptions: z.ZodObject<{
-         none: "none";
-         high: "high";
-     }>>;
-+    promptCacheKey: z.ZodOptional<z.ZodString>;
- }, z.core.$strip>;
- type MistralLanguageModelOptions = z.infer<typeof mistralLanguageModelOptions>;
- 
-diff --git a/dist/index.js b/dist/index.js
-index 45735e524aaff54ea058c99c729c5ffd3c507058..6aca5f6f13da0054ede31c1f1a692e4eaed37d34 100644
---- a/dist/index.js
-+++ b/dist/index.js
-@@ -268,7 +268,8 @@ var mistralLanguageModelOptions = import_v4.z.object({
-    * - `'high'`: Enable reasoning
-    * - `'none'`: Disable reasoning
-    */
--  reasoningEffort: import_v4.z.enum(["high", "none"]).optional()
-+  reasoningEffort: import_v4.z.enum(["high", "none"]).optional(),
-+  promptCacheKey: import_v4.z.string().optional()
- });
- 
- // src/mistral-error.ts
-@@ -413,6 +414,7 @@ var MistralChatLanguageModel = class {
-       top_p: topP,
-       random_seed: seed,
-       reasoning_effort: options.reasoningEffort,
-+      prompt_cache_key: options.promptCacheKey,
-       // response format:
-       response_format: (responseFormat == null ? void 0 : responseFormat.type) === "json" ? structuredOutputs && (responseFormat == null ? void 0 : responseFormat.schema) != null ? {
-         type: "json_schema",
-diff --git a/dist/index.mjs b/dist/index.mjs
-index 4c22df1cd78a1ba81309c8a86ceecefef4ba4aea..30cd3b1f503860109b7fa2107cd1eb17b70c96be 100644
---- a/dist/index.mjs
-+++ b/dist/index.mjs
-@@ -256,7 +256,8 @@ var mistralLanguageModelOptions = z.object({
-    * - `'high'`: Enable reasoning
-    * - `'none'`: Disable reasoning
-    */
--  reasoningEffort: z.enum(["high", "none"]).optional()
-+  reasoningEffort: z.enum(["high", "none"]).optional(),
-+  promptCacheKey: z.string().optional()
- });
- 
- // src/mistral-error.ts
-@@ -403,6 +404,7 @@ var MistralChatLanguageModel = class {
-       top_p: topP,
-       random_seed: seed,
-       reasoning_effort: options.reasoningEffort,
-+      prompt_cache_key: options.promptCacheKey,
-       // response format:
-       response_format: (responseFormat == null ? void 0 : responseFormat.type) === "json" ? structuredOutputs && (responseFormat == null ? void 0 : responseFormat.schema) != null ? {
-         type: "json_schema",
-diff --git a/src/mistral-chat-language-model.ts b/src/mistral-chat-language-model.ts
-index 480c472d534bedbe8897979673453bd1c29a70b7..e46496da94f7d4af9822897202ca6baae67dae3a 100644
---- a/src/mistral-chat-language-model.ts
-+++ b/src/mistral-chat-language-model.ts
-@@ -129,6 +129,7 @@ export class MistralChatLanguageModel implements LanguageModelV3 {
-       top_p: topP,
-       random_seed: seed,
-       reasoning_effort: options.reasoningEffort,
-+      prompt_cache_key: options.promptCacheKey,
- 
-       // response format:
-       response_format:
-diff --git a/src/mistral-chat-options.ts b/src/mistral-chat-options.ts
-index 80fff45fba2c378fa06962f071946bcd2b882a0b..b4fdfa51f3bf11a4220e010c8aca92482cb0c3db 100644
---- a/src/mistral-chat-options.ts
-+++ b/src/mistral-chat-options.ts
-@@ -62,6 +62,11 @@ export const mistralLanguageModelOptions = z.object({
-    * - `'none'`: Disable reasoning
-    */
-   reasoningEffort: z.enum(['high', 'none']).optional(),
-+
-+  /**
-+   * A stable identifier used to route requests with shared prompt prefixes.
-+   */
-+  promptCacheKey: z.string().optional(),
- });
- 
- export type MistralLanguageModelOptions = z.infer<

+ 709 - 0
patches/@ai-sdk%2Fmistral@3.0.51.patch

@@ -0,0 +1,709 @@
+diff --git a/dist/index.d.mts b/dist/index.d.mts
+index 1bde0b9f8cbe6771a52c1041095c9dddfe8e5b6c..0ca2ffb2a0c9327aed5ddcf0004500dc8b42569f 100644
+--- a/dist/index.d.mts
++++ b/dist/index.d.mts
+@@ -14,6 +14,7 @@ declare const mistralLanguageModelOptions: z.ZodObject<{
+         none: "none";
+         high: "high";
+     }>>;
++    promptCacheKey: z.ZodOptional<z.ZodString>;
+ }, z.core.$strip>;
+ type MistralLanguageModelOptions = z.infer<typeof mistralLanguageModelOptions>;
+ 
+diff --git a/dist/index.d.ts b/dist/index.d.ts
+index 1bde0b9f8cbe6771a52c1041095c9dddfe8e5b6c..0ca2ffb2a0c9327aed5ddcf0004500dc8b42569f 100644
+--- a/dist/index.d.ts
++++ b/dist/index.d.ts
+@@ -14,6 +14,7 @@ declare const mistralLanguageModelOptions: z.ZodObject<{
+         none: "none";
+         high: "high";
+     }>>;
++    promptCacheKey: z.ZodOptional<z.ZodString>;
+ }, z.core.$strip>;
+ type MistralLanguageModelOptions = z.infer<typeof mistralLanguageModelOptions>;
+ 
+diff --git a/dist/index.js b/dist/index.js
+index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..267f34e20ea392b7a85ad5259d72d50605a6f971 100644
+--- a/dist/index.js
++++ b/dist/index.js
+@@ -128,11 +128,14 @@ function convertToMistralChatMessages(prompt) {
+       }
+       case "assistant": {
+         let text = "";
++        const structuredContent = [];
++        let hasNativeReasoning = false;
+         const toolCalls = [];
+         for (const part of content) {
+           switch (part.type) {
+             case "text": {
+               text += part.text;
++              structuredContent.push({ type: "text", text: part.text });
+               break;
+             }
+             case "tool-call": {
+@@ -148,6 +151,13 @@ function convertToMistralChatMessages(prompt) {
+             }
+             case "reasoning": {
+               text += part.text;
++              const native = part.providerOptions?.mistral?.thinking;
++              if (native?.type === "thinking") {
++                hasNativeReasoning = true;
++                structuredContent.push(native);
++                break;
++              }
++              structuredContent.push({ type: "text", text: part.text });
+               break;
+             }
+             default: {
+@@ -159,7 +169,7 @@ function convertToMistralChatMessages(prompt) {
+         }
+         messages.push({
+           role: "assistant",
+-          content: text,
++          content: hasNativeReasoning ? structuredContent : text,
+           prefix: isLastMessage ? true : void 0,
+           tool_calls: toolCalls.length > 0 ? toolCalls : void 0
+         });
+@@ -268,7 +278,8 @@ var mistralLanguageModelOptions = import_v4.z.object({
+    * - `'high'`: Enable reasoning
+    * - `'none'`: Disable reasoning
+    */
+-  reasoningEffort: import_v4.z.enum(["high", "none"]).optional()
++  reasoningEffort: import_v4.z.enum(["high", "none"]).optional(),
++  promptCacheKey: import_v4.z.string().optional()
+ });
+ 
+ // src/mistral-error.ts
+@@ -407,6 +418,7 @@ var MistralChatLanguageModel = class {
+       stop: stopSequences,
+       random_seed: seed,
+       reasoning_effort: options.reasoningEffort,
++      prompt_cache_key: options.promptCacheKey,
+       // response format:
+       response_format: (responseFormat == null ? void 0 : responseFormat.type) === "json" ? structuredOutputs && (responseFormat == null ? void 0 : responseFormat.schema) != null ? {
+         type: "json_schema",
+@@ -465,9 +477,11 @@ var MistralChatLanguageModel = class {
+       for (const part of choice.message.content) {
+         if (part.type === "thinking") {
+           const reasoningText = extractReasoningContent(part.thinking);
+-          if (reasoningText.length > 0) {
+-            content.push({ type: "reasoning", text: reasoningText });
+-          }
++          content.push({
++            type: "reasoning",
++            text: reasoningText,
++            providerMetadata: { mistral: { thinking: part } }
++          });
+         } else if (part.type === "text") {
+           if (part.text.length > 0) {
+             content.push({ type: "text", text: part.text });
+@@ -528,6 +542,7 @@ var MistralChatLanguageModel = class {
+     let isFirstChunk = true;
+     let activeText = false;
+     let activeReasoningId = null;
++    let activeThinking = null;
+     const generateId2 = this.generateId;
+     return {
+       stream: response.pipeThrough(
+@@ -561,18 +576,19 @@ var MistralChatLanguageModel = class {
+               for (const part of delta.content) {
+                 if (part.type === "thinking") {
+                   const reasoningDelta = extractReasoningContent(part.thinking);
+-                  if (reasoningDelta.length > 0) {
+-                    if (activeReasoningId == null) {
+-                      if (activeText) {
+-                        controller.enqueue({ type: "text-end", id: "0" });
+-                        activeText = false;
+-                      }
+-                      activeReasoningId = generateId2();
+-                      controller.enqueue({
+-                        type: "reasoning-start",
+-                        id: activeReasoningId
+-                      });
++                  activeThinking = mergeThinking(activeThinking, part);
++                  if (activeReasoningId == null) {
++                    if (activeText) {
++                      controller.enqueue({ type: "text-end", id: "0" });
++                      activeText = false;
+                     }
++                    activeReasoningId = generateId2();
++                    controller.enqueue({
++                      type: "reasoning-start",
++                      id: activeReasoningId
++                    });
++                  }
++                  if (reasoningDelta.length > 0) {
+                     controller.enqueue({
+                       type: "reasoning-delta",
+                       id: activeReasoningId,
+@@ -587,9 +603,11 @@ var MistralChatLanguageModel = class {
+                 if (activeReasoningId != null) {
+                   controller.enqueue({
+                     type: "reasoning-end",
+-                    id: activeReasoningId
++                    id: activeReasoningId,
++                    providerMetadata: { mistral: { thinking: activeThinking } }
+                   });
+                   activeReasoningId = null;
++                  activeThinking = null;
+                 }
+                 controller.enqueue({ type: "text-start", id: "0" });
+                 activeText = true;
+@@ -638,7 +656,8 @@ var MistralChatLanguageModel = class {
+             if (activeReasoningId != null) {
+               controller.enqueue({
+                 type: "reasoning-end",
+-                id: activeReasoningId
++                id: activeReasoningId,
++                providerMetadata: { mistral: { thinking: activeThinking } }
+               });
+             }
+             if (activeText) {
+@@ -660,6 +679,13 @@ var MistralChatLanguageModel = class {
+ function extractReasoningContent(thinking) {
+   return thinking.filter((chunk) => chunk.type === "text").map((chunk) => chunk.text).join("");
+ }
++function mergeThinking(current, next) {
++  if (current === null) return { ...next, thinking: [...next.thinking] };
++  current.thinking.push(...next.thinking);
++  if (next.closed !== void 0) current.closed = next.closed;
++  if (next.signature !== void 0) current.signature = next.signature;
++  return current;
++}
+ function extractTextContent(content) {
+   if (typeof content === "string") {
+     return content;
+@@ -686,6 +712,30 @@ function extractTextContent(content) {
+   }
+   return textContent.length ? textContent.join("") : void 0;
+ }
++var mistralThinkingContentSchema = import_v43.z.discriminatedUnion("type", [
++  import_v43.z.object({
++    type: import_v43.z.literal("text"),
++    text: import_v43.z.string()
++  }),
++  import_v43.z.object({
++    type: import_v43.z.literal("tool_reference"),
++    tool: import_v43.z.string(),
++    title: import_v43.z.string(),
++    url: import_v43.z.string().nullish(),
++    favicon: import_v43.z.string().nullish(),
++    description: import_v43.z.string().nullish()
++  }),
++  import_v43.z.object({
++    type: import_v43.z.literal("reference"),
++    reference_ids: import_v43.z.array(import_v43.z.union([import_v43.z.string(), import_v43.z.number().int()]))
++  })
++]);
++var mistralThinkChunkSchema = import_v43.z.object({
++  type: import_v43.z.literal("thinking"),
++  thinking: import_v43.z.array(mistralThinkingContentSchema),
++  closed: import_v43.z.boolean().optional(),
++  signature: import_v43.z.string().nullish()
++});
+ var mistralContentSchema = import_v43.z.union([
+   import_v43.z.string(),
+   import_v43.z.array(
+@@ -708,15 +758,7 @@ var mistralContentSchema = import_v43.z.union([
+         type: import_v43.z.literal("reference"),
+         reference_ids: import_v43.z.array(import_v43.z.union([import_v43.z.string(), import_v43.z.number()]))
+       }),
+-      import_v43.z.object({
+-        type: import_v43.z.literal("thinking"),
+-        thinking: import_v43.z.array(
+-          import_v43.z.object({
+-            type: import_v43.z.literal("text"),
+-            text: import_v43.z.string()
+-          })
+-        )
+-      })
++      mistralThinkChunkSchema
+     ])
+   )
+ ]).nullish();
+diff --git a/dist/index.mjs b/dist/index.mjs
+index d2eff622c1b84a96bdeb4012cb0206a33012a04d..3bff11ddd6136ada45809568828cbc8f2493a42a 100644
+--- a/dist/index.mjs
++++ b/dist/index.mjs
+@@ -116,11 +116,14 @@ function convertToMistralChatMessages(prompt) {
+       }
+       case "assistant": {
+         let text = "";
++        const structuredContent = [];
++        let hasNativeReasoning = false;
+         const toolCalls = [];
+         for (const part of content) {
+           switch (part.type) {
+             case "text": {
+               text += part.text;
++              structuredContent.push({ type: "text", text: part.text });
+               break;
+             }
+             case "tool-call": {
+@@ -136,6 +139,13 @@ function convertToMistralChatMessages(prompt) {
+             }
+             case "reasoning": {
+               text += part.text;
++              const native = part.providerOptions?.mistral?.thinking;
++              if (native?.type === "thinking") {
++                hasNativeReasoning = true;
++                structuredContent.push(native);
++                break;
++              }
++              structuredContent.push({ type: "text", text: part.text });
+               break;
+             }
+             default: {
+@@ -147,7 +157,7 @@ function convertToMistralChatMessages(prompt) {
+         }
+         messages.push({
+           role: "assistant",
+-          content: text,
++          content: hasNativeReasoning ? structuredContent : text,
+           prefix: isLastMessage ? true : void 0,
+           tool_calls: toolCalls.length > 0 ? toolCalls : void 0
+         });
+@@ -256,7 +266,8 @@ var mistralLanguageModelOptions = z.object({
+    * - `'high'`: Enable reasoning
+    * - `'none'`: Disable reasoning
+    */
+-  reasoningEffort: z.enum(["high", "none"]).optional()
++  reasoningEffort: z.enum(["high", "none"]).optional(),
++  promptCacheKey: z.string().optional()
+ });
+ 
+ // src/mistral-error.ts
+@@ -397,6 +408,7 @@ var MistralChatLanguageModel = class {
+       stop: stopSequences,
+       random_seed: seed,
+       reasoning_effort: options.reasoningEffort,
++      prompt_cache_key: options.promptCacheKey,
+       // response format:
+       response_format: (responseFormat == null ? void 0 : responseFormat.type) === "json" ? structuredOutputs && (responseFormat == null ? void 0 : responseFormat.schema) != null ? {
+         type: "json_schema",
+@@ -455,9 +467,11 @@ var MistralChatLanguageModel = class {
+       for (const part of choice.message.content) {
+         if (part.type === "thinking") {
+           const reasoningText = extractReasoningContent(part.thinking);
+-          if (reasoningText.length > 0) {
+-            content.push({ type: "reasoning", text: reasoningText });
+-          }
++          content.push({
++            type: "reasoning",
++            text: reasoningText,
++            providerMetadata: { mistral: { thinking: part } }
++          });
+         } else if (part.type === "text") {
+           if (part.text.length > 0) {
+             content.push({ type: "text", text: part.text });
+@@ -518,6 +532,7 @@ var MistralChatLanguageModel = class {
+     let isFirstChunk = true;
+     let activeText = false;
+     let activeReasoningId = null;
++    let activeThinking = null;
+     const generateId2 = this.generateId;
+     return {
+       stream: response.pipeThrough(
+@@ -551,18 +566,19 @@ var MistralChatLanguageModel = class {
+               for (const part of delta.content) {
+                 if (part.type === "thinking") {
+                   const reasoningDelta = extractReasoningContent(part.thinking);
+-                  if (reasoningDelta.length > 0) {
+-                    if (activeReasoningId == null) {
+-                      if (activeText) {
+-                        controller.enqueue({ type: "text-end", id: "0" });
+-                        activeText = false;
+-                      }
+-                      activeReasoningId = generateId2();
+-                      controller.enqueue({
+-                        type: "reasoning-start",
+-                        id: activeReasoningId
+-                      });
++                  activeThinking = mergeThinking(activeThinking, part);
++                  if (activeReasoningId == null) {
++                    if (activeText) {
++                      controller.enqueue({ type: "text-end", id: "0" });
++                      activeText = false;
+                     }
++                    activeReasoningId = generateId2();
++                    controller.enqueue({
++                      type: "reasoning-start",
++                      id: activeReasoningId
++                    });
++                  }
++                  if (reasoningDelta.length > 0) {
+                     controller.enqueue({
+                       type: "reasoning-delta",
+                       id: activeReasoningId,
+@@ -577,9 +593,11 @@ var MistralChatLanguageModel = class {
+                 if (activeReasoningId != null) {
+                   controller.enqueue({
+                     type: "reasoning-end",
+-                    id: activeReasoningId
++                    id: activeReasoningId,
++                    providerMetadata: { mistral: { thinking: activeThinking } }
+                   });
+                   activeReasoningId = null;
++                  activeThinking = null;
+                 }
+                 controller.enqueue({ type: "text-start", id: "0" });
+                 activeText = true;
+@@ -628,7 +646,8 @@ var MistralChatLanguageModel = class {
+             if (activeReasoningId != null) {
+               controller.enqueue({
+                 type: "reasoning-end",
+-                id: activeReasoningId
++                id: activeReasoningId,
++                providerMetadata: { mistral: { thinking: activeThinking } }
+               });
+             }
+             if (activeText) {
+@@ -650,6 +669,13 @@ var MistralChatLanguageModel = class {
+ function extractReasoningContent(thinking) {
+   return thinking.filter((chunk) => chunk.type === "text").map((chunk) => chunk.text).join("");
+ }
++function mergeThinking(current, next) {
++  if (current === null) return { ...next, thinking: [...next.thinking] };
++  current.thinking.push(...next.thinking);
++  if (next.closed !== void 0) current.closed = next.closed;
++  if (next.signature !== void 0) current.signature = next.signature;
++  return current;
++}
+ function extractTextContent(content) {
+   if (typeof content === "string") {
+     return content;
+@@ -676,6 +702,30 @@ function extractTextContent(content) {
+   }
+   return textContent.length ? textContent.join("") : void 0;
+ }
++var mistralThinkingContentSchema = z3.discriminatedUnion("type", [
++  z3.object({
++    type: z3.literal("text"),
++    text: z3.string()
++  }),
++  z3.object({
++    type: z3.literal("tool_reference"),
++    tool: z3.string(),
++    title: z3.string(),
++    url: z3.string().nullish(),
++    favicon: z3.string().nullish(),
++    description: z3.string().nullish()
++  }),
++  z3.object({
++    type: z3.literal("reference"),
++    reference_ids: z3.array(z3.union([z3.string(), z3.number().int()]))
++  })
++]);
++var mistralThinkChunkSchema = z3.object({
++  type: z3.literal("thinking"),
++  thinking: z3.array(mistralThinkingContentSchema),
++  closed: z3.boolean().optional(),
++  signature: z3.string().nullish()
++});
+ var mistralContentSchema = z3.union([
+   z3.string(),
+   z3.array(
+@@ -698,15 +748,7 @@ var mistralContentSchema = z3.union([
+         type: z3.literal("reference"),
+         reference_ids: z3.array(z3.union([z3.string(), z3.number()]))
+       }),
+-      z3.object({
+-        type: z3.literal("thinking"),
+-        thinking: z3.array(
+-          z3.object({
+-            type: z3.literal("text"),
+-            text: z3.string()
+-          })
+-        )
+-      })
++      mistralThinkChunkSchema
+     ])
+   )
+ ]).nullish();
+diff --git a/src/convert-to-mistral-chat-messages.ts b/src/convert-to-mistral-chat-messages.ts
+index 3c6914f8da615d7517bc43dd56198298d0a50247..8cd6f4c7577f746ef41e8a0aee682234c473667a 100644
+--- a/src/convert-to-mistral-chat-messages.ts
++++ b/src/convert-to-mistral-chat-messages.ts
+@@ -3,7 +3,11 @@ import {
+   type LanguageModelV3DataContent,
+   type LanguageModelV3Prompt,
+ } from '@ai-sdk/provider';
+-import type { MistralPrompt } from './mistral-chat-prompt';
++import type {
++  MistralAssistantMessageContent,
++  MistralPrompt,
++  MistralThinkChunk,
++} from './mistral-chat-prompt';
+ import { convertToBase64 } from '@ai-sdk/provider-utils';
+ 
+ function formatFileUrl({
+@@ -76,6 +80,8 @@ export function convertToMistralChatMessages(
+ 
+       case 'assistant': {
+         let text = '';
++        const structuredContent: Array<MistralAssistantMessageContent> = [];
++        let hasNativeReasoning = false;
+         const toolCalls: Array<{
+           id: string;
+           type: 'function';
+@@ -86,6 +92,7 @@ export function convertToMistralChatMessages(
+           switch (part.type) {
+             case 'text': {
+               text += part.text;
++              structuredContent.push({ type: 'text', text: part.text });
+               break;
+             }
+             case 'tool-call': {
+@@ -101,6 +108,14 @@ export function convertToMistralChatMessages(
+             }
+             case 'reasoning': {
+               text += part.text;
++              const native = part.providerOptions?.mistral
++                ?.thinking as MistralThinkChunk | undefined;
++              if (native?.type === 'thinking') {
++                hasNativeReasoning = true;
++                structuredContent.push(native);
++                break;
++              }
++              structuredContent.push({ type: 'text', text: part.text });
+               break;
+             }
+             default: {
+@@ -113,7 +128,7 @@ export function convertToMistralChatMessages(
+ 
+         messages.push({
+           role: 'assistant',
+-          content: text,
++          content: hasNativeReasoning ? structuredContent : text,
+           prefix: isLastMessage ? true : undefined,
+           tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
+         });
+diff --git a/src/mistral-chat-language-model.ts b/src/mistral-chat-language-model.ts
+index 7e4a7ab552f1b41b7074e1b3cada8a51d791268d..847d26f9dfe03572a969a122f8c96b8bbfda8066 100644
+--- a/src/mistral-chat-language-model.ts
++++ b/src/mistral-chat-language-model.ts
+@@ -122,6 +122,7 @@ export class MistralChatLanguageModel implements LanguageModelV3 {
+       stop: stopSequences,
+       random_seed: seed,
+       reasoning_effort: options.reasoningEffort,
++      prompt_cache_key: options.promptCacheKey,
+ 
+       // response format:
+       response_format:
+@@ -201,9 +202,11 @@ export class MistralChatLanguageModel implements LanguageModelV3 {
+       for (const part of choice.message.content) {
+         if (part.type === 'thinking') {
+           const reasoningText = extractReasoningContent(part.thinking);
+-          if (reasoningText.length > 0) {
+-            content.push({ type: 'reasoning', text: reasoningText });
+-          }
++          content.push({
++            type: 'reasoning',
++            text: reasoningText,
++            providerMetadata: { mistral: { thinking: part } },
++          });
+         } else if (part.type === 'text') {
+           if (part.text.length > 0) {
+             content.push({ type: 'text', text: part.text });
+@@ -278,6 +281,7 @@ export class MistralChatLanguageModel implements LanguageModelV3 {
+     let isFirstChunk = true;
+     let activeText = false;
+     let activeReasoningId: string | null = null;
++    let activeThinking: z.infer<typeof mistralThinkChunkSchema> | null = null;
+ 
+     const generateId = this.generateId;
+ 
+@@ -326,20 +330,21 @@ export class MistralChatLanguageModel implements LanguageModelV3 {
+               for (const part of delta.content) {
+                 if (part.type === 'thinking') {
+                   const reasoningDelta = extractReasoningContent(part.thinking);
+-                  if (reasoningDelta.length > 0) {
+-                    if (activeReasoningId == null) {
+-                      // end any active text before starting reasoning
+-                      if (activeText) {
+-                        controller.enqueue({ type: 'text-end', id: '0' });
+-                        activeText = false;
+-                      }
+-
+-                      activeReasoningId = generateId();
+-                      controller.enqueue({
+-                        type: 'reasoning-start',
+-                        id: activeReasoningId,
+-                      });
++                  activeThinking = mergeThinking(activeThinking, part);
++                  if (activeReasoningId == null) {
++                    // end any active text before starting reasoning
++                    if (activeText) {
++                      controller.enqueue({ type: 'text-end', id: '0' });
++                      activeText = false;
+                     }
++
++                    activeReasoningId = generateId();
++                    controller.enqueue({
++                      type: 'reasoning-start',
++                      id: activeReasoningId,
++                    });
++                  }
++                  if (reasoningDelta.length > 0) {
+                     controller.enqueue({
+                       type: 'reasoning-delta',
+                       id: activeReasoningId,
+@@ -357,8 +362,12 @@ export class MistralChatLanguageModel implements LanguageModelV3 {
+                   controller.enqueue({
+                     type: 'reasoning-end',
+                     id: activeReasoningId,
++                    providerMetadata: {
++                      mistral: { thinking: activeThinking },
++                    },
+                   });
+                   activeReasoningId = null;
++                  activeThinking = null;
+                 }
+                 controller.enqueue({ type: 'text-start', id: '0' });
+                 activeText = true;
+@@ -416,6 +425,9 @@ export class MistralChatLanguageModel implements LanguageModelV3 {
+               controller.enqueue({
+                 type: 'reasoning-end',
+                 id: activeReasoningId,
++                providerMetadata: {
++                  mistral: { thinking: activeThinking },
++                },
+               });
+             }
+             if (activeText) {
+@@ -437,7 +449,7 @@ export class MistralChatLanguageModel implements LanguageModelV3 {
+ }
+ 
+ function extractReasoningContent(
+-  thinking: Array<{ type: string; text: string }>,
++  thinking: Array<z.infer<typeof mistralThinkingContentSchema>>,
+ ) {
+   return thinking
+     .filter(chunk => chunk.type === 'text')
+@@ -445,6 +457,17 @@ function extractReasoningContent(
+     .join('');
+ }
+ 
++function mergeThinking(
++  current: z.infer<typeof mistralThinkChunkSchema> | null,
++  next: z.infer<typeof mistralThinkChunkSchema>,
++) {
++  if (current === null) return { ...next, thinking: [...next.thinking] };
++  current.thinking.push(...next.thinking);
++  if (next.closed !== undefined) current.closed = next.closed;
++  if (next.signature !== undefined) current.signature = next.signature;
++  return current;
++}
++
+ function extractTextContent(content: z.infer<typeof mistralContentSchema>) {
+   if (typeof content === 'string') {
+     return content;
+@@ -478,6 +501,32 @@ function extractTextContent(content: z.infer<typeof mistralContentSchema>) {
+   return textContent.length ? textContent.join('') : undefined;
+ }
+ 
++const mistralThinkingContentSchema = z.discriminatedUnion('type', [
++  z.object({
++    type: z.literal('text'),
++    text: z.string(),
++  }),
++  z.object({
++    type: z.literal('tool_reference'),
++    tool: z.string(),
++    title: z.string(),
++    url: z.string().nullish(),
++    favicon: z.string().nullish(),
++    description: z.string().nullish(),
++  }),
++  z.object({
++    type: z.literal('reference'),
++    reference_ids: z.array(z.union([z.string(), z.number().int()])),
++  }),
++]);
++
++const mistralThinkChunkSchema = z.object({
++  type: z.literal('thinking'),
++  thinking: z.array(mistralThinkingContentSchema),
++  closed: z.boolean().optional(),
++  signature: z.string().nullish(),
++});
++
+ const mistralContentSchema = z
+   .union([
+     z.string(),
+@@ -501,15 +550,7 @@ const mistralContentSchema = z
+           type: z.literal('reference'),
+           reference_ids: z.array(z.union([z.string(), z.number()])),
+         }),
+-        z.object({
+-          type: z.literal('thinking'),
+-          thinking: z.array(
+-            z.object({
+-              type: z.literal('text'),
+-              text: z.string(),
+-            }),
+-          ),
+-        }),
++        mistralThinkChunkSchema,
+       ]),
+     ),
+   ])
+diff --git a/src/mistral-chat-options.ts b/src/mistral-chat-options.ts
+index 54b29c08517d348995b6ca093b11160e453d5c8b..de30c3e7d924889339e38b1067cb26e9ada05d11 100644
+--- a/src/mistral-chat-options.ts
++++ b/src/mistral-chat-options.ts
+@@ -64,6 +64,11 @@ export const mistralLanguageModelOptions = z.object({
+    * - `'none'`: Disable reasoning
+    */
+   reasoningEffort: z.enum(['high', 'none']).optional(),
++
++  /**
++   * A stable identifier used to route requests with shared prompt prefixes.
++   */
++  promptCacheKey: z.string().optional(),
+ });
+ 
+ export type MistralLanguageModelOptions = z.infer<
+diff --git a/src/mistral-chat-prompt.ts b/src/mistral-chat-prompt.ts
+index 13f1dced55ac4be084128127a57fbdd58115bc28..172b11dde3dd326c2f3befd99237474ed8c79285 100644
+--- a/src/mistral-chat-prompt.ts
++++ b/src/mistral-chat-prompt.ts
+@@ -23,7 +23,7 @@ export type MistralUserMessageContent =
+ 
+ export interface MistralAssistantMessage {
+   role: 'assistant';
+-  content: string;
++  content: string | Array<MistralAssistantMessageContent>;
+   prefix?: boolean;
+   tool_calls?: Array<{
+     id: string;
+@@ -32,6 +32,29 @@ export interface MistralAssistantMessage {
+   }>;
+ }
+ 
++export type MistralAssistantMessageContent =
++  | { type: 'text'; text: string }
++  | MistralThinkChunk;
++
++export type MistralThinkChunk = {
++  type: 'thinking';
++  thinking: Array<MistralThinkingContent>;
++  closed?: boolean;
++  signature?: string | null;
++};
++
++export type MistralThinkingContent =
++  | { type: 'text'; text: string }
++  | {
++      type: 'tool_reference';
++      tool: string;
++      title: string;
++      url?: string | null;
++      favicon?: string | null;
++      description?: string | null;
++    }
++  | { type: 'reference'; reference_ids: Array<string | number> };
++
+ export interface MistralToolMessage {
+   role: 'tool';
+   name: string;