{"record":{"id":"1f37f4343fc5e293","repo":"BerriAI/litellm","slug":"api-key-response-missing-token","errorCode":null,"errorMessage":"API key response missing token","messagePattern":"API key response missing token","errorType":"exception","errorClass":"GetAPIKeyError","httpStatus":401,"severity":"error","filePath":"litellm/llms/github_copilot/authenticator.py","lineNumber":115,"sourceCode":"                        message=\"API key expired\",\n                        status_code=401,\n                    )\n        except OSError:\n            verbose_logger.warning(\"No API key file found or error opening file\")\n        except (json.JSONDecodeError, KeyError) as e:\n            verbose_logger.warning(\"Error reading API key from file: %s\", e)\n        except APIKeyExpiredError:\n            pass  # Already logged in the try block\n\n        try:\n            api_key_info = self._refresh_api_key()\n            with open(self.api_key_file, \"w\") as f:\n                json.dump(api_key_info, f)\n            token: Final = api_key_info.get(\"token\")\n            if token:\n                return token\n            else:\n                raise GetAPIKeyError(\n                    message=\"API key response missing token\",\n                    status_code=401,\n                )\n        except OSError as e:\n            verbose_logger.error(\"Error saving API key to file: %s\", e)\n            raise GetAPIKeyError(\n                message=f\"Failed to save API key: {e}\",\n                status_code=500,\n            )\n        except RefreshAPIKeyError as e:\n            raise GetAPIKeyError(\n                message=f\"Failed to refresh API key: {e}\",\n                status_code=401,\n            )\n\n    def get_api_base(self) -> str | None:\n        \"\"\"\n        Get the API endpoint from the api-key.json file.","sourceCodeStart":97,"sourceCodeEnd":133,"githubUrl":"https://github.com/BerriAI/litellm/blob/6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d/litellm/llms/github_copilot/authenticator.py#L97-L133","documentation":"Raised in get_api_key() when the Copilot API-key refresh HTTP call succeeded (status 2xx) and the response was saved, but the parsed JSON contains no 'token' field (token is None/missing). It is a 401 GetAPIKeyError, signalling that GitHub returned an unexpected 200-body from the api.github.com/copilot_internal/v2/token endpoint — typically an auth-passthrough or contract change rather than a normal expiry (expiry paths raise RefreshAPIKeyError instead).","triggerScenarios":"The token refresh returns 200 with a JSON body lacking 'token' — e.g. an intercepted/rewritten response from a proxy, an unexpected GitHub response shape, or an api-key.json cache file that was hand-edited/corrupted so api_key_info parsed from cache has no token.","commonSituations":"Corporate proxies stripping Authorization headers so the endpoint returns an anonymous 200 JSON error body; GitHub changing the internal Copilot token endpoint response (these internal endpoints are undocumented and can shift); truncated or corrupted api-key.json from concurrent writers.","solutions":["Delete the cached token files (api-key.json and access-token file under the token dir, e.g. ~/.copilot-like dir) to force a clean re-auth, then retry.","Test the refresh endpoint directly with your cached access token and inspect the JSON fields it actually returns.","Disable/allowlist proxies for api.github.com — response interception commonly rewrites the body.","If GitHub changed the response shape, update litellm to the latest version where the Copilot authenticator tracks the current endpoint contract."],"exampleFix":"# before: corrupted/legacy api-key.json without \"token\" -> GetAPIKeyError 401 every call\nimport litellm\nlitellm.completion(model=\"github_copilot/gpt-4o\", messages=[...])\n\n# after: clear the stale cache to force a fresh OAuth + refresh cycle\nimport shutil, pathlib\ncache = pathlib.Path.home() / \".litellm\" / \"github_copilot\"  # token dir used by the authenticator\nif cache.exists():\n    shutil.rmtree(cache)\nlitellm.completion(model=\"github_copilot/gpt-4o\", messages=[...])  # re-authenticates cleanly","handlingStrategy":"fallback","validationCode":"import json, pathlib\n\ncache = pathlib.Path(\"~/.litellm/github_copilot/api-key.json\").expanduser()\nif cache.exists():\n    try:\n        data = json.loads(cache.read_text())\n    except json.JSONDecodeError:\n        data = {}\n    if not data.get(\"token\"):\n        cache.unlink()  # force a clean refresh instead of failing on the bad cache","typeGuard":"def has_valid_cached_api_key(cache_path: pathlib.Path) -> bool:\n    \"\"\"True if the Copilot api-key cache holds a non-empty token.\"\"\"\n    if not cache_path.exists():\n        return False\n    try:\n        return bool(json.loads(cache_path.read_text()).get(\"token\"))\n    except (json.JSONDecodeError, OSError):\n        return False","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 \"missing token\" in str(e):\n        clear_copilot_cache()  # delete api-key.json + access token files\n        resp = litellm.completion(model=\"github_copilot/gpt-4o\", messages=msgs)  # one clean retry\n    else:\n        raise","preventionTips":["Treat the api-key.json cache as opaque — never hand-edit it.","Validate pre-seeded caches contain a non-empty 'token' in CI.","Bypass proxies for api.github.com so refresh responses are not rewritten.","Clear the cache and retry once when this specific error appears; escalate if it recurs."],"tags":["github-copilot","authentication","token-refresh","json","cache"],"backgroundTag":null,"analyzedSha":"6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d","analyzedAt":"2026-08-15T07:12:03.035Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}