Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 36 additions & 16 deletions src/api/providers/openrouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
26 changes: 25 additions & 1 deletion src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down