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/core/test/provider-mistral.test.ts b/packages/core/test/provider-mistral.test.ts index 58904ad3b121..6e3176695f67 100644 --- a/packages/core/test/provider-mistral.test.ts +++ b/packages/core/test/provider-mistral.test.ts @@ -26,3 +26,257 @@ test("Mistral sends promptCacheKey as prompt_cache_key", async () => { expect(body?.prompt_cache_key).toBe("session-123") }) + +test("Mistral round-trips native reasoning in assistant history", async () => { + let body: { messages?: unknown[] } | undefined + const mockFetch = Object.assign( + async (_input: Parameters[0], init?: RequestInit) => { + body = JSON.parse(String(init?.body)) + return Response.json({ + id: "response-1", + created: 0, + model: "mistral-small-latest", + object: "chat.completion", + choices: [ + { + index: 0, + message: { + role: "assistant", + content: [ + { + type: "thinking", + thinking: [ + { type: "text", text: "The user is greeting me." }, + { + type: "tool_reference", + tool: "web_search", + title: "Example result", + url: "https://example.com/tool", + favicon: "https://example.com/favicon.ico", + description: "Example description", + }, + { type: "reference", reference_ids: [1, "source-2"] }, + ], + closed: true, + signature: "sig-123", + }, + { type: "text", text: "Hi" }, + ], + }, + finish_reason: "stop", + }, + ], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + }) + }, + { preconnect: fetch.preconnect }, + ) + const model = createMistral({ apiKey: "test", fetch: mockFetch })("mistral-small-latest") + + const first = await model.doGenerate({ + prompt: [{ role: "user", content: [{ type: "text", text: "Hello" }] }], + }) + const reasoning = first.content.find((part) => part.type === "reasoning") + const text = first.content.find((part) => part.type === "text") + if (!reasoning || !text) throw new Error("expected reasoning and text") + + await model.doGenerate({ + prompt: [ + { role: "user", content: [{ type: "text", text: "Hello" }] }, + { + role: "assistant", + content: [{ ...reasoning, providerOptions: reasoning.providerMetadata }, text], + }, + { role: "user", content: [{ type: "text", text: "Hello again" }] }, + ], + }) + + expect(body?.messages?.[1]).toEqual({ + role: "assistant", + content: [ + { + type: "thinking", + thinking: [ + { type: "text", text: "The user is greeting me." }, + { + type: "tool_reference", + tool: "web_search", + title: "Example result", + url: "https://example.com/tool", + favicon: "https://example.com/favicon.ico", + description: "Example description", + }, + { type: "reference", reference_ids: [1, "source-2"] }, + ], + closed: true, + signature: "sig-123", + }, + { type: "text", text: "Hi" }, + ], + }) + + await model.doGenerate({ + prompt: [ + { role: "user", content: [{ type: "text", text: "Hello" }] }, + { + role: "assistant", + content: [ + { type: "reasoning", text: "thinking" }, + { type: "text", text: "Hi" }, + ], + }, + { role: "user", content: [{ type: "text", text: "Hello again" }] }, + ], + }) + expect(body?.messages?.[1]).toEqual({ role: "assistant", content: "thinkingHi" }) +}) + +test("Mistral preserves native reasoning metadata while streaming", async () => { + const chunks = [ + { + id: "response-1", + created: 0, + model: "mistral-small-latest", + choices: [ + { + index: 0, + delta: { + role: "assistant", + content: [ + { + type: "thinking", + thinking: [ + { type: "text", text: "thinking" }, + { + type: "tool_reference", + tool: "web_search", + title: "Example result", + url: "https://example.com/tool", + favicon: "https://example.com/favicon.ico", + description: "Example description", + }, + ], + }, + ], + }, + }, + ], + }, + { + id: "response-1", + created: 0, + model: "mistral-small-latest", + choices: [ + { + index: 0, + delta: { + content: [ + { + type: "thinking", + thinking: [{ type: "reference", reference_ids: [1, "source-2"] }], + closed: true, + signature: "sig-123", + }, + ], + }, + }, + ], + }, + { + id: "response-1", + created: 0, + model: "mistral-small-latest", + choices: [{ index: 0, delta: { content: [{ type: "text", text: "answer" }] } }], + }, + { + id: "response-1", + created: 0, + model: "mistral-small-latest", + choices: [{ index: 0, delta: {}, finish_reason: "stop" }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + }, + ] + const mockFetch = Object.assign( + async () => + new Response(chunks.map((chunk) => `data: ${JSON.stringify(chunk)}\n\n`).join(""), { + headers: { "Content-Type": "text/event-stream" }, + }), + { preconnect: fetch.preconnect }, + ) + const model = createMistral({ apiKey: "test", fetch: mockFetch })("mistral-small-latest") + const result = await model.doStream({ + prompt: [{ role: "user", content: [{ type: "text", text: "Hello" }] }], + }) + const events = [] + for await (const event of result.stream) events.push(event) + + expect(events.find((event) => event.type === "reasoning-end")?.providerMetadata).toEqual({ + mistral: { + thinking: { + type: "thinking", + thinking: [ + { type: "text", text: "thinking" }, + { + type: "tool_reference", + tool: "web_search", + title: "Example result", + url: "https://example.com/tool", + favicon: "https://example.com/favicon.ico", + description: "Example description", + }, + { type: "reference", reference_ids: [1, "source-2"] }, + ], + closed: true, + signature: "sig-123", + }, + }, + }) + expect( + events + .filter((event) => event.type === "reasoning-start" || event.type === "reasoning-delta") + .every((event) => event.providerMetadata === undefined), + ).toBe(true) +}) + +test("Mistral preserves metadata-only thinking chunks", async () => { + const thinking = { + type: "thinking" as const, + thinking: [ + { + type: "tool_reference", + tool: "web_search", + title: "Example result", + url: "https://example.com/tool", + favicon: "https://example.com/favicon.ico", + description: "Example description", + }, + { type: "reference", reference_ids: [1, "source-2"] }, + ], + closed: true, + signature: "sig-123", + } + const mockFetch = Object.assign( + async () => + Response.json({ + id: "response-1", + created: 0, + model: "mistral-small-latest", + object: "chat.completion", + choices: [{ index: 0, message: { role: "assistant", content: [thinking] }, finish_reason: "stop" }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + }), + { preconnect: fetch.preconnect }, + ) + const model = createMistral({ apiKey: "test", fetch: mockFetch })("mistral-small-latest") + const result = await model.doGenerate({ + prompt: [{ role: "user", content: [{ type: "text", text: "Hello" }] }], + }) + + expect(result.content).toEqual([ + { + type: "reasoning", + text: "", + providerMetadata: { mistral: { thinking } }, + }, + ]) +}) 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/packages/opencode/test/session/llm.test.ts b/packages/opencode/test/session/llm.test.ts index 61aac13ac395..3bfc722e2bec 100644 --- a/packages/opencode/test/session/llm.test.ts +++ b/packages/opencode/test/session/llm.test.ts @@ -906,6 +906,117 @@ describe("session.llm.stream", () => { }, ) + const mistralFixture = { providerID: "mistral", modelID: "mistral-small-latest" } + it.instance( + "replays native Mistral reasoning from chat history", + () => + Effect.gen(function* () { + const fixture = loadFixture(mistralFixture.providerID, mistralFixture.modelID) + const request = waitRequest( + "/chat/completions", + createEventResponse( + [ + { + id: "chatcmpl-mistral", + object: "chat.completion.chunk", + created: 0, + model: fixture.model.id, + choices: [{ index: 0, delta: { role: "assistant", content: "Hello" } }], + }, + { + id: "chatcmpl-mistral", + object: "chat.completion.chunk", + created: 0, + model: fixture.model.id, + choices: [{ index: 0, delta: {}, finish_reason: "stop" }], + }, + ], + true, + ), + ) + + const resolved = yield* Provider.use.getModel( + ProviderV2.ID.make(mistralFixture.providerID), + ModelV2.ID.make(fixture.model.id), + ) + const sessionID = SessionID.make("session-test-mistral-reasoning") + const agent = { + name: "test", + mode: "primary", + options: {}, + permission: [{ permission: "*", pattern: "*", action: "allow" }], + } satisfies Agent.Info + + const user = { + id: MessageID.make("msg_user-mistral-reasoning"), + sessionID, + role: "user", + time: { created: Date.now() }, + agent: agent.name, + model: { providerID: ProviderV2.ID.make(mistralFixture.providerID), modelID: resolved.id }, + } satisfies SessionV1.User + + const thinking = { + type: "thinking", + thinking: [ + { type: "text", text: "thinking" }, + { + type: "tool_reference", + tool: "web_search", + title: "Example result", + url: "https://example.com/tool", + favicon: "https://example.com/favicon.ico", + description: "Example description", + }, + { type: "reference", reference_ids: [1, "source-2"] }, + ], + closed: true, + signature: "sig-123", + } + + yield* drain({ + user, + sessionID, + model: resolved, + agent, + system: ["You are a helpful assistant."], + messages: [ + { role: "user", content: "Hello" }, + { + role: "assistant", + content: [ + { + type: "reasoning", + text: "thinking", + providerOptions: { mistral: { thinking } }, + }, + { type: "text", text: "Previous answer" }, + ], + }, + { role: "user", content: "Continue" }, + ] satisfies ModelMessage[], + tools: {}, + }) + + const capture = yield* Effect.promise(() => request) + const messages = capture.body.messages as Array> + 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.34.patch b/patches/@ai-sdk%2Fmistral@3.0.34.patch deleted file mode 100644 index 1d771f4fd9f7..000000000000 --- a/patches/@ai-sdk%2Fmistral@3.0.34.patch +++ /dev/null @@ -1,84 +0,0 @@ -diff --git a/dist/index.d.ts b/dist/index.d.ts -index 1ca9113bed2728a616db773a8e08d8d6957447d7..15408ec429dc210b5fa43589d81b69c93bf27b2d 100644 ---- a/dist/index.d.ts -+++ b/dist/index.d.ts -@@ -14,6 +14,7 @@ declare const mistralLanguageModelOptions: z.ZodObject<{ - none: "none"; - high: "high"; - }>>; -+ promptCacheKey: z.ZodOptional; - }, z.core.$strip>; - type MistralLanguageModelOptions = z.infer; - -diff --git a/dist/index.js b/dist/index.js -index 45735e524aaff54ea058c99c729c5ffd3c507058..6aca5f6f13da0054ede31c1f1a692e4eaed37d34 100644 ---- a/dist/index.js -+++ b/dist/index.js -@@ -268,7 +268,8 @@ var mistralLanguageModelOptions = import_v4.z.object({ - * - `'high'`: Enable reasoning - * - `'none'`: Disable reasoning - */ -- reasoningEffort: import_v4.z.enum(["high", "none"]).optional() -+ reasoningEffort: import_v4.z.enum(["high", "none"]).optional(), -+ promptCacheKey: import_v4.z.string().optional() - }); - - // src/mistral-error.ts -@@ -413,6 +414,7 @@ var MistralChatLanguageModel = class { - top_p: topP, - random_seed: seed, - reasoning_effort: options.reasoningEffort, -+ prompt_cache_key: options.promptCacheKey, - // response format: - response_format: (responseFormat == null ? void 0 : responseFormat.type) === "json" ? structuredOutputs && (responseFormat == null ? void 0 : responseFormat.schema) != null ? { - type: "json_schema", -diff --git a/dist/index.mjs b/dist/index.mjs -index 4c22df1cd78a1ba81309c8a86ceecefef4ba4aea..30cd3b1f503860109b7fa2107cd1eb17b70c96be 100644 ---- a/dist/index.mjs -+++ b/dist/index.mjs -@@ -256,7 +256,8 @@ var mistralLanguageModelOptions = z.object({ - * - `'high'`: Enable reasoning - * - `'none'`: Disable reasoning - */ -- reasoningEffort: z.enum(["high", "none"]).optional() -+ reasoningEffort: z.enum(["high", "none"]).optional(), -+ promptCacheKey: z.string().optional() - }); - - // src/mistral-error.ts -@@ -403,6 +404,7 @@ var MistralChatLanguageModel = class { - top_p: topP, - random_seed: seed, - reasoning_effort: options.reasoningEffort, -+ prompt_cache_key: options.promptCacheKey, - // response format: - response_format: (responseFormat == null ? void 0 : responseFormat.type) === "json" ? structuredOutputs && (responseFormat == null ? void 0 : responseFormat.schema) != null ? { - type: "json_schema", -diff --git a/src/mistral-chat-language-model.ts b/src/mistral-chat-language-model.ts -index 480c472d534bedbe8897979673453bd1c29a70b7..e46496da94f7d4af9822897202ca6baae67dae3a 100644 ---- a/src/mistral-chat-language-model.ts -+++ b/src/mistral-chat-language-model.ts -@@ -129,6 +129,7 @@ export class MistralChatLanguageModel implements LanguageModelV3 { - top_p: topP, - random_seed: seed, - reasoning_effort: options.reasoningEffort, -+ prompt_cache_key: options.promptCacheKey, - - // response format: - response_format: -diff --git a/src/mistral-chat-options.ts b/src/mistral-chat-options.ts -index 80fff45fba2c378fa06962f071946bcd2b882a0b..b4fdfa51f3bf11a4220e010c8aca92482cb0c3db 100644 ---- a/src/mistral-chat-options.ts -+++ b/src/mistral-chat-options.ts -@@ -62,6 +62,11 @@ export const mistralLanguageModelOptions = z.object({ - * - `'none'`: Disable reasoning - */ - reasoningEffort: z.enum(['high', 'none']).optional(), -+ -+ /** -+ * A stable identifier used to route requests with shared prompt prefixes. -+ */ -+ promptCacheKey: z.string().optional(), - }); - - export type MistralLanguageModelOptions = z.infer< diff --git a/patches/@ai-sdk%2Fmistral@3.0.51.patch b/patches/@ai-sdk%2Fmistral@3.0.51.patch new file mode 100644 index 000000000000..141b14a689b1 --- /dev/null +++ b/patches/@ai-sdk%2Fmistral@3.0.51.patch @@ -0,0 +1,709 @@ +diff --git a/dist/index.d.mts b/dist/index.d.mts +index 1bde0b9f8cbe6771a52c1041095c9dddfe8e5b6c..0ca2ffb2a0c9327aed5ddcf0004500dc8b42569f 100644 +--- a/dist/index.d.mts ++++ b/dist/index.d.mts +@@ -14,6 +14,7 @@ declare const mistralLanguageModelOptions: z.ZodObject<{ + none: "none"; + high: "high"; + }>>; ++ promptCacheKey: z.ZodOptional; + }, z.core.$strip>; + type MistralLanguageModelOptions = z.infer; + +diff --git a/dist/index.d.ts b/dist/index.d.ts +index 1bde0b9f8cbe6771a52c1041095c9dddfe8e5b6c..0ca2ffb2a0c9327aed5ddcf0004500dc8b42569f 100644 +--- a/dist/index.d.ts ++++ b/dist/index.d.ts +@@ -14,6 +14,7 @@ declare const mistralLanguageModelOptions: z.ZodObject<{ + none: "none"; + high: "high"; + }>>; ++ promptCacheKey: z.ZodOptional; + }, z.core.$strip>; + type MistralLanguageModelOptions = z.infer; + +diff --git a/dist/index.js b/dist/index.js +index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..267f34e20ea392b7a85ad5259d72d50605a6f971 100644 +--- a/dist/index.js ++++ b/dist/index.js +@@ -128,11 +128,14 @@ function convertToMistralChatMessages(prompt) { + } + case "assistant": { + let text = ""; ++ const structuredContent = []; ++ let hasNativeReasoning = false; + const toolCalls = []; + for (const part of content) { + switch (part.type) { + case "text": { + text += part.text; ++ structuredContent.push({ type: "text", text: part.text }); + break; + } + case "tool-call": { +@@ -148,6 +151,13 @@ function convertToMistralChatMessages(prompt) { + } + case "reasoning": { + text += part.text; ++ const native = part.providerOptions?.mistral?.thinking; ++ if (native?.type === "thinking") { ++ hasNativeReasoning = true; ++ structuredContent.push(native); ++ break; ++ } ++ structuredContent.push({ type: "text", text: part.text }); + break; + } + default: { +@@ -159,7 +169,7 @@ function convertToMistralChatMessages(prompt) { + } + messages.push({ + role: "assistant", +- content: text, ++ content: hasNativeReasoning ? structuredContent : text, + prefix: isLastMessage ? true : void 0, + tool_calls: toolCalls.length > 0 ? toolCalls : void 0 + }); +@@ -268,7 +278,8 @@ var mistralLanguageModelOptions = import_v4.z.object({ + * - `'high'`: Enable reasoning + * - `'none'`: Disable reasoning + */ +- reasoningEffort: import_v4.z.enum(["high", "none"]).optional() ++ reasoningEffort: import_v4.z.enum(["high", "none"]).optional(), ++ promptCacheKey: import_v4.z.string().optional() + }); + + // src/mistral-error.ts +@@ -407,6 +418,7 @@ var MistralChatLanguageModel = class { + stop: stopSequences, + random_seed: seed, + reasoning_effort: options.reasoningEffort, ++ prompt_cache_key: options.promptCacheKey, + // response format: + response_format: (responseFormat == null ? void 0 : responseFormat.type) === "json" ? structuredOutputs && (responseFormat == null ? void 0 : responseFormat.schema) != null ? { + type: "json_schema", +@@ -465,9 +477,11 @@ var MistralChatLanguageModel = class { + for (const part of choice.message.content) { + if (part.type === "thinking") { + const reasoningText = extractReasoningContent(part.thinking); +- if (reasoningText.length > 0) { +- content.push({ type: "reasoning", text: reasoningText }); +- } ++ content.push({ ++ type: "reasoning", ++ text: reasoningText, ++ providerMetadata: { mistral: { thinking: part } } ++ }); + } else if (part.type === "text") { + if (part.text.length > 0) { + content.push({ type: "text", text: part.text }); +@@ -528,6 +542,7 @@ var MistralChatLanguageModel = class { + let isFirstChunk = true; + let activeText = false; + let activeReasoningId = null; ++ let activeThinking = null; + const generateId2 = this.generateId; + return { + stream: response.pipeThrough( +@@ -561,18 +576,19 @@ var MistralChatLanguageModel = class { + for (const part of delta.content) { + if (part.type === "thinking") { + const reasoningDelta = extractReasoningContent(part.thinking); +- if (reasoningDelta.length > 0) { +- if (activeReasoningId == null) { +- if (activeText) { +- controller.enqueue({ type: "text-end", id: "0" }); +- activeText = false; +- } +- activeReasoningId = generateId2(); +- controller.enqueue({ +- type: "reasoning-start", +- id: activeReasoningId +- }); ++ activeThinking = mergeThinking(activeThinking, part); ++ if (activeReasoningId == null) { ++ if (activeText) { ++ controller.enqueue({ type: "text-end", id: "0" }); ++ activeText = false; + } ++ activeReasoningId = generateId2(); ++ controller.enqueue({ ++ type: "reasoning-start", ++ id: activeReasoningId ++ }); ++ } ++ if (reasoningDelta.length > 0) { + controller.enqueue({ + type: "reasoning-delta", + id: activeReasoningId, +@@ -587,9 +603,11 @@ var MistralChatLanguageModel = class { + if (activeReasoningId != null) { + controller.enqueue({ + type: "reasoning-end", +- id: activeReasoningId ++ id: activeReasoningId, ++ providerMetadata: { mistral: { thinking: activeThinking } } + }); + activeReasoningId = null; ++ activeThinking = null; + } + controller.enqueue({ type: "text-start", id: "0" }); + activeText = true; +@@ -638,7 +656,8 @@ var MistralChatLanguageModel = class { + if (activeReasoningId != null) { + controller.enqueue({ + type: "reasoning-end", +- id: activeReasoningId ++ id: activeReasoningId, ++ providerMetadata: { mistral: { thinking: activeThinking } } + }); + } + if (activeText) { +@@ -660,6 +679,13 @@ var MistralChatLanguageModel = class { + function extractReasoningContent(thinking) { + return thinking.filter((chunk) => chunk.type === "text").map((chunk) => chunk.text).join(""); + } ++function mergeThinking(current, next) { ++ if (current === null) return { ...next, thinking: [...next.thinking] }; ++ current.thinking.push(...next.thinking); ++ if (next.closed !== void 0) current.closed = next.closed; ++ if (next.signature !== void 0) current.signature = next.signature; ++ return current; ++} + function extractTextContent(content) { + if (typeof content === "string") { + return content; +@@ -686,6 +712,30 @@ function extractTextContent(content) { + } + return textContent.length ? textContent.join("") : void 0; + } ++var mistralThinkingContentSchema = import_v43.z.discriminatedUnion("type", [ ++ import_v43.z.object({ ++ type: import_v43.z.literal("text"), ++ text: import_v43.z.string() ++ }), ++ import_v43.z.object({ ++ type: import_v43.z.literal("tool_reference"), ++ tool: import_v43.z.string(), ++ title: import_v43.z.string(), ++ url: import_v43.z.string().nullish(), ++ favicon: import_v43.z.string().nullish(), ++ description: import_v43.z.string().nullish() ++ }), ++ import_v43.z.object({ ++ type: import_v43.z.literal("reference"), ++ reference_ids: import_v43.z.array(import_v43.z.union([import_v43.z.string(), import_v43.z.number().int()])) ++ }) ++]); ++var mistralThinkChunkSchema = import_v43.z.object({ ++ type: import_v43.z.literal("thinking"), ++ thinking: import_v43.z.array(mistralThinkingContentSchema), ++ closed: import_v43.z.boolean().optional(), ++ signature: import_v43.z.string().nullish() ++}); + var mistralContentSchema = import_v43.z.union([ + import_v43.z.string(), + import_v43.z.array( +@@ -708,15 +758,7 @@ var mistralContentSchema = import_v43.z.union([ + type: import_v43.z.literal("reference"), + reference_ids: import_v43.z.array(import_v43.z.union([import_v43.z.string(), import_v43.z.number()])) + }), +- import_v43.z.object({ +- type: import_v43.z.literal("thinking"), +- thinking: import_v43.z.array( +- import_v43.z.object({ +- type: import_v43.z.literal("text"), +- text: import_v43.z.string() +- }) +- ) +- }) ++ mistralThinkChunkSchema + ]) + ) + ]).nullish(); +diff --git a/dist/index.mjs b/dist/index.mjs +index d2eff622c1b84a96bdeb4012cb0206a33012a04d..3bff11ddd6136ada45809568828cbc8f2493a42a 100644 +--- a/dist/index.mjs ++++ b/dist/index.mjs +@@ -116,11 +116,14 @@ function convertToMistralChatMessages(prompt) { + } + case "assistant": { + let text = ""; ++ const structuredContent = []; ++ let hasNativeReasoning = false; + const toolCalls = []; + for (const part of content) { + switch (part.type) { + case "text": { + text += part.text; ++ structuredContent.push({ type: "text", text: part.text }); + break; + } + case "tool-call": { +@@ -136,6 +139,13 @@ function convertToMistralChatMessages(prompt) { + } + case "reasoning": { + text += part.text; ++ const native = part.providerOptions?.mistral?.thinking; ++ if (native?.type === "thinking") { ++ hasNativeReasoning = true; ++ structuredContent.push(native); ++ break; ++ } ++ structuredContent.push({ type: "text", text: part.text }); + break; + } + default: { +@@ -147,7 +157,7 @@ function convertToMistralChatMessages(prompt) { + } + messages.push({ + role: "assistant", +- content: text, ++ content: hasNativeReasoning ? structuredContent : text, + prefix: isLastMessage ? true : void 0, + tool_calls: toolCalls.length > 0 ? toolCalls : void 0 + }); +@@ -256,7 +266,8 @@ var mistralLanguageModelOptions = z.object({ + * - `'high'`: Enable reasoning + * - `'none'`: Disable reasoning + */ +- reasoningEffort: z.enum(["high", "none"]).optional() ++ reasoningEffort: z.enum(["high", "none"]).optional(), ++ promptCacheKey: z.string().optional() + }); + + // src/mistral-error.ts +@@ -397,6 +408,7 @@ var MistralChatLanguageModel = class { + stop: stopSequences, + random_seed: seed, + reasoning_effort: options.reasoningEffort, ++ prompt_cache_key: options.promptCacheKey, + // response format: + response_format: (responseFormat == null ? void 0 : responseFormat.type) === "json" ? structuredOutputs && (responseFormat == null ? void 0 : responseFormat.schema) != null ? { + type: "json_schema", +@@ -455,9 +467,11 @@ var MistralChatLanguageModel = class { + for (const part of choice.message.content) { + if (part.type === "thinking") { + const reasoningText = extractReasoningContent(part.thinking); +- if (reasoningText.length > 0) { +- content.push({ type: "reasoning", text: reasoningText }); +- } ++ content.push({ ++ type: "reasoning", ++ text: reasoningText, ++ providerMetadata: { mistral: { thinking: part } } ++ }); + } else if (part.type === "text") { + if (part.text.length > 0) { + content.push({ type: "text", text: part.text }); +@@ -518,6 +532,7 @@ var MistralChatLanguageModel = class { + let isFirstChunk = true; + let activeText = false; + let activeReasoningId = null; ++ let activeThinking = null; + const generateId2 = this.generateId; + return { + stream: response.pipeThrough( +@@ -551,18 +566,19 @@ var MistralChatLanguageModel = class { + for (const part of delta.content) { + if (part.type === "thinking") { + const reasoningDelta = extractReasoningContent(part.thinking); +- if (reasoningDelta.length > 0) { +- if (activeReasoningId == null) { +- if (activeText) { +- controller.enqueue({ type: "text-end", id: "0" }); +- activeText = false; +- } +- activeReasoningId = generateId2(); +- controller.enqueue({ +- type: "reasoning-start", +- id: activeReasoningId +- }); ++ activeThinking = mergeThinking(activeThinking, part); ++ if (activeReasoningId == null) { ++ if (activeText) { ++ controller.enqueue({ type: "text-end", id: "0" }); ++ activeText = false; + } ++ activeReasoningId = generateId2(); ++ controller.enqueue({ ++ type: "reasoning-start", ++ id: activeReasoningId ++ }); ++ } ++ if (reasoningDelta.length > 0) { + controller.enqueue({ + type: "reasoning-delta", + id: activeReasoningId, +@@ -577,9 +593,11 @@ var MistralChatLanguageModel = class { + if (activeReasoningId != null) { + controller.enqueue({ + type: "reasoning-end", +- id: activeReasoningId ++ id: activeReasoningId, ++ providerMetadata: { mistral: { thinking: activeThinking } } + }); + activeReasoningId = null; ++ activeThinking = null; + } + controller.enqueue({ type: "text-start", id: "0" }); + activeText = true; +@@ -628,7 +646,8 @@ var MistralChatLanguageModel = class { + if (activeReasoningId != null) { + controller.enqueue({ + type: "reasoning-end", +- id: activeReasoningId ++ id: activeReasoningId, ++ providerMetadata: { mistral: { thinking: activeThinking } } + }); + } + if (activeText) { +@@ -650,6 +669,13 @@ var MistralChatLanguageModel = class { + function extractReasoningContent(thinking) { + return thinking.filter((chunk) => chunk.type === "text").map((chunk) => chunk.text).join(""); + } ++function mergeThinking(current, next) { ++ if (current === null) return { ...next, thinking: [...next.thinking] }; ++ current.thinking.push(...next.thinking); ++ if (next.closed !== void 0) current.closed = next.closed; ++ if (next.signature !== void 0) current.signature = next.signature; ++ return current; ++} + function extractTextContent(content) { + if (typeof content === "string") { + return content; +@@ -676,6 +702,30 @@ function extractTextContent(content) { + } + return textContent.length ? textContent.join("") : void 0; + } ++var mistralThinkingContentSchema = z3.discriminatedUnion("type", [ ++ z3.object({ ++ type: z3.literal("text"), ++ text: z3.string() ++ }), ++ z3.object({ ++ type: z3.literal("tool_reference"), ++ tool: z3.string(), ++ title: z3.string(), ++ url: z3.string().nullish(), ++ favicon: z3.string().nullish(), ++ description: z3.string().nullish() ++ }), ++ z3.object({ ++ type: z3.literal("reference"), ++ reference_ids: z3.array(z3.union([z3.string(), z3.number().int()])) ++ }) ++]); ++var mistralThinkChunkSchema = z3.object({ ++ type: z3.literal("thinking"), ++ thinking: z3.array(mistralThinkingContentSchema), ++ closed: z3.boolean().optional(), ++ signature: z3.string().nullish() ++}); + var mistralContentSchema = z3.union([ + z3.string(), + z3.array( +@@ -698,15 +748,7 @@ var mistralContentSchema = z3.union([ + type: z3.literal("reference"), + reference_ids: z3.array(z3.union([z3.string(), z3.number()])) + }), +- z3.object({ +- type: z3.literal("thinking"), +- thinking: z3.array( +- z3.object({ +- type: z3.literal("text"), +- text: z3.string() +- }) +- ) +- }) ++ mistralThinkChunkSchema + ]) + ) + ]).nullish(); +diff --git a/src/convert-to-mistral-chat-messages.ts b/src/convert-to-mistral-chat-messages.ts +index 3c6914f8da615d7517bc43dd56198298d0a50247..8cd6f4c7577f746ef41e8a0aee682234c473667a 100644 +--- a/src/convert-to-mistral-chat-messages.ts ++++ b/src/convert-to-mistral-chat-messages.ts +@@ -3,7 +3,11 @@ import { + type LanguageModelV3DataContent, + type LanguageModelV3Prompt, + } from '@ai-sdk/provider'; +-import type { MistralPrompt } from './mistral-chat-prompt'; ++import type { ++ MistralAssistantMessageContent, ++ MistralPrompt, ++ MistralThinkChunk, ++} from './mistral-chat-prompt'; + import { convertToBase64 } from '@ai-sdk/provider-utils'; + + function formatFileUrl({ +@@ -76,6 +80,8 @@ export function convertToMistralChatMessages( + + case 'assistant': { + let text = ''; ++ const structuredContent: Array = []; ++ let hasNativeReasoning = false; + const toolCalls: Array<{ + id: string; + type: 'function'; +@@ -86,6 +92,7 @@ export function convertToMistralChatMessages( + switch (part.type) { + case 'text': { + text += part.text; ++ structuredContent.push({ type: 'text', text: part.text }); + break; + } + case 'tool-call': { +@@ -101,6 +108,14 @@ export function convertToMistralChatMessages( + } + case 'reasoning': { + text += part.text; ++ const native = part.providerOptions?.mistral ++ ?.thinking as MistralThinkChunk | undefined; ++ if (native?.type === 'thinking') { ++ hasNativeReasoning = true; ++ structuredContent.push(native); ++ break; ++ } ++ structuredContent.push({ type: 'text', text: part.text }); + break; + } + default: { +@@ -113,7 +128,7 @@ export function convertToMistralChatMessages( + + messages.push({ + role: 'assistant', +- content: text, ++ content: hasNativeReasoning ? structuredContent : text, + prefix: isLastMessage ? true : undefined, + tool_calls: toolCalls.length > 0 ? toolCalls : undefined, + }); +diff --git a/src/mistral-chat-language-model.ts b/src/mistral-chat-language-model.ts +index 7e4a7ab552f1b41b7074e1b3cada8a51d791268d..847d26f9dfe03572a969a122f8c96b8bbfda8066 100644 +--- a/src/mistral-chat-language-model.ts ++++ b/src/mistral-chat-language-model.ts +@@ -122,6 +122,7 @@ export class MistralChatLanguageModel implements LanguageModelV3 { + stop: stopSequences, + random_seed: seed, + reasoning_effort: options.reasoningEffort, ++ prompt_cache_key: options.promptCacheKey, + + // response format: + response_format: +@@ -201,9 +202,11 @@ export class MistralChatLanguageModel implements LanguageModelV3 { + for (const part of choice.message.content) { + if (part.type === 'thinking') { + const reasoningText = extractReasoningContent(part.thinking); +- if (reasoningText.length > 0) { +- content.push({ type: 'reasoning', text: reasoningText }); +- } ++ content.push({ ++ type: 'reasoning', ++ text: reasoningText, ++ providerMetadata: { mistral: { thinking: part } }, ++ }); + } else if (part.type === 'text') { + if (part.text.length > 0) { + content.push({ type: 'text', text: part.text }); +@@ -278,6 +281,7 @@ export class MistralChatLanguageModel implements LanguageModelV3 { + let isFirstChunk = true; + let activeText = false; + let activeReasoningId: string | null = null; ++ let activeThinking: z.infer | null = null; + + const generateId = this.generateId; + +@@ -326,20 +330,21 @@ export class MistralChatLanguageModel implements LanguageModelV3 { + for (const part of delta.content) { + if (part.type === 'thinking') { + const reasoningDelta = extractReasoningContent(part.thinking); +- if (reasoningDelta.length > 0) { +- if (activeReasoningId == null) { +- // end any active text before starting reasoning +- if (activeText) { +- controller.enqueue({ type: 'text-end', id: '0' }); +- activeText = false; +- } +- +- activeReasoningId = generateId(); +- controller.enqueue({ +- type: 'reasoning-start', +- id: activeReasoningId, +- }); ++ activeThinking = mergeThinking(activeThinking, part); ++ if (activeReasoningId == null) { ++ // end any active text before starting reasoning ++ if (activeText) { ++ controller.enqueue({ type: 'text-end', id: '0' }); ++ activeText = false; + } ++ ++ activeReasoningId = generateId(); ++ controller.enqueue({ ++ type: 'reasoning-start', ++ id: activeReasoningId, ++ }); ++ } ++ if (reasoningDelta.length > 0) { + controller.enqueue({ + type: 'reasoning-delta', + id: activeReasoningId, +@@ -357,8 +362,12 @@ export class MistralChatLanguageModel implements LanguageModelV3 { + controller.enqueue({ + type: 'reasoning-end', + id: activeReasoningId, ++ providerMetadata: { ++ mistral: { thinking: activeThinking }, ++ }, + }); + activeReasoningId = null; ++ activeThinking = null; + } + controller.enqueue({ type: 'text-start', id: '0' }); + activeText = true; +@@ -416,6 +425,9 @@ export class MistralChatLanguageModel implements LanguageModelV3 { + controller.enqueue({ + type: 'reasoning-end', + id: activeReasoningId, ++ providerMetadata: { ++ mistral: { thinking: activeThinking }, ++ }, + }); + } + if (activeText) { +@@ -437,7 +449,7 @@ export class MistralChatLanguageModel implements LanguageModelV3 { + } + + function extractReasoningContent( +- thinking: Array<{ type: string; text: string }>, ++ thinking: Array>, + ) { + return thinking + .filter(chunk => chunk.type === 'text') +@@ -445,6 +457,17 @@ function extractReasoningContent( + .join(''); + } + ++function mergeThinking( ++ current: z.infer | null, ++ next: z.infer, ++) { ++ 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 +501,32 @@ 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'), ++ tool: z.string(), ++ title: z.string(), ++ url: z.string().nullish(), ++ favicon: z.string().nullish(), ++ description: z.string().nullish(), ++ }), ++ z.object({ ++ type: z.literal('reference'), ++ reference_ids: z.array(z.union([z.string(), z.number().int()])), ++ }), ++]); ++ ++const mistralThinkChunkSchema = z.object({ ++ type: z.literal('thinking'), ++ thinking: z.array(mistralThinkingContentSchema), ++ closed: z.boolean().optional(), ++ signature: z.string().nullish(), ++}); ++ + const mistralContentSchema = z + .union([ + z.string(), +@@ -501,15 +550,7 @@ const mistralContentSchema = z + type: z.literal('reference'), + reference_ids: z.array(z.union([z.string(), z.number()])), + }), +- z.object({ +- type: z.literal('thinking'), +- thinking: z.array( +- z.object({ +- type: z.literal('text'), +- text: z.string(), +- }), +- ), +- }), ++ mistralThinkChunkSchema, + ]), + ), + ]) +diff --git a/src/mistral-chat-options.ts b/src/mistral-chat-options.ts +index 54b29c08517d348995b6ca093b11160e453d5c8b..de30c3e7d924889339e38b1067cb26e9ada05d11 100644 +--- a/src/mistral-chat-options.ts ++++ b/src/mistral-chat-options.ts +@@ -64,6 +64,11 @@ export const mistralLanguageModelOptions = z.object({ + * - `'none'`: Disable reasoning + */ + reasoningEffort: z.enum(['high', 'none']).optional(), ++ ++ /** ++ * A stable identifier used to route requests with shared prompt prefixes. ++ */ ++ promptCacheKey: z.string().optional(), + }); + + export type MistralLanguageModelOptions = z.infer< +diff --git a/src/mistral-chat-prompt.ts b/src/mistral-chat-prompt.ts +index 13f1dced55ac4be084128127a57fbdd58115bc28..172b11dde3dd326c2f3befd99237474ed8c79285 100644 +--- a/src/mistral-chat-prompt.ts ++++ b/src/mistral-chat-prompt.ts +@@ -23,7 +23,7 @@ export type MistralUserMessageContent = + + export interface MistralAssistantMessage { + role: 'assistant'; +- content: string; ++ content: string | Array; + prefix?: boolean; + tool_calls?: Array<{ + id: string; +@@ -32,6 +32,29 @@ export interface MistralAssistantMessage { + }>; + } + ++export type MistralAssistantMessageContent = ++ | { type: 'text'; text: string } ++ | MistralThinkChunk; ++ ++export type MistralThinkChunk = { ++ type: 'thinking'; ++ thinking: Array; ++ closed?: boolean; ++ signature?: string | null; ++}; ++ ++export type MistralThinkingContent = ++ | { type: 'text'; text: string } ++ | { ++ type: 'tool_reference'; ++ tool: string; ++ title: string; ++ url?: string | null; ++ favicon?: string | null; ++ description?: string | null; ++ } ++ | { type: 'reference'; reference_ids: Array }; ++ + export interface MistralToolMessage { + role: 'tool'; + name: string;