BerriAI/litellm · critical · RefreshAPIKeyError

Failed to refresh API key after maximum retries

Error message

Failed to refresh API key after maximum retries

What it means

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

Source

Thrown at litellm/llms/github_copilot/authenticator.py:180

        max_retries: Final = 3
        for attempt in range(max_retries):
            try:
                sync_client = _get_httpx_client()
                response = sync_client.get(api_key_url, headers=headers)
                response.raise_for_status()

                response_json = response.json()

                if "token" in response_json:
                    return response_json
                else:
                    verbose_logger.warning("API key response missing token: %s", response_json)
            except httpx.HTTPStatusError as e:
                verbose_logger.error("HTTP error refreshing API key (attempt %s/%s): %s", attempt + 1, max_retries, e)
            except Exception as e:
                verbose_logger.error("Unexpected error refreshing API key: %s", e)

        raise RefreshAPIKeyError(
            message="Failed to refresh API key after maximum retries",
            status_code=401,
        )

    def _ensure_token_dir(self) -> None:
        """Ensure the token directory exists."""
        if not os.path.exists(self.token_dir):
            os.makedirs(self.token_dir, exist_ok=True)

    def _get_github_headers(self, access_token: str | None = None) -> dict[str, str]:
        """
        Generate standard GitHub headers for API requests.

        Args:
            access_token: Optional access token to include in the headers.

        Returns:
            Dict[str, str]: Headers for GitHub API requests.

View on GitHub (pinned to 6c2dcb801b)

Solutions

  1. Enable verbose logging and read the three per-attempt error lines — they distinguish 401 (dead token) from 429 (rate limit) from missing-token (interception).
  2. If 401/403: delete cached tokens and redo the device-flow login; verify the account still holds a Copilot subscription.
  3. If 429: reduce the number of processes/threads refreshing concurrently; share one authenticator or pre-fetch the key.
  4. Upgrade litellm to pick up current endpoint URLs and headers for the Copilot token exchange.

Example fix

# before: many workers each refresh with a dead token -> max retries hit every time
for _ in range(20):
    litellm.completion(model="github_copilot/gpt-4o", messages=[...])

# after: authenticate once, then serialize refreshes (single worker / shared cache)
# 1) litellm --login github_copilot   (one-time, interactive)
# 2) run a single warmup call so only one process refreshes:
litellm.completion(model="github_copilot/gpt-4o", messages=[{"role":"user","content":"ping"}])
Defensive patterns

Strategy: fallback

Validate before calling

import json, pathlib, time

info_path = pathlib.Path("~/.litellm/github_copilot/api-key.json").expanduser()
if not info_path.exists():
    raise SystemExit("No cached Copilot API key — authenticate first (device flow)")
info = json.loads(info_path.read_text())
if not info.get("token"):
    raise SystemExit("Cached Copilot API key has no token — clear cache and re-authenticate")
if info.get("expires_at", 0) < time.time():
    print("warning: cached key expired; refresh will run — watch for repeated 401s")

Try / catch

from litellm.exceptions import AuthenticationError

try:
    resp = litellm.completion(model="github_copilot/gpt-4o", messages=msgs)
except AuthenticationError as e:
    if "after maximum retries" in str(e):
        # upstream refresh is dead; fall back to another provider if available
        resp = litellm.completion(model="openai/gpt-4o", messages=msgs, api_key=os.environ["OPENAI_API_KEY"])
    else:
        raise

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of BerriAI/litellm@6c2dcb801b (2026-08-15). Data as JSON: /api/errors/f2e5c1d3f1c6f033. Report an issue: GitHub.