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) continue

View on GitHub (pinned to fad3f12a14)

Solutions

  1. Replace at least one key in the rotation with a known-good key and re-fetch the catalog.
  2. If 429: the /models endpoint is rate-limited — cache the catalog result rather than refreshing on every open.
  3. Verify the auth header scheme the provider expects (x-api-key vs Authorization Bearer vs x-goog-api-key).
  4. 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

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


AI-assisted analysis of janhq/jan@fad3f12a14 (2026-08-12). Data as JSON: /api/errors/8202175c729cf704. Report an issue: GitHub.