Skip to content

Commit fd96ae8

Browse files
committed
fix: handle Mistral thinking content chunks in streaming responses
- Added ContentChunkWithThinking type helper to handle thinking chunks - Properly converts thinking content to reasoning chunks in streaming - Filters out thinking content in non-streaming completePrompt responses - Confirmed that Mistral API does send thinking chunks with type 'thinking' - Works with Mistral SDK v1.9.18
1 parent a449209 commit fd96ae8

File tree

4 files changed

+44
-39
lines changed

4 files changed

+44
-39
lines changed

pnpm-lock.yaml

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ describe("MistralHandler", () => {
137137
})
138138

139139
it("should handle thinking content as reasoning chunks", async () => {
140-
// Mock stream with thinking content
140+
// Mock stream with thinking content matching new SDK structure
141141
mockCreate.mockImplementationOnce(async (_options) => {
142142
const stream = {
143143
[Symbol.asyncIterator]: async function* () {
@@ -147,7 +147,10 @@ describe("MistralHandler", () => {
147147
{
148148
delta: {
149149
content: [
150-
{ type: "thinking", text: "Let me think about this..." },
150+
{
151+
type: "thinking",
152+
thinking: [{ type: "text", text: "Let me think about this..." }],
153+
},
151154
{ type: "text", text: "Here's the answer" },
152155
],
153156
},
@@ -176,7 +179,7 @@ describe("MistralHandler", () => {
176179
})
177180

178181
it("should handle mixed content arrays correctly", async () => {
179-
// Mock stream with mixed content
182+
// Mock stream with mixed content matching new SDK structure
180183
mockCreate.mockImplementationOnce(async (_options) => {
181184
const stream = {
182185
[Symbol.asyncIterator]: async function* () {
@@ -187,7 +190,10 @@ describe("MistralHandler", () => {
187190
delta: {
188191
content: [
189192
{ type: "text", text: "First text" },
190-
{ type: "thinking", text: "Some reasoning" },
193+
{
194+
type: "thinking",
195+
thinking: [{ type: "text", text: "Some reasoning" }],
196+
},
191197
{ type: "text", text: "Second text" },
192198
],
193199
},

src/api/providers/mistral.ts

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,14 @@ import { ApiStream } from "../transform/stream"
1111
import { BaseProvider } from "./base-provider"
1212
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
1313

14-
// Define TypeScript interfaces for Mistral content types
15-
interface MistralTextContent {
16-
type: "text"
17-
text: string
14+
// Type helper to handle thinking chunks from Mistral API
15+
// The SDK includes ThinkChunk but TypeScript has trouble with the discriminated union
16+
type ContentChunkWithThinking = {
17+
type: string
18+
text?: string
19+
thinking?: Array<{ type: string; text?: string }>
1820
}
1921

20-
interface MistralThinkingContent {
21-
type: "thinking"
22-
text: string
23-
}
24-
25-
type MistralContent = MistralTextContent | MistralThinkingContent | string
26-
2722
export class MistralHandler extends BaseProvider implements SingleCompletionHandler {
2823
protected options: ApiHandlerOptions
2924
private client: Mistral
@@ -61,34 +56,38 @@ export class MistralHandler extends BaseProvider implements SingleCompletionHand
6156
temperature,
6257
})
6358

64-
for await (const chunk of response) {
65-
const delta = chunk.data.choices[0]?.delta
59+
for await (const event of response) {
60+
const delta = event.data.choices[0]?.delta
6661

6762
if (delta?.content) {
6863
if (typeof delta.content === "string") {
6964
// Handle string content as text
7065
yield { type: "text", text: delta.content }
7166
} else if (Array.isArray(delta.content)) {
72-
// Handle array of content blocks
73-
for (const c of delta.content as MistralContent[]) {
74-
if (typeof c === "object" && c !== null) {
75-
if (c.type === "thinking" && c.text) {
76-
// Handle thinking content as reasoning chunks
77-
yield { type: "reasoning", text: c.text }
78-
} else if (c.type === "text" && c.text) {
79-
// Handle text content normally
80-
yield { type: "text", text: c.text }
67+
// Handle array of content chunks
68+
// The SDK v1.9.18 supports ThinkChunk with type "thinking"
69+
for (const chunk of delta.content as ContentChunkWithThinking[]) {
70+
if (chunk.type === "thinking" && chunk.thinking) {
71+
// Handle thinking content as reasoning chunks
72+
// ThinkChunk has a 'thinking' property that contains an array of text/reference chunks
73+
for (const thinkingPart of chunk.thinking) {
74+
if (thinkingPart.type === "text" && thinkingPart.text) {
75+
yield { type: "reasoning", text: thinkingPart.text }
76+
}
8177
}
78+
} else if (chunk.type === "text" && chunk.text) {
79+
// Handle text content normally
80+
yield { type: "text", text: chunk.text }
8281
}
8382
}
8483
}
8584
}
8685

87-
if (chunk.data.usage) {
86+
if (event.data.usage) {
8887
yield {
8988
type: "usage",
90-
inputTokens: chunk.data.usage.promptTokens || 0,
91-
outputTokens: chunk.data.usage.completionTokens || 0,
89+
inputTokens: event.data.usage.promptTokens || 0,
90+
outputTokens: event.data.usage.completionTokens || 0,
9291
}
9392
}
9493
}
@@ -119,9 +118,9 @@ export class MistralHandler extends BaseProvider implements SingleCompletionHand
119118

120119
if (Array.isArray(content)) {
121120
// Only return text content, filter out thinking content for non-streaming
122-
return content
123-
.filter((c: any) => typeof c === "object" && c !== null && c.type === "text")
124-
.map((c: any) => c.text || "")
121+
return (content as ContentChunkWithThinking[])
122+
.filter((c) => c.type === "text" && c.text)
123+
.map((c) => c.text || "")
125124
.join("")
126125
}
127126

src/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@
424424
"@aws-sdk/credential-providers": "^3.848.0",
425425
"@google/genai": "^1.0.0",
426426
"@lmstudio/sdk": "^1.1.1",
427-
"@mistralai/mistralai": "^1.3.6",
427+
"@mistralai/mistralai": "^1.9.18",
428428
"@modelcontextprotocol/sdk": "^1.9.0",
429429
"@qdrant/js-client-rest": "^1.14.0",
430430
"@roo-code/cloud": "^0.14.0",

0 commit comments

Comments
 (0)