Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
68 changes: 64 additions & 4 deletions packages/types/src/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,15 +276,75 @@ export const EXPECTED_API_ERROR_CODES = new Set([
429, // Rate limit - expected when hitting API limits
])

/**
* Patterns in error messages that indicate expected errors (rate limits, etc.)
* These are checked when no numeric error code is available.
*/
const EXPECTED_ERROR_MESSAGE_PATTERNS = [
/^429\b/, // Message starts with "429"
/rate limit/i, // Contains "rate limit" (case insensitive)
]

/**
* Interface for SDK errors that have HTTP status information.
* OpenAI SDK errors (APIError, AuthenticationError, RateLimitError, etc.)
* all extend this interface with a numeric status property.
*/
interface SdkErrorWithStatus {
status: number
code?: number | string
message: string
}

/**
* Type guard to check if an error object is an SDK error with status property.
* OpenAI SDK errors (APIError and subclasses) have: status, code, message properties.
*/
function isSdkErrorWithStatus(error: unknown): error is SdkErrorWithStatus {
return (
typeof error === "object" &&
error !== null &&
"status" in error &&
typeof (error as SdkErrorWithStatus).status === "number"
)
}

/**
* Extracts the HTTP status code from an error object.
* Supports SDK errors that have a status property (e.g., OpenAI APIError).
* @param error - The error to extract status from
* @returns The status code if available, undefined otherwise
*/
export function getErrorStatusCode(error: unknown): number | undefined {
if (isSdkErrorWithStatus(error)) {
return error.status
}
return undefined
}

/**
* Helper to check if an API error should be reported to telemetry.
* Filters out expected errors like rate limits.
* Filters out expected errors like rate limits by checking both error codes and messages.
* @param errorCode - The HTTP error code (if available)
* @param errorMessage - The error message (if available)
* @returns true if the error should be reported, false if it should be filtered out
*/
export function shouldReportApiErrorToTelemetry(errorCode?: number): boolean {
if (errorCode === undefined) return true
return !EXPECTED_API_ERROR_CODES.has(errorCode)
export function shouldReportApiErrorToTelemetry(errorCode?: number, errorMessage?: string): boolean {
// Check numeric error code
if (errorCode !== undefined && EXPECTED_API_ERROR_CODES.has(errorCode)) {
return false
}

// Check error message for expected patterns (e.g., "429 Rate limit exceeded")
if (errorMessage) {
for (const pattern of EXPECTED_ERROR_MESSAGE_PATTERNS) {
if (pattern.test(errorMessage)) {
return false
}
}
}

return true
}

/**
Expand Down
85 changes: 85 additions & 0 deletions src/api/providers/__tests__/openrouter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,50 @@ describe("OpenRouterHandler", () => {
})
})

it("does NOT capture telemetry for SDK exceptions with status 429", async () => {
const handler = new OpenRouterHandler(mockOptions)
const error = new Error("Rate limit exceeded: free-models-per-day") as any
error.status = 429
const mockCreate = vitest.fn().mockRejectedValue(error)
;(OpenAI as any).prototype.chat = {
completions: { create: mockCreate },
} as any

const generator = handler.createMessage("test", [])
await expect(generator.next()).rejects.toThrow("Rate limit exceeded")

// Verify telemetry was NOT captured for rate limit errors
expect(mockCaptureException).not.toHaveBeenCalled()
})

it("does NOT capture telemetry for SDK exceptions with 429 in message (fallback)", async () => {
const handler = new OpenRouterHandler(mockOptions)
const mockCreate = vitest.fn().mockRejectedValue(new Error("429 Rate limit exceeded: free-models-per-day"))
;(OpenAI as any).prototype.chat = {
completions: { create: mockCreate },
} as any

const generator = handler.createMessage("test", [])
await expect(generator.next()).rejects.toThrow("429 Rate limit exceeded")

// Verify telemetry was NOT captured for rate limit errors (via message parsing)
expect(mockCaptureException).not.toHaveBeenCalled()
})

it("does NOT capture telemetry for SDK exceptions containing 'rate limit'", async () => {
const handler = new OpenRouterHandler(mockOptions)
const mockCreate = vitest.fn().mockRejectedValue(new Error("Request failed due to rate limit"))
;(OpenAI as any).prototype.chat = {
completions: { create: mockCreate },
} as any

const generator = handler.createMessage("test", [])
await expect(generator.next()).rejects.toThrow("rate limit")

// Verify telemetry was NOT captured for rate limit errors
expect(mockCaptureException).not.toHaveBeenCalled()
})

it("does NOT capture telemetry for 429 rate limit errors", async () => {
const handler = new OpenRouterHandler(mockOptions)
const mockStream = {
Expand Down Expand Up @@ -483,6 +527,47 @@ describe("OpenRouterHandler", () => {
})
})

it("does NOT capture telemetry for SDK exceptions with status 429", async () => {
const handler = new OpenRouterHandler(mockOptions)
const error = new Error("Rate limit exceeded: free-models-per-day") as any
error.status = 429
const mockCreate = vitest.fn().mockRejectedValue(error)
;(OpenAI as any).prototype.chat = {
completions: { create: mockCreate },
} as any

await expect(handler.completePrompt("test prompt")).rejects.toThrow("Rate limit exceeded")

// Verify telemetry was NOT captured for rate limit errors
expect(mockCaptureException).not.toHaveBeenCalled()
})

it("does NOT capture telemetry for SDK exceptions with 429 in message (fallback)", async () => {
const handler = new OpenRouterHandler(mockOptions)
const mockCreate = vitest.fn().mockRejectedValue(new Error("429 Rate limit exceeded: free-models-per-day"))
;(OpenAI as any).prototype.chat = {
completions: { create: mockCreate },
} as any

await expect(handler.completePrompt("test prompt")).rejects.toThrow("429 Rate limit exceeded")

// Verify telemetry was NOT captured for rate limit errors (via message parsing)
expect(mockCaptureException).not.toHaveBeenCalled()
})

it("does NOT capture telemetry for SDK exceptions containing 'rate limit'", async () => {
const handler = new OpenRouterHandler(mockOptions)
const mockCreate = vitest.fn().mockRejectedValue(new Error("Request failed due to rate limit"))
;(OpenAI as any).prototype.chat = {
completions: { create: mockCreate },
} as any

await expect(handler.completePrompt("test prompt")).rejects.toThrow("rate limit")

// Verify telemetry was NOT captured for rate limit errors
expect(mockCaptureException).not.toHaveBeenCalled()
})

it("does NOT capture telemetry for 429 rate limit errors", async () => {
const handler = new OpenRouterHandler(mockOptions)
const mockError = {
Expand Down
39 changes: 19 additions & 20 deletions src/api/providers/openrouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
OPEN_ROUTER_PROMPT_CACHING_MODELS,
DEEP_SEEK_DEFAULT_TEMPERATURE,
shouldReportApiErrorToTelemetry,
getErrorStatusCode,
ApiProviderError,
} from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
Expand Down Expand Up @@ -227,15 +228,14 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
try {
stream = await this.client.chat.completions.create(completionParams, requestOptions)
} catch (error) {
TelemetryService.instance.captureException(
new ApiProviderError(
error instanceof Error ? error.message : String(error),
this.providerName,
modelId,
"createMessage",
),
{ provider: this.providerName, modelId, operation: "createMessage" },
)
const errorMessage = error instanceof Error ? error.message : String(error)
const errorStatus = getErrorStatusCode(error)
if (shouldReportApiErrorToTelemetry(errorStatus, errorMessage)) {
TelemetryService.instance.captureException(
Comment thread
cte marked this conversation as resolved.
Outdated
new ApiProviderError(errorMessage, this.providerName, modelId, "createMessage", errorStatus),
{ provider: this.providerName, modelId, operation: "createMessage", errorCode: errorStatus },
)
}
throw handleOpenAIError(error, this.providerName)
}

Expand All @@ -260,7 +260,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
if ("error" in chunk) {
const error = chunk.error as { message?: string; code?: number }
console.error(`OpenRouter API Error: ${error?.code} - ${error?.message}`)
if (shouldReportApiErrorToTelemetry(error?.code)) {
if (shouldReportApiErrorToTelemetry(error?.code, error?.message)) {
TelemetryService.instance.captureException(
new ApiProviderError(
error?.message ?? "Unknown error",
Expand Down Expand Up @@ -466,21 +466,20 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
try {
response = await this.client.chat.completions.create(completionParams, requestOptions)
} catch (error) {
TelemetryService.instance.captureException(
new ApiProviderError(
error instanceof Error ? error.message : String(error),
this.providerName,
modelId,
"completePrompt",
),
{ provider: this.providerName, modelId, operation: "completePrompt" },
)
const errorMessage = error instanceof Error ? error.message : String(error)
const errorStatus = getErrorStatusCode(error)
if (shouldReportApiErrorToTelemetry(errorStatus, errorMessage)) {
TelemetryService.instance.captureException(
new ApiProviderError(errorMessage, this.providerName, modelId, "completePrompt", errorStatus),
{ provider: this.providerName, modelId, operation: "completePrompt", errorCode: errorStatus },
)
}
throw handleOpenAIError(error, this.providerName)
}

if ("error" in response) {
const error = response.error as { message?: string; code?: number }
if (shouldReportApiErrorToTelemetry(error?.code)) {
if (shouldReportApiErrorToTelemetry(error?.code, error?.message)) {
TelemetryService.instance.captureException(
new ApiProviderError(
error?.message ?? "Unknown error",
Expand Down
Loading