Skip to content

Commit cf5c75d

Browse files
committed
feat: add telemetry error capture to Mistral provider
- Import TelemetryService and ApiProviderError - Add providerName property to MistralHandler class - Wrap createMessage() client.chat.stream() call in try/catch with telemetry capture - Add telemetry capture in completePrompt() catch block - Add tests to verify telemetry is called on error for both methods Follows the pattern established in PR #10073 for OpenRouter provider.
1 parent bf81fa7 commit cf5c75d

File tree

2 files changed

+75
-15
lines changed

2 files changed

+75
-15
lines changed

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

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1+
// Hoist mock functions so they can be used in vi.mock factories
2+
const { mockCreate, mockComplete, mockCaptureException } = vi.hoisted(() => ({
3+
mockCreate: vi.fn(),
4+
mockComplete: vi.fn(),
5+
mockCaptureException: vi.fn(),
6+
}))
7+
18
// Mock Mistral client - must come before other imports
2-
const mockCreate = vi.fn()
3-
const mockComplete = vi.fn()
49
vi.mock("@mistralai/mistralai", () => {
510
return {
611
Mistral: vi.fn().mockImplementation(() => ({
@@ -38,8 +43,18 @@ vi.mock("@mistralai/mistralai", () => {
3843
}
3944
})
4045

46+
// Mock TelemetryService
47+
vi.mock("@roo-code/telemetry", () => ({
48+
TelemetryService: {
49+
instance: {
50+
captureException: mockCaptureException,
51+
},
52+
},
53+
}))
54+
4155
import type { Anthropic } from "@anthropic-ai/sdk"
4256
import type OpenAI from "openai"
57+
import { ApiProviderError } from "@roo-code/types"
4358
import { MistralHandler } from "../mistral"
4459
import type { ApiHandlerOptions } from "../../../shared/api"
4560
import type { ApiHandlerCreateMessageMetadata } from "../../index"
@@ -59,6 +74,7 @@ describe("MistralHandler", () => {
5974
handler = new MistralHandler(mockOptions)
6075
mockCreate.mockClear()
6176
mockComplete.mockClear()
77+
mockCaptureException.mockClear()
6278
})
6379

6480
describe("constructor", () => {
@@ -135,7 +151,24 @@ describe("MistralHandler", () => {
135151

136152
it("should handle errors gracefully", async () => {
137153
mockCreate.mockRejectedValueOnce(new Error("API Error"))
138-
await expect(handler.createMessage(systemPrompt, messages).next()).rejects.toThrow("API Error")
154+
await expect(handler.createMessage(systemPrompt, messages).next()).rejects.toThrow(
155+
"Mistral completion error: API Error",
156+
)
157+
})
158+
159+
it("should capture telemetry exception on createMessage error", async () => {
160+
mockCreate.mockRejectedValueOnce(new Error("API Error"))
161+
162+
await expect(handler.createMessage(systemPrompt, messages).next()).rejects.toThrow()
163+
164+
expect(mockCaptureException).toHaveBeenCalledTimes(1)
165+
expect(mockCaptureException).toHaveBeenCalledWith(expect.any(ApiProviderError))
166+
167+
const capturedError = mockCaptureException.mock.calls[0][0] as ApiProviderError
168+
expect(capturedError.message).toBe("API Error")
169+
expect(capturedError.provider).toBe("Mistral")
170+
expect(capturedError.modelId).toBe("codestral-latest")
171+
expect(capturedError.operation).toBe("createMessage")
139172
})
140173

141174
it("should handle thinking content as reasoning chunks", async () => {
@@ -483,5 +516,20 @@ describe("MistralHandler", () => {
483516
mockComplete.mockRejectedValueOnce(new Error("API Error"))
484517
await expect(handler.completePrompt("Test prompt")).rejects.toThrow("Mistral completion error: API Error")
485518
})
519+
520+
it("should capture telemetry exception on completePrompt error", async () => {
521+
mockComplete.mockRejectedValueOnce(new Error("API Error"))
522+
523+
await expect(handler.completePrompt("Test prompt")).rejects.toThrow()
524+
525+
expect(mockCaptureException).toHaveBeenCalledTimes(1)
526+
expect(mockCaptureException).toHaveBeenCalledWith(expect.any(ApiProviderError))
527+
528+
const capturedError = mockCaptureException.mock.calls[0][0] as ApiProviderError
529+
expect(capturedError.message).toBe("API Error")
530+
expect(capturedError.provider).toBe("Mistral")
531+
expect(capturedError.modelId).toBe("codestral-latest")
532+
expect(capturedError.operation).toBe("completePrompt")
533+
})
486534
})
487535
})

src/api/providers/mistral.ts

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@ import { Anthropic } from "@anthropic-ai/sdk"
22
import { Mistral } from "@mistralai/mistralai"
33
import OpenAI from "openai"
44

5-
import { type MistralModelId, mistralDefaultModelId, mistralModels, MISTRAL_DEFAULT_TEMPERATURE } from "@roo-code/types"
5+
import {
6+
type MistralModelId,
7+
mistralDefaultModelId,
8+
mistralModels,
9+
MISTRAL_DEFAULT_TEMPERATURE,
10+
ApiProviderError,
11+
} from "@roo-code/types"
12+
import { TelemetryService } from "@roo-code/telemetry"
613

714
import { ApiHandlerOptions } from "../../shared/api"
815

@@ -43,6 +50,7 @@ type MistralTool = {
4350
export class MistralHandler extends BaseProvider implements SingleCompletionHandler {
4451
protected options: ApiHandlerOptions
4552
private client: Mistral
53+
private readonly providerName = "Mistral"
4654

4755
constructor(options: ApiHandlerOptions) {
4856
super()
@@ -93,10 +101,15 @@ export class MistralHandler extends BaseProvider implements SingleCompletionHand
93101
requestOptions.toolChoice = "any"
94102
}
95103

96-
// Temporary debug log for QA
97-
// console.log("[MISTRAL DEBUG] Raw API request body:", requestOptions)
98-
99-
const response = await this.client.chat.stream(requestOptions)
104+
let response
105+
try {
106+
response = await this.client.chat.stream(requestOptions)
107+
} catch (error) {
108+
const errorMessage = error instanceof Error ? error.message : String(error)
109+
const apiError = new ApiProviderError(errorMessage, this.providerName, model, "createMessage")
110+
TelemetryService.instance.captureException(apiError)
111+
throw new Error(`Mistral completion error: ${errorMessage}`)
112+
}
100113

101114
for await (const event of response) {
102115
const delta = event.data.choices[0]?.delta
@@ -181,9 +194,9 @@ export class MistralHandler extends BaseProvider implements SingleCompletionHand
181194
}
182195

183196
async completePrompt(prompt: string): Promise<string> {
184-
try {
185-
const { id: model, temperature } = this.getModel()
197+
const { id: model, temperature } = this.getModel()
186198

199+
try {
187200
const response = await this.client.chat.complete({
188201
model,
189202
messages: [{ role: "user", content: prompt }],
@@ -202,11 +215,10 @@ export class MistralHandler extends BaseProvider implements SingleCompletionHand
202215

203216
return content || ""
204217
} catch (error) {
205-
if (error instanceof Error) {
206-
throw new Error(`Mistral completion error: ${error.message}`)
207-
}
208-
209-
throw error
218+
const errorMessage = error instanceof Error ? error.message : String(error)
219+
const apiError = new ApiProviderError(errorMessage, this.providerName, model, "completePrompt")
220+
TelemetryService.instance.captureException(apiError)
221+
throw new Error(`Mistral completion error: ${errorMessage}`)
210222
}
211223
}
212224
}

0 commit comments

Comments
 (0)