Skip to content

Commit 3312739

Browse files
committed
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
1 parent c47de36 commit 3312739

File tree

2 files changed

+61
-17
lines changed

2 files changed

+61
-17
lines changed

src/api/providers/openrouter.ts

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -137,28 +137,48 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
137137
const stream = await this.client.chat.completions.create(completionParams)
138138

139139
let lastUsage: CompletionUsage | undefined = undefined
140-
141-
for await (const chunk of stream) {
142-
// OpenRouter returns an error object instead of the OpenAI SDK throwing an error.
143-
if ("error" in chunk) {
144-
const error = chunk.error as { message?: string; code?: number }
145-
console.error(`OpenRouter API Error: ${error?.code} - ${error?.message}`)
146-
throw new Error(`OpenRouter API Error ${error?.code}: ${error?.message}`)
140+
let lastChunkTime = Date.now()
141+
const CHUNK_TIMEOUT = 60000 // 60 seconds timeout between chunks
142+
143+
// Set up a timeout checker
144+
const timeoutChecker = setInterval(() => {
145+
const timeSinceLastChunk = Date.now() - lastChunkTime
146+
if (timeSinceLastChunk > CHUNK_TIMEOUT) {
147+
clearInterval(timeoutChecker)
148+
console.error(`OpenRouter stream timeout: No data received for ${CHUNK_TIMEOUT / 1000} seconds`)
149+
// The stream will be aborted when the iterator is abandoned
147150
}
151+
}, 5000) // Check every 5 seconds
148152

149-
const delta = chunk.choices[0]?.delta
153+
try {
154+
for await (const chunk of stream) {
155+
// Update last chunk time
156+
lastChunkTime = Date.now()
150157

151-
if ("reasoning" in delta && delta.reasoning && typeof delta.reasoning === "string") {
152-
yield { type: "reasoning", text: delta.reasoning }
153-
}
158+
// OpenRouter returns an error object instead of the OpenAI SDK throwing an error.
159+
if ("error" in chunk) {
160+
const error = chunk.error as { message?: string; code?: number }
161+
console.error(`OpenRouter API Error: ${error?.code} - ${error?.message}`)
162+
throw new Error(`OpenRouter API Error ${error?.code}: ${error?.message}`)
163+
}
154164

155-
if (delta?.content) {
156-
yield { type: "text", text: delta.content }
157-
}
165+
const delta = chunk.choices[0]?.delta
166+
167+
if ("reasoning" in delta && delta.reasoning && typeof delta.reasoning === "string") {
168+
yield { type: "reasoning", text: delta.reasoning }
169+
}
170+
171+
if (delta?.content) {
172+
yield { type: "text", text: delta.content }
173+
}
158174

159-
if (chunk.usage) {
160-
lastUsage = chunk.usage
175+
if (chunk.usage) {
176+
lastUsage = chunk.usage
177+
}
161178
}
179+
} finally {
180+
// Clean up the timeout checker
181+
clearInterval(timeoutChecker)
162182
}
163183

164184
if (lastUsage) {

src/core/task/Task.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1810,7 +1810,31 @@ export class Task extends EventEmitter<ClineEvents> {
18101810
try {
18111811
// Awaiting first chunk to see if it will throw an error.
18121812
this.isWaitingForFirstChunk = true
1813-
const firstChunk = await iterator.next()
1813+
1814+
// Add timeout for OpenRouter to prevent indefinite hanging
1815+
const OPENROUTER_FIRST_CHUNK_TIMEOUT = 30000 // 30 seconds
1816+
const isOpenRouter = this.apiConfiguration.apiProvider === "openrouter"
1817+
1818+
let firstChunk: Awaited<ReturnType<typeof iterator.next>>
1819+
if (isOpenRouter) {
1820+
// Create a timeout promise that rejects after the specified time
1821+
const timeoutPromise = new Promise<never>((_, reject) => {
1822+
setTimeout(() => {
1823+
reject(
1824+
new Error(
1825+
`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.`,
1826+
),
1827+
)
1828+
}, OPENROUTER_FIRST_CHUNK_TIMEOUT)
1829+
})
1830+
1831+
// Race between the actual API call and the timeout
1832+
firstChunk = await Promise.race([iterator.next(), timeoutPromise])
1833+
} else {
1834+
// For non-OpenRouter providers, use the original logic
1835+
firstChunk = await iterator.next()
1836+
}
1837+
18141838
yield firstChunk.value
18151839
this.isWaitingForFirstChunk = false
18161840
} catch (error) {

0 commit comments

Comments
 (0)