From 4199a2506de5789899d23a85d980a87d2bac82f3 Mon Sep 17 00:00:00 2001 From: 404oops Date: Thu, 27 Aug 2026 23:14:51 +0200 Subject: [PATCH] feat(ai): forward Mistral prompt_mode so magistral can emit separable thinking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live-probing magistral-small-latest showed the model inlines its reasoning as answer prose in a flat string — no ThinkChunk content, no markers, nothing a client can separate. Mistral's chunked thinking shape is requested via `prompt_mode: 'reasoning'`, which the provider previously dropped on the floor: there was no way to even ask for it. `custom.prompt_mode` now forwards to the SDK's `promptMode`, following the BytePlus custom-params precedent. Opt-in rather than a default because the API rejects the mode where the account/model lacks it ('Reasoning prompt mode is not enabled for this model', code 3051) — verified end-to-end: the 3051 travels back through the stack, which also proves the parameter is delivered. The moment Mistral enables the mode, the ThinkChunk content flows into the existing splitter and comes out as `message.reasoning` and `reasoning` stream chunks with no further changes. Co-Authored-By: Claude Fable 5 (1M context) --- .../mistral/MistralAiProvider.test.ts | 45 +++++++++++++++++++ .../providers/mistral/MistralAiProvider.ts | 15 +++++++ 2 files changed, 60 insertions(+) diff --git a/src/backend/drivers/ai-chat/providers/mistral/MistralAiProvider.test.ts b/src/backend/drivers/ai-chat/providers/mistral/MistralAiProvider.test.ts index 3c38ba1f8..54da23411 100644 --- a/src/backend/drivers/ai-chat/providers/mistral/MistralAiProvider.test.ts +++ b/src/backend/drivers/ai-chat/providers/mistral/MistralAiProvider.test.ts @@ -210,6 +210,51 @@ describe('MistralAIProvider.complete request shape', () => { expect(args.temperature).toBe(0.4); }); + it('forwards custom.prompt_mode as the SDK promptMode', async () => { + const { provider } = makeProvider(); + completeMock.mockResolvedValueOnce({ + choices: [ + { + message: { role: 'assistant', content: 'ok' }, + finishReason: 'stop', + }, + ], + usage: { promptTokens: 1, completionTokens: 1 }, + }); + + await withTestActor(() => + provider.complete({ + model: 'magistral-small-latest', + messages: [{ role: 'user', content: 'think' }], + custom: { prompt_mode: 'reasoning' }, + }), + ); + + expect(completeMock.mock.calls[0]![0].promptMode).toBe('reasoning'); + }); + + it('omits promptMode when custom does not carry prompt_mode', async () => { + const { provider } = makeProvider(); + completeMock.mockResolvedValueOnce({ + choices: [ + { + message: { role: 'assistant', content: 'ok' }, + finishReason: 'stop', + }, + ], + usage: { promptTokens: 1, completionTokens: 1 }, + }); + + await withTestActor(() => + provider.complete({ + model: 'mistral-small-2603', + messages: [{ role: 'user', content: 'hi' }], + }), + ); + + expect('promptMode' in completeMock.mock.calls[0]![0]).toBe(false); + }); + it('omits the `tools` key when no tools are supplied', async () => { const { provider } = makeProvider(); completeMock.mockResolvedValueOnce(baseCompletion); diff --git a/src/backend/drivers/ai-chat/providers/mistral/MistralAiProvider.ts b/src/backend/drivers/ai-chat/providers/mistral/MistralAiProvider.ts index 96de9da5a..1618cb8be 100644 --- a/src/backend/drivers/ai-chat/providers/mistral/MistralAiProvider.ts +++ b/src/backend/drivers/ai-chat/providers/mistral/MistralAiProvider.ts @@ -152,7 +152,19 @@ export class MistralAIProvider implements IChatProvider { temperature, normalize, response, + custom, }: ICompleteArguments): Promise { + // Mistral's reasoning prompt mode: with `prompt_mode: 'reasoning'`, + // magistral models return their thinking as structured ThinkChunk + // content (which the splitter below separates into `reasoning`) + // instead of inlining it as answer prose. Opt-in passthrough rather + // than a default because the API rejects it on accounts/models where + // the mode is not enabled ('Reasoning prompt mode is not enabled for + // this model', code 3051). + const customParams = + custom && typeof custom === 'object' && !Array.isArray(custom) + ? (custom as { prompt_mode?: 'reasoning' | null }) + : {}; messages = await OpenAIUtil.process_input_messages(messages); messages = this.#coerceImageUrls(messages); for (const message of messages) { @@ -177,6 +189,9 @@ export class MistralAIProvider implements IChatProvider { ]({ model: selectedModel.id, ...(tools ? { tools: tools as any[] } : {}), + ...(customParams.prompt_mode !== undefined + ? { promptMode: customParams.prompt_mode } + : {}), messages, maxTokens: max_tokens, temperature,