From 3a054a6a9a45eeb9f43ea02050d8cedb13043fc2 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Thu, 26 Feb 2026 00:17:39 +0000 Subject: [PATCH] fix: add timeouts to model fetchers and send router models incrementally Addresses #11747 where blocked DNS for provider domains (e.g. openrouter.ai, router.requesty.ai) caused all providers to hang until the DNS timeout expired, even for providers with working DNS. Changes: 1. Add 10-second timeout to axios.get() calls in openrouter.ts, requesty.ts, unbound.ts, and vercel-ai-gateway.ts fetchers. The roo.ts and litellm.ts fetchers already had timeouts. 2. Send router models incrementally in the requestRouterModels handler: each provider posts its models to the webview as soon as it resolves, rather than waiting for all providers to finish via Promise.allSettled. A final aggregated message is still sent for backward compatibility. --- src/api/providers/fetchers/openrouter.ts | 6 ++++-- src/api/providers/fetchers/requesty.ts | 2 +- src/api/providers/fetchers/unbound.ts | 2 +- .../providers/fetchers/vercel-ai-gateway.ts | 2 +- src/core/webview/webviewMessageHandler.ts | 18 +++++++++++++++--- 5 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/api/providers/fetchers/openrouter.ts b/src/api/providers/fetchers/openrouter.ts index 0cf65fb09c3..274a3e28de7 100644 --- a/src/api/providers/fetchers/openrouter.ts +++ b/src/api/providers/fetchers/openrouter.ts @@ -99,7 +99,7 @@ export async function getOpenRouterModels(options?: ApiHandlerOptions): Promise< const baseURL = options?.openRouterBaseUrl || "https://openrouter.ai/api/v1" try { - const response = await axios.get(`${baseURL}/models`) + const response = await axios.get(`${baseURL}/models`, { timeout: 10_000 }) const result = openRouterModelsResponseSchema.safeParse(response.data) const data = result.success ? result.data.data : response.data.data @@ -147,7 +147,9 @@ export async function getOpenRouterModelEndpoints( const baseURL = options?.openRouterBaseUrl || "https://openrouter.ai/api/v1" try { - const response = await axios.get(`${baseURL}/models/${modelId}/endpoints`) + const response = await axios.get(`${baseURL}/models/${modelId}/endpoints`, { + timeout: 10_000, + }) const result = openRouterModelEndpointsResponseSchema.safeParse(response.data) const data = result.success ? result.data.data : response.data.data diff --git a/src/api/providers/fetchers/requesty.ts b/src/api/providers/fetchers/requesty.ts index 64c7de66892..49e5cde4e1f 100644 --- a/src/api/providers/fetchers/requesty.ts +++ b/src/api/providers/fetchers/requesty.ts @@ -18,7 +18,7 @@ export async function getRequestyModels(baseUrl?: string, apiKey?: string): Prom const resolvedBaseUrl = toRequestyServiceUrl(baseUrl) const modelsUrl = new URL("v1/models", resolvedBaseUrl) - const response = await axios.get(modelsUrl.toString(), { headers }) + const response = await axios.get(modelsUrl.toString(), { headers, timeout: 10_000 }) const rawModels = response.data.data for (const rawModel of rawModels) { diff --git a/src/api/providers/fetchers/unbound.ts b/src/api/providers/fetchers/unbound.ts index 38410118093..24702a9925f 100644 --- a/src/api/providers/fetchers/unbound.ts +++ b/src/api/providers/fetchers/unbound.ts @@ -14,7 +14,7 @@ export async function getUnboundModels(apiKey?: string | null): Promise(`${baseURL}/models`) + const response = await axios.get(`${baseURL}/models`, { timeout: 10_000 }) const result = vercelAiGatewayModelsResponseSchema.safeParse(response.data) const data = result.success ? result.data.data : response.data.data diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 4ec715cf104..dd19ba05e18 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -956,10 +956,23 @@ export const webviewMessageHandler = async ( await flushModels(targetCandidate.options, true) } + // Fetch models incrementally: send each provider's models to the webview + // as soon as they resolve, rather than waiting for all providers to finish. + // This ensures providers with working DNS are available immediately while + // blocked providers fail gracefully in the background (see #11747). const results = await Promise.allSettled( modelFetchPromises.map(async ({ key, options }) => { const models = await safeGetModels(options) - return { key, models } // The key is `ProviderName` here. + + // Send this provider's models to the webview immediately. + routerModels[key] = models + provider.postMessageToWebview({ + type: "routerModels", + routerModels: { ...routerModels }, + values: providerFilter ? { provider: requestedProvider } : undefined, + }) + + return { key, models } }), ) @@ -968,8 +981,6 @@ export const webviewMessageHandler = async ( if (result.status === "fulfilled") { routerModels[routerName] = result.value.models - - // Ollama and LM Studio settings pages still need these events. They are not fetched here. } else { // Handle rejection: Post a specific error message for this provider. const errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason) @@ -986,6 +997,7 @@ export const webviewMessageHandler = async ( } }) + // Send final aggregated message for backward compatibility. provider.postMessageToWebview({ type: "routerModels", routerModels,