From 3312739e9f8ef2b9a43171ab4bd003be98823fee Mon Sep 17 00:00:00 2001 From: Roo Code Date: Wed, 23 Jul 2025 20:57:54 +0000 Subject: [PATCH] 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 --- src/api/providers/openrouter.ts | 52 +++++++++++++++++++++++---------- src/core/task/Task.ts | 26 ++++++++++++++++- 2 files changed, 61 insertions(+), 17 deletions(-) diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index 6565daa238b..ad0e8bfc586 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -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 - - 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}`) + let lastChunkTime = Date.now() + const CHUNK_TIMEOUT = 60000 // 60 seconds timeout between chunks + + // 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 } - } + // 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}`) + } - if (delta?.content) { - yield { type: "text", text: delta.content } - } + 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 (chunk.usage) { + lastUsage = chunk.usage + } } + } finally { + // Clean up the timeout checker + clearInterval(timeoutChecker) } if (lastUsage) { diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index d12c0a2ffe9..91874173488 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1810,7 +1810,31 @@ export class Task extends EventEmitter { 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> + if (isOpenRouter) { + // Create a timeout promise that rejects after the specified time + const timeoutPromise = new Promise((_, 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) {