{"record":{"id":"f2e5c1d3f1c6f033","repo":"BerriAI/litellm","slug":"failed-to-refresh-api-key-after-maximum-retries","errorCode":null,"errorMessage":"Failed to refresh API key after maximum retries","messagePattern":"Failed to refresh API key after maximum retries","errorType":"exception","errorClass":"RefreshAPIKeyError","httpStatus":401,"severity":"critical","filePath":"litellm/llms/github_copilot/authenticator.py","lineNumber":180,"sourceCode":"        max_retries: Final = 3\n        for attempt in range(max_retries):\n            try:\n                sync_client = _get_httpx_client()\n                response = sync_client.get(api_key_url, headers=headers)\n                response.raise_for_status()\n\n                response_json = response.json()\n\n                if \"token\" in response_json:\n                    return response_json\n                else:\n                    verbose_logger.warning(\"API key response missing token: %s\", response_json)\n            except httpx.HTTPStatusError as e:\n                verbose_logger.error(\"HTTP error refreshing API key (attempt %s/%s): %s\", attempt + 1, max_retries, e)\n            except Exception as e:\n                verbose_logger.error(\"Unexpected error refreshing API key: %s\", e)\n\n        raise RefreshAPIKeyError(\n            message=\"Failed to refresh API key after maximum retries\",\n            status_code=401,\n        )\n\n    def _ensure_token_dir(self) -> None:\n        \"\"\"Ensure the token directory exists.\"\"\"\n        if not os.path.exists(self.token_dir):\n            os.makedirs(self.token_dir, exist_ok=True)\n\n    def _get_github_headers(self, access_token: str | None = None) -> dict[str, str]:\n        \"\"\"\n        Generate standard GitHub headers for API requests.\n\n        Args:\n            access_token: Optional access token to include in the headers.\n\n        Returns:\n            Dict[str, str]: Headers for GitHub API requests.","sourceCodeStart":162,"sourceCodeEnd":198,"githubUrl":"https://github.com/BerriAI/litellm/blob/6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d/litellm/llms/github_copilot/authenticator.py#L162-L198","documentation":"Raised at the end of _refresh_api_key's retry loop (default max_retries attempts): every attempt to POST the Copilot token endpoint either raised HTTPStatusError, returned JSON without a 'token' key, or threw an unexpected exception. The per-attempt failures are logged (HTTP error / unexpected error / missing token warnings); after the loop the RefreshAPIKeyError(401) terminates the chain and is usually wrapped further by get_api_key (see error 1628).","triggerScenarios":"Polling api.github.com/copilot_internal/v2/token with the cached access token yields repeated 401/403 (expired or revoked access token, lost Copilot entitlement) or 429/5xx across all retries; or responses consistently lack 'token' (proxy interference). Network-level total failure would instead surface as the 'unexpected error' branch.","commonSituations":"Copilot seat removed from the GitHub org (403s on refresh); access token cached weeks ago and long expired; shared environments hammering the token endpoint into rate limits; proxies rewriting responses; expired TLS/calendar drift causing consistent failures.","solutions":["Enable verbose logging and read the three per-attempt error lines — they distinguish 401 (dead token) from 429 (rate limit) from missing-token (interception).","If 401/403: delete cached tokens and redo the device-flow login; verify the account still holds a Copilot subscription.","If 429: reduce the number of processes/threads refreshing concurrently; share one authenticator or pre-fetch the key.","Upgrade litellm to pick up current endpoint URLs and headers for the Copilot token exchange."],"exampleFix":"# before: many workers each refresh with a dead token -> max retries hit every time\nfor _ in range(20):\n    litellm.completion(model=\"github_copilot/gpt-4o\", messages=[...])\n\n# after: authenticate once, then serialize refreshes (single worker / shared cache)\n# 1) litellm --login github_copilot   (one-time, interactive)\n# 2) run a single warmup call so only one process refreshes:\nlitellm.completion(model=\"github_copilot/gpt-4o\", messages=[{\"role\":\"user\",\"content\":\"ping\"}])","handlingStrategy":"fallback","validationCode":"import json, pathlib, time\n\ninfo_path = pathlib.Path(\"~/.litellm/github_copilot/api-key.json\").expanduser()\nif not info_path.exists():\n    raise SystemExit(\"No cached Copilot API key — authenticate first (device flow)\")\ninfo = json.loads(info_path.read_text())\nif not info.get(\"token\"):\n    raise SystemExit(\"Cached Copilot API key has no token — clear cache and re-authenticate\")\nif info.get(\"expires_at\", 0) < time.time():\n    print(\"warning: cached key expired; refresh will run — watch for repeated 401s\")","typeGuard":null,"tryCatchPattern":"from litellm.exceptions import AuthenticationError\n\ntry:\n    resp = litellm.completion(model=\"github_copilot/gpt-4o\", messages=msgs)\nexcept AuthenticationError as e:\n    if \"after maximum retries\" in str(e):\n        # upstream refresh is dead; fall back to another provider if available\n        resp = litellm.completion(model=\"openai/gpt-4o\", messages=msgs, api_key=os.environ[\"OPENAI_API_KEY\"])\n    else:\n        raise","preventionTips":["Consolidate refreshes into one worker/process to avoid rate-limit storms across retries.","Read the per-attempt verbose logs to classify 401 vs 429 before choosing a fix.","Keep a fallback provider routing rule for Copilot auth outages.","Re-login immediately when refresh 401s appear — retries will not revive a dead token."],"tags":["github-copilot","authentication","token-refresh","retry-exhausted","rate-limit"],"backgroundTag":null,"analyzedSha":"6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d","analyzedAt":"2026-08-15T07:12:03.035Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}