From 7dad13cd3a9b20448fea951baeb6a74ba3ec6a28 Mon Sep 17 00:00:00 2001 From: Dave Walker Date: Thu, 29 Jan 2026 08:15:00 +0000 Subject: [PATCH] fix(auth): refresh all OAuth profiles per provider Previously, resolveOAuthToken() would return after finding the first valid profile for a provider, leaving other profiles expired. This caused multi-account setups to only refresh one account per provider. Additionally, the status display would show stale expiry times because it read credentials before the usage fetch triggered refreshes. Changes: - Renamed resolveOAuthToken() to resolveOAuthTokens() and changed it to collect all valid profiles instead of returning early - Updated resolveProviderAuths() to spread all resolved tokens - Added store re-read in list.status-command.ts after usage fetch to display refreshed credential state Fixes #3803 --- CHANGELOG.md | 1 + src/commands/models/list.status-command.ts | 16 ++++++++++++++-- src/infra/provider-usage.auth.ts | 15 ++++++++------- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a134359f5..68661b8e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Status: beta. - Memory Search: allow extra paths for memory indexing. (#3600) Thanks @kira-ariaki. ### Changes +- Auth: fix OAuth refresh only refreshing one profile per provider, leaving other accounts expired. (#3803) - Providers: add Venice AI integration; update Moonshot Kimi references to kimi-k2.5; update MiniMax API endpoint/format. (#2762, #3064) - Providers: add Xiaomi MiMo (mimo-v2-flash) support and onboarding flow. (#3454) Thanks @WqyJh. - Telegram: quote replies, edit-message action, silent sends, sticker support + vision caching, linkPreview toggle, plugin sendPayload support. (#2900, #2394, #2382, #2548, #1700, #1917) diff --git a/src/commands/models/list.status-command.ts b/src/commands/models/list.status-command.ts index 3e18dc41f..4ba7c041c 100644 --- a/src/commands/models/list.status-command.ts +++ b/src/commands/models/list.status-command.ts @@ -528,6 +528,18 @@ export async function modelsStatusCommand( } } + // Re-read auth health after usage fetch, which may have refreshed expired tokens + const refreshedStore = ensureAuthProfileStore(); + const refreshedHealth = buildAuthHealthSummary({ + store: refreshedStore, + cfg, + warnAfterMs: DEFAULT_OAUTH_WARN_MS, + providers, + }); + const refreshedOauthProfiles = refreshedHealth.profiles.filter( + (profile) => profile.type === "oauth" || profile.type === "token", + ); + const formatStatus = (status: string) => { if (status === "ok") return colorize(rich, theme.success, "ok"); if (status === "static") return colorize(rich, theme.muted, "static"); @@ -536,8 +548,8 @@ export async function modelsStatusCommand( return colorize(rich, theme.error, "expired"); }; - const profilesByProvider = new Map(); - for (const profile of oauthProfiles) { + const profilesByProvider = new Map(); + for (const profile of refreshedOauthProfiles) { const current = profilesByProvider.get(profile.provider); if (current) current.push(profile); else profilesByProvider.set(profile.provider, [profile]); diff --git a/src/infra/provider-usage.auth.ts b/src/infra/provider-usage.auth.ts index e0d9a6ef9..e22c2c3fa 100644 --- a/src/infra/provider-usage.auth.ts +++ b/src/infra/provider-usage.auth.ts @@ -123,10 +123,10 @@ function resolveXiaomiApiKey(): string | undefined { return undefined; } -async function resolveOAuthToken(params: { +async function resolveOAuthTokens(params: { provider: UsageProviderId; agentDir?: string; -}): Promise { +}): Promise { const cfg = loadConfig(); const store = ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false, @@ -143,6 +143,7 @@ async function resolveOAuthToken(params: { if (!deduped.includes(entry)) deduped.push(entry); } + const results: ProviderAuth[] = []; for (const profileId of deduped) { const cred = store.profiles[profileId]; if (!cred || (cred.type !== "oauth" && cred.type !== "token")) continue; @@ -161,20 +162,20 @@ async function resolveOAuthToken(params: { const parsed = parseGoogleToken(resolved.apiKey); token = parsed?.token ?? resolved.apiKey; } - return { + results.push({ provider: params.provider, token, accountId: cred.type === "oauth" && "accountId" in cred ? (cred as { accountId?: string }).accountId : undefined, - }; + }); } catch { // ignore } } - return null; + return results; } function resolveOAuthProviders(agentDir?: string): UsageProviderId[] { @@ -233,11 +234,11 @@ export async function resolveProviderAuths(params: { } if (!oauthProviders.includes(provider)) continue; - const auth = await resolveOAuthToken({ + const resolved = await resolveOAuthTokens({ provider, agentDir: params.agentDir, }); - if (auth) auths.push(auth); + auths.push(...resolved); } return auths;