fix: add timeout mechanism for OpenRouter stream hanging issue

- Add 30-second timeout for first chunk in Task.ts specifically for OpenRouter
- Add chunk timeout monitoring in OpenRouterHandler to detect hanging streams
- Provide clear error messages when timeouts occur
- Fixes #6137 where OpenRouter requests would hang indefinitely
This commit is contained in:
Roo Code 2025-07-23 20:57:54 +00:00
parent c47de36b05
commit 3312739e9f
2 changed files with 62 additions and 18 deletions

View file

@ -137,28 +137,48 @@ 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 = 60000 // 60 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 checker
const timeoutChecker = setInterval(() => {
const timeSinceLastChunk = Date.now() - lastChunkTime
if (timeSinceLastChunk > CHUNK_TIMEOUT) {
clearInterval(timeoutChecker)
console.error(`OpenRouter stream timeout: No data received for ${CHUNK_TIMEOUT / 1000} seconds`)
// The stream will be aborted when the iterator is abandoned
}
}, 5000) // Check every 5 seconds
const delta = chunk.choices[0]?.delta
try {
for await (const chunk of stream) {
// Update last chunk time
lastChunkTime = Date.now()
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
// 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}`)
}
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
}
}
} finally {
// Clean up the timeout checker
clearInterval(timeoutChecker)
}
if (lastUsage) {

View file

@ -1810,7 +1810,31 @@ export class Task extends EventEmitter<ClineEvents> {
try {
// Awaiting first chunk to see if it will throw an error.
this.isWaitingForFirstChunk = true
const firstChunk = await iterator.next()
// Add timeout for OpenRouter to prevent indefinite hanging
const OPENROUTER_FIRST_CHUNK_TIMEOUT = 30000 // 30 seconds
const isOpenRouter = this.apiConfiguration.apiProvider === "openrouter"
let firstChunk: Awaited<ReturnType<typeof iterator.next>>
if (isOpenRouter) {
// Create a timeout promise that rejects after the specified time
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => {
reject(
new Error(
`OpenRouter API request timed out after ${OPENROUTER_FIRST_CHUNK_TIMEOUT / 1000} seconds. This may be due to high load on OpenRouter's servers. Please try again later or switch to a different provider.`,
),
)
}, OPENROUTER_FIRST_CHUNK_TIMEOUT)
})
// Race between the actual API call and the timeout
firstChunk = await Promise.race([iterator.next(), timeoutPromise])
} else {
// For non-OpenRouter providers, use the original logic
firstChunk = await iterator.next()
}
yield firstChunk.value
this.isWaitingForFirstChunk = false
} catch (error) {