fix: honor effective model auth health

This commit is contained in:
Peter Steinberger
2026-05-09 10:31:26 +01:00
parent aab7756859
commit d479216d92
6 changed files with 37 additions and 7 deletions

View File

@@ -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.

View File

@@ -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,
},
]);

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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(
{

View File

@@ -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" };