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
This commit is contained in:
Dave Walker 2026-01-29 08:15:00 +00:00
parent 4583f88626
commit 7dad13cd3a
3 changed files with 23 additions and 9 deletions

View File

@ -14,6 +14,7 @@ Status: beta.
- Memory Search: allow extra paths for memory indexing. (#3600) Thanks @kira-ariaki. - Memory Search: allow extra paths for memory indexing. (#3600) Thanks @kira-ariaki.
### Changes ### 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 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. - 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) - Telegram: quote replies, edit-message action, silent sends, sticker support + vision caching, linkPreview toggle, plugin sendPayload support. (#2900, #2394, #2382, #2548, #1700, #1917)

View File

@ -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) => { const formatStatus = (status: string) => {
if (status === "ok") return colorize(rich, theme.success, "ok"); if (status === "ok") return colorize(rich, theme.success, "ok");
if (status === "static") return colorize(rich, theme.muted, "static"); if (status === "static") return colorize(rich, theme.muted, "static");
@ -536,8 +548,8 @@ export async function modelsStatusCommand(
return colorize(rich, theme.error, "expired"); return colorize(rich, theme.error, "expired");
}; };
const profilesByProvider = new Map<string, typeof oauthProfiles>(); const profilesByProvider = new Map<string, typeof refreshedOauthProfiles>();
for (const profile of oauthProfiles) { for (const profile of refreshedOauthProfiles) {
const current = profilesByProvider.get(profile.provider); const current = profilesByProvider.get(profile.provider);
if (current) current.push(profile); if (current) current.push(profile);
else profilesByProvider.set(profile.provider, [profile]); else profilesByProvider.set(profile.provider, [profile]);

View File

@ -123,10 +123,10 @@ function resolveXiaomiApiKey(): string | undefined {
return undefined; return undefined;
} }
async function resolveOAuthToken(params: { async function resolveOAuthTokens(params: {
provider: UsageProviderId; provider: UsageProviderId;
agentDir?: string; agentDir?: string;
}): Promise<ProviderAuth | null> { }): Promise<ProviderAuth[]> {
const cfg = loadConfig(); const cfg = loadConfig();
const store = ensureAuthProfileStore(params.agentDir, { const store = ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: false, allowKeychainPrompt: false,
@ -143,6 +143,7 @@ async function resolveOAuthToken(params: {
if (!deduped.includes(entry)) deduped.push(entry); if (!deduped.includes(entry)) deduped.push(entry);
} }
const results: ProviderAuth[] = [];
for (const profileId of deduped) { for (const profileId of deduped) {
const cred = store.profiles[profileId]; const cred = store.profiles[profileId];
if (!cred || (cred.type !== "oauth" && cred.type !== "token")) continue; if (!cred || (cred.type !== "oauth" && cred.type !== "token")) continue;
@ -161,20 +162,20 @@ async function resolveOAuthToken(params: {
const parsed = parseGoogleToken(resolved.apiKey); const parsed = parseGoogleToken(resolved.apiKey);
token = parsed?.token ?? resolved.apiKey; token = parsed?.token ?? resolved.apiKey;
} }
return { results.push({
provider: params.provider, provider: params.provider,
token, token,
accountId: accountId:
cred.type === "oauth" && "accountId" in cred cred.type === "oauth" && "accountId" in cred
? (cred as { accountId?: string }).accountId ? (cred as { accountId?: string }).accountId
: undefined, : undefined,
}; });
} catch { } catch {
// ignore // ignore
} }
} }
return null; return results;
} }
function resolveOAuthProviders(agentDir?: string): UsageProviderId[] { function resolveOAuthProviders(agentDir?: string): UsageProviderId[] {
@ -233,11 +234,11 @@ export async function resolveProviderAuths(params: {
} }
if (!oauthProviders.includes(provider)) continue; if (!oauthProviders.includes(provider)) continue;
const auth = await resolveOAuthToken({ const resolved = await resolveOAuthTokens({
provider, provider,
agentDir: params.agentDir, agentDir: params.agentDir,
}); });
if (auth) auths.push(auth); auths.push(...resolved);
} }
return auths; return auths;