{"record":{"id":"4bd2cdcc91cccf52","repo":"paperclipai/paperclip","slug":"could-not-load-openrouter-models-retry-or-enter-a-model-id","errorCode":null,"errorMessage":"Could not load OpenRouter models. Retry or enter a model ID manually.","messagePattern":"Could not load OpenRouter models\\. Retry or enter a model ID manually\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"server/src/services/openrouter-models.ts","lineNumber":12,"sourceCode":"import type { AdapterModel } from \"@paperclipai/adapter-utils\";\n\nlet cached: { until: number; models: AdapterModel[] } | undefined;\nlet pending: Promise<AdapterModel[]> | undefined;\n\n/** OpenRouter's public catalog does not require access to anyone's credentials. */\nexport async function listOpenRouterModels(refresh = false): Promise<AdapterModel[]> {\n  if (!refresh && cached && cached.until > Date.now()) return cached.models;\n  if (pending) return pending;\n  pending = (async () => {\n    const response = await fetch(\"https://openrouter.ai/api/v1/models\", { signal: AbortSignal.timeout(10_000) });\n    if (!response.ok) throw new Error(\"Could not load OpenRouter models. Retry or enter a model ID manually.\");\n    const body = await response.json() as { data?: Array<{ id?: unknown; name?: unknown }> };\n    if (!Array.isArray(body.data)) throw new Error(\"OpenRouter returned an invalid model catalog.\");\n    const models = body.data.flatMap(model => typeof model.id === \"string\" && model.id.includes(\"/\")\n      ? [{ id: `openrouter/${model.id}`, label: typeof model.name === \"string\" ? model.name : model.id }]\n      : []).sort((a, b) => a.label.localeCompare(b.label));\n    cached = { until: Date.now() + 60_000, models };\n    return models;\n  })();\n  try { return await pending; } finally { pending = undefined; }\n}\n","sourceCodeStart":1,"sourceCodeEnd":23,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/openrouter-models.ts#L1-L23","documentation":"listOpenRouterModels fetches OpenRouter's public model catalog (https://openrouter.ai/api/v1/models) with a 10s timeout. If the HTTP response status is not ok, it throws 'Could not load OpenRouter models. Retry or enter a model ID manually.' The message is user-facing: the catalog could not be fetched, but a model ID can still be entered manually.","triggerScenarios":"Calling the OpenRouter models endpoint (via agentRoutes) when the upstream returns a non-2xx status: network outage, OpenRouter 429/5xx, blocked egress, or DNS failure with a non-ok response.","commonSituations":"Corporate firewall blocking openrouter.ai; OpenRouter outage or rate limiting; request exceeding the 10-second AbortSignal timeout in an environment with slow egress.","solutions":["Retry the request (call listOpenRouterModels(true) to force refresh) — the error message itself suggests retrying.","Enter a model ID manually in the agent configuration instead of picking from the catalog.","Check network egress to https://openrouter.ai/api/v1/models (curl it to confirm reachability).","If OpenRouter is rate limiting, wait and retry; note successful results are cached for 60 seconds."],"exampleFix":"// before\nconst models = await listOpenRouterModels();\n// after\nlet models;\ntry {\n  models = await listOpenRouterModels();\n} catch {\n  models = []; // fall back to manual model ID entry in the UI\n}","handlingStrategy":"retry","validationCode":"const res = await fetch(\"https://openrouter.ai/api/v1/models\", { signal: AbortSignal.timeout(10_000) });\nif (!res.ok) console.warn(\"openrouter catalog unreachable, status\", res.status);","typeGuard":null,"tryCatchPattern":"let models = [];\nfor (let attempt = 0; attempt < 3 && models.length === 0; attempt++) {\n  try { models = await listOpenRouterModels(attempt > 0); }\n  catch (e) { if (!e.message.includes(\"OpenRouter models\")) throw e; await sleep(2 ** attempt * 500); }\n}","preventionTips":["Verify egress to openrouter.ai from the deployment environment.","Cache successful catalog fetches (the service caches 60s) and serve stale data on failure.","Always offer manual model-ID entry as a fallback in the UI."],"tags":["network","http","openrouter"],"backgroundTag":"http-error-response","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-22T06:17:15.046Z"}