feat(models): show auth overview
This commit is contained in:
@@ -105,6 +105,7 @@
|
|||||||
- Bash tool: inherit gateway PATH so Nix-provided tools resolve during commands. Thanks @joshp123 for PR #202.
|
- Bash tool: inherit gateway PATH so Nix-provided tools resolve during commands. Thanks @joshp123 for PR #202.
|
||||||
- Delivery chunking: keep Markdown fenced code blocks valid when splitting long replies (close + reopen fences).
|
- Delivery chunking: keep Markdown fenced code blocks valid when splitting long replies (close + reopen fences).
|
||||||
- Auth: prefer OAuth profiles over API keys during round-robin selection (prevents OAuth “lost after one message” when both are configured).
|
- Auth: prefer OAuth profiles over API keys during round-robin selection (prevents OAuth “lost after one message” when both are configured).
|
||||||
|
- Models: extend `clawdbot models` status output with a masked auth overview (profiles, env sources, and OAuth counts).
|
||||||
|
|
||||||
### Maintenance
|
### Maintenance
|
||||||
- Agent: add `skipBootstrap` config option. Thanks @onutc for PR #292.
|
- Agent: add `skipBootstrap` config option. Thanks @onutc for PR #292.
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ New install? Start here: https://docs.clawd.bot/getting-started
|
|||||||
|
|
||||||
Model note: while any model is supported, I strongly recommend **Anthropic Pro/Max (100/200) + Opus 4.5** for long‑context strength and better prompt‑injection resistance. See [Onboarding](https://docs.clawd.bot/onboarding).
|
Model note: while any model is supported, I strongly recommend **Anthropic Pro/Max (100/200) + Opus 4.5** for long‑context strength and better prompt‑injection resistance. See [Onboarding](https://docs.clawd.bot/onboarding).
|
||||||
|
|
||||||
|
## Models (selection + auth)
|
||||||
|
|
||||||
|
- Models config + CLI: https://docs.clawd.bot/models
|
||||||
|
- Auth profile rotation (OAuth vs API keys) + fallbacks: https://docs.clawd.bot/model-failover
|
||||||
|
|
||||||
## Recommended setup (from source)
|
## Recommended setup (from source)
|
||||||
|
|
||||||
Do **not** download prebuilt binaries. Run from source.
|
Do **not** download prebuilt binaries. Run from source.
|
||||||
|
|||||||
@@ -13,6 +13,18 @@ Clawdbot handles failures in two stages:
|
|||||||
|
|
||||||
This doc explains the runtime rules and the data that backs them.
|
This doc explains the runtime rules and the data that backs them.
|
||||||
|
|
||||||
|
## Auth storage (keys + OAuth)
|
||||||
|
|
||||||
|
Clawdbot uses **auth profiles** for both API keys and OAuth tokens.
|
||||||
|
|
||||||
|
- Secrets live in `~/.clawdbot/agent/auth-profiles.json` (default agent; multi-agent stores under `~/.clawdbot/agents/<agentId>/agent/auth-profiles.json`).
|
||||||
|
- Config `auth.profiles` / `auth.order` are **metadata + routing only** (no secrets).
|
||||||
|
- Legacy import-only OAuth file: `~/.clawdbot/credentials/oauth.json` (imported into `auth-profiles.json` on first use).
|
||||||
|
|
||||||
|
Credential types:
|
||||||
|
- `type: "api_key"` → `{ provider, key }`
|
||||||
|
- `type: "oauth"` → `{ provider, access, refresh, expires, email? }` (+ `projectId`/`enterpriseUrl` for some providers)
|
||||||
|
|
||||||
## Profile IDs
|
## Profile IDs
|
||||||
|
|
||||||
OAuth logins create distinct profiles so multiple accounts can coexist.
|
OAuth logins create distinct profiles so multiple accounts can coexist.
|
||||||
@@ -30,10 +42,16 @@ When a provider has multiple profiles, Clawdbot chooses an order like this:
|
|||||||
3) **Stored profiles**: entries in `auth-profiles.json` for the provider.
|
3) **Stored profiles**: entries in `auth-profiles.json` for the provider.
|
||||||
|
|
||||||
If no explicit order is configured, Clawdbot uses a round‑robin order:
|
If no explicit order is configured, Clawdbot uses a round‑robin order:
|
||||||
- **Primary key:** `usageStats.lastUsed` (oldest first).
|
- **Primary key:** profile type (**OAuth before API keys**).
|
||||||
- **Secondary key:** profile type (OAuth before API keys).
|
- **Secondary key:** `usageStats.lastUsed` (oldest first, within each type).
|
||||||
- **Cooldown profiles** are moved to the end, ordered by soonest cooldown expiry.
|
- **Cooldown profiles** are moved to the end, ordered by soonest cooldown expiry.
|
||||||
|
|
||||||
|
### Why OAuth can “look lost”
|
||||||
|
|
||||||
|
If you have both an OAuth profile and an API key profile for the same provider, round‑robin can switch between them across messages unless pinned. To force a single profile:
|
||||||
|
- Pin with `auth.order[provider] = ["provider:profileId"]`, or
|
||||||
|
- Use a per-session override via `/model …` with a profile override (when supported by your UI/chat surface).
|
||||||
|
|
||||||
## Cooldowns
|
## Cooldowns
|
||||||
|
|
||||||
When a profile fails due to auth/rate‑limit errors (or a timeout that looks
|
When a profile fails due to auth/rate‑limit errors (or a timeout that looks
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ read_when:
|
|||||||
---
|
---
|
||||||
# Models CLI plan
|
# Models CLI plan
|
||||||
|
|
||||||
|
See [`docs/model-failover.md`](https://docs.clawd.bot/model-failover) for how auth profiles rotate (OAuth vs API keys), cooldowns, and how that interacts with model fallbacks.
|
||||||
|
|
||||||
Goal: give clear model visibility + control (configured vs available), plus scan tooling
|
Goal: give clear model visibility + control (configured vs available), plus scan tooling
|
||||||
that prefers tool-call + image-capable models and maintains ordered fallbacks.
|
that prefers tool-call + image-capable models and maintains ordered fallbacks.
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,13 @@ import { defaultRuntime } from "../runtime.js";
|
|||||||
export function registerModelsCli(program: Command) {
|
export function registerModelsCli(program: Command) {
|
||||||
const models = program
|
const models = program
|
||||||
.command("models")
|
.command("models")
|
||||||
.description("Model discovery, scanning, and configuration");
|
.description("Model discovery, scanning, and configuration")
|
||||||
|
.option("--json", "Output JSON (alias for `models status --json`)", false)
|
||||||
|
.option(
|
||||||
|
"--plain",
|
||||||
|
"Plain output (alias for `models status --plain`)",
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
models
|
models
|
||||||
.command("list")
|
.command("list")
|
||||||
@@ -264,9 +270,9 @@ export function registerModelsCli(program: Command) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
models.action(async () => {
|
models.action(async (opts) => {
|
||||||
try {
|
try {
|
||||||
await modelsStatusCommand({}, defaultRuntime);
|
await modelsStatusCommand(opts ?? {}, defaultRuntime);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
defaultRuntime.error(String(err));
|
defaultRuntime.error(String(err));
|
||||||
defaultRuntime.exit(1);
|
defaultRuntime.exit(1);
|
||||||
|
|||||||
155
src/commands/models/list.status.test.ts
Normal file
155
src/commands/models/list.status.test.ts
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => {
|
||||||
|
const store = {
|
||||||
|
version: 1,
|
||||||
|
profiles: {
|
||||||
|
"anthropic:default": {
|
||||||
|
type: "oauth",
|
||||||
|
provider: "anthropic",
|
||||||
|
access: "sk-ant-oat01-ACCESS-TOKEN-1234567890",
|
||||||
|
refresh: "sk-ant-ort01-REFRESH-TOKEN-1234567890",
|
||||||
|
expires: Date.now() + 60_000,
|
||||||
|
email: "peter@example.com",
|
||||||
|
},
|
||||||
|
"anthropic:work": {
|
||||||
|
type: "api_key",
|
||||||
|
provider: "anthropic",
|
||||||
|
key: "sk-ant-api-0123456789abcdefghijklmnopqrstuvwxyz",
|
||||||
|
},
|
||||||
|
"openai-codex:default": {
|
||||||
|
type: "oauth",
|
||||||
|
provider: "openai-codex",
|
||||||
|
access: "eyJhbGciOi-ACCESS",
|
||||||
|
refresh: "oai-refresh-1234567890",
|
||||||
|
expires: Date.now() + 60_000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
store,
|
||||||
|
resolveClawdbotAgentDir: vi.fn().mockReturnValue("/tmp/clawdbot-agent"),
|
||||||
|
ensureAuthProfileStore: vi.fn().mockReturnValue(store),
|
||||||
|
listProfilesForProvider: vi.fn((s: typeof store, provider: string) => {
|
||||||
|
return Object.entries(s.profiles)
|
||||||
|
.filter(([, cred]) => cred.provider === provider)
|
||||||
|
.map(([id]) => id);
|
||||||
|
}),
|
||||||
|
resolveAuthProfileDisplayLabel: vi.fn(
|
||||||
|
({ profileId }: { profileId: string }) => profileId,
|
||||||
|
),
|
||||||
|
resolveAuthStorePathForDisplay: vi
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue("/tmp/clawdbot-agent/auth-profiles.json"),
|
||||||
|
resolveEnvApiKey: vi.fn((provider: string) => {
|
||||||
|
if (provider === "openai") {
|
||||||
|
return {
|
||||||
|
apiKey: "sk-openai-0123456789abcdefghijklmnopqrstuvwxyz",
|
||||||
|
source: "shell env: OPENAI_API_KEY",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (provider === "anthropic") {
|
||||||
|
return {
|
||||||
|
apiKey: "sk-ant-oat01-ACCESS-TOKEN-1234567890",
|
||||||
|
source: "env: ANTHROPIC_OAUTH_TOKEN",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
|
getCustomProviderApiKey: vi.fn().mockReturnValue(undefined),
|
||||||
|
getShellEnvAppliedKeys: vi
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue(["OPENAI_API_KEY", "ANTHROPIC_OAUTH_TOKEN"]),
|
||||||
|
shouldEnableShellEnvFallback: vi.fn().mockReturnValue(true),
|
||||||
|
loadConfig: vi.fn().mockReturnValue({
|
||||||
|
agent: {
|
||||||
|
model: { primary: "anthropic/claude-opus-4-5", fallbacks: [] },
|
||||||
|
models: { "anthropic/claude-opus-4-5": { alias: "Opus" } },
|
||||||
|
},
|
||||||
|
models: { providers: {} },
|
||||||
|
env: { shellEnv: { enabled: true } },
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("../../agents/agent-paths.js", () => ({
|
||||||
|
resolveClawdbotAgentDir: mocks.resolveClawdbotAgentDir,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../../agents/auth-profiles.js", () => ({
|
||||||
|
ensureAuthProfileStore: mocks.ensureAuthProfileStore,
|
||||||
|
listProfilesForProvider: mocks.listProfilesForProvider,
|
||||||
|
resolveAuthProfileDisplayLabel: mocks.resolveAuthProfileDisplayLabel,
|
||||||
|
resolveAuthStorePathForDisplay: mocks.resolveAuthStorePathForDisplay,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../../agents/model-auth.js", () => ({
|
||||||
|
resolveEnvApiKey: mocks.resolveEnvApiKey,
|
||||||
|
getCustomProviderApiKey: mocks.getCustomProviderApiKey,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../../infra/shell-env.js", () => ({
|
||||||
|
getShellEnvAppliedKeys: mocks.getShellEnvAppliedKeys,
|
||||||
|
shouldEnableShellEnvFallback: mocks.shouldEnableShellEnvFallback,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../../config/config.js", async (importOriginal) => {
|
||||||
|
const actual =
|
||||||
|
await importOriginal<typeof import("../../config/config.js")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
loadConfig: mocks.loadConfig,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
import { modelsStatusCommand } from "./list.js";
|
||||||
|
|
||||||
|
const runtime = {
|
||||||
|
log: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
exit: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("modelsStatusCommand auth overview", () => {
|
||||||
|
it("includes masked auth sources in JSON output", async () => {
|
||||||
|
await modelsStatusCommand({ json: true }, runtime as never);
|
||||||
|
const payload = JSON.parse(
|
||||||
|
String((runtime.log as vi.Mock).mock.calls[0][0]),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(payload.defaultModel).toBe("anthropic/claude-opus-4-5");
|
||||||
|
expect(payload.auth.storePath).toBe(
|
||||||
|
"/tmp/clawdbot-agent/auth-profiles.json",
|
||||||
|
);
|
||||||
|
expect(payload.auth.shellEnvFallback.enabled).toBe(true);
|
||||||
|
expect(payload.auth.shellEnvFallback.appliedKeys).toContain(
|
||||||
|
"OPENAI_API_KEY",
|
||||||
|
);
|
||||||
|
|
||||||
|
const providers = payload.auth.providers as Array<{
|
||||||
|
provider: string;
|
||||||
|
profiles: { labels: string[] };
|
||||||
|
env?: { value: string; source: string };
|
||||||
|
}>;
|
||||||
|
const anthropic = providers.find((p) => p.provider === "anthropic");
|
||||||
|
expect(anthropic).toBeTruthy();
|
||||||
|
expect(anthropic?.profiles.labels.join(" ")).toContain("OAuth");
|
||||||
|
expect(anthropic?.profiles.labels.join(" ")).toContain("...");
|
||||||
|
|
||||||
|
const openai = providers.find((p) => p.provider === "openai");
|
||||||
|
expect(openai?.env?.source).toContain("OPENAI_API_KEY");
|
||||||
|
expect(openai?.env?.value).toContain("...");
|
||||||
|
|
||||||
|
expect(
|
||||||
|
(payload.auth.providersWithOAuth as string[]).some((e) =>
|
||||||
|
e.startsWith("anthropic"),
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
(payload.auth.providersWithOAuth as string[]).some((e) =>
|
||||||
|
e.startsWith("openai-codex"),
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
|
||||||
import type { Api, Model } from "@mariozechner/pi-ai";
|
import type { Api, Model } from "@mariozechner/pi-ai";
|
||||||
import {
|
import {
|
||||||
discoverAuthStorage,
|
discoverAuthStorage,
|
||||||
@@ -10,6 +12,8 @@ import {
|
|||||||
type AuthProfileStore,
|
type AuthProfileStore,
|
||||||
ensureAuthProfileStore,
|
ensureAuthProfileStore,
|
||||||
listProfilesForProvider,
|
listProfilesForProvider,
|
||||||
|
resolveAuthProfileDisplayLabel,
|
||||||
|
resolveAuthStorePathForDisplay,
|
||||||
} from "../../agents/auth-profiles.js";
|
} from "../../agents/auth-profiles.js";
|
||||||
import {
|
import {
|
||||||
getCustomProviderApiKey,
|
getCustomProviderApiKey,
|
||||||
@@ -28,7 +32,12 @@ import {
|
|||||||
loadConfig,
|
loadConfig,
|
||||||
} from "../../config/config.js";
|
} from "../../config/config.js";
|
||||||
import { info } from "../../globals.js";
|
import { info } from "../../globals.js";
|
||||||
|
import {
|
||||||
|
getShellEnvAppliedKeys,
|
||||||
|
shouldEnableShellEnvFallback,
|
||||||
|
} from "../../infra/shell-env.js";
|
||||||
import type { RuntimeEnv } from "../../runtime.js";
|
import type { RuntimeEnv } from "../../runtime.js";
|
||||||
|
import { shortenHomePath } from "../../utils.js";
|
||||||
import {
|
import {
|
||||||
DEFAULT_MODEL,
|
DEFAULT_MODEL,
|
||||||
DEFAULT_PROVIDER,
|
DEFAULT_PROVIDER,
|
||||||
@@ -56,6 +65,13 @@ const truncate = (value: string, max: number) => {
|
|||||||
return `${value.slice(0, max - 3)}...`;
|
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 = {
|
type ConfiguredEntry = {
|
||||||
key: string;
|
key: string;
|
||||||
ref: { provider: string; model: string };
|
ref: { provider: string; model: string };
|
||||||
@@ -101,6 +117,109 @@ const hasAuthForProvider = (
|
|||||||
return false;
|
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 resolveConfiguredEntries = (cfg: ClawdbotConfig) => {
|
||||||
const resolvedDefault = resolveConfiguredModelRef({
|
const resolvedDefault = resolveConfiguredModelRef({
|
||||||
cfg,
|
cfg,
|
||||||
@@ -462,11 +581,97 @@ export async function modelsStatusCommand(
|
|||||||
}, {});
|
}, {});
|
||||||
const allowed = Object.keys(cfg.agent?.models ?? {});
|
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) {
|
if (opts.json) {
|
||||||
runtime.log(
|
runtime.log(
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{
|
{
|
||||||
configPath: CONFIG_PATH_CLAWDBOT,
|
configPath: CONFIG_PATH_CLAWDBOT,
|
||||||
|
agentDir,
|
||||||
defaultModel: defaultLabel,
|
defaultModel: defaultLabel,
|
||||||
resolvedDefault: `${resolved.provider}/${resolved.model}`,
|
resolvedDefault: `${resolved.provider}/${resolved.model}`,
|
||||||
fallbacks,
|
fallbacks,
|
||||||
@@ -474,6 +679,15 @@ export async function modelsStatusCommand(
|
|||||||
imageFallbacks,
|
imageFallbacks,
|
||||||
aliases,
|
aliases,
|
||||||
allowed,
|
allowed,
|
||||||
|
auth: {
|
||||||
|
storePath: resolveAuthStorePathForDisplay(),
|
||||||
|
shellEnvFallback: {
|
||||||
|
enabled: shellFallbackEnabled,
|
||||||
|
appliedKeys: applied,
|
||||||
|
},
|
||||||
|
providersWithOAuth: providersWithOauth,
|
||||||
|
providers: providerAuth,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
2,
|
2,
|
||||||
@@ -488,6 +702,7 @@ export async function modelsStatusCommand(
|
|||||||
}
|
}
|
||||||
|
|
||||||
runtime.log(info(`Config: ${CONFIG_PATH_CLAWDBOT}`));
|
runtime.log(info(`Config: ${CONFIG_PATH_CLAWDBOT}`));
|
||||||
|
runtime.log(info(`Agent dir: ${shortenHomePath(agentDir)}`));
|
||||||
runtime.log(`Default: ${defaultLabel}`);
|
runtime.log(`Default: ${defaultLabel}`);
|
||||||
runtime.log(
|
runtime.log(
|
||||||
`Fallbacks (${fallbacks.length || 0}): ${fallbacks.join(", ") || "-"}`,
|
`Fallbacks (${fallbacks.length || 0}): ${fallbacks.join(", ") || "-"}`,
|
||||||
@@ -512,4 +727,36 @@ export async function modelsStatusCommand(
|
|||||||
allowed.length ? allowed.join(", ") : "all"
|
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(" | ")}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user