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
- 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.
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
- 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.
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
- Failed to refresh API key: {e}
- Failed to get access token after 3 attempts
- API key response missing token
- str(e)
- GitHub Copilot API key is required. Please authenticate via
AI-assisted analysis of BerriAI/litellm@6c2dcb801b (2026-08-15).
Data as JSON: /api/errors/f2e5c1d3f1c6f033.
Report an issue: GitHub.