diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index 6565daa238..ad0e8bfc58 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 + 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) { diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index d12c0a2ffe..9187417348 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) {