This repository was archived by the owner on May 15, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Expand file tree
/
Copy pathmistral.ts
More file actions
228 lines (198 loc) · 7.2 KB
/
Copy pathmistral.ts
File metadata and controls
228 lines (198 loc) · 7.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
import { Anthropic } from "@anthropic-ai/sdk"
import { Mistral } from "@mistralai/mistralai"
import OpenAI from "openai"
import {
type MistralModelId,
mistralDefaultModelId,
mistralModels,
MISTRAL_DEFAULT_TEMPERATURE,
ApiProviderError,
} from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { ApiHandlerOptions } from "../../shared/api"
import { convertToMistralMessages } from "../transform/mistral-format"
import { ApiStream } from "../transform/stream"
import { handleProviderError } from "./utils/error-handler"
import { BaseProvider } from "./base-provider"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
// Type helper to handle thinking chunks from Mistral API
// The SDK includes ThinkChunk but TypeScript has trouble with the discriminated union
type ContentChunkWithThinking = {
type: string
text?: string
thinking?: Array<{ type: string; text?: string }>
}
// Type for Mistral tool calls in stream delta
type MistralToolCall = {
id?: string
type?: string
function?: {
name?: string
arguments?: string
}
}
// Type for Mistral tool definition - matches Mistral SDK Tool type
type MistralTool = {
type: "function"
function: {
name: string
description?: string
parameters: Record<string, unknown>
}
}
export class MistralHandler extends BaseProvider implements SingleCompletionHandler {
protected options: ApiHandlerOptions
private client: Mistral
private readonly providerName = "Mistral"
constructor(options: ApiHandlerOptions) {
super()
if (!options.mistralApiKey) {
throw new Error("Mistral API key is required")
}
// Set default model ID if not provided.
const apiModelId = options.apiModelId || mistralDefaultModelId
this.options = { ...options, apiModelId }
this.client = new Mistral({
serverURL: apiModelId.startsWith("codestral-")
? this.options.mistralCodestralUrl || "https://codestral.mistral.ai"
: "https://api.mistral.ai",
apiKey: this.options.mistralApiKey,
})
}
override async *createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
metadata?: ApiHandlerCreateMessageMetadata,
): ApiStream {
const { id: model, info, maxTokens, temperature } = this.getModel()
// Build request options
const requestOptions: {
model: string
messages: ReturnType<typeof convertToMistralMessages>
maxTokens: number
temperature: number
tools?: MistralTool[]
toolChoice?: "auto" | "none" | "any" | "required" | { type: "function"; function: { name: string } }
} = {
model,
messages: [{ role: "system", content: systemPrompt }, ...convertToMistralMessages(messages)],
maxTokens: maxTokens ?? info.maxTokens,
temperature,
}
// Add tools if provided and toolProtocol is not 'xml' and model supports native tools
const supportsNativeTools = info.supportsNativeTools ?? false
if (metadata?.tools && metadata.tools.length > 0 && metadata?.toolProtocol !== "xml" && supportsNativeTools) {
requestOptions.tools = this.convertToolsForMistral(metadata.tools)
// Always use "any" to require tool use
requestOptions.toolChoice = "any"
}
// Temporary debug log for QA
// console.log("[MISTRAL DEBUG] Raw API request body:", requestOptions)
let response
try {
response = await this.client.chat.stream(requestOptions)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
const apiError = new ApiProviderError(errorMessage, this.providerName, model, "createMessage")
TelemetryService.instance.captureException(apiError)
throw new Error(`Mistral completion error: ${errorMessage}`)
}
for await (const event of response) {
const delta = event.data.choices[0]?.delta
if (delta?.content) {
if (typeof delta.content === "string") {
// Handle string content as text
yield { type: "text", text: delta.content }
} else if (Array.isArray(delta.content)) {
// Handle array of content chunks
// The SDK v1.9.18 supports ThinkChunk with type "thinking"
for (const chunk of delta.content as ContentChunkWithThinking[]) {
if (chunk.type === "thinking" && chunk.thinking) {
// Handle thinking content as reasoning chunks
// ThinkChunk has a 'thinking' property that contains an array of text/reference chunks
for (const thinkingPart of chunk.thinking) {
if (thinkingPart.type === "text" && thinkingPart.text) {
yield { type: "reasoning", text: thinkingPart.text }
}
}
} else if (chunk.type === "text" && chunk.text) {
// Handle text content normally
yield { type: "text", text: chunk.text }
}
}
}
}
// Handle tool calls in stream
// Mistral SDK provides tool_calls in delta similar to OpenAI format
const toolCalls = (delta as { toolCalls?: MistralToolCall[] })?.toolCalls
if (toolCalls) {
for (let i = 0; i < toolCalls.length; i++) {
const toolCall = toolCalls[i]
yield {
type: "tool_call_partial",
index: i,
id: toolCall.id,
name: toolCall.function?.name,
arguments: toolCall.function?.arguments,
}
}
}
if (event.data.usage) {
yield {
type: "usage",
inputTokens: event.data.usage.promptTokens || 0,
outputTokens: event.data.usage.completionTokens || 0,
}
}
}
}
/**
* Convert OpenAI tool definitions to Mistral format.
* Mistral uses the same format as OpenAI for function tools.
*/
private convertToolsForMistral(tools: OpenAI.Chat.ChatCompletionTool[]): MistralTool[] {
return tools
.filter((tool) => tool.type === "function")
.map((tool) => ({
type: "function" as const,
function: {
name: tool.function.name,
description: tool.function.description,
// Mistral SDK requires parameters to be defined, use empty object as fallback
parameters: (tool.function.parameters as Record<string, unknown>) || {},
},
}))
}
override getModel() {
const id = this.options.apiModelId ?? mistralDefaultModelId
const info = mistralModels[id as MistralModelId] ?? mistralModels[mistralDefaultModelId]
// @TODO: Move this to the `getModelParams` function.
const maxTokens = this.options.includeMaxTokens ? info.maxTokens : undefined
const temperature = this.options.modelTemperature ?? MISTRAL_DEFAULT_TEMPERATURE
return { id, info, maxTokens, temperature }
}
async completePrompt(prompt: string): Promise<string> {
const { id: model, temperature } = this.getModel()
try {
const response = await this.client.chat.complete({
model,
messages: [{ role: "user", content: prompt }],
temperature,
})
const content = response.choices?.[0]?.message.content
if (Array.isArray(content)) {
// Only return text content, filter out thinking content for non-streaming
return (content as ContentChunkWithThinking[])
.filter((c) => c.type === "text" && c.text)
.map((c) => c.text || "")
.join("")
}
return content || ""
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
const apiError = new ApiProviderError(errorMessage, this.providerName, model, "completePrompt")
TelemetryService.instance.captureException(apiError)
throw new Error(`Mistral completion error: ${errorMessage}`)
}
}
}