fix: improve OpenRouter error handling and add timeout mechanism

- Add detailed error logging with model ID and error context
- Provide user-friendly error messages for common issues (model not found, rate limit, auth)
- Add 30-second timeout mechanism to handle hanging streams
- Add comprehensive test coverage for error scenarios

Fixes #6232
This commit is contained in:
Roo Code 2025-07-26 02:11:51 +00:00
parent 0504199ce4
commit fa056dc307
2 changed files with 160 additions and 31 deletions

View file

@ -265,6 +265,42 @@ describe("OpenRouterHandler", () => {
const generator = handler.createMessage("test", [])
await expect(generator.next()).rejects.toThrow("OpenRouter API Error 500: API Error")
})
it("handles model not found errors with user-friendly message", async () => {
const handler = new OpenRouterHandler(mockOptions)
const mockStream = {
async *[Symbol.asyncIterator]() {
yield { error: { message: "Model not found", code: 404 } }
},
}
const mockCreate = vitest.fn().mockResolvedValue(mockStream)
;(OpenAI as any).prototype.chat = {
completions: { create: mockCreate },
} as any
const generator = handler.createMessage("test", [])
await expect(generator.next()).rejects.toThrow(
`Model "${mockOptions.openRouterModelId}" is not available on OpenRouter`,
)
})
it("handles rate limit errors with user-friendly message", async () => {
const handler = new OpenRouterHandler(mockOptions)
const mockStream = {
async *[Symbol.asyncIterator]() {
yield { error: { message: "Rate limit exceeded", code: 429 } }
},
}
const mockCreate = vitest.fn().mockResolvedValue(mockStream)
;(OpenAI as any).prototype.chat = {
completions: { create: mockCreate },
} as any
const generator = handler.createMessage("test", [])
await expect(generator.next()).rejects.toThrow("OpenRouter rate limit exceeded")
})
})
describe("completePrompt", () => {
@ -308,6 +344,25 @@ describe("OpenRouterHandler", () => {
await expect(handler.completePrompt("test prompt")).rejects.toThrow("OpenRouter API Error 500: API Error")
})
it("handles model not found errors in completePrompt", async () => {
const handler = new OpenRouterHandler(mockOptions)
const mockError = {
error: {
message: "Invalid model",
code: 404,
},
}
const mockCreate = vitest.fn().mockResolvedValue(mockError)
;(OpenAI as any).prototype.chat = {
completions: { create: mockCreate },
} as any
await expect(handler.completePrompt("test prompt")).rejects.toThrow(
`Model "${mockOptions.openRouterModelId}" is not available on OpenRouter`,
)
})
it("handles unexpected errors", async () => {
const handler = new OpenRouterHandler(mockOptions)
const mockCreate = vitest.fn().mockRejectedValue(new Error("Unexpected error"))

View file

@ -137,39 +137,85 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
const stream = await this.client.chat.completions.create(completionParams)
let lastUsage: CompletionUsage | undefined = undefined
let lastChunkTime = Date.now()
const CHUNK_TIMEOUT_MS = 30000 // 30 seconds timeout between chunks
for await (const chunk of stream) {
// OpenRouter returns an error object instead of the OpenAI SDK throwing an error.
if ("error" in chunk) {
const error = chunk.error as { message?: string; code?: number }
console.error(`OpenRouter API Error: ${error?.code} - ${error?.message}`)
throw new Error(`OpenRouter API Error ${error?.code}: ${error?.message}`)
// Set up a timeout check
const timeoutCheck = setInterval(() => {
const timeSinceLastChunk = Date.now() - lastChunkTime
if (timeSinceLastChunk > CHUNK_TIMEOUT_MS) {
clearInterval(timeoutCheck)
console.error(`OpenRouter stream timeout: No chunks received for ${CHUNK_TIMEOUT_MS}ms`, {
modelId,
timeSinceLastChunk,
})
}
}, 5000) // Check every 5 seconds
try {
for await (const chunk of stream) {
lastChunkTime = Date.now() // Reset timeout on each chunk
// OpenRouter returns an error object instead of the OpenAI SDK throwing an error.
if ("error" in chunk) {
const error = chunk.error as { message?: string; code?: number; type?: string }
const errorMessage = error?.message || "Unknown error"
const errorCode = error?.code || "unknown"
const errorType = error?.type || "unknown"
// Log detailed error information
console.error(`OpenRouter API Error:`, {
code: errorCode,
type: errorType,
message: errorMessage,
modelId,
chunk: JSON.stringify(chunk),
})
// Provide more specific error messages for common issues
let userFriendlyMessage = `OpenRouter API Error ${errorCode}: ${errorMessage}`
if (
errorMessage.toLowerCase().includes("model not found") ||
errorMessage.toLowerCase().includes("invalid model") ||
errorCode === 404
) {
userFriendlyMessage = `Model "${modelId}" is not available on OpenRouter. Please check if the model ID is correct and if you have access to this model.`
} else if (errorMessage.toLowerCase().includes("rate limit")) {
userFriendlyMessage = `OpenRouter rate limit exceeded. Please wait a moment and try again.`
} else if (errorMessage.toLowerCase().includes("unauthorized") || errorCode === 401) {
userFriendlyMessage = `OpenRouter authentication failed. Please check your API key.`
}
throw new Error(userFriendlyMessage)
}
const delta = chunk.choices[0]?.delta
if ("reasoning" in delta && delta.reasoning && typeof delta.reasoning === "string") {
yield { type: "reasoning", text: delta.reasoning }
}
if (delta?.content) {
yield { type: "text", text: delta.content }
}
if (chunk.usage) {
lastUsage = chunk.usage
}
}
const delta = chunk.choices[0]?.delta
if ("reasoning" in delta && delta.reasoning && typeof delta.reasoning === "string") {
yield { type: "reasoning", text: delta.reasoning }
}
if (delta?.content) {
yield { type: "text", text: delta.content }
}
if (chunk.usage) {
lastUsage = chunk.usage
}
}
if (lastUsage) {
yield {
type: "usage",
inputTokens: lastUsage.prompt_tokens || 0,
outputTokens: lastUsage.completion_tokens || 0,
cacheReadTokens: lastUsage.prompt_tokens_details?.cached_tokens,
reasoningTokens: lastUsage.completion_tokens_details?.reasoning_tokens,
totalCost: (lastUsage.cost_details?.upstream_inference_cost || 0) + (lastUsage.cost || 0),
if (lastUsage) {
yield {
type: "usage",
inputTokens: lastUsage.prompt_tokens || 0,
outputTokens: lastUsage.completion_tokens || 0,
cacheReadTokens: lastUsage.prompt_tokens_details?.cached_tokens,
reasoningTokens: lastUsage.completion_tokens_details?.reasoning_tokens,
totalCost: (lastUsage.cost_details?.upstream_inference_cost || 0) + (lastUsage.cost || 0),
}
}
} finally {
clearInterval(timeoutCheck)
}
}
@ -235,8 +281,36 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
const response = await this.client.chat.completions.create(completionParams)
if ("error" in response) {
const error = response.error as { message?: string; code?: number }
throw new Error(`OpenRouter API Error ${error?.code}: ${error?.message}`)
const error = response.error as { message?: string; code?: number; type?: string }
const errorMessage = error?.message || "Unknown error"
const errorCode = error?.code || "unknown"
const errorType = error?.type || "unknown"
// Log detailed error information
console.error(`OpenRouter API Error:`, {
code: errorCode,
type: errorType,
message: errorMessage,
modelId,
response: JSON.stringify(response),
})
// Provide more specific error messages for common issues
let userFriendlyMessage = `OpenRouter API Error ${errorCode}: ${errorMessage}`
if (
errorMessage.toLowerCase().includes("model not found") ||
errorMessage.toLowerCase().includes("invalid model") ||
errorCode === 404
) {
userFriendlyMessage = `Model "${modelId}" is not available on OpenRouter. Please check if the model ID is correct and if you have access to this model.`
} else if (errorMessage.toLowerCase().includes("rate limit")) {
userFriendlyMessage = `OpenRouter rate limit exceeded. Please wait a moment and try again.`
} else if (errorMessage.toLowerCase().includes("unauthorized") || errorCode === 401) {
userFriendlyMessage = `OpenRouter authentication failed. Please check your API key.`
}
throw new Error(userFriendlyMessage)
}
const completion = response as OpenAI.Chat.ChatCompletion