diff --git a/packages/ai/src/protocols/mistral-chat.ts b/packages/ai/src/protocols/mistral-chat.ts index b1b1720f204c..99f20487341a 100644 --- a/packages/ai/src/protocols/mistral-chat.ts +++ b/packages/ai/src/protocols/mistral-chat.ts @@ -426,6 +426,7 @@ interface ActiveContent { readonly type: "text" | "reasoning" readonly id: string readonly thinking?: MistralThinkingContent + readonly thinkingUnits?: MistralThinkingUnit[] } export interface ParserState { @@ -502,8 +503,8 @@ const closeActive = (state: ParserState, events: LLMEvent[]) => { state.lifecycle, events, state.active.id, - thinkingMetadata(state.active.thinking ?? { type: "thinking", thinking: [] }), - thinkingText(state.active.thinking?.thinking ?? []), + thinkingMetadata({ ...state.active.thinking, type: "thinking", thinking: state.active.thinkingUnits ?? [] }), + thinkingText(state.active.thinkingUnits ?? []), ) return { ...state, lifecycle, active: undefined } } @@ -524,20 +525,23 @@ const appendThinking = (state: ParserState, events: LLMEvent[], part: MistralOut const current = state.active?.type === "reasoning" ? state : closeActive(state, events) const units = thinkingUnits(part.thinking) const active = current.active ?? { type: "reasoning" as const, id: `reasoning-${current.nextContent}` } + // Keep native units out of streamed events until the block is complete. + const accumulated = active.thinkingUnits ?? [] + accumulated.push(...units) const thinking = { ...active.thinking, ...part, type: "thinking" as const, - thinking: [...(active.thinking?.thinking ?? []), ...units], + thinking: [], } const text = thinkingText(units) return { ...current, lifecycle: text.length > 0 - ? Lifecycle.reasoningDelta(current.lifecycle, events, active.id, text, thinkingMetadata(thinking)) - : Lifecycle.reasoningStart(current.lifecycle, events, active.id, thinkingMetadata(thinking)), - active: { ...active, thinking }, + ? Lifecycle.reasoningDelta(current.lifecycle, events, active.id, text) + : Lifecycle.reasoningStart(current.lifecycle, events, active.id), + active: { ...active, thinking, thinkingUnits: accumulated }, nextContent: current.active ? current.nextContent : current.nextContent + 1, } } diff --git a/packages/ai/test/provider/mistral-chat.test.ts b/packages/ai/test/provider/mistral-chat.test.ts index c8e51d3cff69..fddfee4a9f58 100644 --- a/packages/ai/test/provider/mistral-chat.test.ts +++ b/packages/ai/test/provider/mistral-chat.test.ts @@ -303,7 +303,8 @@ describe("Mistral Chat", () => { fixedResponse( sseEvents( chunk({ content: [{ type: "thinking", thinking: [], marker: "empty" }] }), - chunk({ content: [{ type: "thinking", thinking: [{ type: "text", text: "Consider" }] }] }), + chunk({ content: [{ type: "thinking", thinking: [{ type: "text", text: "Con" }] }] }), + chunk({ content: [{ type: "thinking", thinking: [{ type: "text", text: "sider" }], closed: true }] }), chunk({ content: [{ type: "text", text: "Answer" }] }), chunk({}, "stop"), ), @@ -313,6 +314,11 @@ describe("Mistral Chat", () => { expect(response.reasoning).toBe("Consider") expect(response.text).toBe("Answer") + expect(response.events.find(LLMEvent.is.reasoningStart)?.providerMetadata).toBeUndefined() + expect(response.events.filter(LLMEvent.is.reasoningDelta).map((event) => [event.text, event.providerMetadata])).toEqual([ + ["Con", undefined], + ["sider", undefined], + ]) expect(response.message.content).toEqual([ { type: "reasoning", @@ -321,8 +327,12 @@ describe("Mistral Chat", () => { mistral: { thinking: { type: "thinking", - thinking: [{ type: "text", text: "Consider" }], + thinking: [ + { type: "text", text: "Con" }, + { type: "text", text: "sider" }, + ], marker: "empty", + closed: true, }, }, }, @@ -337,8 +347,12 @@ describe("Mistral Chat", () => { content: [ { type: "thinking", - thinking: [{ type: "text", text: "Consider" }], + thinking: [ + { type: "text", text: "Con" }, + { type: "text", text: "sider" }, + ], marker: "empty", + closed: true, }, { type: "text", text: "Answer" }, ], @@ -357,6 +371,8 @@ describe("Mistral Chat", () => { ), ), ) + expect(response.events.find(LLMEvent.is.reasoningStart)?.providerMetadata).toBeUndefined() + expect(response.events.filter(LLMEvent.is.reasoningDelta)).toEqual([]) expect(response.message.content).toEqual([ { type: "reasoning",