| 460 | auth: { |
| 461 | provider: "xai", |
| 462 | async loader(getAuth) { |
| 463 | const auth = await getAuth() |
| 464 | if (auth.type !== "oauth") return {} |
| 465 | |
| 466 | // Single-flight refresh: collapse concurrent fetches from this loaded |
| 467 | // provider onto one HTTP call so we don't replay a rotating refresh_token. |
| 468 | let refreshPromise: Promise<RefreshResult> | undefined |
| 469 | |
| 470 | return { |
| 471 | // Dummy bearer keeps the AI SDK from bailing on "missing apiKey"; the |
| 472 | // real OAuth token is injected by the fetch override below. |
| 473 | // We intentionally do NOT set baseURL — @ai-sdk/xai already defaults |
| 474 | // to https://api.x.ai/v1 and overriding here would silently route |
| 475 | // around a user-configured gateway. |
| 476 | apiKey: OAUTH_DUMMY_KEY, |
| 477 | async fetch(requestInput: RequestInfo | URL, init?: RequestInit) { |
| 478 | let currentAuth = await getAuth() |
| 479 | // Auth can flip from oauth to api mid-session (user re-runs |
| 480 | // /connect with a pasted key). When that happens, pass the |
| 481 | // request through untouched so the AI SDK's own apiKey-based |
| 482 | // Authorization header reaches xAI unmodified. |
| 483 | if (currentAuth.type !== "oauth") return fetch(requestInput, init) |
| 484 | |
| 485 | // Refresh either when the stored expires timestamp is within the |
| 486 | // skew window, or — for JWT access tokens — when the JWT exp |
| 487 | // claim itself is. The stored expires field is best-effort |
| 488 | // (xAI doesn't always return expires_in) so the JWT check is the |
| 489 | // load-bearing one for tokens that lack a fresh stored deadline. |
| 490 | const expiresSoon = |
| 491 | !currentAuth.expires || |
| 492 | currentAuth.expires - Date.now() <= ACCESS_TOKEN_REFRESH_SKEW_MS || |
| 493 | accessTokenIsExpiring(currentAuth.access) |
| 494 | if (expiresSoon) { |
| 495 | if (!refreshPromise) { |
| 496 | const refreshToken = currentAuth.refresh |
| 497 | refreshPromise = refreshAccessToken(refreshToken, options) |
| 498 | .then(async (tokens) => { |
| 499 | const refreshedExpires = Date.now() + (tokens.expires_in ?? 3600) * 1000 |
| 500 | const refreshedRefresh = tokens.refresh_token || refreshToken |
| 501 | // Persist the rotated pair as best-effort. xAI has already consumed the |
| 502 | // old refresh_token by the time we get here; an auth.set failure leaves |
| 503 | // the on-disk state stale but the in-memory result is still valid for |
| 504 | // this turn. The next live refresh against the stale disk state will |
| 505 | // 4xx and force re-login — a known cross-process limitation. |
| 506 | await input.client.auth |
| 507 | .set({ |
| 508 | path: { id: "xai" }, |
| 509 | body: { |
| 510 | type: "oauth", |
| 511 | access: tokens.access_token, |
| 512 | refresh: refreshedRefresh, |
| 513 | expires: refreshedExpires, |
| 514 | }, |
| 515 | }) |
| 516 | .catch(() => {}) |
| 517 | return { access: tokens.access_token, refresh: refreshedRefresh, expires: refreshedExpires } |
| 518 | }) |
| 519 | .finally(() => { |