{"record":{"id":"fcbe7637d042aa4e","repo":"janhq/jan","slug":"api-key-rotation-exhausted","errorCode":null,"errorMessage":"API key rotation exhausted","messagePattern":"API key rotation exhausted","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"web-app/src/lib/model-factory.ts","lineNumber":870,"sourceCode":"  ): Promise<Response> => {\n    for (let i = 0; i < apiKeys.length; i++) {\n      const key = apiKeys[i]!\n      const nextHeaders = new Headers(init?.headers as HeadersInit | undefined)\n      if (headerMode === 'authorization-bearer') {\n        nextHeaders.set('Authorization', `Bearer ${key}`)\n      } else if (headerMode === 'x-goog-api-key') {\n        nextHeaders.set('x-goog-api-key', key)\n      } else {\n        nextHeaders.set('x-api-key', key)\n      }\n      const res = await inner(input, { ...init, headers: nextHeaders })\n      if ([401, 403, 429].includes(res.status) && i < apiKeys.length - 1) {\n        res.body?.cancel().catch(() => {})\n        continue\n      }\n      return res\n    }\n    throw new Error('API key rotation exhausted')\n  }\n}\n\n// An empty apiKey still puts an empty auth header on the wire, which upstreams\n// answer with misleading 401s (e.g. Anthropic's \"x-api-key header is\n// required\"). Fail here with an actionable message instead.\nfunction requireRemoteApiKey(\n  provider: ProviderObject,\n  keyChain: string[]\n): string {\n  const key = keyChain[0] ?? provider.api_key?.trim()\n  if (!key) {\n    throw new Error(\n      `No API key configured for ${provider.provider}. Add one in Settings > Model Providers.`\n    )\n  }\n  return key\n}","sourceCodeStart":852,"sourceCodeEnd":888,"githubUrl":"https://github.com/janhq/jan/blob/7205d770c1e097c3daf35a911176410e93bc5564/web-app/src/lib/model-factory.ts#L852-L888","documentation":"Thrown by the multi-key rotation fetch loop after every configured API key has been tried and each attempt returned 401/403/429 (the loop only 'continue's to the next key for those statuses; any other status returns immediately). It means no key in the chain produced an acceptable response and there are no more keys to rotate to.","triggerScenarios":"All configured API keys are invalid (401) or revoked (403); all keys hit the provider's rate limit (429) so rotation cannot help; apiKeys array entries resolve to the same broken key.","commonSituations":"Expired or rotated-out keys left in settings; org-level rate limits shared by every key; free-tier keys throttled simultaneously during a burst; key chain accidentally containing duplicates.","solutions":["Verify each API key in Settings > Model Providers is valid and test one directly against the provider.","Wait out the rate-limit window or add keys with distinct rate-limit pools.","Remove duplicate/revoked keys from the key chain and re-add a fresh key from the provider dashboard."],"exampleFix":"// before\napiKeys = [oldRevokedKey, oldRevokedKey]\n// after\napiKeys = [freshKeyFromDashboard]","handlingStrategy":"retry","validationCode":"if (!apiKeys.length) throw new Error('Configure at least one API key before calling the provider')\nawait Promise.all(apiKeys.map(k => probeKey(k))) // optional preflight","typeGuard":"const hasUsableKeys = (keys) => Array.isArray(keys) && keys.length > 0 && keys.every(k => typeof k === 'string' && k.length > 0)","tryCatchPattern":"try { return await rotatedFetch(url, init) } catch (e) { if (e.message === 'API key rotation exhausted') { notifyUserToRefreshKeys(); } else throw e }","preventionTips":["Proactively test keys on save in settings.","Alert when a key returns repeated 401/429 instead of silently rotating.","Use distinct keys with separate rate-limit pools for rotation."],"tags":["api-key","rotation","authentication","rate-limit"],"backgroundTag":"missing-api-key","analyzedSha":"7205d770c1e097c3daf35a911176410e93bc5564","analyzedAt":"2026-09-17T14:27:30.100Z","contentChangedAt":"2026-09-17T14:27:30.100Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}