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
This commit is contained in:
Roo Code 2025-08-08 10:36:22 +00:00
parent ad0e33e2d9
commit 9fbaf516d8
2 changed files with 94 additions and 3 deletions

View file

@ -118,6 +118,86 @@ describe("MistralHandler", () => {
expect(results[0].text).toBe("Test response")
})
it("should handle thinking type in content array", async () => {
// Override the mock to return content with thinking type
mockCreate.mockImplementationOnce(async (_options) => {
const stream = {
[Symbol.asyncIterator]: async function* () {
yield {
data: {
choices: [
{
delta: {
content: [
{ type: "thinking", text: "Let me think about this..." },
{ type: "text", text: "Here is my response" },
],
},
index: 0,
},
],
},
}
},
}
return stream
})
const iterator = handler.createMessage(systemPrompt, messages)
const results: ApiStreamTextChunk[] = []
for await (const chunk of iterator) {
if ("text" in chunk) {
results.push(chunk as ApiStreamTextChunk)
}
}
// Should only include the text type content, not thinking
expect(results.length).toBe(1)
expect(results[0].text).toBe("Here is my response")
})
it("should handle mixed content types gracefully", async () => {
// Override the mock to return various content types
mockCreate.mockImplementationOnce(async (_options) => {
const stream = {
[Symbol.asyncIterator]: async function* () {
yield {
data: {
choices: [
{
delta: {
content: [
{ type: "thinking", text: "Processing..." },
{ type: "text", text: "First part" },
{ type: "unknown", data: "some data" },
{ type: "text", text: " Second part" },
],
},
index: 0,
},
],
},
}
},
}
return stream
})
const iterator = handler.createMessage(systemPrompt, messages)
const results: ApiStreamTextChunk[] = []
for await (const chunk of iterator) {
if ("text" in chunk) {
results.push(chunk as ApiStreamTextChunk)
}
}
// Should concatenate only text type content
expect(results.length).toBe(1)
expect(results[0].text).toBe("First part Second part")
})
it("should handle errors gracefully", async () => {
mockCreate.mockRejectedValueOnce(new Error("API Error"))
await expect(handler.createMessage(systemPrompt, messages).next()).rejects.toThrow("API Error")

View file

@ -57,10 +57,17 @@ export class MistralHandler extends BaseProvider implements SingleCompletionHand
if (typeof delta.content === "string") {
content = delta.content
} else if (Array.isArray(delta.content)) {
content = delta.content.map((c) => (c.type === "text" ? c.text : "")).join("")
// Handle array content, filtering out "thinking" type and other non-text types
content = delta.content
.filter((c: any) => c.type === "text")
.map((c: any) => c.text || "")
.join("")
}
yield { type: "text", text: content }
// Only yield if we have actual content to send
if (content) {
yield { type: "text", text: content }
}
}
if (chunk.data.usage) {
@ -97,7 +104,11 @@ export class MistralHandler extends BaseProvider implements SingleCompletionHand
const content = response.choices?.[0]?.message.content
if (Array.isArray(content)) {
return content.map((c) => (c.type === "text" ? c.text : "")).join("")
// Handle array content, filtering out "thinking" type and other non-text types
return content
.filter((c: any) => c.type === "text")
.map((c: any) => c.text || "")
.join("")
}
return content || ""