From d479216d929f047d136bcf2183b84296c8b4dd28 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 9 May 2026 10:31:26 +0100 Subject: [PATCH] fix: honor effective model auth health --- CHANGELOG.md | 1 + src/agents/auth-health.test.ts | 5 +++++ src/agents/auth-health.ts | 15 ++++++++++----- src/commands/models/list.status-command.ts | 2 +- .../server-methods/models-auth-status.test.ts | 16 ++++++++++++++++ src/gateway/server-methods/models-auth-status.ts | 5 ++++- 6 files changed, 37 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c114b273728..7d38b15edd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -148,6 +148,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Memory/QMD: warn with a manual stale collection removal hint when QMD reports a path/pattern conflict but `collection list` lacks verifiable metadata, avoiding unsafe stderr-only rebinds. Refs #71783. (#72297) Thanks @MonkeyLeeT. +- Models/auth: make `openclaw models status --check` and dashboard auth health honor effective auth profile order while keeping stale profiles visible. (#79685) Thanks @nimbleenigma. - Docs/Subagents: correct the listed sub-agent bootstrap context files to include `SOUL.md`, `IDENTITY.md`, and `USER.md`. (#79470) Thanks @lastguru-net. - Backup: keep live backup archives from copying current agent session transcripts, cron run logs, and delivery queues while preserving workspace lock/temp files and keeping `--json` output parseable when volatile files are skipped. Fixes #72249. (#72251) Thanks @abnershang. - OpenAI/Codex: install the Codex runtime plugin from npm during OpenAI onboarding and load it automatically for implicit OpenAI model routes, while preserving manual PI runtime overrides. Fixes #79358. diff --git a/src/agents/auth-health.test.ts b/src/agents/auth-health.test.ts index 461c8c417d0..7b7c3fe72b4 100644 --- a/src/agents/auth-health.test.ts +++ b/src/agents/auth-health.test.ts @@ -157,6 +157,9 @@ describe("buildAuthHealthSummary", () => { const provider = summary.providers.find((entry) => entry.provider === "openai-codex"); expect(provider?.status).toBe("ok"); expect(provider?.expiresAt).toBe(now + DEFAULT_OAUTH_WARN_MS + 60_000); + expect(provider?.effectiveProfiles?.map((profile) => profile.profileId)).toEqual([ + "openai-codex:named", + ]); expect(provider?.profiles.map((profile) => profile.profileId)).toEqual([ "openai-codex:default", "openai-codex:named", @@ -188,6 +191,7 @@ describe("buildAuthHealthSummary", () => { const provider = summary.providers.find((entry) => entry.provider === "codex-cli"); expect(provider?.status).toBe("missing"); + expect(provider?.effectiveProfiles).toEqual([]); expect(provider?.profiles.map((profile) => profile.profileId)).toEqual(["codex-cli:legacy"]); }); @@ -403,6 +407,7 @@ describe("buildAuthHealthSummary", () => { { provider: "zai", status: "static", + effectiveProfiles: summary.profiles, profiles: summary.profiles, }, ]); diff --git a/src/agents/auth-health.ts b/src/agents/auth-health.ts index 46d169c32cb..23776786016 100644 --- a/src/agents/auth-health.ts +++ b/src/agents/auth-health.ts @@ -10,8 +10,7 @@ import { resolveEffectiveOAuthCredential } from "./auth-profiles/effective-oauth import { resolveAuthProfileOrder } from "./auth-profiles/order.js"; import type { AuthProfileCredential, AuthProfileStore } from "./auth-profiles/types.js"; import { resolveProviderIdForAuth } from "./provider-auth-aliases.js"; -import { findNormalizedProviderValue } from "./provider-id.js"; -import { normalizeProviderId } from "./provider-id.js"; +import { findNormalizedProviderValue, normalizeProviderId } from "./provider-id.js"; type AuthProfileSource = "store"; @@ -36,6 +35,11 @@ export type AuthProviderHealth = { status: AuthProviderHealthStatus; expiresAt?: number; remainingMs?: number; + /** + * Full credential inventory stays in `profiles`; provider rollups use this + * effective subset after auth order, aliases, and explicit exclusions apply. + */ + effectiveProfiles?: AuthProfileHealth[]; profiles: AuthProfileHealth[]; }; @@ -298,8 +302,9 @@ export function buildAuthHealthSummary(params: { }; for (const provider of providersMap.values()) { - const statusProfiles = resolveProviderStatusProfiles(provider); - if (statusProfiles.length === 0) { + const effectiveProfiles = resolveProviderStatusProfiles(provider); + provider.effectiveProfiles = effectiveProfiles; + if (effectiveProfiles.length === 0) { provider.status = "missing"; provider.expiresAt = undefined; provider.remainingMs = undefined; @@ -311,7 +316,7 @@ export function buildAuthHealthSummary(params: { let hasExpiredOrMissing = false; let hasExpiring = false; let earliestExpiry: number | undefined; - for (const profile of statusProfiles) { + for (const profile of effectiveProfiles) { if (profile.type === "api_key") { hasApiKeyProfile = true; continue; diff --git a/src/commands/models/list.status-command.ts b/src/commands/models/list.status-command.ts index fca1cf08a1c..a153a6501bc 100644 --- a/src/commands/models/list.status-command.ts +++ b/src/commands/models/list.status-command.ts @@ -573,7 +573,7 @@ export async function modelsStatusCommand( if ( usage.allowCodexRuntimeFallback && openAIProviderUsesCodexRuntimeByDefault({ provider: usage.provider, config: cfg }) && - providerAuthMap.has(OPENAI_CODEX_PROVIDER_ID) + hasUsableProviderAuth(OPENAI_CODEX_PROVIDER_ID) ) { providersInUse.add(OPENAI_CODEX_PROVIDER_ID); } diff --git a/src/gateway/server-methods/models-auth-status.test.ts b/src/gateway/server-methods/models-auth-status.test.ts index 26cd1c30c71..0548efcd529 100644 --- a/src/gateway/server-methods/models-auth-status.test.ts +++ b/src/gateway/server-methods/models-auth-status.test.ts @@ -548,6 +548,22 @@ describe("aggregateOAuthStatus", () => { expect(result.status).toBe("ok"); }); + it("uses effective OAuth profiles while keeping stale inventory visible", () => { + const healthy = oauth("ok", expiring + 10_000_000); + const stale = oauth("expired", NOW - 1); + const result = aggregateOAuthStatus( + { + provider: "openai-codex", + status: "ok", + effectiveProfiles: [healthy], + profiles: [stale, healthy], + }, + NOW, + ); + expect(result.status).toBe("ok"); + expect(result.expiresAt).toBe(healthy.expiresAt); + }); + it("falls back to prov.status when no OAuth profiles exist", () => { const result = aggregateOAuthStatus( { diff --git a/src/gateway/server-methods/models-auth-status.ts b/src/gateway/server-methods/models-auth-status.ts index 6f7d637046c..dfcdb87a506 100644 --- a/src/gateway/server-methods/models-auth-status.ts +++ b/src/gateway/server-methods/models-auth-status.ts @@ -106,6 +106,8 @@ function providerDisplayName(provider: string): string { * where a healthy OAuth sits alongside an expired/missing bearer token. * For the dashboard's OAuth-health signal, token profiles are a separate * concern — we want "is OAuth healthy?", not "is every credential healthy?" + * It also consumes the provider's effective profile subset when auth order + * excludes stale inventory from the runtime credential path. * * `expectsOAuth` surfaces the configured-OAuth-but-no-oauth-profile case as * `missing` instead of silently falling back to the provider's rollup (which @@ -124,7 +126,8 @@ export function aggregateOAuthStatus( expiresAt?: number; remainingMs?: number; } { - const oauth = prov.profiles.filter((p) => p.type === "oauth"); + const profiles = prov.effectiveProfiles ?? prov.profiles; + const oauth = profiles.filter((p) => p.type === "oauth"); if (oauth.length === 0) { if (expectsOAuth) { return { status: "missing" };