mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-13 16:40:43 +00:00
* route openai agent runs through codex * fix: load codex plugin for implicit openai runtime * fix: preserve explicit OpenAI PI Codex auth routing * fix: show codex auth for openai model listing * fix: map codex auth into configured openai list rows * fix: preserve explicit openai pi auth routes * docs: keep openai model route examples canonical * fix: clean openai codex test fixtures * fix: scope codex auth status fallback * fix: repair current ci boundary drift
910 lines
30 KiB
TypeScript
910 lines
30 KiB
TypeScript
import path from "node:path";
|
|
import {
|
|
resolveAgentDir,
|
|
resolveAgentExplicitModelPrimary,
|
|
resolveAgentModelFallbacksOverride,
|
|
resolveAgentWorkspaceDir,
|
|
resolveDefaultAgentId,
|
|
} from "../../agents/agent-scope.js";
|
|
import {
|
|
buildAuthHealthSummary,
|
|
DEFAULT_OAUTH_WARN_MS,
|
|
formatRemainingShort,
|
|
} from "../../agents/auth-health.js";
|
|
import { resolveAuthStorePathForDisplay } from "../../agents/auth-profiles/paths.js";
|
|
import { ensureAuthProfileStoreWithoutExternalProfiles as ensureAuthProfileStore } from "../../agents/auth-profiles/store.js";
|
|
import type { AuthProfileCredential } from "../../agents/auth-profiles/types.js";
|
|
import { resolveProfileUnusableUntilForDisplay } from "../../agents/auth-profiles/usage.js";
|
|
import {
|
|
listProviderEnvAuthLookupKeys,
|
|
resolveProviderEnvApiKeyCandidates,
|
|
resolveProviderEnvAuthEvidence,
|
|
} from "../../agents/model-auth-env-vars.js";
|
|
import { resolveEnvApiKey } from "../../agents/model-auth.js";
|
|
import {
|
|
buildModelAliasIndex,
|
|
isCliProvider,
|
|
modelKey,
|
|
normalizeProviderId,
|
|
parseModelRef,
|
|
resolveConfiguredModelRef,
|
|
resolveModelRefFromString,
|
|
} from "../../agents/model-selection.js";
|
|
import {
|
|
OPENAI_CODEX_PROVIDER_ID,
|
|
openAIProviderUsesCodexRuntimeByDefault,
|
|
} from "../../agents/openai-codex-routing.js";
|
|
import { resolveDefaultAgentWorkspaceDir } from "../../agents/workspace.js";
|
|
import { createConfigIO } from "../../config/config.js";
|
|
import {
|
|
resolveAgentModelFallbackValues,
|
|
resolveAgentModelPrimaryValue,
|
|
} from "../../config/model-input.js";
|
|
import { getShellEnvAppliedKeys, shouldEnableShellEnvFallback } from "../../infra/shell-env.js";
|
|
import type { ProviderSyntheticAuthResult } from "../../plugins/provider-external-auth.types.js";
|
|
import { resolveProviderSyntheticAuthWithPlugin } from "../../plugins/provider-runtime.js";
|
|
import { resolveRuntimeSyntheticAuthProviderRefs } from "../../plugins/synthetic-auth.runtime.js";
|
|
import { type RuntimeEnv, writeRuntimeJson } from "../../runtime.js";
|
|
import { createLazyImportLoader } from "../../shared/lazy-promise.js";
|
|
import { normalizeOptionalString } from "../../shared/string-coerce.js";
|
|
import { colorize, theme } from "../../terminal/theme.js";
|
|
import { resolveUserPath, shortenHomePath } from "../../utils.js";
|
|
import { resolveProviderAuthOverview } from "./list.auth-overview.js";
|
|
import { isRich } from "./list.format.js";
|
|
import { type AuthProbeSummary } from "./list.probe.js";
|
|
import { loadModelsConfig } from "./load-config.js";
|
|
import {
|
|
DEFAULT_MODEL,
|
|
DEFAULT_PROVIDER,
|
|
ensureFlagCompatibility,
|
|
resolveKnownAgentId,
|
|
} from "./shared.js";
|
|
|
|
type ProviderUsageRuntime = typeof import("../../infra/provider-usage.js");
|
|
type ProgressRuntime = typeof import("../../cli/progress.js");
|
|
|
|
function resolveEnvAgentDirOverride(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
const override = env.OPENCLAW_AGENT_DIR?.trim() || env.PI_CODING_AGENT_DIR?.trim();
|
|
return override ? resolveUserPath(override, env) : undefined;
|
|
}
|
|
type TerminalTableRuntime = typeof import("../../terminal/table.js");
|
|
type ListProbeRuntime = typeof import("./list.probe.js");
|
|
|
|
const providerUsageRuntimeLoader = createLazyImportLoader<ProviderUsageRuntime>(
|
|
() => import("../../infra/provider-usage.js"),
|
|
);
|
|
const progressRuntimeLoader = createLazyImportLoader<ProgressRuntime>(
|
|
() => import("../../cli/progress.js"),
|
|
);
|
|
const terminalTableRuntimeLoader = createLazyImportLoader<TerminalTableRuntime>(
|
|
() => import("../../terminal/table.js"),
|
|
);
|
|
const listProbeRuntimeLoader = createLazyImportLoader<ListProbeRuntime>(
|
|
() => import("./list.probe.js"),
|
|
);
|
|
|
|
const DISPLAY_MODEL_PARSE_OPTIONS = { allowPluginNormalization: false } as const;
|
|
|
|
type StatusSyntheticAuth = {
|
|
value: string;
|
|
source: string;
|
|
credential?: string;
|
|
mode?: ProviderSyntheticAuthResult["mode"];
|
|
expiresAt?: number;
|
|
};
|
|
|
|
function loadProviderUsageRuntime(): Promise<ProviderUsageRuntime> {
|
|
return providerUsageRuntimeLoader.load();
|
|
}
|
|
|
|
function loadProgressRuntime(): Promise<ProgressRuntime> {
|
|
return progressRuntimeLoader.load();
|
|
}
|
|
|
|
function loadTerminalTableRuntime(): Promise<TerminalTableRuntime> {
|
|
return terminalTableRuntimeLoader.load();
|
|
}
|
|
|
|
function loadListProbeRuntime(): Promise<ListProbeRuntime> {
|
|
return listProbeRuntimeLoader.load();
|
|
}
|
|
|
|
function resolveProviderConfigForStatus(
|
|
cfg: Awaited<ReturnType<typeof loadModelsConfig>>,
|
|
provider: string,
|
|
) {
|
|
const providers = cfg.models?.providers ?? {};
|
|
const direct = providers[provider];
|
|
if (direct) {
|
|
return direct;
|
|
}
|
|
const normalized = normalizeProviderId(provider);
|
|
return (
|
|
providers[normalized] ??
|
|
Object.entries(providers).find(([key]) => normalizeProviderId(key) === normalized)?.[1]
|
|
);
|
|
}
|
|
|
|
function syntheticAuthCredential(
|
|
provider: string,
|
|
auth: StatusSyntheticAuth,
|
|
): AuthProfileCredential | undefined {
|
|
if (!auth.mode) {
|
|
return undefined;
|
|
}
|
|
if (auth.mode === "api-key") {
|
|
return {
|
|
type: "api_key",
|
|
provider,
|
|
key: auth.credential,
|
|
};
|
|
}
|
|
if (auth.mode === "token") {
|
|
return {
|
|
type: "token",
|
|
provider,
|
|
token: auth.credential,
|
|
expires: auth.expiresAt,
|
|
};
|
|
}
|
|
if (auth.expiresAt === undefined) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
type: "oauth",
|
|
provider,
|
|
access: auth.credential ?? "",
|
|
refresh: "",
|
|
expires: auth.expiresAt,
|
|
};
|
|
}
|
|
|
|
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 configPath = createConfigIO().configPath;
|
|
const cfg = await loadModelsConfig({ commandName: "models status", runtime });
|
|
const agentId = resolveKnownAgentId({ cfg, rawAgentId: opts.agent });
|
|
const workspaceAgentId = agentId ?? resolveDefaultAgentId(cfg);
|
|
const agentDir = agentId
|
|
? resolveAgentDir(cfg, agentId)
|
|
: (resolveEnvAgentDirOverride() ?? resolveAgentDir(cfg, workspaceAgentId));
|
|
const workspaceDir =
|
|
resolveAgentWorkspaceDir(cfg, workspaceAgentId) ?? resolveDefaultAgentWorkspaceDir();
|
|
const agentModelPrimary = agentId ? resolveAgentExplicitModelPrimary(cfg, agentId) : undefined;
|
|
const agentFallbacksOverride = agentId
|
|
? resolveAgentModelFallbacksOverride(cfg, agentId)
|
|
: undefined;
|
|
const resolvedConfig =
|
|
agentModelPrimary && agentModelPrimary.length > 0
|
|
? {
|
|
...cfg,
|
|
agents: {
|
|
...cfg.agents,
|
|
defaults: {
|
|
...cfg.agents?.defaults,
|
|
model: {
|
|
...(typeof cfg.agents?.defaults?.model === "object"
|
|
? cfg.agents.defaults.model
|
|
: {}),
|
|
primary: agentModelPrimary,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
: cfg;
|
|
const resolved = resolveConfiguredModelRef({
|
|
cfg: resolvedConfig,
|
|
defaultProvider: DEFAULT_PROVIDER,
|
|
defaultModel: DEFAULT_MODEL,
|
|
...DISPLAY_MODEL_PARSE_OPTIONS,
|
|
});
|
|
|
|
const rawDefaultsModel = resolveAgentModelPrimaryValue(cfg.agents?.defaults?.model) ?? "";
|
|
const rawModel = agentModelPrimary ?? rawDefaultsModel;
|
|
const resolvedLabel = modelKey(resolved.provider, resolved.model);
|
|
const defaultLabel = rawModel || resolvedLabel;
|
|
const defaultsFallbacks = resolveAgentModelFallbackValues(cfg.agents?.defaults?.model);
|
|
const fallbacks = agentFallbacksOverride ?? defaultsFallbacks;
|
|
const imageModel = resolveAgentModelPrimaryValue(cfg.agents?.defaults?.imageModel) ?? "";
|
|
const imageFallbacks = resolveAgentModelFallbackValues(cfg.agents?.defaults?.imageModel);
|
|
const aliases = Object.entries(cfg.agents?.defaults?.models ?? {}).reduce<Record<string, string>>(
|
|
(acc, [key, entry]) => {
|
|
const alias = normalizeOptionalString(entry?.alias);
|
|
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) => normalizeProviderId(profile.provider))
|
|
.filter((p): p is string => Boolean(p)),
|
|
);
|
|
const providersFromConfig = new Set(
|
|
Object.keys(cfg.models?.providers ?? {})
|
|
.map((p) => (typeof p === "string" ? normalizeProviderId(p) : ""))
|
|
.filter(Boolean),
|
|
);
|
|
const providersFromModels = new Set<string>();
|
|
const providerUses: Array<{ provider: string; allowCodexRuntimeFallback: boolean }> = [];
|
|
const addProviderUse = (raw: string | undefined, allowCodexRuntimeFallback: boolean) => {
|
|
const modelRef = raw?.trim();
|
|
if (!modelRef) {
|
|
return;
|
|
}
|
|
const parsed = parseModelRef(modelRef, DEFAULT_PROVIDER, DISPLAY_MODEL_PARSE_OPTIONS);
|
|
if (parsed?.provider) {
|
|
providerUses.push({
|
|
provider: normalizeProviderId(parsed.provider),
|
|
allowCodexRuntimeFallback,
|
|
});
|
|
}
|
|
};
|
|
for (const raw of [defaultLabel, ...fallbacks, imageModel, ...imageFallbacks, ...allowed]) {
|
|
const parsed = parseModelRef(raw ?? "", DEFAULT_PROVIDER, DISPLAY_MODEL_PARSE_OPTIONS);
|
|
if (parsed?.provider) {
|
|
providersFromModels.add(normalizeProviderId(parsed.provider));
|
|
}
|
|
}
|
|
for (const raw of [defaultLabel, ...fallbacks]) {
|
|
addProviderUse(raw, true);
|
|
}
|
|
for (const raw of [imageModel, ...imageFallbacks]) {
|
|
addProviderUse(raw, false);
|
|
}
|
|
|
|
const providersFromEnv = new Set<string>();
|
|
// Use the shared provider-env registry so `models status` stays aligned with
|
|
// env-backed providers beyond the text-model defaults (for example image-gen).
|
|
const envLookupParams = {
|
|
config: cfg,
|
|
workspaceDir,
|
|
};
|
|
const envCandidateMap = resolveProviderEnvApiKeyCandidates(envLookupParams);
|
|
const authEvidenceMap = resolveProviderEnvAuthEvidence(envLookupParams);
|
|
for (const provider of listProviderEnvAuthLookupKeys({ envCandidateMap, authEvidenceMap })) {
|
|
if (
|
|
resolveEnvApiKey(provider, process.env, {
|
|
config: cfg,
|
|
workspaceDir,
|
|
candidateMap: envCandidateMap,
|
|
authEvidenceMap,
|
|
})
|
|
) {
|
|
providersFromEnv.add(provider);
|
|
}
|
|
}
|
|
const syntheticAuthByProvider = new Map<string, StatusSyntheticAuth>();
|
|
for (const provider of resolveRuntimeSyntheticAuthProviderRefs()) {
|
|
const normalized = normalizeProviderId(provider);
|
|
const resolved = resolveProviderSyntheticAuthWithPlugin({
|
|
provider: normalized,
|
|
config: cfg,
|
|
context: {
|
|
config: cfg,
|
|
provider: normalized,
|
|
providerConfig: resolveProviderConfigForStatus(cfg, normalized),
|
|
},
|
|
});
|
|
syntheticAuthByProvider.set(normalized, {
|
|
value: "plugin-owned",
|
|
source: resolved?.source ?? "plugin synthetic auth",
|
|
credential: resolved?.apiKey,
|
|
mode: resolved?.mode,
|
|
expiresAt: resolved?.expiresAt,
|
|
});
|
|
}
|
|
const runtimeCredentialsByProvider = new Map(
|
|
Array.from(syntheticAuthByProvider.entries())
|
|
.map(([provider, auth]) => [provider, syntheticAuthCredential(provider, auth)] as const)
|
|
.filter((entry): entry is readonly [string, AuthProfileCredential] => Boolean(entry[1])),
|
|
);
|
|
|
|
const providers = Array.from(
|
|
new Set([
|
|
...providersFromStore,
|
|
...providersFromConfig,
|
|
...providersFromModels,
|
|
...providersFromEnv,
|
|
]),
|
|
)
|
|
.map((p) => normalizeOptionalString(p) ?? "")
|
|
.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,
|
|
agentDir,
|
|
workspaceDir,
|
|
syntheticAuth: syntheticAuthByProvider.get(provider),
|
|
}),
|
|
)
|
|
.filter((entry) => {
|
|
const hasAny =
|
|
entry.profiles.count > 0 ||
|
|
Boolean(entry.env) ||
|
|
Boolean(entry.modelsJson) ||
|
|
Boolean(entry.syntheticAuth);
|
|
return hasAny;
|
|
});
|
|
const providerAuthMap = new Map(providerAuth.map((entry) => [entry.provider, entry]));
|
|
const hasUsableAuthForProviderInUse = (
|
|
provider: string,
|
|
options: { allowCodexRuntimeFallback: boolean },
|
|
): boolean => {
|
|
if (providerAuthMap.has(provider) || syntheticAuthByProvider.has(provider)) {
|
|
return true;
|
|
}
|
|
if (!options.allowCodexRuntimeFallback) {
|
|
return false;
|
|
}
|
|
return (
|
|
openAIProviderUsesCodexRuntimeByDefault({ provider, config: cfg }) &&
|
|
providerAuthMap.has(OPENAI_CODEX_PROVIDER_ID)
|
|
);
|
|
};
|
|
const missingProvidersInUse = Array.from(
|
|
new Set(
|
|
providerUses
|
|
.filter(
|
|
(usage) =>
|
|
!hasUsableAuthForProviderInUse(usage.provider, {
|
|
allowCodexRuntimeFallback: usage.allowCodexRuntimeFallback,
|
|
}),
|
|
)
|
|
.map((usage) => usage.provider),
|
|
),
|
|
)
|
|
.filter((provider) => !isCliProvider(provider, cfg))
|
|
.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) => (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,
|
|
...DISPLAY_MODEL_PARSE_OPTIONS,
|
|
});
|
|
const rawCandidates = [
|
|
rawModel || resolvedLabel,
|
|
...fallbacks,
|
|
imageModel,
|
|
...imageFallbacks,
|
|
...allowed,
|
|
].filter(Boolean);
|
|
const resolvedCandidates = rawCandidates
|
|
.map(
|
|
(raw) =>
|
|
resolveModelRefFromString({
|
|
raw: raw ?? "",
|
|
defaultProvider: DEFAULT_PROVIDER,
|
|
aliasIndex,
|
|
...DISPLAY_MODEL_PARSE_OPTIONS,
|
|
})?.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) {
|
|
const [{ withProgressTotals }, { runAuthProbes }] = await Promise.all([
|
|
loadProgressRuntime(),
|
|
loadListProbeRuntime(),
|
|
]);
|
|
probeSummary = await withProgressTotals(
|
|
{ label: "Probing auth profiles…", total: 1 },
|
|
async (update) => {
|
|
return await runAuthProbes({
|
|
cfg,
|
|
agentId: workspaceAgentId,
|
|
agentDir,
|
|
workspaceDir,
|
|
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,
|
|
runtimeCredentialsByProvider,
|
|
});
|
|
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) {
|
|
writeRuntimeJson(runtime, {
|
|
configPath,
|
|
...(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,
|
|
},
|
|
});
|
|
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(configPath))}`,
|
|
);
|
|
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)}`,
|
|
),
|
|
);
|
|
}
|
|
if (entry.syntheticAuth) {
|
|
bits.push(
|
|
formatKeyValue(
|
|
"synthetic",
|
|
`${entry.syntheticAuth.value}${separator}${formatKeyValue("source", entry.syntheticAuth.source)}`,
|
|
),
|
|
);
|
|
}
|
|
runtime.log(`- ${theme.heading(entry.provider)} ${bits.join(separator)}`);
|
|
}
|
|
|
|
if (missingProvidersInUse.length > 0) {
|
|
const { buildProviderAuthRecoveryHint } = await import("../provider-auth-guidance.js");
|
|
runtime.log("");
|
|
runtime.log(colorize(rich, theme.heading, "Missing auth"));
|
|
for (const provider of missingProvidersInUse) {
|
|
const hint = buildProviderAuthRecoveryHint({
|
|
provider,
|
|
config: cfg,
|
|
includeEnvVar: true,
|
|
});
|
|
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 { formatUsageWindowSummary, loadProviderUsageSummary, resolveUsageProviderId } =
|
|
await loadProviderUsageRuntime();
|
|
const usageByProvider = new Map<string, string>();
|
|
const usageProviders = Array.from(
|
|
new Set(
|
|
oauthProfiles
|
|
.map((profile) => resolveUsageProviderId(profile.provider))
|
|
.filter((provider): provider is NonNullable<typeof provider> => 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<string, typeof oauthProfiles>();
|
|
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) {
|
|
const [
|
|
{ getTerminalTableWidth, renderTable },
|
|
{ describeProbeSummary, formatProbeLatency, sortProbeResults },
|
|
] = await Promise.all([loadTerminalTableRuntime(), loadListProbeRuntime()]);
|
|
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 = getTerminalTableWidth();
|
|
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);
|
|
}
|
|
}
|