import path from "node:path"; import type { RuntimeEnv } from "../../runtime.js"; import { resolveOpenClawAgentDir } from "../../agents/agent-paths.js"; import { resolveAgentDir, resolveAgentModelFallbacksOverride, resolveAgentModelPrimary, } from "../../agents/agent-scope.js"; import { buildAuthHealthSummary, DEFAULT_OAUTH_WARN_MS, formatRemainingShort, } from "../../agents/auth-health.js"; import { ensureAuthProfileStore, resolveAuthStorePathForDisplay, resolveProfileUnusableUntilForDisplay, } from "../../agents/auth-profiles.js"; import { resolveEnvApiKey } from "../../agents/model-auth.js"; import { buildModelAliasIndex, parseModelRef, resolveConfiguredModelRef, resolveDefaultModelForAgent, resolveModelRefFromString, } from "../../agents/model-selection.js"; import { formatCliCommand } from "../../cli/command-format.js"; import { withProgressTotals } from "../../cli/progress.js"; import { CONFIG_PATH, loadConfig } from "../../config/config.js"; import { formatUsageWindowSummary, loadProviderUsageSummary, resolveUsageProviderId, type UsageProviderId, } from "../../infra/provider-usage.js"; import { getShellEnvAppliedKeys, shouldEnableShellEnvFallback } from "../../infra/shell-env.js"; import { renderTable } from "../../terminal/table.js"; import { colorize, theme } from "../../terminal/theme.js"; import { shortenHomePath } from "../../utils.js"; import { resolveProviderAuthOverview } from "./list.auth-overview.js"; import { isRich } from "./list.format.js"; import { describeProbeSummary, formatProbeLatency, runAuthProbes, sortProbeResults, type AuthProbeSummary, } from "./list.probe.js"; import { DEFAULT_MODEL, DEFAULT_PROVIDER, ensureFlagCompatibility, resolveKnownAgentId, } from "./shared.js"; export async function modelsStatusCommand( opts: { json?: boolean; plain?: boolean; check?: boolean; probe?: boolean; probeProvider?: string; probeProfile?: string | string[]; probeTimeout?: string; probeConcurrency?: string; probeMaxTokens?: string; agent?: string; }, runtime: RuntimeEnv, ) { ensureFlagCompatibility(opts); if (opts.plain && opts.probe) { throw new Error("--probe cannot be used with --plain output."); } const cfg = loadConfig(); const agentId = resolveKnownAgentId({ cfg, rawAgentId: opts.agent }); const agentDir = agentId ? resolveAgentDir(cfg, agentId) : resolveOpenClawAgentDir(); const agentModelPrimary = agentId ? resolveAgentModelPrimary(cfg, agentId) : undefined; const agentFallbacksOverride = agentId ? resolveAgentModelFallbacksOverride(cfg, agentId) : undefined; const resolved = agentId ? resolveDefaultModelForAgent({ cfg, agentId }) : resolveConfiguredModelRef({ cfg, defaultProvider: DEFAULT_PROVIDER, defaultModel: DEFAULT_MODEL, }); const modelConfig = cfg.agents?.defaults?.model as | { primary?: string; fallbacks?: string[] } | string | undefined; const imageConfig = cfg.agents?.defaults?.imageModel as | { primary?: string; fallbacks?: string[] } | string | undefined; const rawDefaultsModel = typeof modelConfig === "string" ? modelConfig.trim() : (modelConfig?.primary?.trim() ?? ""); const rawModel = agentModelPrimary ?? rawDefaultsModel; const resolvedLabel = `${resolved.provider}/${resolved.model}`; const defaultLabel = rawModel || resolvedLabel; const defaultsFallbacks = typeof modelConfig === "object" ? (modelConfig?.fallbacks ?? []) : []; const fallbacks = agentFallbacksOverride ?? defaultsFallbacks; const imageModel = typeof imageConfig === "string" ? imageConfig.trim() : (imageConfig?.primary?.trim() ?? ""); const imageFallbacks = typeof imageConfig === "object" ? (imageConfig?.fallbacks ?? []) : []; const aliases = Object.entries(cfg.agents?.defaults?.models ?? {}).reduce>( (acc, [key, entry]) => { const alias = entry?.alias?.trim(); if (alias) { acc[alias] = key; } return acc; }, {}, ); const allowed = Object.keys(cfg.agents?.defaults?.models ?? {}); const store = ensureAuthProfileStore(agentDir); const modelsPath = path.join(agentDir, "models.json"); const providersFromStore = new Set( Object.values(store.profiles) .map((profile) => profile.provider) .filter((p): p is string => Boolean(p)), ); const providersFromConfig = new Set( Object.keys(cfg.models?.providers ?? {}) .map((p) => p.trim()) .filter(Boolean), ); const providersFromModels = new Set(); const providersInUse = new Set(); for (const raw of [defaultLabel, ...fallbacks, imageModel, ...imageFallbacks, ...allowed]) { const parsed = parseModelRef(String(raw ?? ""), DEFAULT_PROVIDER); if (parsed?.provider) { providersFromModels.add(parsed.provider); } } for (const raw of [defaultLabel, ...fallbacks, imageModel, ...imageFallbacks]) { const parsed = parseModelRef(String(raw ?? ""), DEFAULT_PROVIDER); if (parsed?.provider) { providersInUse.add(parsed.provider); } } const providersFromEnv = new Set(); // Keep in sync with resolveEnvApiKey() mappings (we want visibility even when // a provider isn't currently selected in config/models). const envProbeProviders = [ "anthropic", "github-copilot", "google-vertex", "openai", "google", "groq", "cerebras", "xai", "openrouter", "zai", "mistral", "synthetic", ]; for (const provider of envProbeProviders) { if (resolveEnvApiKey(provider)) { providersFromEnv.add(provider); } } const providers = Array.from( new Set([ ...providersFromStore, ...providersFromConfig, ...providersFromModels, ...providersFromEnv, ]), ) .map((p) => p.trim()) .filter(Boolean) .toSorted((a, b) => a.localeCompare(b)); const applied = getShellEnvAppliedKeys(); const shellFallbackEnabled = shouldEnableShellEnvFallback(process.env) || cfg.env?.shellEnv?.enabled === true; const providerAuth = providers .map((provider) => resolveProviderAuthOverview({ provider, cfg, store, modelsPath })) .filter((entry) => { const hasAny = entry.profiles.count > 0 || Boolean(entry.env) || Boolean(entry.modelsJson); return hasAny; }); const providerAuthMap = new Map(providerAuth.map((entry) => [entry.provider, entry])); const missingProvidersInUse = Array.from(providersInUse) .filter((provider) => !providerAuthMap.has(provider)) .toSorted((a, b) => a.localeCompare(b)); const probeProfileIds = (() => { if (!opts.probeProfile) { return []; } const raw = Array.isArray(opts.probeProfile) ? opts.probeProfile : [opts.probeProfile]; return raw .flatMap((value) => String(value ?? "").split(",")) .map((value) => value.trim()) .filter(Boolean); })(); const probeTimeoutMs = opts.probeTimeout ? Number(opts.probeTimeout) : 8000; if (!Number.isFinite(probeTimeoutMs) || probeTimeoutMs <= 0) { throw new Error("--probe-timeout must be a positive number (ms)."); } const probeConcurrency = opts.probeConcurrency ? Number(opts.probeConcurrency) : 2; if (!Number.isFinite(probeConcurrency) || probeConcurrency <= 0) { throw new Error("--probe-concurrency must be > 0."); } const probeMaxTokens = opts.probeMaxTokens ? Number(opts.probeMaxTokens) : 8; if (!Number.isFinite(probeMaxTokens) || probeMaxTokens <= 0) { throw new Error("--probe-max-tokens must be > 0."); } const aliasIndex = buildModelAliasIndex({ cfg, defaultProvider: DEFAULT_PROVIDER }); const rawCandidates = [ rawModel || resolvedLabel, ...fallbacks, imageModel, ...imageFallbacks, ...allowed, ].filter(Boolean); const resolvedCandidates = rawCandidates .map( (raw) => resolveModelRefFromString({ raw: String(raw ?? ""), defaultProvider: DEFAULT_PROVIDER, aliasIndex, })?.ref, ) .filter((ref): ref is { provider: string; model: string } => Boolean(ref)); const modelCandidates = resolvedCandidates.map((ref) => `${ref.provider}/${ref.model}`); let probeSummary: AuthProbeSummary | undefined; if (opts.probe) { probeSummary = await withProgressTotals( { label: "Probing auth profiles…", total: 1 }, async (update) => { return await runAuthProbes({ cfg, providers, modelCandidates, options: { provider: opts.probeProvider, profileIds: probeProfileIds, timeoutMs: probeTimeoutMs, concurrency: probeConcurrency, maxTokens: probeMaxTokens, }, onProgress: update, }); }, ); } const providersWithOauth = providerAuth .filter( (entry) => entry.profiles.oauth > 0 || entry.profiles.token > 0 || entry.env?.value === "OAuth (env)", ) .map((entry) => { const count = entry.profiles.oauth + entry.profiles.token + (entry.env?.value === "OAuth (env)" ? 1 : 0); return `${entry.provider} (${count})`; }); const authHealth = buildAuthHealthSummary({ store, cfg, warnAfterMs: DEFAULT_OAUTH_WARN_MS, providers, }); const oauthProfiles = authHealth.profiles.filter( (profile) => profile.type === "oauth" || profile.type === "token", ); const unusableProfiles = (() => { const now = Date.now(); const out: Array<{ profileId: string; provider?: string; kind: "cooldown" | "disabled"; reason?: string; until: number; remainingMs: number; }> = []; for (const profileId of Object.keys(store.usageStats ?? {})) { const unusableUntil = resolveProfileUnusableUntilForDisplay(store, profileId); if (!unusableUntil || now >= unusableUntil) { continue; } const stats = store.usageStats?.[profileId]; const kind = typeof stats?.disabledUntil === "number" && now < stats.disabledUntil ? "disabled" : "cooldown"; out.push({ profileId, provider: store.profiles[profileId]?.provider, kind, reason: stats?.disabledReason, until: unusableUntil, remainingMs: unusableUntil - now, }); } return out.toSorted((a, b) => a.remainingMs - b.remainingMs); })(); const checkStatus = (() => { const hasExpiredOrMissing = oauthProfiles.some((profile) => ["expired", "missing"].includes(profile.status)) || missingProvidersInUse.length > 0; const hasExpiring = oauthProfiles.some((profile) => profile.status === "expiring"); if (hasExpiredOrMissing) { return 1; } if (hasExpiring) { return 2; } return 0; })(); if (opts.json) { runtime.log( JSON.stringify( { configPath: CONFIG_PATH, ...(agentId ? { agentId } : {}), agentDir, defaultModel: defaultLabel, resolvedDefault: resolvedLabel, fallbacks, imageModel: imageModel || null, imageFallbacks, ...(agentId ? { modelConfig: { defaultSource: agentModelPrimary ? "agent" : "defaults", fallbacksSource: agentFallbacksOverride !== undefined ? "agent" : "defaults", }, } : {}), aliases, allowed, auth: { storePath: resolveAuthStorePathForDisplay(agentDir), shellEnvFallback: { enabled: shellFallbackEnabled, appliedKeys: applied, }, providersWithOAuth: providersWithOauth, missingProvidersInUse, providers: providerAuth, unusableProfiles, oauth: { warnAfterMs: authHealth.warnAfterMs, profiles: authHealth.profiles, providers: authHealth.providers, }, probes: probeSummary, }, }, null, 2, ), ); if (opts.check) { runtime.exit(checkStatus); } return; } if (opts.plain) { runtime.log(resolvedLabel); if (opts.check) { runtime.exit(checkStatus); } return; } const rich = isRich(opts); type ModelConfigSource = "agent" | "defaults"; const label = (value: string) => colorize(rich, theme.accent, value.padEnd(14)); const labelWithSource = (value: string, source?: ModelConfigSource) => label(source ? `${value} (${source})` : value); const displayDefault = rawModel && rawModel !== resolvedLabel ? `${resolvedLabel} (from ${rawModel})` : resolvedLabel; runtime.log( `${label("Config")}${colorize(rich, theme.muted, ":")} ${colorize(rich, theme.info, shortenHomePath(CONFIG_PATH))}`, ); runtime.log( `${label("Agent dir")}${colorize(rich, theme.muted, ":")} ${colorize( rich, theme.info, shortenHomePath(agentDir), )}`, ); runtime.log( `${labelWithSource("Default", agentId ? (agentModelPrimary ? "agent" : "defaults") : undefined)}${colorize( rich, theme.muted, ":", )} ${colorize(rich, theme.success, displayDefault)}`, ); runtime.log( `${labelWithSource( `Fallbacks (${fallbacks.length || 0})`, agentId ? (agentFallbacksOverride !== undefined ? "agent" : "defaults") : undefined, )}${colorize(rich, theme.muted, ":")} ${colorize( rich, fallbacks.length ? theme.warn : theme.muted, fallbacks.length ? fallbacks.join(", ") : "-", )}`, ); runtime.log( `${labelWithSource("Image model", agentId ? "defaults" : undefined)}${colorize( rich, theme.muted, ":", )} ${colorize(rich, imageModel ? theme.accentBright : theme.muted, imageModel || "-")}`, ); runtime.log( `${labelWithSource( `Image fallbacks (${imageFallbacks.length || 0})`, agentId ? "defaults" : undefined, )}${colorize(rich, theme.muted, ":")} ${colorize( rich, imageFallbacks.length ? theme.accentBright : theme.muted, imageFallbacks.length ? imageFallbacks.join(", ") : "-", )}`, ); runtime.log( `${label(`Aliases (${Object.keys(aliases).length || 0})`)}${colorize(rich, theme.muted, ":")} ${colorize( rich, Object.keys(aliases).length ? theme.accent : theme.muted, Object.keys(aliases).length ? Object.entries(aliases) .map(([alias, target]) => rich ? `${theme.accentDim(alias)} ${theme.muted("->")} ${theme.info(target)}` : `${alias} -> ${target}`, ) .join(", ") : "-", )}`, ); runtime.log( `${label(`Configured models (${allowed.length || 0})`)}${colorize(rich, theme.muted, ":")} ${colorize( rich, allowed.length ? theme.info : theme.muted, allowed.length ? allowed.join(", ") : "all", )}`, ); runtime.log(""); runtime.log(colorize(rich, theme.heading, "Auth overview")); runtime.log( `${label("Auth store")}${colorize(rich, theme.muted, ":")} ${colorize( rich, theme.info, shortenHomePath(resolveAuthStorePathForDisplay(agentDir)), )}`, ); runtime.log( `${label("Shell env")}${colorize(rich, theme.muted, ":")} ${colorize( rich, shellFallbackEnabled ? theme.success : theme.muted, shellFallbackEnabled ? "on" : "off", )}${applied.length ? colorize(rich, theme.muted, ` (applied: ${applied.join(", ")})`) : ""}`, ); runtime.log( `${label(`Providers w/ OAuth/tokens (${providersWithOauth.length || 0})`)}${colorize( rich, theme.muted, ":", )} ${colorize( rich, providersWithOauth.length ? theme.info : theme.muted, providersWithOauth.length ? providersWithOauth.join(", ") : "-", )}`, ); const formatKey = (key: string) => colorize(rich, theme.warn, key); const formatKeyValue = (key: string, value: string) => `${formatKey(key)}=${colorize(rich, theme.info, value)}`; const formatSeparator = () => colorize(rich, theme.muted, " | "); for (const entry of providerAuth) { const separator = formatSeparator(); const bits: string[] = []; bits.push( formatKeyValue( "effective", `${colorize(rich, theme.accentBright, entry.effective.kind)}:${colorize( rich, theme.muted, entry.effective.detail, )}`, ), ); if (entry.profiles.count > 0) { bits.push( formatKeyValue( "profiles", `${entry.profiles.count} (oauth=${entry.profiles.oauth}, token=${entry.profiles.token}, api_key=${entry.profiles.apiKey})`, ), ); if (entry.profiles.labels.length > 0) { bits.push(colorize(rich, theme.info, entry.profiles.labels.join(", "))); } } if (entry.env) { bits.push( formatKeyValue( "env", `${entry.env.value}${separator}${formatKeyValue("source", entry.env.source)}`, ), ); } if (entry.modelsJson) { bits.push( formatKeyValue( "models.json", `${entry.modelsJson.value}${separator}${formatKeyValue("source", entry.modelsJson.source)}`, ), ); } runtime.log(`- ${theme.heading(entry.provider)} ${bits.join(separator)}`); } if (missingProvidersInUse.length > 0) { runtime.log(""); runtime.log(colorize(rich, theme.heading, "Missing auth")); for (const provider of missingProvidersInUse) { const hint = provider === "anthropic" ? `Run \`claude setup-token\`, then \`${formatCliCommand("openclaw models auth setup-token")}\` or \`${formatCliCommand("openclaw configure")}\`.` : `Run \`${formatCliCommand("openclaw configure")}\` or set an API key env var.`; runtime.log(`- ${theme.heading(provider)} ${hint}`); } } runtime.log(""); runtime.log(colorize(rich, theme.heading, "OAuth/token status")); if (oauthProfiles.length === 0) { runtime.log(colorize(rich, theme.muted, "- none")); } else { const usageByProvider = new Map(); const usageProviders = Array.from( new Set( oauthProfiles .map((profile) => resolveUsageProviderId(profile.provider)) .filter((provider): provider is UsageProviderId => Boolean(provider)), ), ); if (usageProviders.length > 0) { try { const usageSummary = await loadProviderUsageSummary({ providers: usageProviders, agentDir, timeoutMs: 3500, }); for (const snapshot of usageSummary.providers) { const formatted = formatUsageWindowSummary(snapshot, { now: Date.now(), maxWindows: 2, includeResets: true, }); if (formatted) { usageByProvider.set(snapshot.provider, formatted); } } } catch { // ignore usage failures } } const formatStatus = (status: string) => { if (status === "ok") { return colorize(rich, theme.success, "ok"); } if (status === "static") { return colorize(rich, theme.muted, "static"); } if (status === "expiring") { return colorize(rich, theme.warn, "expiring"); } if (status === "missing") { return colorize(rich, theme.warn, "unknown"); } return colorize(rich, theme.error, "expired"); }; const profilesByProvider = new Map(); for (const profile of oauthProfiles) { const current = profilesByProvider.get(profile.provider); if (current) { current.push(profile); } else { profilesByProvider.set(profile.provider, [profile]); } } for (const [provider, profiles] of profilesByProvider) { const usageKey = resolveUsageProviderId(provider); const usage = usageKey ? usageByProvider.get(usageKey) : undefined; const usageSuffix = usage ? colorize(rich, theme.muted, ` usage: ${usage}`) : ""; runtime.log(`- ${colorize(rich, theme.heading, provider)}${usageSuffix}`); for (const profile of profiles) { const labelText = profile.label || profile.profileId; const label = colorize(rich, theme.accent, labelText); const status = formatStatus(profile.status); const expiry = profile.status === "static" ? "" : profile.expiresAt ? ` expires in ${formatRemainingShort(profile.remainingMs)}` : " expires unknown"; runtime.log(` - ${label} ${status}${expiry}`); } } } if (probeSummary) { runtime.log(""); runtime.log(colorize(rich, theme.heading, "Auth probes")); if (probeSummary.results.length === 0) { runtime.log(colorize(rich, theme.muted, "- none")); } else { const tableWidth = Math.max(60, (process.stdout.columns ?? 120) - 1); const sorted = sortProbeResults(probeSummary.results); const statusColor = (status: string) => { if (status === "ok") { return theme.success; } if (status === "rate_limit") { return theme.warn; } if (status === "timeout" || status === "billing") { return theme.warn; } if (status === "auth" || status === "format") { return theme.error; } if (status === "no_model") { return theme.muted; } return theme.muted; }; const rows = sorted.map((result) => { const status = colorize(rich, statusColor(result.status), result.status); const latency = formatProbeLatency(result.latencyMs); const modelLabel = result.model ?? `${result.provider}/-`; const modeLabel = result.mode ? ` ${colorize(rich, theme.muted, `(${result.mode})`)}` : ""; const profile = `${colorize(rich, theme.accent, result.label)}${modeLabel}`; const detail = result.error?.trim(); const detailLabel = detail ? `\n${colorize(rich, theme.muted, `↳ ${detail}`)}` : ""; const statusLabel = `${status}${colorize(rich, theme.muted, ` · ${latency}`)}${detailLabel}`; return { Model: colorize(rich, theme.heading, modelLabel), Profile: profile, Status: statusLabel, }; }); runtime.log( renderTable({ width: tableWidth, columns: [ { key: "Model", header: "Model", minWidth: 18 }, { key: "Profile", header: "Profile", minWidth: 24 }, { key: "Status", header: "Status", minWidth: 12 }, ], rows, }).trimEnd(), ); runtime.log(colorize(rich, theme.muted, describeProbeSummary(probeSummary))); } } if (opts.check) { runtime.exit(checkStatus); } }