janhq/jan · error · Error
Failed to fetch models from ${provider.provider}: ${lastStat
Error message
Failed to fetch models from ${provider.provider}: ${lastStatus} ${lastStatusText} What it means
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.
Source
Thrown at web-app/src/lib/remoteModelCatalog.ts:231
for (let i = 0; i < attempts.length; i++) {
const result = await getJson(
fetchImpl,
`${provider.base_url}/models`,
buildHeaders(provider, attempts[i])
)
lastStatus = result.status
lastStatusText = result.statusText
if (!result.ok) {
if ([401, 403, 429].includes(result.status) && i < attempts.length - 1) continue
throw new Error(`Failed to fetch models from ${provider.provider}: ${result.status} ${result.statusText}`)
}
const body = result.body as { data?: unknown }
const rows = Array.isArray(body?.data) ? (body.data as unknown[]) : []
return normalizeCatalog(kind, rows)
}
throw new Error(`Failed to fetch models from ${provider.provider}: ${lastStatus} ${lastStatusText}`)
}
function normalizeCatalog(kind: CatalogKind, rows: unknown[]): RemoteCatalogModel[] {
const inferCaps =
kind === 'openai'
? inferOpenAICapabilities
: kind === 'gemini'
? inferGeminiCapabilities
: inferAnthropicCapabilities
const parsed: RemoteCatalogModel[] = []
for (const raw of rows) {
if (!raw || typeof raw !== 'object') continue
const row = raw as Record<string, unknown>
const id = typeof row.id === 'string' ? row.id : null
if (!id) continue
const caps = inferCaps(id)
if (!caps) continueView on GitHub (pinned to fad3f12a14)
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).
Example fix
// before
throw new Error(`Failed to fetch models from ${provider.provider}: ${lastStatus} ${lastStatusText}`)
// after: distinguish auth-exhaustion from generic failure for clearer UX
throw new Error(
[401, 403].includes(lastStatus)
? `All API keys were rejected by ${provider.provider} (${lastStatus}). Update them in Settings.`
: `Failed to fetch models from ${provider.provider}: ${lastStatus} ${lastStatusText}`
) Defensive patterns
Strategy: validation
Validate before calling
function pickFirstValidKey(chain: string[]): string | undefined {
return chain.find(k => typeof k === 'string' && k.trim().length > 0)
}
// before fetchTopRemoteModels:
const keyChain = providerRemoteApiKeyChain(provider)
if (!pickFirstValidKey(keyChain)) {
return { models: [], error: 'auth_exhausted' }
} Type guard
function hasUsableKeyChain(keys: unknown): keys is string[] {
return Array.isArray(keys) && keys.some(k => typeof k === 'string' && k.trim().length > 0)
} Try / catch
try {
return await fetchTopRemoteModels(provider, fetchImpl)
} catch (e) {
const m = e instanceof Error ? e.message : ''
if (/Failed to fetch models/.test(m)) {
return [] // catalog refresh fails soft
}
throw e
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- API key rotation exhausted
- Failed to fetch supported backends: ${error instanceof Error
- Failed to fetch models from ${provider.provider}: ${result.s
- Failed to fetch model catalog: ${response.status} ${response
- Failed to fetch model catalog: ${error instanceof Error ? er
AI-assisted analysis of janhq/jan@fad3f12a14 (2026-08-12).
Data as JSON: /api/errors/8202175c729cf704.
Report an issue: GitHub.