From 20324df9fb76bb63954b113a457936092ac12742 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 22 Jul 2026 23:43:25 -0500 Subject: [PATCH 1/5] fix(provider): preserve Mistral reasoning history --- packages/core/test/provider-mistral.test.ts | 44 ++++++ patches/@ai-sdk%2Fmistral@3.0.34.patch | 162 +++++++++++++++++++- 2 files changed, 200 insertions(+), 6 deletions(-) diff --git a/packages/core/test/provider-mistral.test.ts b/packages/core/test/provider-mistral.test.ts index 58904ad3b121..e66149609140 100644 --- a/packages/core/test/provider-mistral.test.ts +++ b/packages/core/test/provider-mistral.test.ts @@ -26,3 +26,47 @@ test("Mistral sends promptCacheKey as prompt_cache_key", async () => { expect(body?.prompt_cache_key).toBe("session-123") }) + +test("Mistral preserves structured reasoning in assistant history", async () => { + let body: { messages?: unknown[] } | undefined + const mockFetch = Object.assign( + async (_input: Parameters[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: "Hello" }, 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") + + await model.doGenerate({ + prompt: [ + { role: "user", content: [{ type: "text", text: "Hello" }] }, + { + role: "assistant", + content: [ + { type: "reasoning", text: "The user is greeting me." }, + { type: "text", text: "Hi" }, + ], + }, + { 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: "text", text: "Hi" }, + ], + }) +}) diff --git a/patches/@ai-sdk%2Fmistral@3.0.34.patch b/patches/@ai-sdk%2Fmistral@3.0.34.patch index 1d771f4fd9f7..45bc3332744c 100644 --- a/patches/@ai-sdk%2Fmistral@3.0.34.patch +++ b/patches/@ai-sdk%2Fmistral@3.0.34.patch @@ -11,10 +11,46 @@ index 1ca9113bed2728a616db773a8e08d8d6957447d7..15408ec429dc210b5fa43589d81b69c9 type MistralLanguageModelOptions = z.infer; diff --git a/dist/index.js b/dist/index.js -index 45735e524aaff54ea058c99c729c5ffd3c507058..6aca5f6f13da0054ede31c1f1a692e4eaed37d34 100644 +index 45735e524aaff54ea058c99c729c5ffd3c507058..779bd50f2b18e0dbe61bf9aaad88e9c87ed3c419 100644 --- a/dist/index.js +++ b/dist/index.js -@@ -268,7 +268,8 @@ var mistralLanguageModelOptions = import_v4.z.object({ +@@ -128,11 +128,14 @@ function convertToMistralChatMessages(prompt) { + } + case "assistant": { + let text = ""; ++ const structuredContent = []; ++ let hasReasoning = 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,11 @@ function convertToMistralChatMessages(prompt) { + } + case "reasoning": { + text += part.text; ++ hasReasoning = true; ++ structuredContent.push({ ++ type: "thinking", ++ thinking: [{ type: "text", text: part.text }] ++ }); + break; + } + default: { +@@ -159,7 +167,7 @@ function convertToMistralChatMessages(prompt) { + } + messages.push({ + role: "assistant", +- content: text, ++ content: hasReasoning ? structuredContent : text, + prefix: isLastMessage ? true : void 0, + tool_calls: toolCalls.length > 0 ? toolCalls : void 0 + }); +@@ -268,7 +276,8 @@ var mistralLanguageModelOptions = import_v4.z.object({ * - `'high'`: Enable reasoning * - `'none'`: Disable reasoning */ @@ -24,7 +60,7 @@ index 45735e524aaff54ea058c99c729c5ffd3c507058..6aca5f6f13da0054ede31c1f1a692e4e }); // src/mistral-error.ts -@@ -413,6 +414,7 @@ var MistralChatLanguageModel = class { +@@ -413,6 +422,7 @@ var MistralChatLanguageModel = class { top_p: topP, random_seed: seed, reasoning_effort: options.reasoningEffort, @@ -33,10 +69,46 @@ index 45735e524aaff54ea058c99c729c5ffd3c507058..6aca5f6f13da0054ede31c1f1a692e4e 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 +index 4c22df1cd78a1ba81309c8a86ceecefef4ba4aea..929a5fe79c9fc12b437f82532157e775d14a999f 100644 --- a/dist/index.mjs +++ b/dist/index.mjs -@@ -256,7 +256,8 @@ var mistralLanguageModelOptions = z.object({ +@@ -116,11 +116,14 @@ function convertToMistralChatMessages(prompt) { + } + case "assistant": { + let text = ""; ++ const structuredContent = []; ++ let hasReasoning = 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,11 @@ function convertToMistralChatMessages(prompt) { + } + case "reasoning": { + text += part.text; ++ hasReasoning = true; ++ structuredContent.push({ ++ type: "thinking", ++ thinking: [{ type: "text", text: part.text }] ++ }); + break; + } + default: { +@@ -147,7 +155,7 @@ function convertToMistralChatMessages(prompt) { + } + messages.push({ + role: "assistant", +- content: text, ++ content: hasReasoning ? structuredContent : text, + prefix: isLastMessage ? true : void 0, + tool_calls: toolCalls.length > 0 ? toolCalls : void 0 + }); +@@ -256,7 +264,8 @@ var mistralLanguageModelOptions = z.object({ * - `'high'`: Enable reasoning * - `'none'`: Disable reasoning */ @@ -46,7 +118,7 @@ index 4c22df1cd78a1ba81309c8a86ceecefef4ba4aea..30cd3b1f503860109b7fa2107cd1eb17 }); // src/mistral-error.ts -@@ -403,6 +404,7 @@ var MistralChatLanguageModel = class { +@@ -403,6 +412,7 @@ var MistralChatLanguageModel = class { top_p: topP, random_seed: seed, reasoning_effort: options.reasoningEffort, @@ -54,6 +126,60 @@ index 4c22df1cd78a1ba81309c8a86ceecefef4ba4aea..30cd3b1f503860109b7fa2107cd1eb17 // 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/convert-to-mistral-chat-messages.ts b/src/convert-to-mistral-chat-messages.ts +index 1c74a7ee8959664635e729d1776d3ee516027ef9..80b52650d292522bd3da4979271cdeb58552fcf7 100644 +--- a/src/convert-to-mistral-chat-messages.ts ++++ b/src/convert-to-mistral-chat-messages.ts +@@ -3,7 +3,10 @@ import { + type LanguageModelV3DataContent, + type LanguageModelV3Prompt, + } from '@ai-sdk/provider'; +-import type { MistralPrompt } from './mistral-chat-prompt'; ++import type { ++ MistralAssistantMessageContent, ++ MistralPrompt, ++} from './mistral-chat-prompt'; + import { convertToBase64 } from '@ai-sdk/provider-utils'; + + function formatFileUrl({ +@@ -76,6 +79,8 @@ export function convertToMistralChatMessages( + + case 'assistant': { + let text = ''; ++ const structuredContent: Array = []; ++ let hasReasoning = false; + const toolCalls: Array<{ + id: string; + type: 'function'; +@@ -86,6 +91,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 +107,11 @@ export function convertToMistralChatMessages( + } + case 'reasoning': { + text += part.text; ++ hasReasoning = true; ++ structuredContent.push({ ++ type: 'thinking', ++ thinking: [{ type: 'text', text: part.text }], ++ }); + break; + } + default: { +@@ -113,7 +124,7 @@ export function convertToMistralChatMessages( + + messages.push({ + role: 'assistant', +- content: text, ++ content: hasReasoning ? 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 480c472d534bedbe8897979673453bd1c29a70b7..e46496da94f7d4af9822897202ca6baae67dae3a 100644 --- a/src/mistral-chat-language-model.ts @@ -82,3 +208,27 @@ index 80fff45fba2c378fa06962f071946bcd2b882a0b..b4fdfa51f3bf11a4220e010c8aca9248 }); export type MistralLanguageModelOptions = z.infer< +diff --git a/src/mistral-chat-prompt.ts b/src/mistral-chat-prompt.ts +index 13f1dced55ac4be084128127a57fbdd58115bc28..60969ded9e7abc48852da022bc6a6fb9d74901f5 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; + prefix?: boolean; + tool_calls?: Array<{ + id: string; +@@ -32,6 +32,10 @@ export interface MistralAssistantMessage { + }>; + } + ++export type MistralAssistantMessageContent = ++ | { type: 'text'; text: string } ++ | { type: 'thinking'; thinking: Array<{ type: 'text'; text: string }> }; ++ + export interface MistralToolMessage { + role: 'tool'; + name: string; From c44d0723b7af0af88530f77c035fc145f88e24cd Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 22 Jul 2026 23:50:11 -0500 Subject: [PATCH 2/5] chore(provider): update Mistral AI SDK --- bun.lock | 14 +++---- package.json | 2 +- packages/core/package.json | 2 +- packages/opencode/package.json | 2 +- ...4.patch => @ai-sdk%2Fmistral@3.0.51.patch} | 38 ++++++++++++------- 5 files changed, 35 insertions(+), 23 deletions(-) rename patches/{@ai-sdk%2Fmistral@3.0.34.patch => @ai-sdk%2Fmistral@3.0.51.patch} (85%) diff --git a/bun.lock b/bun.lock index 5114ec200db5..e8cf5f0ed676 100644 --- a/bun.lock +++ b/bun.lock @@ -302,7 +302,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", @@ -577,7 +577,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", @@ -1074,12 +1074,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:", @@ -1200,7 +1200,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=="], @@ -5700,9 +5700,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=="], diff --git a/package.json b/package.json index 372335d724e3..5fd0f1d51ad8 100644 --- a/package.json +++ b/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", diff --git a/packages/core/package.json b/packages/core/package.json index e0445e616f5c..761bee109a9d 100644 --- a/packages/core/package.json +++ b/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", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 6781bf488e51..0876f4badb1c 100644 --- a/packages/opencode/package.json +++ b/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", diff --git a/patches/@ai-sdk%2Fmistral@3.0.34.patch b/patches/@ai-sdk%2Fmistral@3.0.51.patch similarity index 85% rename from patches/@ai-sdk%2Fmistral@3.0.34.patch rename to patches/@ai-sdk%2Fmistral@3.0.51.patch index 45bc3332744c..aa8b58ea0607 100644 --- a/patches/@ai-sdk%2Fmistral@3.0.34.patch +++ b/patches/@ai-sdk%2Fmistral@3.0.51.patch @@ -1,5 +1,17 @@ +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.core.$strip>; + type MistralLanguageModelOptions = z.infer; + diff --git a/dist/index.d.ts b/dist/index.d.ts -index 1ca9113bed2728a616db773a8e08d8d6957447d7..15408ec429dc210b5fa43589d81b69c93bf27b2d 100644 +index 1bde0b9f8cbe6771a52c1041095c9dddfe8e5b6c..0ca2ffb2a0c9327aed5ddcf0004500dc8b42569f 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -14,6 +14,7 @@ declare const mistralLanguageModelOptions: z.ZodObject<{ @@ -11,7 +23,7 @@ index 1ca9113bed2728a616db773a8e08d8d6957447d7..15408ec429dc210b5fa43589d81b69c9 type MistralLanguageModelOptions = z.infer; diff --git a/dist/index.js b/dist/index.js -index 45735e524aaff54ea058c99c729c5ffd3c507058..779bd50f2b18e0dbe61bf9aaad88e9c87ed3c419 100644 +index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..f3476d19933277a9e7d41c4a688f36aa34bebda1 100644 --- a/dist/index.js +++ b/dist/index.js @@ -128,11 +128,14 @@ function convertToMistralChatMessages(prompt) { @@ -60,8 +72,8 @@ index 45735e524aaff54ea058c99c729c5ffd3c507058..779bd50f2b18e0dbe61bf9aaad88e9c8 }); // src/mistral-error.ts -@@ -413,6 +422,7 @@ var MistralChatLanguageModel = class { - top_p: topP, +@@ -407,6 +416,7 @@ var MistralChatLanguageModel = class { + stop: stopSequences, random_seed: seed, reasoning_effort: options.reasoningEffort, + prompt_cache_key: options.promptCacheKey, @@ -69,7 +81,7 @@ index 45735e524aaff54ea058c99c729c5ffd3c507058..779bd50f2b18e0dbe61bf9aaad88e9c8 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..929a5fe79c9fc12b437f82532157e775d14a999f 100644 +index d2eff622c1b84a96bdeb4012cb0206a33012a04d..0efee3bbc04d8134c32724694318c87b58b6bfa2 100644 --- a/dist/index.mjs +++ b/dist/index.mjs @@ -116,11 +116,14 @@ function convertToMistralChatMessages(prompt) { @@ -118,8 +130,8 @@ index 4c22df1cd78a1ba81309c8a86ceecefef4ba4aea..929a5fe79c9fc12b437f82532157e775 }); // src/mistral-error.ts -@@ -403,6 +412,7 @@ var MistralChatLanguageModel = class { - top_p: topP, +@@ -397,6 +406,7 @@ var MistralChatLanguageModel = class { + stop: stopSequences, random_seed: seed, reasoning_effort: options.reasoningEffort, + prompt_cache_key: options.promptCacheKey, @@ -127,7 +139,7 @@ index 4c22df1cd78a1ba81309c8a86ceecefef4ba4aea..929a5fe79c9fc12b437f82532157e775 response_format: (responseFormat == null ? void 0 : responseFormat.type) === "json" ? structuredOutputs && (responseFormat == null ? void 0 : responseFormat.schema) != null ? { type: "json_schema", diff --git a/src/convert-to-mistral-chat-messages.ts b/src/convert-to-mistral-chat-messages.ts -index 1c74a7ee8959664635e729d1776d3ee516027ef9..80b52650d292522bd3da4979271cdeb58552fcf7 100644 +index 3c6914f8da615d7517bc43dd56198298d0a50247..fef2830282c11d3c99482115ed769349f56886cc 100644 --- a/src/convert-to-mistral-chat-messages.ts +++ b/src/convert-to-mistral-chat-messages.ts @@ -3,7 +3,10 @@ import { @@ -181,11 +193,11 @@ index 1c74a7ee8959664635e729d1776d3ee516027ef9..80b52650d292522bd3da4979271cdeb5 tool_calls: toolCalls.length > 0 ? toolCalls : undefined, }); diff --git a/src/mistral-chat-language-model.ts b/src/mistral-chat-language-model.ts -index 480c472d534bedbe8897979673453bd1c29a70b7..e46496da94f7d4af9822897202ca6baae67dae3a 100644 +index 7e4a7ab552f1b41b7074e1b3cada8a51d791268d..2e7191d8164982834d7db7aa26767fbc665db099 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, +@@ -122,6 +122,7 @@ export class MistralChatLanguageModel implements LanguageModelV3 { + stop: stopSequences, random_seed: seed, reasoning_effort: options.reasoningEffort, + prompt_cache_key: options.promptCacheKey, @@ -193,10 +205,10 @@ index 480c472d534bedbe8897979673453bd1c29a70b7..e46496da94f7d4af9822897202ca6baa // response format: response_format: diff --git a/src/mistral-chat-options.ts b/src/mistral-chat-options.ts -index 80fff45fba2c378fa06962f071946bcd2b882a0b..b4fdfa51f3bf11a4220e010c8aca92482cb0c3db 100644 +index 54b29c08517d348995b6ca093b11160e453d5c8b..de30c3e7d924889339e38b1067cb26e9ada05d11 100644 --- a/src/mistral-chat-options.ts +++ b/src/mistral-chat-options.ts -@@ -62,6 +62,11 @@ export const mistralLanguageModelOptions = z.object({ +@@ -64,6 +64,11 @@ export const mistralLanguageModelOptions = z.object({ * - `'none'`: Disable reasoning */ reasoningEffort: z.enum(['high', 'none']).optional(), From 763916e661cca8374e5ad0e1e8a1e8a1aa7e8478 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Thu, 23 Jul 2026 11:39:09 -0500 Subject: [PATCH 3/5] fix(provider): retain Mistral thinking metadata --- packages/core/test/provider-mistral.test.ts | 133 ++++- packages/opencode/test/session/llm.test.ts | 104 ++++ patches/@ai-sdk%2Fmistral@3.0.51.patch | 535 ++++++++++++++++++-- 3 files changed, 733 insertions(+), 39 deletions(-) diff --git a/packages/core/test/provider-mistral.test.ts b/packages/core/test/provider-mistral.test.ts index e66149609140..a5f27c129cda 100644 --- a/packages/core/test/provider-mistral.test.ts +++ b/packages/core/test/provider-mistral.test.ts @@ -27,7 +27,7 @@ test("Mistral sends promptCacheKey as prompt_cache_key", async () => { expect(body?.prompt_cache_key).toBe("session-123") }) -test("Mistral preserves structured reasoning in assistant history", async () => { +test("Mistral round-trips native reasoning in assistant history", async () => { let body: { messages?: unknown[] } | undefined const mockFetch = Object.assign( async (_input: Parameters[0], init?: RequestInit) => { @@ -37,7 +37,28 @@ test("Mistral preserves structured reasoning in assistant history", async () => created: 0, model: "mistral-small-latest", object: "chat.completion", - choices: [{ index: 0, message: { role: "assistant", content: "Hello" }, finish_reason: "stop" }], + choices: [ + { + index: 0, + message: { + role: "assistant", + content: [ + { + type: "thinking", + thinking: [ + { type: "text", text: "The user is greeting me." }, + { type: "tool_reference", url: "https://example.com/tool" }, + { 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 }, }) }, @@ -45,15 +66,19 @@ test("Mistral preserves structured reasoning in assistant history", async () => ) 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: [ - { type: "reasoning", text: "The user is greeting me." }, - { type: "text", text: "Hi" }, - ], + content: [{ ...reasoning, providerOptions: reasoning.providerMetadata }, text], }, { role: "user", content: [{ type: "text", text: "Hello again" }] }, ], @@ -64,9 +89,103 @@ test("Mistral preserves structured reasoning in assistant history", async () => content: [ { type: "thinking", - thinking: [{ type: "text", text: "The user is greeting me." }], + thinking: [ + { type: "text", text: "The user is greeting me." }, + { type: "tool_reference", url: "https://example.com/tool" }, + { type: "reference", reference_ids: [1, "source-2"] }, + ], + closed: true, + signature: "sig-123", }, { type: "text", text: "Hi" }, ], }) }) + +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", url: "https://example.com/tool" }, + ], + }, + ], + }, + }, + ], + }, + { + 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", url: "https://example.com/tool" }, + { type: "reference", reference_ids: [1, "source-2"] }, + ], + closed: true, + signature: "sig-123", + }, + }, + }) +}) diff --git a/packages/opencode/test/session/llm.test.ts b/packages/opencode/test/session/llm.test.ts index 61aac13ac395..a8a03abb3f16 100644 --- a/packages/opencode/test/session/llm.test.ts +++ b/packages/opencode/test/session/llm.test.ts @@ -906,6 +906,110 @@ 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", url: "https://example.com/tool" }, + { 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> + 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", diff --git a/patches/@ai-sdk%2Fmistral@3.0.51.patch b/patches/@ai-sdk%2Fmistral@3.0.51.patch index aa8b58ea0607..2db8da5060bd 100644 --- a/patches/@ai-sdk%2Fmistral@3.0.51.patch +++ b/patches/@ai-sdk%2Fmistral@3.0.51.patch @@ -23,7 +23,7 @@ index 1bde0b9f8cbe6771a52c1041095c9dddfe8e5b6c..0ca2ffb2a0c9327aed5ddcf0004500dc type MistralLanguageModelOptions = z.infer; diff --git a/dist/index.js b/dist/index.js -index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..f3476d19933277a9e7d41c4a688f36aa34bebda1 100644 +index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..ea22b039d3c235aa4f4474e11f0e67fa88ea0aac 100644 --- a/dist/index.js +++ b/dist/index.js @@ -128,11 +128,14 @@ function convertToMistralChatMessages(prompt) { @@ -41,19 +41,22 @@ index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..f3476d19933277a9e7d41c4a688f36aa break; } case "tool-call": { -@@ -148,6 +151,11 @@ function convertToMistralChatMessages(prompt) { +@@ -148,6 +151,14 @@ function convertToMistralChatMessages(prompt) { } case "reasoning": { text += part.text; + hasReasoning = true; -+ structuredContent.push({ -+ type: "thinking", -+ thinking: [{ type: "text", text: part.text }] -+ }); ++ const native = part.providerOptions?.mistral?.thinking; ++ structuredContent.push( ++ native?.type === "thinking" ? native : { ++ type: "thinking", ++ thinking: [{ type: "text", text: part.text }] ++ } ++ ); break; } default: { -@@ -159,7 +167,7 @@ function convertToMistralChatMessages(prompt) { +@@ -159,7 +170,7 @@ function convertToMistralChatMessages(prompt) { } messages.push({ role: "assistant", @@ -62,7 +65,7 @@ index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..f3476d19933277a9e7d41c4a688f36aa prefix: isLastMessage ? true : void 0, tool_calls: toolCalls.length > 0 ? toolCalls : void 0 }); -@@ -268,7 +276,8 @@ var mistralLanguageModelOptions = import_v4.z.object({ +@@ -268,7 +279,8 @@ var mistralLanguageModelOptions = import_v4.z.object({ * - `'high'`: Enable reasoning * - `'none'`: Disable reasoning */ @@ -72,7 +75,7 @@ index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..f3476d19933277a9e7d41c4a688f36aa }); // src/mistral-error.ts -@@ -407,6 +416,7 @@ var MistralChatLanguageModel = class { +@@ -407,6 +419,7 @@ var MistralChatLanguageModel = class { stop: stopSequences, random_seed: seed, reasoning_effort: options.reasoningEffort, @@ -80,8 +83,149 @@ index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..f3476d19933277a9e7d41c4a688f36aa // response format: response_format: (responseFormat == null ? void 0 : responseFormat.type) === "json" ? structuredOutputs && (responseFormat == null ? void 0 : responseFormat.schema) != null ? { type: "json_schema", +@@ -466,7 +479,11 @@ var MistralChatLanguageModel = class { + 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) { +@@ -528,6 +545,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,22 +579,25 @@ 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, ++ providerMetadata: { mistral: { thinking: activeThinking } } ++ }); ++ } ++ if (reasoningDelta.length > 0) { + controller.enqueue({ + type: "reasoning-delta", + id: activeReasoningId, +- delta: reasoningDelta ++ delta: reasoningDelta, ++ providerMetadata: { mistral: { thinking: activeThinking } } + }); + } + } +@@ -587,9 +608,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 +661,8 @@ var MistralChatLanguageModel = class { + if (activeReasoningId != null) { + controller.enqueue({ + type: "reasoning-end", +- id: activeReasoningId ++ id: activeReasoningId, ++ providerMetadata: { mistral: { thinking: activeThinking } } + }); + } + if (activeText) { +@@ -660,6 +684,14 @@ var MistralChatLanguageModel = class { + function extractReasoningContent(thinking) { + return thinking.filter((chunk) => chunk.type === "text").map((chunk) => chunk.text).join(""); + } ++function mergeThinking(current, next) { ++ return { ++ type: "thinking", ++ thinking: [...(current?.thinking ?? []), ...next.thinking], ++ closed: next.closed ?? current?.closed, ++ signature: next.signature === void 0 ? current?.signature : next.signature ++ }; ++} + function extractTextContent(content) { + if (typeof content === "string") { + return content; +@@ -686,6 +718,26 @@ 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"), ++ url: 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()])) ++ }) ++]); ++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 +760,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..0efee3bbc04d8134c32724694318c87b58b6bfa2 100644 +index d2eff622c1b84a96bdeb4012cb0206a33012a04d..d96fd4bc01a6ec9cbec4323b3f547a0202a9267a 100644 --- a/dist/index.mjs +++ b/dist/index.mjs @@ -116,11 +116,14 @@ function convertToMistralChatMessages(prompt) { @@ -99,19 +243,22 @@ index d2eff622c1b84a96bdeb4012cb0206a33012a04d..0efee3bbc04d8134c32724694318c87b break; } case "tool-call": { -@@ -136,6 +139,11 @@ function convertToMistralChatMessages(prompt) { +@@ -136,6 +139,14 @@ function convertToMistralChatMessages(prompt) { } case "reasoning": { text += part.text; + hasReasoning = true; -+ structuredContent.push({ -+ type: "thinking", -+ thinking: [{ type: "text", text: part.text }] -+ }); ++ const native = part.providerOptions?.mistral?.thinking; ++ structuredContent.push( ++ native?.type === "thinking" ? native : { ++ type: "thinking", ++ thinking: [{ type: "text", text: part.text }] ++ } ++ ); break; } default: { -@@ -147,7 +155,7 @@ function convertToMistralChatMessages(prompt) { +@@ -147,7 +158,7 @@ function convertToMistralChatMessages(prompt) { } messages.push({ role: "assistant", @@ -120,7 +267,7 @@ index d2eff622c1b84a96bdeb4012cb0206a33012a04d..0efee3bbc04d8134c32724694318c87b prefix: isLastMessage ? true : void 0, tool_calls: toolCalls.length > 0 ? toolCalls : void 0 }); -@@ -256,7 +264,8 @@ var mistralLanguageModelOptions = z.object({ +@@ -256,7 +267,8 @@ var mistralLanguageModelOptions = z.object({ * - `'high'`: Enable reasoning * - `'none'`: Disable reasoning */ @@ -130,7 +277,7 @@ index d2eff622c1b84a96bdeb4012cb0206a33012a04d..0efee3bbc04d8134c32724694318c87b }); // src/mistral-error.ts -@@ -397,6 +406,7 @@ var MistralChatLanguageModel = class { +@@ -397,6 +409,7 @@ var MistralChatLanguageModel = class { stop: stopSequences, random_seed: seed, reasoning_effort: options.reasoningEffort, @@ -138,11 +285,152 @@ index d2eff622c1b84a96bdeb4012cb0206a33012a04d..0efee3bbc04d8134c32724694318c87b // response format: response_format: (responseFormat == null ? void 0 : responseFormat.type) === "json" ? structuredOutputs && (responseFormat == null ? void 0 : responseFormat.schema) != null ? { type: "json_schema", +@@ -456,7 +469,11 @@ var MistralChatLanguageModel = class { + 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) { +@@ -518,6 +535,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,22 +569,25 @@ 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, ++ providerMetadata: { mistral: { thinking: activeThinking } } ++ }); ++ } ++ if (reasoningDelta.length > 0) { + controller.enqueue({ + type: "reasoning-delta", + id: activeReasoningId, +- delta: reasoningDelta ++ delta: reasoningDelta, ++ providerMetadata: { mistral: { thinking: activeThinking } } + }); + } + } +@@ -577,9 +598,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 +651,8 @@ var MistralChatLanguageModel = class { + if (activeReasoningId != null) { + controller.enqueue({ + type: "reasoning-end", +- id: activeReasoningId ++ id: activeReasoningId, ++ providerMetadata: { mistral: { thinking: activeThinking } } + }); + } + if (activeText) { +@@ -650,6 +674,14 @@ var MistralChatLanguageModel = class { + function extractReasoningContent(thinking) { + return thinking.filter((chunk) => chunk.type === "text").map((chunk) => chunk.text).join(""); + } ++function mergeThinking(current, next) { ++ return { ++ type: "thinking", ++ thinking: [...(current?.thinking ?? []), ...next.thinking], ++ closed: next.closed ?? current?.closed, ++ signature: next.signature === void 0 ? current?.signature : next.signature ++ }; ++} + function extractTextContent(content) { + if (typeof content === "string") { + return content; +@@ -676,6 +708,26 @@ 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"), ++ url: z3.string().nullish() ++ }), ++ z3.object({ ++ type: z3.literal("reference"), ++ reference_ids: z3.array(z3.union([z3.string(), z3.number()])) ++ }) ++]); ++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 +750,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..fef2830282c11d3c99482115ed769349f56886cc 100644 +index 3c6914f8da615d7517bc43dd56198298d0a50247..3f097187eec83dc8fe320b928b4b7d9b6f6cce11 100644 --- a/src/convert-to-mistral-chat-messages.ts +++ b/src/convert-to-mistral-chat-messages.ts -@@ -3,7 +3,10 @@ import { +@@ -3,7 +3,11 @@ import { type LanguageModelV3DataContent, type LanguageModelV3Prompt, } from '@ai-sdk/provider'; @@ -150,11 +438,12 @@ index 3c6914f8da615d7517bc43dd56198298d0a50247..fef2830282c11d3c99482115ed769349 +import type { + MistralAssistantMessageContent, + MistralPrompt, ++ MistralThinkChunk, +} from './mistral-chat-prompt'; import { convertToBase64 } from '@ai-sdk/provider-utils'; function formatFileUrl({ -@@ -76,6 +79,8 @@ export function convertToMistralChatMessages( +@@ -76,6 +80,8 @@ export function convertToMistralChatMessages( case 'assistant': { let text = ''; @@ -163,7 +452,7 @@ index 3c6914f8da615d7517bc43dd56198298d0a50247..fef2830282c11d3c99482115ed769349 const toolCalls: Array<{ id: string; type: 'function'; -@@ -86,6 +91,7 @@ export function convertToMistralChatMessages( +@@ -86,6 +92,7 @@ export function convertToMistralChatMessages( switch (part.type) { case 'text': { text += part.text; @@ -171,19 +460,25 @@ index 3c6914f8da615d7517bc43dd56198298d0a50247..fef2830282c11d3c99482115ed769349 break; } case 'tool-call': { -@@ -101,6 +107,11 @@ export function convertToMistralChatMessages( +@@ -101,6 +108,17 @@ export function convertToMistralChatMessages( } case 'reasoning': { text += part.text; + hasReasoning = true; -+ structuredContent.push({ -+ type: 'thinking', -+ thinking: [{ type: 'text', text: part.text }], -+ }); ++ const native = part.providerOptions?.mistral ++ ?.thinking as MistralThinkChunk | undefined; ++ structuredContent.push( ++ native?.type === 'thinking' ++ ? native ++ : { ++ type: 'thinking', ++ thinking: [{ type: 'text', text: part.text }], ++ }, ++ ); break; } default: { -@@ -113,7 +124,7 @@ export function convertToMistralChatMessages( +@@ -113,7 +131,7 @@ export function convertToMistralChatMessages( messages.push({ role: 'assistant', @@ -193,7 +488,7 @@ index 3c6914f8da615d7517bc43dd56198298d0a50247..fef2830282c11d3c99482115ed769349 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..2e7191d8164982834d7db7aa26767fbc665db099 100644 +index 7e4a7ab552f1b41b7074e1b3cada8a51d791268d..dfa1927be11836642c42901b7e8d751e41e470ed 100644 --- a/src/mistral-chat-language-model.ts +++ b/src/mistral-chat-language-model.ts @@ -122,6 +122,7 @@ export class MistralChatLanguageModel implements LanguageModelV3 { @@ -204,6 +499,170 @@ index 7e4a7ab552f1b41b7074e1b3cada8a51d791268d..2e7191d8164982834d7db7aa26767fbc // response format: response_format: +@@ -202,7 +203,11 @@ export class MistralChatLanguageModel implements LanguageModelV3 { + 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) { +@@ -278,6 +283,7 @@ export class MistralChatLanguageModel implements LanguageModelV3 { + let isFirstChunk = true; + let activeText = false; + let activeReasoningId: string | null = null; ++ let activeThinking: z.infer | null = null; + + const generateId = this.generateId; + +@@ -326,24 +332,31 @@ 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, ++ providerMetadata: { ++ mistral: { thinking: activeThinking }, ++ }, ++ }); ++ } ++ if (reasoningDelta.length > 0) { + controller.enqueue({ + type: 'reasoning-delta', + id: activeReasoningId, + delta: reasoningDelta, ++ providerMetadata: { ++ mistral: { thinking: activeThinking }, ++ }, + }); + } + } +@@ -357,8 +370,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 +433,9 @@ export class MistralChatLanguageModel implements LanguageModelV3 { + controller.enqueue({ + type: 'reasoning-end', + id: activeReasoningId, ++ providerMetadata: { ++ mistral: { thinking: activeThinking }, ++ }, + }); + } + if (activeText) { +@@ -437,7 +457,7 @@ export class MistralChatLanguageModel implements LanguageModelV3 { + } + + function extractReasoningContent( +- thinking: Array<{ type: string; text: string }>, ++ thinking: Array>, + ) { + return thinking + .filter(chunk => chunk.type === 'text') +@@ -445,6 +465,19 @@ function extractReasoningContent( + .join(''); + } + ++function mergeThinking( ++ current: z.infer | null, ++ next: z.infer, ++) { ++ return { ++ type: 'thinking' as const, ++ thinking: [...(current?.thinking ?? []), ...next.thinking], ++ closed: next.closed ?? current?.closed, ++ signature: ++ next.signature === undefined ? current?.signature : next.signature, ++ }; ++} ++ + function extractTextContent(content: z.infer) { + if (typeof content === 'string') { + return content; +@@ -478,6 +511,28 @@ function extractTextContent(content: z.infer) { + 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'), ++ url: z.string().nullish(), ++ }), ++ z.object({ ++ type: z.literal('reference'), ++ reference_ids: z.array(z.union([z.string(), z.number()])), ++ }), ++]); ++ ++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 +556,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 @@ -221,7 +680,7 @@ index 54b29c08517d348995b6ca093b11160e453d5c8b..de30c3e7d924889339e38b1067cb26e9 export type MistralLanguageModelOptions = z.infer< diff --git a/src/mistral-chat-prompt.ts b/src/mistral-chat-prompt.ts -index 13f1dced55ac4be084128127a57fbdd58115bc28..60969ded9e7abc48852da022bc6a6fb9d74901f5 100644 +index 13f1dced55ac4be084128127a57fbdd58115bc28..ed553c0b61e327b95d99675e54b3dd94514ce62f 100644 --- a/src/mistral-chat-prompt.ts +++ b/src/mistral-chat-prompt.ts @@ -23,7 +23,7 @@ export type MistralUserMessageContent = @@ -233,13 +692,25 @@ index 13f1dced55ac4be084128127a57fbdd58115bc28..60969ded9e7abc48852da022bc6a6fb9 prefix?: boolean; tool_calls?: Array<{ id: string; -@@ -32,6 +32,10 @@ export interface MistralAssistantMessage { +@@ -32,6 +32,22 @@ export interface MistralAssistantMessage { }>; } +export type MistralAssistantMessageContent = + | { type: 'text'; text: string } -+ | { type: 'thinking'; thinking: Array<{ type: 'text'; text: string }> }; ++ | MistralThinkChunk; ++ ++export type MistralThinkChunk = { ++ type: 'thinking'; ++ thinking: Array; ++ closed?: boolean; ++ signature?: string | null; ++}; ++ ++export type MistralThinkingContent = ++ | { type: 'text'; text: string } ++ | { type: 'tool_reference'; url?: string | null } ++ | { type: 'reference'; reference_ids: Array }; + export interface MistralToolMessage { role: 'tool'; From 5c7de5db64b3860904dcd79022adc6e2a54e8771 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Thu, 23 Jul 2026 11:44:52 -0500 Subject: [PATCH 4/5] test(provider): cover metadata-only Mistral thinking --- packages/core/test/provider-mistral.test.ts | 36 +++++++ patches/@ai-sdk%2Fmistral@3.0.51.patch | 104 +++++++++++--------- 2 files changed, 91 insertions(+), 49 deletions(-) diff --git a/packages/core/test/provider-mistral.test.ts b/packages/core/test/provider-mistral.test.ts index a5f27c129cda..b92d26c32fe6 100644 --- a/packages/core/test/provider-mistral.test.ts +++ b/packages/core/test/provider-mistral.test.ts @@ -189,3 +189,39 @@ test("Mistral preserves native reasoning metadata while streaming", async () => }, }) }) + +test("Mistral preserves metadata-only thinking chunks", async () => { + const thinking = { + type: "thinking" as const, + thinking: [ + { type: "tool_reference", url: "https://example.com/tool" }, + { 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 } }, + }, + ]) +}) diff --git a/patches/@ai-sdk%2Fmistral@3.0.51.patch b/patches/@ai-sdk%2Fmistral@3.0.51.patch index 2db8da5060bd..0e43855222f1 100644 --- a/patches/@ai-sdk%2Fmistral@3.0.51.patch +++ b/patches/@ai-sdk%2Fmistral@3.0.51.patch @@ -23,7 +23,7 @@ index 1bde0b9f8cbe6771a52c1041095c9dddfe8e5b6c..0ca2ffb2a0c9327aed5ddcf0004500dc type MistralLanguageModelOptions = z.infer; diff --git a/dist/index.js b/dist/index.js -index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..ea22b039d3c235aa4f4474e11f0e67fa88ea0aac 100644 +index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..d141d6a5244de898ebdf3c5ccdbd39cdead67bd1 100644 --- a/dist/index.js +++ b/dist/index.js @@ -128,11 +128,14 @@ function convertToMistralChatMessages(prompt) { @@ -83,20 +83,22 @@ index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..ea22b039d3c235aa4f4474e11f0e67fa // response format: response_format: (responseFormat == null ? void 0 : responseFormat.type) === "json" ? structuredOutputs && (responseFormat == null ? void 0 : responseFormat.schema) != null ? { type: "json_schema", -@@ -466,7 +479,11 @@ var MistralChatLanguageModel = class { +@@ -465,9 +478,11 @@ var MistralChatLanguageModel = class { + for (const part of choice.message.content) { if (part.type === "thinking") { const reasoningText = extractReasoningContent(part.thinking); - if (reasoningText.length > 0) { +- if (reasoningText.length > 0) { - content.push({ type: "reasoning", text: reasoningText }); -+ content.push({ -+ type: "reasoning", -+ text: reasoningText, -+ providerMetadata: { mistral: { thinking: part } } -+ }); - } +- } ++ content.push({ ++ type: "reasoning", ++ text: reasoningText, ++ providerMetadata: { mistral: { thinking: part } } ++ }); } else if (part.type === "text") { if (part.text.length > 0) { -@@ -528,6 +545,7 @@ var MistralChatLanguageModel = class { + content.push({ type: "text", text: part.text }); +@@ -528,6 +543,7 @@ var MistralChatLanguageModel = class { let isFirstChunk = true; let activeText = false; let activeReasoningId = null; @@ -104,7 +106,7 @@ index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..ea22b039d3c235aa4f4474e11f0e67fa const generateId2 = this.generateId; return { stream: response.pipeThrough( -@@ -561,22 +579,25 @@ var MistralChatLanguageModel = class { +@@ -561,22 +577,25 @@ var MistralChatLanguageModel = class { for (const part of delta.content) { if (part.type === "thinking") { const reasoningDelta = extractReasoningContent(part.thinking); @@ -142,7 +144,7 @@ index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..ea22b039d3c235aa4f4474e11f0e67fa }); } } -@@ -587,9 +608,11 @@ var MistralChatLanguageModel = class { +@@ -587,9 +606,11 @@ var MistralChatLanguageModel = class { if (activeReasoningId != null) { controller.enqueue({ type: "reasoning-end", @@ -155,7 +157,7 @@ index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..ea22b039d3c235aa4f4474e11f0e67fa } controller.enqueue({ type: "text-start", id: "0" }); activeText = true; -@@ -638,7 +661,8 @@ var MistralChatLanguageModel = class { +@@ -638,7 +659,8 @@ var MistralChatLanguageModel = class { if (activeReasoningId != null) { controller.enqueue({ type: "reasoning-end", @@ -165,7 +167,7 @@ index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..ea22b039d3c235aa4f4474e11f0e67fa }); } if (activeText) { -@@ -660,6 +684,14 @@ var MistralChatLanguageModel = class { +@@ -660,6 +682,14 @@ var MistralChatLanguageModel = class { function extractReasoningContent(thinking) { return thinking.filter((chunk) => chunk.type === "text").map((chunk) => chunk.text).join(""); } @@ -180,7 +182,7 @@ index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..ea22b039d3c235aa4f4474e11f0e67fa function extractTextContent(content) { if (typeof content === "string") { return content; -@@ -686,6 +718,26 @@ function extractTextContent(content) { +@@ -686,6 +716,26 @@ function extractTextContent(content) { } return textContent.length ? textContent.join("") : void 0; } @@ -207,7 +209,7 @@ index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..ea22b039d3c235aa4f4474e11f0e67fa var mistralContentSchema = import_v43.z.union([ import_v43.z.string(), import_v43.z.array( -@@ -708,15 +760,7 @@ var mistralContentSchema = import_v43.z.union([ +@@ -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()])) }), @@ -225,7 +227,7 @@ index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..ea22b039d3c235aa4f4474e11f0e67fa ) ]).nullish(); diff --git a/dist/index.mjs b/dist/index.mjs -index d2eff622c1b84a96bdeb4012cb0206a33012a04d..d96fd4bc01a6ec9cbec4323b3f547a0202a9267a 100644 +index d2eff622c1b84a96bdeb4012cb0206a33012a04d..fa6a0f13962e569f4e44f43db7018031f0931ee4 100644 --- a/dist/index.mjs +++ b/dist/index.mjs @@ -116,11 +116,14 @@ function convertToMistralChatMessages(prompt) { @@ -285,20 +287,22 @@ index d2eff622c1b84a96bdeb4012cb0206a33012a04d..d96fd4bc01a6ec9cbec4323b3f547a02 // response format: response_format: (responseFormat == null ? void 0 : responseFormat.type) === "json" ? structuredOutputs && (responseFormat == null ? void 0 : responseFormat.schema) != null ? { type: "json_schema", -@@ -456,7 +469,11 @@ var MistralChatLanguageModel = class { +@@ -455,9 +468,11 @@ var MistralChatLanguageModel = class { + for (const part of choice.message.content) { if (part.type === "thinking") { const reasoningText = extractReasoningContent(part.thinking); - if (reasoningText.length > 0) { +- if (reasoningText.length > 0) { - content.push({ type: "reasoning", text: reasoningText }); -+ content.push({ -+ type: "reasoning", -+ text: reasoningText, -+ providerMetadata: { mistral: { thinking: part } } -+ }); - } +- } ++ content.push({ ++ type: "reasoning", ++ text: reasoningText, ++ providerMetadata: { mistral: { thinking: part } } ++ }); } else if (part.type === "text") { if (part.text.length > 0) { -@@ -518,6 +535,7 @@ var MistralChatLanguageModel = class { + content.push({ type: "text", text: part.text }); +@@ -518,6 +533,7 @@ var MistralChatLanguageModel = class { let isFirstChunk = true; let activeText = false; let activeReasoningId = null; @@ -306,7 +310,7 @@ index d2eff622c1b84a96bdeb4012cb0206a33012a04d..d96fd4bc01a6ec9cbec4323b3f547a02 const generateId2 = this.generateId; return { stream: response.pipeThrough( -@@ -551,22 +569,25 @@ var MistralChatLanguageModel = class { +@@ -551,22 +567,25 @@ var MistralChatLanguageModel = class { for (const part of delta.content) { if (part.type === "thinking") { const reasoningDelta = extractReasoningContent(part.thinking); @@ -344,7 +348,7 @@ index d2eff622c1b84a96bdeb4012cb0206a33012a04d..d96fd4bc01a6ec9cbec4323b3f547a02 }); } } -@@ -577,9 +598,11 @@ var MistralChatLanguageModel = class { +@@ -577,9 +596,11 @@ var MistralChatLanguageModel = class { if (activeReasoningId != null) { controller.enqueue({ type: "reasoning-end", @@ -357,7 +361,7 @@ index d2eff622c1b84a96bdeb4012cb0206a33012a04d..d96fd4bc01a6ec9cbec4323b3f547a02 } controller.enqueue({ type: "text-start", id: "0" }); activeText = true; -@@ -628,7 +651,8 @@ var MistralChatLanguageModel = class { +@@ -628,7 +649,8 @@ var MistralChatLanguageModel = class { if (activeReasoningId != null) { controller.enqueue({ type: "reasoning-end", @@ -367,7 +371,7 @@ index d2eff622c1b84a96bdeb4012cb0206a33012a04d..d96fd4bc01a6ec9cbec4323b3f547a02 }); } if (activeText) { -@@ -650,6 +674,14 @@ var MistralChatLanguageModel = class { +@@ -650,6 +672,14 @@ var MistralChatLanguageModel = class { function extractReasoningContent(thinking) { return thinking.filter((chunk) => chunk.type === "text").map((chunk) => chunk.text).join(""); } @@ -382,7 +386,7 @@ index d2eff622c1b84a96bdeb4012cb0206a33012a04d..d96fd4bc01a6ec9cbec4323b3f547a02 function extractTextContent(content) { if (typeof content === "string") { return content; -@@ -676,6 +708,26 @@ function extractTextContent(content) { +@@ -676,6 +706,26 @@ function extractTextContent(content) { } return textContent.length ? textContent.join("") : void 0; } @@ -409,7 +413,7 @@ index d2eff622c1b84a96bdeb4012cb0206a33012a04d..d96fd4bc01a6ec9cbec4323b3f547a02 var mistralContentSchema = z3.union([ z3.string(), z3.array( -@@ -698,15 +750,7 @@ var mistralContentSchema = z3.union([ +@@ -698,15 +748,7 @@ var mistralContentSchema = z3.union([ type: z3.literal("reference"), reference_ids: z3.array(z3.union([z3.string(), z3.number()])) }), @@ -488,7 +492,7 @@ index 3c6914f8da615d7517bc43dd56198298d0a50247..3f097187eec83dc8fe320b928b4b7d9b 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..dfa1927be11836642c42901b7e8d751e41e470ed 100644 +index 7e4a7ab552f1b41b7074e1b3cada8a51d791268d..b19fd069337d16cd1eecbe39ebad5ac7d07bd556 100644 --- a/src/mistral-chat-language-model.ts +++ b/src/mistral-chat-language-model.ts @@ -122,6 +122,7 @@ export class MistralChatLanguageModel implements LanguageModelV3 { @@ -499,20 +503,22 @@ index 7e4a7ab552f1b41b7074e1b3cada8a51d791268d..dfa1927be11836642c42901b7e8d751e // response format: response_format: -@@ -202,7 +203,11 @@ export class MistralChatLanguageModel implements LanguageModelV3 { +@@ -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) { +- if (reasoningText.length > 0) { - content.push({ type: 'reasoning', text: reasoningText }); -+ content.push({ -+ type: 'reasoning', -+ text: reasoningText, -+ providerMetadata: { mistral: { thinking: part } }, -+ }); - } +- } ++ content.push({ ++ type: 'reasoning', ++ text: reasoningText, ++ providerMetadata: { mistral: { thinking: part } }, ++ }); } else if (part.type === 'text') { if (part.text.length > 0) { -@@ -278,6 +283,7 @@ export class MistralChatLanguageModel implements LanguageModelV3 { + 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; @@ -520,7 +526,7 @@ index 7e4a7ab552f1b41b7074e1b3cada8a51d791268d..dfa1927be11836642c42901b7e8d751e const generateId = this.generateId; -@@ -326,24 +332,31 @@ export class MistralChatLanguageModel implements LanguageModelV3 { +@@ -326,24 +330,31 @@ export class MistralChatLanguageModel implements LanguageModelV3 { for (const part of delta.content) { if (part.type === 'thinking') { const reasoningDelta = extractReasoningContent(part.thinking); @@ -565,7 +571,7 @@ index 7e4a7ab552f1b41b7074e1b3cada8a51d791268d..dfa1927be11836642c42901b7e8d751e }); } } -@@ -357,8 +370,12 @@ export class MistralChatLanguageModel implements LanguageModelV3 { +@@ -357,8 +368,12 @@ export class MistralChatLanguageModel implements LanguageModelV3 { controller.enqueue({ type: 'reasoning-end', id: activeReasoningId, @@ -578,7 +584,7 @@ index 7e4a7ab552f1b41b7074e1b3cada8a51d791268d..dfa1927be11836642c42901b7e8d751e } controller.enqueue({ type: 'text-start', id: '0' }); activeText = true; -@@ -416,6 +433,9 @@ export class MistralChatLanguageModel implements LanguageModelV3 { +@@ -416,6 +431,9 @@ export class MistralChatLanguageModel implements LanguageModelV3 { controller.enqueue({ type: 'reasoning-end', id: activeReasoningId, @@ -588,7 +594,7 @@ index 7e4a7ab552f1b41b7074e1b3cada8a51d791268d..dfa1927be11836642c42901b7e8d751e }); } if (activeText) { -@@ -437,7 +457,7 @@ export class MistralChatLanguageModel implements LanguageModelV3 { +@@ -437,7 +455,7 @@ export class MistralChatLanguageModel implements LanguageModelV3 { } function extractReasoningContent( @@ -597,7 +603,7 @@ index 7e4a7ab552f1b41b7074e1b3cada8a51d791268d..dfa1927be11836642c42901b7e8d751e ) { return thinking .filter(chunk => chunk.type === 'text') -@@ -445,6 +465,19 @@ function extractReasoningContent( +@@ -445,6 +463,19 @@ function extractReasoningContent( .join(''); } @@ -617,7 +623,7 @@ index 7e4a7ab552f1b41b7074e1b3cada8a51d791268d..dfa1927be11836642c42901b7e8d751e function extractTextContent(content: z.infer) { if (typeof content === 'string') { return content; -@@ -478,6 +511,28 @@ function extractTextContent(content: z.infer) { +@@ -478,6 +509,28 @@ function extractTextContent(content: z.infer) { return textContent.length ? textContent.join('') : undefined; } @@ -646,7 +652,7 @@ index 7e4a7ab552f1b41b7074e1b3cada8a51d791268d..dfa1927be11836642c42901b7e8d751e const mistralContentSchema = z .union([ z.string(), -@@ -501,15 +556,7 @@ const mistralContentSchema = z +@@ -501,15 +554,7 @@ const mistralContentSchema = z type: z.literal('reference'), reference_ids: z.array(z.union([z.string(), z.number()])), }), From f8e4f4e28ee6476df3c1b0e15c4efb35de5f210a Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Thu, 23 Jul 2026 12:16:55 -0500 Subject: [PATCH 5/5] fix(provider): align Mistral thinking replay --- packages/core/test/provider-mistral.test.ts | 65 +++++- packages/opencode/test/session/llm.test.ts | 9 +- patches/@ai-sdk%2Fmistral@3.0.51.patch | 220 +++++++++----------- 3 files changed, 171 insertions(+), 123 deletions(-) diff --git a/packages/core/test/provider-mistral.test.ts b/packages/core/test/provider-mistral.test.ts index b92d26c32fe6..6e3176695f67 100644 --- a/packages/core/test/provider-mistral.test.ts +++ b/packages/core/test/provider-mistral.test.ts @@ -47,7 +47,14 @@ test("Mistral round-trips native reasoning in assistant history", async () => { type: "thinking", thinking: [ { type: "text", text: "The user is greeting me." }, - { type: "tool_reference", url: "https://example.com/tool" }, + { + 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, @@ -91,7 +98,14 @@ test("Mistral round-trips native reasoning in assistant history", async () => { type: "thinking", thinking: [ { type: "text", text: "The user is greeting me." }, - { type: "tool_reference", url: "https://example.com/tool" }, + { + 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, @@ -100,6 +114,21 @@ test("Mistral round-trips native reasoning in assistant history", async () => { { 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 () => { @@ -118,7 +147,14 @@ test("Mistral preserves native reasoning metadata while streaming", async () => type: "thinking", thinking: [ { type: "text", text: "thinking" }, - { type: "tool_reference", url: "https://example.com/tool" }, + { + type: "tool_reference", + tool: "web_search", + title: "Example result", + url: "https://example.com/tool", + favicon: "https://example.com/favicon.ico", + description: "Example description", + }, ], }, ], @@ -180,7 +216,14 @@ test("Mistral preserves native reasoning metadata while streaming", async () => type: "thinking", thinking: [ { type: "text", text: "thinking" }, - { type: "tool_reference", url: "https://example.com/tool" }, + { + 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, @@ -188,13 +231,25 @@ test("Mistral preserves native reasoning metadata while streaming", async () => }, }, }) + 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", url: "https://example.com/tool" }, + { + 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, diff --git a/packages/opencode/test/session/llm.test.ts b/packages/opencode/test/session/llm.test.ts index a8a03abb3f16..3bfc722e2bec 100644 --- a/packages/opencode/test/session/llm.test.ts +++ b/packages/opencode/test/session/llm.test.ts @@ -960,7 +960,14 @@ describe("session.llm.stream", () => { type: "thinking", thinking: [ { type: "text", text: "thinking" }, - { type: "tool_reference", url: "https://example.com/tool" }, + { + 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, diff --git a/patches/@ai-sdk%2Fmistral@3.0.51.patch b/patches/@ai-sdk%2Fmistral@3.0.51.patch index 0e43855222f1..141b14a689b1 100644 --- a/patches/@ai-sdk%2Fmistral@3.0.51.patch +++ b/patches/@ai-sdk%2Fmistral@3.0.51.patch @@ -23,7 +23,7 @@ index 1bde0b9f8cbe6771a52c1041095c9dddfe8e5b6c..0ca2ffb2a0c9327aed5ddcf0004500dc type MistralLanguageModelOptions = z.infer; diff --git a/dist/index.js b/dist/index.js -index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..d141d6a5244de898ebdf3c5ccdbd39cdead67bd1 100644 +index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..267f34e20ea392b7a85ad5259d72d50605a6f971 100644 --- a/dist/index.js +++ b/dist/index.js @@ -128,11 +128,14 @@ function convertToMistralChatMessages(prompt) { @@ -31,7 +31,7 @@ index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..d141d6a5244de898ebdf3c5ccdbd39cd case "assistant": { let text = ""; + const structuredContent = []; -+ let hasReasoning = false; ++ let hasNativeReasoning = false; const toolCalls = []; for (const part of content) { switch (part.type) { @@ -41,31 +41,30 @@ index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..d141d6a5244de898ebdf3c5ccdbd39cd break; } case "tool-call": { -@@ -148,6 +151,14 @@ function convertToMistralChatMessages(prompt) { +@@ -148,6 +151,13 @@ function convertToMistralChatMessages(prompt) { } case "reasoning": { text += part.text; -+ hasReasoning = true; + const native = part.providerOptions?.mistral?.thinking; -+ structuredContent.push( -+ native?.type === "thinking" ? native : { -+ type: "thinking", -+ thinking: [{ type: "text", text: part.text }] -+ } -+ ); ++ if (native?.type === "thinking") { ++ hasNativeReasoning = true; ++ structuredContent.push(native); ++ break; ++ } ++ structuredContent.push({ type: "text", text: part.text }); break; } default: { -@@ -159,7 +170,7 @@ function convertToMistralChatMessages(prompt) { +@@ -159,7 +169,7 @@ function convertToMistralChatMessages(prompt) { } messages.push({ role: "assistant", - content: text, -+ content: hasReasoning ? structuredContent : text, ++ content: hasNativeReasoning ? structuredContent : text, prefix: isLastMessage ? true : void 0, tool_calls: toolCalls.length > 0 ? toolCalls : void 0 }); -@@ -268,7 +279,8 @@ var mistralLanguageModelOptions = import_v4.z.object({ +@@ -268,7 +278,8 @@ var mistralLanguageModelOptions = import_v4.z.object({ * - `'high'`: Enable reasoning * - `'none'`: Disable reasoning */ @@ -75,7 +74,7 @@ index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..d141d6a5244de898ebdf3c5ccdbd39cd }); // src/mistral-error.ts -@@ -407,6 +419,7 @@ var MistralChatLanguageModel = class { +@@ -407,6 +418,7 @@ var MistralChatLanguageModel = class { stop: stopSequences, random_seed: seed, reasoning_effort: options.reasoningEffort, @@ -83,7 +82,7 @@ index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..d141d6a5244de898ebdf3c5ccdbd39cd // response format: response_format: (responseFormat == null ? void 0 : responseFormat.type) === "json" ? structuredOutputs && (responseFormat == null ? void 0 : responseFormat.schema) != null ? { type: "json_schema", -@@ -465,9 +478,11 @@ var MistralChatLanguageModel = class { +@@ -465,9 +477,11 @@ var MistralChatLanguageModel = class { for (const part of choice.message.content) { if (part.type === "thinking") { const reasoningText = extractReasoningContent(part.thinking); @@ -98,7 +97,7 @@ index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..d141d6a5244de898ebdf3c5ccdbd39cd } else if (part.type === "text") { if (part.text.length > 0) { content.push({ type: "text", text: part.text }); -@@ -528,6 +543,7 @@ var MistralChatLanguageModel = class { +@@ -528,6 +542,7 @@ var MistralChatLanguageModel = class { let isFirstChunk = true; let activeText = false; let activeReasoningId = null; @@ -106,7 +105,7 @@ index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..d141d6a5244de898ebdf3c5ccdbd39cd const generateId2 = this.generateId; return { stream: response.pipeThrough( -@@ -561,22 +577,25 @@ var MistralChatLanguageModel = class { +@@ -561,18 +576,19 @@ var MistralChatLanguageModel = class { for (const part of delta.content) { if (part.type === "thinking") { const reasoningDelta = extractReasoningContent(part.thinking); @@ -130,21 +129,14 @@ index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..d141d6a5244de898ebdf3c5ccdbd39cd + activeReasoningId = generateId2(); + controller.enqueue({ + type: "reasoning-start", -+ id: activeReasoningId, -+ providerMetadata: { mistral: { thinking: activeThinking } } ++ id: activeReasoningId + }); + } + if (reasoningDelta.length > 0) { controller.enqueue({ type: "reasoning-delta", id: activeReasoningId, -- delta: reasoningDelta -+ delta: reasoningDelta, -+ providerMetadata: { mistral: { thinking: activeThinking } } - }); - } - } -@@ -587,9 +606,11 @@ var MistralChatLanguageModel = class { +@@ -587,9 +603,11 @@ var MistralChatLanguageModel = class { if (activeReasoningId != null) { controller.enqueue({ type: "reasoning-end", @@ -157,7 +149,7 @@ index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..d141d6a5244de898ebdf3c5ccdbd39cd } controller.enqueue({ type: "text-start", id: "0" }); activeText = true; -@@ -638,7 +659,8 @@ var MistralChatLanguageModel = class { +@@ -638,7 +656,8 @@ var MistralChatLanguageModel = class { if (activeReasoningId != null) { controller.enqueue({ type: "reasoning-end", @@ -167,22 +159,21 @@ index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..d141d6a5244de898ebdf3c5ccdbd39cd }); } if (activeText) { -@@ -660,6 +682,14 @@ var MistralChatLanguageModel = class { +@@ -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) { -+ return { -+ type: "thinking", -+ thinking: [...(current?.thinking ?? []), ...next.thinking], -+ closed: next.closed ?? current?.closed, -+ signature: next.signature === void 0 ? current?.signature : next.signature -+ }; ++ 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 +716,26 @@ function extractTextContent(content) { +@@ -686,6 +712,30 @@ function extractTextContent(content) { } return textContent.length ? textContent.join("") : void 0; } @@ -193,11 +184,15 @@ index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..d141d6a5244de898ebdf3c5ccdbd39cd + }), + import_v43.z.object({ + type: import_v43.z.literal("tool_reference"), -+ url: import_v43.z.string().nullish() ++ 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()])) ++ 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({ @@ -227,7 +222,7 @@ index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..d141d6a5244de898ebdf3c5ccdbd39cd ) ]).nullish(); diff --git a/dist/index.mjs b/dist/index.mjs -index d2eff622c1b84a96bdeb4012cb0206a33012a04d..fa6a0f13962e569f4e44f43db7018031f0931ee4 100644 +index d2eff622c1b84a96bdeb4012cb0206a33012a04d..3bff11ddd6136ada45809568828cbc8f2493a42a 100644 --- a/dist/index.mjs +++ b/dist/index.mjs @@ -116,11 +116,14 @@ function convertToMistralChatMessages(prompt) { @@ -235,7 +230,7 @@ index d2eff622c1b84a96bdeb4012cb0206a33012a04d..fa6a0f13962e569f4e44f43db7018031 case "assistant": { let text = ""; + const structuredContent = []; -+ let hasReasoning = false; ++ let hasNativeReasoning = false; const toolCalls = []; for (const part of content) { switch (part.type) { @@ -245,31 +240,30 @@ index d2eff622c1b84a96bdeb4012cb0206a33012a04d..fa6a0f13962e569f4e44f43db7018031 break; } case "tool-call": { -@@ -136,6 +139,14 @@ function convertToMistralChatMessages(prompt) { +@@ -136,6 +139,13 @@ function convertToMistralChatMessages(prompt) { } case "reasoning": { text += part.text; -+ hasReasoning = true; + const native = part.providerOptions?.mistral?.thinking; -+ structuredContent.push( -+ native?.type === "thinking" ? native : { -+ type: "thinking", -+ thinking: [{ type: "text", text: part.text }] -+ } -+ ); ++ if (native?.type === "thinking") { ++ hasNativeReasoning = true; ++ structuredContent.push(native); ++ break; ++ } ++ structuredContent.push({ type: "text", text: part.text }); break; } default: { -@@ -147,7 +158,7 @@ function convertToMistralChatMessages(prompt) { +@@ -147,7 +157,7 @@ function convertToMistralChatMessages(prompt) { } messages.push({ role: "assistant", - content: text, -+ content: hasReasoning ? structuredContent : text, ++ content: hasNativeReasoning ? structuredContent : text, prefix: isLastMessage ? true : void 0, tool_calls: toolCalls.length > 0 ? toolCalls : void 0 }); -@@ -256,7 +267,8 @@ var mistralLanguageModelOptions = z.object({ +@@ -256,7 +266,8 @@ var mistralLanguageModelOptions = z.object({ * - `'high'`: Enable reasoning * - `'none'`: Disable reasoning */ @@ -279,7 +273,7 @@ index d2eff622c1b84a96bdeb4012cb0206a33012a04d..fa6a0f13962e569f4e44f43db7018031 }); // src/mistral-error.ts -@@ -397,6 +409,7 @@ var MistralChatLanguageModel = class { +@@ -397,6 +408,7 @@ var MistralChatLanguageModel = class { stop: stopSequences, random_seed: seed, reasoning_effort: options.reasoningEffort, @@ -287,7 +281,7 @@ index d2eff622c1b84a96bdeb4012cb0206a33012a04d..fa6a0f13962e569f4e44f43db7018031 // response format: response_format: (responseFormat == null ? void 0 : responseFormat.type) === "json" ? structuredOutputs && (responseFormat == null ? void 0 : responseFormat.schema) != null ? { type: "json_schema", -@@ -455,9 +468,11 @@ var MistralChatLanguageModel = class { +@@ -455,9 +467,11 @@ var MistralChatLanguageModel = class { for (const part of choice.message.content) { if (part.type === "thinking") { const reasoningText = extractReasoningContent(part.thinking); @@ -302,7 +296,7 @@ index d2eff622c1b84a96bdeb4012cb0206a33012a04d..fa6a0f13962e569f4e44f43db7018031 } else if (part.type === "text") { if (part.text.length > 0) { content.push({ type: "text", text: part.text }); -@@ -518,6 +533,7 @@ var MistralChatLanguageModel = class { +@@ -518,6 +532,7 @@ var MistralChatLanguageModel = class { let isFirstChunk = true; let activeText = false; let activeReasoningId = null; @@ -310,7 +304,7 @@ index d2eff622c1b84a96bdeb4012cb0206a33012a04d..fa6a0f13962e569f4e44f43db7018031 const generateId2 = this.generateId; return { stream: response.pipeThrough( -@@ -551,22 +567,25 @@ var MistralChatLanguageModel = class { +@@ -551,18 +566,19 @@ var MistralChatLanguageModel = class { for (const part of delta.content) { if (part.type === "thinking") { const reasoningDelta = extractReasoningContent(part.thinking); @@ -334,21 +328,14 @@ index d2eff622c1b84a96bdeb4012cb0206a33012a04d..fa6a0f13962e569f4e44f43db7018031 + activeReasoningId = generateId2(); + controller.enqueue({ + type: "reasoning-start", -+ id: activeReasoningId, -+ providerMetadata: { mistral: { thinking: activeThinking } } ++ id: activeReasoningId + }); + } + if (reasoningDelta.length > 0) { controller.enqueue({ type: "reasoning-delta", id: activeReasoningId, -- delta: reasoningDelta -+ delta: reasoningDelta, -+ providerMetadata: { mistral: { thinking: activeThinking } } - }); - } - } -@@ -577,9 +596,11 @@ var MistralChatLanguageModel = class { +@@ -577,9 +593,11 @@ var MistralChatLanguageModel = class { if (activeReasoningId != null) { controller.enqueue({ type: "reasoning-end", @@ -361,7 +348,7 @@ index d2eff622c1b84a96bdeb4012cb0206a33012a04d..fa6a0f13962e569f4e44f43db7018031 } controller.enqueue({ type: "text-start", id: "0" }); activeText = true; -@@ -628,7 +649,8 @@ var MistralChatLanguageModel = class { +@@ -628,7 +646,8 @@ var MistralChatLanguageModel = class { if (activeReasoningId != null) { controller.enqueue({ type: "reasoning-end", @@ -371,22 +358,21 @@ index d2eff622c1b84a96bdeb4012cb0206a33012a04d..fa6a0f13962e569f4e44f43db7018031 }); } if (activeText) { -@@ -650,6 +672,14 @@ var MistralChatLanguageModel = class { +@@ -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) { -+ return { -+ type: "thinking", -+ thinking: [...(current?.thinking ?? []), ...next.thinking], -+ closed: next.closed ?? current?.closed, -+ signature: next.signature === void 0 ? current?.signature : next.signature -+ }; ++ 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 +706,26 @@ function extractTextContent(content) { +@@ -676,6 +702,30 @@ function extractTextContent(content) { } return textContent.length ? textContent.join("") : void 0; } @@ -397,11 +383,15 @@ index d2eff622c1b84a96bdeb4012cb0206a33012a04d..fa6a0f13962e569f4e44f43db7018031 + }), + z3.object({ + type: z3.literal("tool_reference"), -+ url: z3.string().nullish() ++ 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()])) ++ reference_ids: z3.array(z3.union([z3.string(), z3.number().int()])) + }) +]); +var mistralThinkChunkSchema = z3.object({ @@ -431,7 +421,7 @@ index d2eff622c1b84a96bdeb4012cb0206a33012a04d..fa6a0f13962e569f4e44f43db7018031 ) ]).nullish(); diff --git a/src/convert-to-mistral-chat-messages.ts b/src/convert-to-mistral-chat-messages.ts -index 3c6914f8da615d7517bc43dd56198298d0a50247..3f097187eec83dc8fe320b928b4b7d9b6f6cce11 100644 +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 { @@ -452,7 +442,7 @@ index 3c6914f8da615d7517bc43dd56198298d0a50247..3f097187eec83dc8fe320b928b4b7d9b case 'assistant': { let text = ''; + const structuredContent: Array = []; -+ let hasReasoning = false; ++ let hasNativeReasoning = false; const toolCalls: Array<{ id: string; type: 'function'; @@ -464,35 +454,32 @@ index 3c6914f8da615d7517bc43dd56198298d0a50247..3f097187eec83dc8fe320b928b4b7d9b break; } case 'tool-call': { -@@ -101,6 +108,17 @@ export function convertToMistralChatMessages( +@@ -101,6 +108,14 @@ export function convertToMistralChatMessages( } case 'reasoning': { text += part.text; -+ hasReasoning = true; + const native = part.providerOptions?.mistral + ?.thinking as MistralThinkChunk | undefined; -+ structuredContent.push( -+ native?.type === 'thinking' -+ ? native -+ : { -+ type: 'thinking', -+ thinking: [{ type: 'text', text: part.text }], -+ }, -+ ); ++ if (native?.type === 'thinking') { ++ hasNativeReasoning = true; ++ structuredContent.push(native); ++ break; ++ } ++ structuredContent.push({ type: 'text', text: part.text }); break; } default: { -@@ -113,7 +131,7 @@ export function convertToMistralChatMessages( +@@ -113,7 +128,7 @@ export function convertToMistralChatMessages( messages.push({ role: 'assistant', - content: text, -+ content: hasReasoning ? structuredContent : 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..b19fd069337d16cd1eecbe39ebad5ac7d07bd556 100644 +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 { @@ -526,7 +513,7 @@ index 7e4a7ab552f1b41b7074e1b3cada8a51d791268d..b19fd069337d16cd1eecbe39ebad5ac7 const generateId = this.generateId; -@@ -326,24 +330,31 @@ export class MistralChatLanguageModel implements LanguageModelV3 { +@@ -326,20 +330,21 @@ export class MistralChatLanguageModel implements LanguageModelV3 { for (const part of delta.content) { if (part.type === 'thinking') { const reasoningDelta = extractReasoningContent(part.thinking); @@ -555,23 +542,13 @@ index 7e4a7ab552f1b41b7074e1b3cada8a51d791268d..b19fd069337d16cd1eecbe39ebad5ac7 + controller.enqueue({ + type: 'reasoning-start', + id: activeReasoningId, -+ providerMetadata: { -+ mistral: { thinking: activeThinking }, -+ }, + }); + } + if (reasoningDelta.length > 0) { controller.enqueue({ type: 'reasoning-delta', id: activeReasoningId, - delta: reasoningDelta, -+ providerMetadata: { -+ mistral: { thinking: activeThinking }, -+ }, - }); - } - } -@@ -357,8 +368,12 @@ export class MistralChatLanguageModel implements LanguageModelV3 { +@@ -357,8 +362,12 @@ export class MistralChatLanguageModel implements LanguageModelV3 { controller.enqueue({ type: 'reasoning-end', id: activeReasoningId, @@ -584,7 +561,7 @@ index 7e4a7ab552f1b41b7074e1b3cada8a51d791268d..b19fd069337d16cd1eecbe39ebad5ac7 } controller.enqueue({ type: 'text-start', id: '0' }); activeText = true; -@@ -416,6 +431,9 @@ export class MistralChatLanguageModel implements LanguageModelV3 { +@@ -416,6 +425,9 @@ export class MistralChatLanguageModel implements LanguageModelV3 { controller.enqueue({ type: 'reasoning-end', id: activeReasoningId, @@ -594,7 +571,7 @@ index 7e4a7ab552f1b41b7074e1b3cada8a51d791268d..b19fd069337d16cd1eecbe39ebad5ac7 }); } if (activeText) { -@@ -437,7 +455,7 @@ export class MistralChatLanguageModel implements LanguageModelV3 { +@@ -437,7 +449,7 @@ export class MistralChatLanguageModel implements LanguageModelV3 { } function extractReasoningContent( @@ -603,7 +580,7 @@ index 7e4a7ab552f1b41b7074e1b3cada8a51d791268d..b19fd069337d16cd1eecbe39ebad5ac7 ) { return thinking .filter(chunk => chunk.type === 'text') -@@ -445,6 +463,19 @@ function extractReasoningContent( +@@ -445,6 +457,17 @@ function extractReasoningContent( .join(''); } @@ -611,19 +588,17 @@ index 7e4a7ab552f1b41b7074e1b3cada8a51d791268d..b19fd069337d16cd1eecbe39ebad5ac7 + current: z.infer | null, + next: z.infer, +) { -+ return { -+ type: 'thinking' as const, -+ thinking: [...(current?.thinking ?? []), ...next.thinking], -+ closed: next.closed ?? current?.closed, -+ signature: -+ next.signature === undefined ? current?.signature : next.signature, -+ }; ++ 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) { if (typeof content === 'string') { return content; -@@ -478,6 +509,28 @@ function extractTextContent(content: z.infer) { +@@ -478,6 +501,32 @@ function extractTextContent(content: z.infer) { return textContent.length ? textContent.join('') : undefined; } @@ -634,11 +609,15 @@ index 7e4a7ab552f1b41b7074e1b3cada8a51d791268d..b19fd069337d16cd1eecbe39ebad5ac7 + }), + 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()])), ++ reference_ids: z.array(z.union([z.string(), z.number().int()])), + }), +]); + @@ -652,7 +631,7 @@ index 7e4a7ab552f1b41b7074e1b3cada8a51d791268d..b19fd069337d16cd1eecbe39ebad5ac7 const mistralContentSchema = z .union([ z.string(), -@@ -501,15 +554,7 @@ const mistralContentSchema = z +@@ -501,15 +550,7 @@ const mistralContentSchema = z type: z.literal('reference'), reference_ids: z.array(z.union([z.string(), z.number()])), }), @@ -686,7 +665,7 @@ index 54b29c08517d348995b6ca093b11160e453d5c8b..de30c3e7d924889339e38b1067cb26e9 export type MistralLanguageModelOptions = z.infer< diff --git a/src/mistral-chat-prompt.ts b/src/mistral-chat-prompt.ts -index 13f1dced55ac4be084128127a57fbdd58115bc28..ed553c0b61e327b95d99675e54b3dd94514ce62f 100644 +index 13f1dced55ac4be084128127a57fbdd58115bc28..172b11dde3dd326c2f3befd99237474ed8c79285 100644 --- a/src/mistral-chat-prompt.ts +++ b/src/mistral-chat-prompt.ts @@ -23,7 +23,7 @@ export type MistralUserMessageContent = @@ -698,7 +677,7 @@ index 13f1dced55ac4be084128127a57fbdd58115bc28..ed553c0b61e327b95d99675e54b3dd94 prefix?: boolean; tool_calls?: Array<{ id: string; -@@ -32,6 +32,22 @@ export interface MistralAssistantMessage { +@@ -32,6 +32,29 @@ export interface MistralAssistantMessage { }>; } @@ -715,7 +694,14 @@ index 13f1dced55ac4be084128127a57fbdd58115bc28..ed553c0b61e327b95d99675e54b3dd94 + +export type MistralThinkingContent = + | { type: 'text'; text: string } -+ | { type: 'tool_reference'; url?: string | null } ++ | { ++ type: 'tool_reference'; ++ tool: string; ++ title: string; ++ url?: string | null; ++ favicon?: string | null; ++ description?: string | null; ++ } + | { type: 'reference'; reference_ids: Array }; + export interface MistralToolMessage {