feat(models): show auth overview

This commit is contained in:
Peter Steinberger
2026-01-06 20:07:04 +00:00
parent ea7836afad
commit 1bf44bf30c
7 changed files with 439 additions and 5 deletions

View File

@@ -1,3 +1,5 @@
import path from "node:path";
import type { Api, Model } from "@mariozechner/pi-ai";
import {
discoverAuthStorage,
@@ -10,6 +12,8 @@ import {
type AuthProfileStore,
ensureAuthProfileStore,
listProfilesForProvider,
resolveAuthProfileDisplayLabel,
resolveAuthStorePathForDisplay,
} from "../../agents/auth-profiles.js";
import {
getCustomProviderApiKey,
@@ -28,7 +32,12 @@ import {
loadConfig,
} from "../../config/config.js";
import { info } from "../../globals.js";
import {
getShellEnvAppliedKeys,
shouldEnableShellEnvFallback,
} from "../../infra/shell-env.js";
import type { RuntimeEnv } from "../../runtime.js";
import { shortenHomePath } from "../../utils.js";
import {
DEFAULT_MODEL,
DEFAULT_PROVIDER,
@@ -56,6 +65,13 @@ const truncate = (value: string, max: number) => {
return `${value.slice(0, max - 3)}...`;
};
const maskApiKey = (value: string): string => {
const trimmed = value.trim();
if (!trimmed) return "missing";
if (trimmed.length <= 16) return trimmed;
return `${trimmed.slice(0, 8)}...${trimmed.slice(-8)}`;
};
type ConfiguredEntry = {
key: string;
ref: { provider: string; model: string };
@@ -101,6 +117,109 @@ const hasAuthForProvider = (
return false;
};
type ProviderAuthOverview = {
provider: string;
effective: {
kind: "profiles" | "env" | "models.json" | "missing";
detail: string;
};
profiles: {
count: number;
oauth: number;
apiKey: number;
labels: string[];
};
env?: { value: string; source: string };
modelsJson?: { value: string; source: string };
};
function resolveProviderAuthOverview(params: {
provider: string;
cfg: ClawdbotConfig;
store: AuthProfileStore;
modelsPath: string;
}): ProviderAuthOverview {
const { provider, cfg, store } = params;
const profiles = listProfilesForProvider(store, provider);
const labels = profiles.map((profileId) => {
const profile = store.profiles[profileId];
if (!profile) return `${profileId}=missing`;
if (profile.type === "api_key") {
return `${profileId}=${maskApiKey(profile.key)}`;
}
const display = resolveAuthProfileDisplayLabel({ cfg, store, profileId });
const suffix =
display === profileId
? ""
: display.startsWith(profileId)
? display.slice(profileId.length).trim()
: `(${display})`;
return `${profileId}=OAuth${suffix ? ` ${suffix}` : ""}`;
});
const oauthCount = profiles.filter(
(id) => store.profiles[id]?.type === "oauth",
).length;
const apiKeyCount = profiles.filter(
(id) => store.profiles[id]?.type === "api_key",
).length;
const envKey = resolveEnvApiKey(provider);
const customKey = getCustomProviderApiKey(cfg, provider);
const effective: ProviderAuthOverview["effective"] = (() => {
if (profiles.length > 0) {
return {
kind: "profiles",
detail: shortenHomePath(resolveAuthStorePathForDisplay()),
};
}
if (envKey) {
const isOAuthEnv =
envKey.source.includes("OAUTH_TOKEN") ||
envKey.source.toLowerCase().includes("oauth");
return {
kind: "env",
detail: isOAuthEnv ? "OAuth (env)" : maskApiKey(envKey.apiKey),
};
}
if (customKey) {
return { kind: "models.json", detail: maskApiKey(customKey) };
}
return { kind: "missing", detail: "missing" };
})();
return {
provider,
effective,
profiles: {
count: profiles.length,
oauth: oauthCount,
apiKey: apiKeyCount,
labels,
},
...(envKey
? {
env: {
value:
envKey.source.includes("OAUTH_TOKEN") ||
envKey.source.toLowerCase().includes("oauth")
? "OAuth (env)"
: maskApiKey(envKey.apiKey),
source: envKey.source,
},
}
: {}),
...(customKey
? {
modelsJson: {
value: maskApiKey(customKey),
source: `models.json: ${shortenHomePath(params.modelsPath)}`,
},
}
: {}),
};
}
const resolveConfiguredEntries = (cfg: ClawdbotConfig) => {
const resolvedDefault = resolveConfiguredModelRef({
cfg,
@@ -462,11 +581,97 @@ export async function modelsStatusCommand(
}, {});
const allowed = Object.keys(cfg.agent?.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<string>();
for (const raw of [
defaultLabel,
...fallbacks,
imageModel,
...imageFallbacks,
...allowed,
]) {
const parsed = parseModelRef(String(raw ?? ""), DEFAULT_PROVIDER);
if (parsed?.provider) providersFromModels.add(parsed.provider);
}
const providersFromEnv = new Set<string>();
// 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",
];
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 providersWithOauth = providerAuth
.filter(
(entry) => entry.profiles.oauth > 0 || entry.env?.value === "OAuth (env)",
)
.map((entry) => {
const count =
entry.profiles.oauth || (entry.env?.value === "OAuth (env)" ? 1 : 0);
return `${entry.provider} (${count})`;
});
if (opts.json) {
runtime.log(
JSON.stringify(
{
configPath: CONFIG_PATH_CLAWDBOT,
agentDir,
defaultModel: defaultLabel,
resolvedDefault: `${resolved.provider}/${resolved.model}`,
fallbacks,
@@ -474,6 +679,15 @@ export async function modelsStatusCommand(
imageFallbacks,
aliases,
allowed,
auth: {
storePath: resolveAuthStorePathForDisplay(),
shellEnvFallback: {
enabled: shellFallbackEnabled,
appliedKeys: applied,
},
providersWithOAuth: providersWithOauth,
providers: providerAuth,
},
},
null,
2,
@@ -488,6 +702,7 @@ export async function modelsStatusCommand(
}
runtime.log(info(`Config: ${CONFIG_PATH_CLAWDBOT}`));
runtime.log(info(`Agent dir: ${shortenHomePath(agentDir)}`));
runtime.log(`Default: ${defaultLabel}`);
runtime.log(
`Fallbacks (${fallbacks.length || 0}): ${fallbacks.join(", ") || "-"}`,
@@ -512,4 +727,36 @@ export async function modelsStatusCommand(
allowed.length ? allowed.join(", ") : "all"
}`,
);
runtime.log("");
runtime.log(info("Auth overview"));
runtime.log(
`Auth store: ${shortenHomePath(resolveAuthStorePathForDisplay())}`,
);
runtime.log(
`Shell env fallback: ${shellFallbackEnabled ? "on" : "off"}${
applied.length ? ` (applied: ${applied.join(", ")})` : ""
}`,
);
runtime.log(
`Providers with OAuth (${providersWithOauth.length || 0}): ${
providersWithOauth.length ? providersWithOauth.join(", ") : "-"
}`,
);
for (const entry of providerAuth) {
const bits: string[] = [];
bits.push(`effective=${entry.effective.kind}:${entry.effective.detail}`);
if (entry.profiles.count > 0) {
bits.push(
`profiles=${entry.profiles.count} (oauth=${entry.profiles.oauth}, api_key=${entry.profiles.apiKey})`,
);
if (entry.profiles.labels.length > 0) {
bits.push(entry.profiles.labels.join(", "));
}
}
if (entry.env) bits.push(`env=${entry.env.value} (${entry.env.source})`);
if (entry.modelsJson) bits.push(`models.json=${entry.modelsJson.value}`);
runtime.log(`${entry.provider}: ${bits.join(" | ")}`);
}
}