Skip to content

Commit 9fbaf51

Browse files
committed
fix: handle Mistral thinking type in streaming responses
- Filter out "thinking" type from content arrays in Mistral responses - Only yield text content when actual text is present - Add comprehensive test coverage for thinking type handling - Fixes #6842: Magistral responses failing validation due to unsupported thinking type
1 parent ad0e33e commit 9fbaf51

File tree

2 files changed

+94
-3
lines changed

2 files changed

+94
-3
lines changed

src/api/providers/__tests__/mistral.spec.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,86 @@ describe("MistralHandler", () => {
118118
expect(results[0].text).toBe("Test response")
119119
})
120120

121+
it("should handle thinking type in content array", async () => {
122+
// Override the mock to return content with thinking type
123+
mockCreate.mockImplementationOnce(async (_options) => {
124+
const stream = {
125+
[Symbol.asyncIterator]: async function* () {
126+
yield {
127+
data: {
128+
choices: [
129+
{
130+
delta: {
131+
content: [
132+
{ type: "thinking", text: "Let me think about this..." },
133+
{ type: "text", text: "Here is my response" },
134+
],
135+
},
136+
index: 0,
137+
},
138+
],
139+
},
140+
}
141+
},
142+
}
143+
return stream
144+
})
145+
146+
const iterator = handler.createMessage(systemPrompt, messages)
147+
const results: ApiStreamTextChunk[] = []
148+
149+
for await (const chunk of iterator) {
150+
if ("text" in chunk) {
151+
results.push(chunk as ApiStreamTextChunk)
152+
}
153+
}
154+
155+
// Should only include the text type content, not thinking
156+
expect(results.length).toBe(1)
157+
expect(results[0].text).toBe("Here is my response")
158+
})
159+
160+
it("should handle mixed content types gracefully", async () => {
161+
// Override the mock to return various content types
162+
mockCreate.mockImplementationOnce(async (_options) => {
163+
const stream = {
164+
[Symbol.asyncIterator]: async function* () {
165+
yield {
166+
data: {
167+
choices: [
168+
{
169+
delta: {
170+
content: [
171+
{ type: "thinking", text: "Processing..." },
172+
{ type: "text", text: "First part" },
173+
{ type: "unknown", data: "some data" },
174+
{ type: "text", text: " Second part" },
175+
],
176+
},
177+
index: 0,
178+
},
179+
],
180+
},
181+
}
182+
},
183+
}
184+
return stream
185+
})
186+
187+
const iterator = handler.createMessage(systemPrompt, messages)
188+
const results: ApiStreamTextChunk[] = []
189+
190+
for await (const chunk of iterator) {
191+
if ("text" in chunk) {
192+
results.push(chunk as ApiStreamTextChunk)
193+
}
194+
}
195+
196+
// Should concatenate only text type content
197+
expect(results.length).toBe(1)
198+
expect(results[0].text).toBe("First part Second part")
199+
})
200+
121201
it("should handle errors gracefully", async () => {
122202
mockCreate.mockRejectedValueOnce(new Error("API Error"))
123203
await expect(handler.createMessage(systemPrompt, messages).next()).rejects.toThrow("API Error")

src/api/providers/mistral.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,17 @@ export class MistralHandler extends BaseProvider implements SingleCompletionHand
5757
if (typeof delta.content === "string") {
5858
content = delta.content
5959
} else if (Array.isArray(delta.content)) {
60-
content = delta.content.map((c) => (c.type === "text" ? c.text : "")).join("")
60+
// Handle array content, filtering out "thinking" type and other non-text types
61+
content = delta.content
62+
.filter((c: any) => c.type === "text")
63+
.map((c: any) => c.text || "")
64+
.join("")
6165
}
6266

63-
yield { type: "text", text: content }
67+
// Only yield if we have actual content to send
68+
if (content) {
69+
yield { type: "text", text: content }
70+
}
6471
}
6572

6673
if (chunk.data.usage) {
@@ -97,7 +104,11 @@ export class MistralHandler extends BaseProvider implements SingleCompletionHand
97104
const content = response.choices?.[0]?.message.content
98105

99106
if (Array.isArray(content)) {
100-
return content.map((c) => (c.type === "text" ? c.text : "")).join("")
107+
// Handle array content, filtering out "thinking" type and other non-text types
108+
return content
109+
.filter((c: any) => c.type === "text")
110+
.map((c: any) => c.text || "")
111+
.join("")
101112
}
102113

103114
return content || ""

0 commit comments

Comments
 (0)