diff --git a/src/api/providers/__tests__/mistral.spec.ts b/src/api/providers/__tests__/mistral.spec.ts index 9c1a26763c1..029b25a29d1 100644 --- a/src/api/providers/__tests__/mistral.spec.ts +++ b/src/api/providers/__tests__/mistral.spec.ts @@ -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") + }) }) }) diff --git a/src/api/providers/mistral.ts b/src/api/providers/mistral.ts index 96d2c332552..57f9b405ace 100644 --- a/src/api/providers/mistral.ts +++ b/src/api/providers/mistral.ts @@ -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 { - 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}`) } } }