{"record":{"id":"ba07f619baf7a101","repo":"BerriAI/litellm","slug":"xai-oauth-token-response-missing-refresh-token","errorCode":null,"errorMessage":"xAI OAuth token response missing refresh_token","messagePattern":"xAI OAuth token response missing refresh_token","errorType":"exception","errorClass":"XAIOAuthError","httpStatus":null,"severity":"error","filePath":"litellm/llms/xai/oauth.py","lineNumber":341,"sourceCode":"            body: Final = response.json()\n        except ValueError as exc:\n            raise XAIOAuthError(\"xAI OAuth token response was not valid JSON\") from exc\n        if not isinstance(body, dict):\n            raise XAIOAuthError(\"xAI OAuth token response was not an object\")\n        return body\n\n    def _build_auth_record(\n        self,\n        token_payload: dict[str, Any],\n        token_endpoint: str,\n        fallback_refresh_token: str | None = None,\n    ) -> dict[str, Any]:\n        access_token: Final = token_payload.get(\"access_token\")\n        refresh_token: Final = token_payload.get(\"refresh_token\") or fallback_refresh_token\n        if not access_token:\n            raise XAIOAuthError(\"xAI OAuth token response missing access_token\")\n        if not refresh_token:\n            raise XAIOAuthError(\"xAI OAuth token response missing refresh_token\")\n        expires_in: Final = token_payload.get(\"expires_in\") or 3600\n        try:\n            expires_at = int(time.time() + int(expires_in))\n        except (TypeError, ValueError):\n            expires_at = int(time.time() + 3600)\n        return {\n            \"access_token\": access_token,\n            \"refresh_token\": refresh_token,\n            \"id_token\": token_payload.get(\"id_token\"),\n            \"token_type\": token_payload.get(\"token_type\") or \"Bearer\",\n            \"token_endpoint\": token_endpoint,\n            \"expires_at\": expires_at,\n        }\n\n    def _refresh_tokens(self, auth_data: dict[str, Any]) -> dict[str, Any]:\n        token_endpoint = auth_data.get(\"token_endpoint\")\n        if not token_endpoint:\n            token_endpoint = self._discover()[\"token_endpoint\"]","sourceCodeStart":323,"sourceCodeEnd":359,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/llms/xai/oauth.py#L323-L359","documentation":"Raised as XAIOAuthError by XAIOAuthAuthenticator._build_auth_record when an xAI OAuth token exchange (login or refresh) returns a payload with no refresh_token field and no fallback_refresh_token was passed. LiteLLM refuses to build the auth record because every future silent refresh depends on the refresh_token, so storing a record without one would just defer the failure.","triggerScenarios":"Running `litellm xai-oauth login` (or an automatic refresh) where the xAI token endpoint response omits refresh_token -- typically because the authorization request did not ask for offline access -- or building an auth record from a hand-crafted token_payload dict that lacks the key.","commonSituations":"xAI OAuth app registered without the offline-access/refresh scope; reusing an already-consumed authorization code (some servers then omit refresh_token); xAI changing its token payload shape; test fixtures with incomplete token payloads.","solutions":["Re-run `litellm xai-oauth login` to start a fresh authorization-code flow that mints a full token set","Verify the xAI OAuth client requests the offline access / refresh scope so the token endpoint returns refresh_token","When refreshing, keep the previously stored refresh token so _build_auth_record can fall back to it","Enable verbose logging to inspect the raw token response and compare it with xAI's current OAuth docs; file a litellm issue if xAI changed the payload"],"exampleFix":"# before: auth record built from a payload without refresh_token\nauth = xai_auth._build_auth_record(token_payload, token_endpoint)  # XAIOAuthError\n\n# after: pass the stored token as fallback, or re-login for a fresh token set\nauth = xai_auth._build_auth_record(token_payload, token_endpoint, fallback_refresh_token=stored.get('refresh_token'))\n# shell: litellm xai-oauth login","handlingStrategy":"try-catch","validationCode":"import os, json\n# before enabling OAuth, confirm the stored auth record is complete\nif os.path.exists(auth_path):\n    rec = json.load(open(auth_path))\n    if not rec.get('refresh_token'):\n        raise SystemExit('Run `litellm xai-oauth login` first: stored record cannot refresh')","typeGuard":"def has_refresh_token(payload: dict) -> bool:\n    return isinstance(payload, dict) and bool(payload.get('refresh_token'))","tryCatchPattern":"from litellm.llms.xai.oauth import XAIOAuthError, XAIOAuthLoginRequiredError\ntry:\n    token = XAIOAuthAuthenticator().get_access_token()\nexcept XAIOAuthLoginRequiredError:\n    raise RuntimeError('xAI OAuth re-login required: run `litellm xai-oauth login`')\nexcept XAIOAuthError as exc:\n    if 'refresh_token' in str(exc):\n        # token endpoint refused to mint a refresh token; surface scope problem\n        raise RuntimeError(f'xAI OAuth misconfigured (no refresh_token): {exc}') from exc\n    raise","preventionTips":["Complete `litellm xai-oauth login` once per environment before setting use_xai_oauth=True","Ensure the xAI OAuth client requests offline access so refresh tokens are issued","Treat the stored auth file as stateful: do not edit, truncate, or partially copy it between machines","Wrap OAuth token acquisition in one helper with error handling instead of scattering raw calls"],"tags":["xai","oauth","refresh-token","authentication","litellm"],"backgroundTag":"oauth-refresh-token-missing","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}