import path from "node:path"; import { resolveClawdbotAgentDir } from "../../agents/agent-paths.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 { parseModelRef, resolveConfiguredModelRef } from "../../agents/model-selection.js"; import { CONFIG_PATH_CLAWDBOT, loadConfig } from "../../config/config.js"; import { getShellEnvAppliedKeys, shouldEnableShellEnvFallback } from "../../infra/shell-env.js"; import { formatUsageWindowSummary, loadProviderUsageSummary, resolveUsageProviderId, type UsageProviderId, } from "../../infra/provider-usage.js"; import type { RuntimeEnv } from "../../runtime.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 { DEFAULT_MODEL, DEFAULT_PROVIDER, ensureFlagCompatibility } from "./shared.js"; export async function modelsStatusCommand( opts: { json?: boolean; plain?: boolean; check?: boolean }, runtime: RuntimeEnv, ) { ensureFlagCompatibility(opts); const cfg = loadConfig(); const resolved = 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 rawModel = typeof modelConfig === "string" ? modelConfig.trim() : (modelConfig?.primary?.trim() ?? ""); const resolvedLabel = `${resolved.provider}/${resolved.model}`; const defaultLabel = rawModel || resolvedLabel; const fallbacks = typeof modelConfig === "object" ? (modelConfig?.fallbacks ?? []) : []; 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 agentDir = resolveClawdbotAgentDir(); const store = ensureAuthProfileStore(); 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) .sort((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)) .sort((a, b) => a.localeCompare(b)); 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.sort((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_CLAWDBOT, agentDir, defaultModel: defaultLabel, resolvedDefault: resolvedLabel, fallbacks, imageModel: imageModel || null, imageFallbacks, aliases, allowed, auth: { storePath: resolveAuthStorePathForDisplay(), shellEnvFallback: { enabled: shellFallbackEnabled, appliedKeys: applied, }, providersWithOAuth: providersWithOauth, missingProvidersInUse, providers: providerAuth, unusableProfiles, oauth: { warnAfterMs: authHealth.warnAfterMs, profiles: authHealth.profiles, providers: authHealth.providers, }, }, }, 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); const label = (value: string) => colorize(rich, theme.accent, value.padEnd(14)); const displayDefault = rawModel && rawModel !== resolvedLabel ? `${resolvedLabel} (from ${rawModel})` : resolvedLabel; runtime.log( `${label("Config")}${colorize(rich, theme.muted, ":")} ${colorize(rich, theme.info, CONFIG_PATH_CLAWDBOT)}`, ); runtime.log( `${label("Agent dir")}${colorize(rich, theme.muted, ":")} ${colorize( rich, theme.info, shortenHomePath(agentDir), )}`, ); runtime.log( `${label("Default")}${colorize(rich, theme.muted, ":")} ${colorize( rich, theme.success, displayDefault, )}`, ); runtime.log( `${label(`Fallbacks (${fallbacks.length || 0})`)}${colorize(rich, theme.muted, ":")} ${colorize( rich, fallbacks.length ? theme.warn : theme.muted, fallbacks.length ? fallbacks.join(", ") : "-", )}`, ); runtime.log( `${label("Image model")}${colorize(rich, theme.muted, ":")} ${colorize( rich, imageModel ? theme.accentBright : theme.muted, imageModel || "-", )}`, ); runtime.log( `${label(`Image fallbacks (${imageFallbacks.length || 0})`)}${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()), )}`, ); 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` or `clawdbot configure`." : "Run `clawdbot 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")); return; } 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"; const source = profile.source !== "store" ? colorize(rich, theme.muted, ` (${profile.source})`) : ""; runtime.log(` - ${label} ${status}${expiry}${source}`); } } if (opts.check) runtime.exit(checkStatus); }