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.
This commit is contained in:
Roo Code 2026-02-26 00:17:39 +00:00
parent 0aea65d1e6
commit 3a054a6a9a
5 changed files with 22 additions and 8 deletions

View file

@ -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<OpenRouterModelsResponse>(`${baseURL}/models`)
const response = await axios.get<OpenRouterModelsResponse>(`${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<OpenRouterModelEndpointsResponse>(`${baseURL}/models/${modelId}/endpoints`)
const response = await axios.get<OpenRouterModelEndpointsResponse>(`${baseURL}/models/${modelId}/endpoints`, {
timeout: 10_000,
})
const result = openRouterModelEndpointsResponseSchema.safeParse(response.data)
const data = result.success ? result.data.data : response.data.data

View file

@ -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) {

View file

@ -14,7 +14,7 @@ export async function getUnboundModels(apiKey?: string | null): Promise<Record<s
headers["Authorization"] = `Bearer ${apiKey}`
}
const response = await axios.get("https://api.getunbound.ai/models", { headers })
const response = await axios.get("https://api.getunbound.ai/models", { headers, timeout: 10_000 })
const rawModels = response.data?.data ?? response.data
for (const rawModel of rawModels) {

View file

@ -58,7 +58,7 @@ export async function getVercelAiGatewayModels(options?: ApiHandlerOptions): Pro
const baseURL = "https://ai-gateway.vercel.sh/v1"
try {
const response = await axios.get<VercelAiGatewayModelsResponse>(`${baseURL}/models`)
const response = await axios.get<VercelAiGatewayModelsResponse>(`${baseURL}/models`, { timeout: 10_000 })
const result = vercelAiGatewayModelsResponseSchema.safeParse(response.data)
const data = result.success ? result.data.data : response.data.data

View file

@ -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,