{"record":{"id":"426298b62b10ed9f","repo":"janhq/jan","slug":"failed-to-fetch-models-from-provider-provider","errorCode":null,"errorMessage":"Failed to fetch models from ${provider.provider}: ${result.status} ${result.statusText}","messagePattern":"Failed to fetch models from (.+?): (.+?) (.+?)","errorType":"http","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"web-app/src/lib/remoteModelCatalog.ts","lineNumber":223,"sourceCode":"    throw new Error('Provider must have base_url configured')\n  }\n\n  const keyChain = providerRemoteApiKeyChain(provider)\n  const attempts: (string | undefined)[] = keyChain.length > 0 ? keyChain : [undefined]\n\n  let lastStatus = 0\n  let lastStatusText = ''\n  for (let i = 0; i < attempts.length; i++) {\n    const result = await getJson(\n      fetchImpl,\n      `${provider.base_url}/models`,\n      buildHeaders(provider, attempts[i])\n    )\n    lastStatus = result.status\n    lastStatusText = result.statusText\n    if (!result.ok) {\n      if ([401, 403, 429].includes(result.status) && i < attempts.length - 1) continue\n      throw new Error(`Failed to fetch models from ${provider.provider}: ${result.status} ${result.statusText}`)\n    }\n\n    const body = result.body as { data?: unknown }\n    const rows = Array.isArray(body?.data) ? (body.data as unknown[]) : []\n    return normalizeCatalog(kind, rows)\n  }\n\n  throw new Error(`Failed to fetch models from ${provider.provider}: ${lastStatus} ${lastStatusText}`)\n}\n\nfunction normalizeCatalog(kind: CatalogKind, rows: unknown[]): RemoteCatalogModel[] {\n  const inferCaps =\n    kind === 'openai'\n      ? inferOpenAICapabilities\n      : kind === 'gemini'\n        ? inferGeminiCapabilities\n        : inferAnthropicCapabilities\n","sourceCodeStart":205,"sourceCodeEnd":241,"githubUrl":"https://github.com/janhq/jan/blob/fad3f12a147d138388a66f0d92a02b2675f65294/web-app/src/lib/remoteModelCatalog.ts#L205-L241","documentation":"Thrown inside the fetchTopRemoteModels key-rotation loop when a GET to `${provider.base_url}/models` returns non-OK and either the status is not in [401,403,429] or this is the final key attempt (remoteModelCatalog.ts:223). This is the in-loop terminal error carrying the exact failing status/statusText.","triggerScenarios":"The /models endpoint returns 404 (wrong base URL path), 500/502/503 (provider outage), 400 (malformed request), or — on the last key of the chain — a 401/403/429 that exhausts rotation.","commonSituations":"User set base_url with a trailing path that already includes /models so the request hits /models/models (404); provider deprecated the v1 endpoint; transient 502 from a load balancer; the provider requires a different auth scheme so every key 401s.","solutions":["Read the status code in the message: 404 → fix base_url (usually should end at /v1, not include /models); 5xx → retry later or check provider status page; 401/403 on last key → refresh API key.","Confirm base_url does not already end with /models — the code appends /models itself.","For 429, reduce catalog refresh frequency or add backoff.","Verify the provider's actual /models path against their API docs (some use /v1/models, some /openai/v1/models)."],"exampleFix":"// before\nthrow new Error(`Failed to fetch models from ${provider.provider}: ${result.status} ${result.statusText}`)\n// after: include the response body hint when available\nconst hint = result.body?.error?.message ?? result.statusText\nthrow new Error(`Failed to fetch models from ${provider.provider}: ${result.status} ${hint}`)","handlingStrategy":"try-catch","validationCode":"function normalizeCatalogBaseUrl(raw: string): string {\n  // the code appends /models itself; strip a user-supplied /models suffix\n  return raw.replace(/\\/models\\/?$/, '').replace(/\\/$/, '')\n}\n// before the loop:\nprovider.base_url = normalizeCatalogBaseUrl(provider.base_url!)","typeGuard":"function isCatalogOk(r: { ok: boolean; status: number }): boolean {\n  return r.ok || ![401, 403, 404, 429, 500, 502, 503].includes(r.status)\n}","tryCatchPattern":"try {\n  return await fetchTopRemoteModels(provider, fetchImpl)\n} catch (e) {\n  if (e instanceof Error && /Failed to fetch models/.test(e.message)) {\n    toast.error(e.message)\n    return []  // degrade catalog UI instead of crashing\n  }\n  throw e\n}","preventionTips":["Normalize base_url so it never ends with /models (the code appends it).","Cache the last successful catalog result for graceful degradation.","Rate-limit catalog refreshes to avoid 429s.","Show the status code in the UI so users can distinguish 404 (URL) from 401 (key)."],"tags":["catalog","network","http-status","provider","base-url","typescript"],"backgroundTag":null,"analyzedSha":"fad3f12a147d138388a66f0d92a02b2675f65294","analyzedAt":"2026-08-12T20:33:47.516Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}