diff --git a/open-sse/handlers/chatCore.js b/open-sse/handlers/chatCore.js index 4e3475f4..4f91e020 100644 --- a/open-sse/handlers/chatCore.js +++ b/open-sse/handlers/chatCore.js @@ -330,7 +330,18 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred // Handle 401/403 - try token refresh (skip for noAuth providers) if (!executor.noAuth && (providerResponse.status === HTTP_STATUS.UNAUTHORIZED || providerResponse.status === HTTP_STATUS.FORBIDDEN)) { try { - const newCredentials = await refreshWithRetry(() => executor.refreshCredentials(credentials, log), 3, log); + // Mutate credentials after each successful refresh: rotating refresh_token + // providers (xAI/grok-cli) issue a new RT on every refresh; without this, + // refreshWithRetry's 2nd/3rd attempt reuses the already-consumed RT โ†’ + // invalid_grant โ†’ auth_failed retryable=false. + const newCredentials = await refreshWithRetry(async () => { + const result = await executor.refreshCredentials(credentials, log); + if (result?.refreshToken && result.refreshToken !== credentials.refreshToken) { + if (result.accessToken) credentials.accessToken = result.accessToken; + credentials.refreshToken = result.refreshToken; + } + return result; + }, 3, log); if (newCredentials?.accessToken || newCredentials?.copilotToken) { if (log?.line) log.line(reqTag, "๐Ÿ”‘", `TOKEN REFRESHED ยท ${provider}/${model}`); Object.assign(credentials, newCredentials);