{"record":{"id":"8202175c729cf704","repo":"janhq/jan","slug":"failed-to-fetch-models-from-provider-provider-820217","errorCode":null,"errorMessage":"Failed to fetch models from ${provider.provider}: ${lastStatus} ${lastStatusText}","messagePattern":"Failed to fetch models from (.+?): (.+?) (.+?)","errorType":"http","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"web-app/src/lib/remoteModelCatalog.ts","lineNumber":231,"sourceCode":"  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\n  const parsed: RemoteCatalogModel[] = []\n  for (const raw of rows) {\n    if (!raw || typeof raw !== 'object') continue\n    const row = raw as Record<string, unknown>\n    const id = typeof row.id === 'string' ? row.id : null\n    if (!id) continue\n    const caps = inferCaps(id)\n    if (!caps) continue","sourceCodeStart":213,"sourceCodeEnd":249,"githubUrl":"https://github.com/janhq/jan/blob/fad3f12a147d138388a66f0d92a02b2675f65294/web-app/src/lib/remoteModelCatalog.ts#L213-L249","documentation":"Thrown after the fetchTopRemoteModels key-rotation loop exits without returning (remoteModelCatalog.ts:231). Reached only when every attempt continued (i.e. every key returned 401/403/429). Carries lastStatus/lastStatusText captured across iterations rather than the per-iteration values. Practically the 'all keys failed with auth/rate-limit' terminal.","triggerScenarios":"All keys in the keyChain failed with 401/403/429 on the /models request, so the loop's continue branch ran on every iteration and fell through to the post-loop throw. With a single key this line is shadowed by the in-loop throw at :223, so it manifests mainly with multi-key rotation.","commonSituations":"Every rotated key is expired or revoked; the provider rate-limits the /models listing across all keys; a proxy strips auth so all keys 401; keychain returned only stale entries.","solutions":["Replace at least one key in the rotation with a known-good key and re-fetch the catalog.","If 429: the /models endpoint is rate-limited — cache the catalog result rather than refreshing on every open.","Verify the auth header scheme the provider expects (x-api-key vs Authorization Bearer vs x-goog-api-key).","Confirm the keychain is returning the keys you expect (not empty / not duplicated dead keys)."],"exampleFix":"// before\nthrow new Error(`Failed to fetch models from ${provider.provider}: ${lastStatus} ${lastStatusText}`)\n// after: distinguish auth-exhaustion from generic failure for clearer UX\nthrow new Error(\n  [401, 403].includes(lastStatus)\n    ? `All API keys were rejected by ${provider.provider} (${lastStatus}). Update them in Settings.`\n    : `Failed to fetch models from ${provider.provider}: ${lastStatus} ${lastStatusText}`\n)","handlingStrategy":"validation","validationCode":"function pickFirstValidKey(chain: string[]): string | undefined {\n  return chain.find(k => typeof k === 'string' && k.trim().length > 0)\n}\n// before fetchTopRemoteModels:\nconst keyChain = providerRemoteApiKeyChain(provider)\nif (!pickFirstValidKey(keyChain)) {\n  return { models: [], error: 'auth_exhausted' }\n}","typeGuard":"function hasUsableKeyChain(keys: unknown): keys is string[] {\n  return Array.isArray(keys) && keys.some(k => typeof k === 'string' && k.trim().length > 0)\n}","tryCatchPattern":"try {\n  return await fetchTopRemoteModels(provider, fetchImpl)\n} catch (e) {\n  const m = e instanceof Error ? e.message : ''\n  if (/Failed to fetch models/.test(m)) {\n    return []  // catalog refresh fails soft\n  }\n  throw e\n}","preventionTips":["Cache catalog results so a transient auth/rate-limit failure does not blank the list.","Distinguish 401/403 (auth) from 429 (rate) in the surfaced message.","Verify the provider's required auth header scheme matches buildHeaders output.","Keep at least one verified key in the keychain."],"tags":["catalog","auth","rate-limit","rotation","network","typescript"],"backgroundTag":null,"analyzedSha":"fad3f12a147d138388a66f0d92a02b2675f65294","analyzedAt":"2026-08-12T20:33:47.516Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}