Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 51 additions & 3 deletions src/api/providers/__tests__/mistral.spec.ts
Original file line number Diff line number Diff line change
@@ -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(() => ({
Expand Down Expand Up @@ -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"
Expand All @@ -59,6 +74,7 @@ describe("MistralHandler", () => {
handler = new MistralHandler(mockOptions)
mockCreate.mockClear()
mockComplete.mockClear()
mockCaptureException.mockClear()
})

describe("constructor", () => {
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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")
})
})
})
36 changes: 24 additions & 12 deletions src/api/providers/mistral.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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}`)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider preserving the original error’s stack trace by attaching the caught error as a cause when rethrowing (e.g. using the { cause: error } option) to provide better debugging context.

Suggested change
throw new Error(`Mistral completion error: ${errorMessage}`)
throw new Error(`Mistral completion error: ${errorMessage}`, { cause: error })

}

for await (const event of response) {
const delta = event.data.choices[0]?.delta
Expand Down Expand Up @@ -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 }],
Expand All @@ -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}`)
}
}
}
Loading