diff --git a/src/api/providers/__tests__/xai.spec.ts b/src/api/providers/__tests__/xai.spec.ts index 6bdf7fa044c..5015ff558b1 100644 --- a/src/api/providers/__tests__/xai.spec.ts +++ b/src/api/providers/__tests__/xai.spec.ts @@ -11,6 +11,16 @@ vitest.mock("openai", () => { } }) +const mockCaptureException = vitest.fn() + +vitest.mock("@roo-code/telemetry", () => ({ + TelemetryService: { + instance: { + captureException: (...args: unknown[]) => mockCaptureException(...args), + }, + }, +})) + import OpenAI from "openai" import type { Anthropic } from "@anthropic-ai/sdk" @@ -25,6 +35,7 @@ describe("XAIHandler", () => { // Reset all mocks vi.clearAllMocks() mockCreate.mockClear() + mockCaptureException.mockClear() // Create handler with mock handler = new XAIHandler({}) @@ -140,6 +151,66 @@ describe("XAIHandler", () => { await expect(handler.completePrompt("test prompt")).rejects.toThrow(`xAI completion error: ${errorMessage}`) }) + describe("Telemetry Error Capture", () => { + it("captures telemetry when createMessage throws an exception", async () => { + const errorMessage = "Connection failed" + mockCreate.mockRejectedValueOnce(new Error(errorMessage)) + + const generator = handler.createMessage("test prompt", []) + await expect(generator.next()).rejects.toThrow() + + expect(mockCaptureException).toHaveBeenCalledWith( + expect.objectContaining({ + message: errorMessage, + provider: "xAI", + modelId: xaiDefaultModelId, + operation: "createMessage", + }), + ) + }) + + it("captures telemetry when completePrompt throws an exception", async () => { + const errorMessage = "API error" + mockCreate.mockRejectedValueOnce(new Error(errorMessage)) + + await expect(handler.completePrompt("test prompt")).rejects.toThrow() + + expect(mockCaptureException).toHaveBeenCalledWith( + expect.objectContaining({ + message: errorMessage, + provider: "xAI", + modelId: xaiDefaultModelId, + operation: "completePrompt", + }), + ) + }) + + it("captures telemetry with correct model ID when using a specific model", async () => { + const specificModelHandler = new XAIHandler({ apiModelId: "grok-3" }) + const errorMessage = "Model error" + mockCreate.mockRejectedValueOnce(new Error(errorMessage)) + + await expect(specificModelHandler.completePrompt("test prompt")).rejects.toThrow() + + expect(mockCaptureException).toHaveBeenCalledWith( + expect.objectContaining({ + message: errorMessage, + provider: "xAI", + modelId: "grok-3", + operation: "completePrompt", + }), + ) + }) + + it("captures telemetry and still re-throws the error", async () => { + const errorMessage = "Network error" + mockCreate.mockRejectedValueOnce(new Error(errorMessage)) + + await expect(handler.completePrompt("test prompt")).rejects.toThrow(`xAI completion error: ${errorMessage}`) + expect(mockCaptureException).toHaveBeenCalledTimes(1) + }) + }) + it("createMessage should yield text content from stream", async () => { const testContent = "This is test content" diff --git a/src/api/providers/xai.ts b/src/api/providers/xai.ts index 36c1ab17dcb..a1377a1317a 100644 --- a/src/api/providers/xai.ts +++ b/src/api/providers/xai.ts @@ -1,7 +1,8 @@ import { Anthropic } from "@anthropic-ai/sdk" import OpenAI from "openai" -import { type XAIModelId, xaiDefaultModelId, xaiModels } from "@roo-code/types" +import { type XAIModelId, xaiDefaultModelId, xaiModels, ApiProviderError } from "@roo-code/types" +import { TelemetryService } from "@roo-code/telemetry" import { NativeToolCallParser } from "../../core/assistant-message/NativeToolCallParser" import type { ApiHandlerOptions } from "../../shared/api" @@ -79,6 +80,9 @@ export class XAIHandler extends BaseProvider implements SingleCompletionHandler try { stream = await this.client.chat.completions.create(requestOptions) } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + const apiError = new ApiProviderError(errorMessage, this.providerName, modelId, "createMessage") + TelemetryService.instance.captureException(apiError) throw handleOpenAIError(error, this.providerName) } @@ -158,6 +162,9 @@ export class XAIHandler extends BaseProvider implements SingleCompletionHandler return response.choices[0]?.message.content || "" } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + const apiError = new ApiProviderError(errorMessage, this.providerName, modelId, "completePrompt") + TelemetryService.instance.captureException(apiError) throw handleOpenAIError(error, this.providerName) } }