BerriAI/litellm · error · XAIOAuthError

xAI OAuth token response missing refresh_token

Error message

xAI OAuth token response missing refresh_token

What it means

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.

Source

Thrown at litellm/llms/xai/oauth.py:341

            body: Final = response.json()
        except ValueError as exc:
            raise XAIOAuthError("xAI OAuth token response was not valid JSON") from exc
        if not isinstance(body, dict):
            raise XAIOAuthError("xAI OAuth token response was not an object")
        return body

    def _build_auth_record(
        self,
        token_payload: dict[str, Any],
        token_endpoint: str,
        fallback_refresh_token: str | None = None,
    ) -> dict[str, Any]:
        access_token: Final = token_payload.get("access_token")
        refresh_token: Final = token_payload.get("refresh_token") or fallback_refresh_token
        if not access_token:
            raise XAIOAuthError("xAI OAuth token response missing access_token")
        if not refresh_token:
            raise XAIOAuthError("xAI OAuth token response missing refresh_token")
        expires_in: Final = token_payload.get("expires_in") or 3600
        try:
            expires_at = int(time.time() + int(expires_in))
        except (TypeError, ValueError):
            expires_at = int(time.time() + 3600)
        return {
            "access_token": access_token,
            "refresh_token": refresh_token,
            "id_token": token_payload.get("id_token"),
            "token_type": token_payload.get("token_type") or "Bearer",
            "token_endpoint": token_endpoint,
            "expires_at": expires_at,
        }

    def _refresh_tokens(self, auth_data: dict[str, Any]) -> dict[str, Any]:
        token_endpoint = auth_data.get("token_endpoint")
        if not token_endpoint:
            token_endpoint = self._discover()["token_endpoint"]

View on GitHub (pinned to 77b7c6c40c)

Solutions

  1. Re-run `litellm xai-oauth login` to start a fresh authorization-code flow that mints a full token set
  2. Verify the xAI OAuth client requests the offline access / refresh scope so the token endpoint returns refresh_token
  3. When refreshing, keep the previously stored refresh token so _build_auth_record can fall back to it
  4. 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

Example fix

# before: auth record built from a payload without refresh_token
auth = xai_auth._build_auth_record(token_payload, token_endpoint)  # XAIOAuthError

# after: pass the stored token as fallback, or re-login for a fresh token set
auth = xai_auth._build_auth_record(token_payload, token_endpoint, fallback_refresh_token=stored.get('refresh_token'))
# shell: litellm xai-oauth login
Defensive patterns

Strategy: try-catch

Validate before calling

import os, json
# before enabling OAuth, confirm the stored auth record is complete
if os.path.exists(auth_path):
    rec = json.load(open(auth_path))
    if not rec.get('refresh_token'):
        raise SystemExit('Run `litellm xai-oauth login` first: stored record cannot refresh')

Type guard

def has_refresh_token(payload: dict) -> bool:
    return isinstance(payload, dict) and bool(payload.get('refresh_token'))

Try / catch

from litellm.llms.xai.oauth import XAIOAuthError, XAIOAuthLoginRequiredError
try:
    token = XAIOAuthAuthenticator().get_access_token()
except XAIOAuthLoginRequiredError:
    raise RuntimeError('xAI OAuth re-login required: run `litellm xai-oauth login`')
except XAIOAuthError as exc:
    if 'refresh_token' in str(exc):
        # token endpoint refused to mint a refresh token; surface scope problem
        raise RuntimeError(f'xAI OAuth misconfigured (no refresh_token): {exc}') from exc
    raise

Prevention

When it happens

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

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

Related errors


AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18). Data as JSON: /api/errors/ba07f619baf7a101. Report an issue: GitHub.