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.
This commit is contained in:
daniel-lxs 2025-12-15 18:07:51 -05:00
parent bf81fa7237
commit cf5c75db36
No known key found for this signature in database
GPG key ID: 21C74479048B3AA6
2 changed files with 75 additions and 15 deletions

View file

@ -1,6 +1,11 @@
// Hoist mock functions so they can be used in vi.mock factories
const { mockCreate, mockComplete, mockCaptureException } = vi.hoisted(() => ({
mockCreate: vi.fn(),
mockComplete: vi.fn(),
mockCaptureException: vi.fn(),
}))
// Mock Mistral client - must come before other imports
const mockCreate = vi.fn()
const mockComplete = vi.fn()
vi.mock("@mistralai/mistralai", () => {
return {
Mistral: vi.fn().mockImplementation(() => ({
@ -38,8 +43,18 @@ vi.mock("@mistralai/mistralai", () => {
}
})
// Mock TelemetryService
vi.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureException: mockCaptureException,
},
},
}))
import type { Anthropic } from "@anthropic-ai/sdk"
import type OpenAI from "openai"
import { ApiProviderError } from "@roo-code/types"
import { MistralHandler } from "../mistral"
import type { ApiHandlerOptions } from "../../../shared/api"
import type { ApiHandlerCreateMessageMetadata } from "../../index"
@ -59,6 +74,7 @@ describe("MistralHandler", () => {
handler = new MistralHandler(mockOptions)
mockCreate.mockClear()
mockComplete.mockClear()
mockCaptureException.mockClear()
})
describe("constructor", () => {
@ -135,7 +151,24 @@ describe("MistralHandler", () => {
it("should handle errors gracefully", async () => {
mockCreate.mockRejectedValueOnce(new Error("API Error"))
await expect(handler.createMessage(systemPrompt, messages).next()).rejects.toThrow("API Error")
await expect(handler.createMessage(systemPrompt, messages).next()).rejects.toThrow(
"Mistral completion error: API Error",
)
})
it("should capture telemetry exception on createMessage error", async () => {
mockCreate.mockRejectedValueOnce(new Error("API Error"))
await expect(handler.createMessage(systemPrompt, messages).next()).rejects.toThrow()
expect(mockCaptureException).toHaveBeenCalledTimes(1)
expect(mockCaptureException).toHaveBeenCalledWith(expect.any(ApiProviderError))
const capturedError = mockCaptureException.mock.calls[0][0] as ApiProviderError
expect(capturedError.message).toBe("API Error")
expect(capturedError.provider).toBe("Mistral")
expect(capturedError.modelId).toBe("codestral-latest")
expect(capturedError.operation).toBe("createMessage")
})
it("should handle thinking content as reasoning chunks", async () => {
@ -483,5 +516,20 @@ describe("MistralHandler", () => {
mockComplete.mockRejectedValueOnce(new Error("API Error"))
await expect(handler.completePrompt("Test prompt")).rejects.toThrow("Mistral completion error: API Error")
})
it("should capture telemetry exception on completePrompt error", async () => {
mockComplete.mockRejectedValueOnce(new Error("API Error"))
await expect(handler.completePrompt("Test prompt")).rejects.toThrow()
expect(mockCaptureException).toHaveBeenCalledTimes(1)
expect(mockCaptureException).toHaveBeenCalledWith(expect.any(ApiProviderError))
const capturedError = mockCaptureException.mock.calls[0][0] as ApiProviderError
expect(capturedError.message).toBe("API Error")
expect(capturedError.provider).toBe("Mistral")
expect(capturedError.modelId).toBe("codestral-latest")
expect(capturedError.operation).toBe("completePrompt")
})
})
})

View file

@ -2,7 +2,14 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { Mistral } from "@mistralai/mistralai"
import OpenAI from "openai"
import { type MistralModelId, mistralDefaultModelId, mistralModels, MISTRAL_DEFAULT_TEMPERATURE } from "@roo-code/types"
import {
type MistralModelId,
mistralDefaultModelId,
mistralModels,
MISTRAL_DEFAULT_TEMPERATURE,
ApiProviderError,
} from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { ApiHandlerOptions } from "../../shared/api"
@ -43,6 +50,7 @@ type MistralTool = {
export class MistralHandler extends BaseProvider implements SingleCompletionHandler {
protected options: ApiHandlerOptions
private client: Mistral
private readonly providerName = "Mistral"
constructor(options: ApiHandlerOptions) {
super()
@ -93,10 +101,15 @@ export class MistralHandler extends BaseProvider implements SingleCompletionHand
requestOptions.toolChoice = "any"
}
// Temporary debug log for QA
// console.log("[MISTRAL DEBUG] Raw API request body:", requestOptions)
const response = await this.client.chat.stream(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
@ -181,9 +194,9 @@ export class MistralHandler extends BaseProvider implements SingleCompletionHand
}
async completePrompt(prompt: string): Promise<string> {
try {
const { id: model, temperature } = this.getModel()
const { id: model, temperature } = this.getModel()
try {
const response = await this.client.chat.complete({
model,
messages: [{ role: "user", content: prompt }],
@ -202,11 +215,10 @@ export class MistralHandler extends BaseProvider implements SingleCompletionHand
return content || ""
} catch (error) {
if (error instanceof Error) {
throw new Error(`Mistral completion error: ${error.message}`)
}
throw 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}`)
}
}
}