fix: local updates for PR #4780

Co-authored-by: jlowin <jlowin@users.noreply.github.com>
This commit is contained in:
Gustavo Madeira Santana 2026-01-30 15:39:05 -05:00 committed by Gustavo Madeira Santana
parent dd4715a2c4
commit f24e3cdae5
8 changed files with 250 additions and 57 deletions

View file

@ -27,6 +27,9 @@ When provider usage snapshots are available, the OAuth/token status section incl
provider usage headers. provider usage headers.
Add `--probe` to run live auth probes against each configured provider profile. Add `--probe` to run live auth probes against each configured provider profile.
Probes are real requests (may consume tokens and trigger rate limits). Probes are real requests (may consume tokens and trigger rate limits).
Use `--agent <id>` to inspect a configured agents model/auth state. When omitted,
the command uses `OPENCLAW_AGENT_DIR`/`PI_CODING_AGENT_DIR` if set, otherwise the
configured default agent.
Notes: Notes:
- `models set <model-or-alias>` accepts `provider/model` or an alias. - `models set <model-or-alias>` accepts `provider/model` or an alias.
@ -44,6 +47,7 @@ Options:
- `--probe-timeout <ms>` - `--probe-timeout <ms>`
- `--probe-concurrency <n>` - `--probe-concurrency <n>`
- `--probe-max-tokens <n>` - `--probe-max-tokens <n>`
- `--agent <id>` (configured agent id; overrides `OPENCLAW_AGENT_DIR`/`PI_CODING_AGENT_DIR`)
## Aliases + fallbacks ## Aliases + fallbacks

View file

@ -1,3 +1,5 @@
import type { Command } from "commander";
export type ManagerLookupResult<T> = { export type ManagerLookupResult<T> = {
manager: T | null; manager: T | null;
error?: string; error?: string;
@ -46,3 +48,16 @@ export async function runCommandWithRuntime(
runtime.exit(1); runtime.exit(1);
} }
} }
export function resolveOptionFromCommand<T>(
command: Command | undefined,
key: string,
): T | undefined {
let current: Command | null | undefined = command;
while (current) {
const opts = (current.opts?.() ?? {}) as Record<string, T | undefined>;
if (opts[key] !== undefined) return opts[key];
current = current.parent ?? undefined;
}
return undefined;
}

View file

@ -1,6 +1,7 @@
import { describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
const githubCopilotLoginCommand = vi.fn(); const githubCopilotLoginCommand = vi.fn();
const modelsStatusCommand = vi.fn().mockResolvedValue(undefined);
vi.mock("../commands/models.js", async () => { vi.mock("../commands/models.js", async () => {
const actual = (await vi.importActual<typeof import("../commands/models.js")>( const actual = (await vi.importActual<typeof import("../commands/models.js")>(
@ -10,10 +11,16 @@ vi.mock("../commands/models.js", async () => {
return { return {
...actual, ...actual,
githubCopilotLoginCommand, githubCopilotLoginCommand,
modelsStatusCommand,
}; };
}); });
describe("models cli", () => { describe("models cli", () => {
beforeEach(() => {
githubCopilotLoginCommand.mockClear();
modelsStatusCommand.mockClear();
});
it("registers github-copilot login command", { timeout: 60_000 }, async () => { it("registers github-copilot login command", { timeout: 60_000 }, async () => {
const { Command } = await import("commander"); const { Command } = await import("commander");
const { registerModelsCli } = await import("./models-cli.js"); const { registerModelsCli } = await import("./models-cli.js");
@ -40,4 +47,51 @@ describe("models cli", () => {
expect.any(Object), expect.any(Object),
); );
}); });
it("passes --agent to models status", async () => {
const { Command } = await import("commander");
const { registerModelsCli } = await import("./models-cli.js");
const program = new Command();
registerModelsCli(program);
await program.parseAsync(["models", "status", "--agent", "poe"], { from: "user" });
expect(modelsStatusCommand).toHaveBeenCalledWith(
expect.objectContaining({ agent: "poe" }),
expect.any(Object),
);
});
it("passes parent --agent to models status", async () => {
const { Command } = await import("commander");
const { registerModelsCli } = await import("./models-cli.js");
const program = new Command();
registerModelsCli(program);
await program.parseAsync(["models", "--agent", "poe", "status"], { from: "user" });
expect(modelsStatusCommand).toHaveBeenCalledWith(
expect.objectContaining({ agent: "poe" }),
expect.any(Object),
);
});
it("shows help for models auth without error exit", async () => {
const { Command } = await import("commander");
const { registerModelsCli } = await import("./models-cli.js");
const program = new Command();
program.exitOverride();
registerModelsCli(program);
try {
await program.parseAsync(["models", "auth"], { from: "user" });
expect.fail("expected help to exit");
} catch (err) {
const error = err as { exitCode?: number };
expect(error.exitCode).toBe(0);
}
});
}); });

View file

@ -29,7 +29,7 @@ import {
import { defaultRuntime } from "../runtime.js"; import { defaultRuntime } from "../runtime.js";
import { formatDocsLink } from "../terminal/links.js"; import { formatDocsLink } from "../terminal/links.js";
import { theme } from "../terminal/theme.js"; import { theme } from "../terminal/theme.js";
import { runCommandWithRuntime } from "./cli-utils.js"; import { resolveOptionFromCommand, runCommandWithRuntime } from "./cli-utils.js";
function runModelsCommand(action: () => Promise<void>) { function runModelsCommand(action: () => Promise<void>) {
return runCommandWithRuntime(defaultRuntime, action); return runCommandWithRuntime(defaultRuntime, action);
@ -41,7 +41,10 @@ export function registerModelsCli(program: Command) {
.description("Model discovery, scanning, and configuration") .description("Model discovery, scanning, and configuration")
.option("--status-json", "Output JSON (alias for `models status --json`)", false) .option("--status-json", "Output JSON (alias for `models status --json`)", false)
.option("--status-plain", "Plain output (alias for `models status --plain`)", false) .option("--status-plain", "Plain output (alias for `models status --plain`)", false)
.option("--agent <id>", "Agent id (default: configured default agent)") .option(
"--agent <id>",
"Agent id to inspect (overrides OPENCLAW_AGENT_DIR/PI_CODING_AGENT_DIR)",
)
.addHelpText( .addHelpText(
"after", "after",
() => () =>
@ -86,8 +89,13 @@ export function registerModelsCli(program: Command) {
.option("--probe-timeout <ms>", "Per-probe timeout in ms") .option("--probe-timeout <ms>", "Per-probe timeout in ms")
.option("--probe-concurrency <n>", "Concurrent probes") .option("--probe-concurrency <n>", "Concurrent probes")
.option("--probe-max-tokens <n>", "Probe max tokens (best-effort)") .option("--probe-max-tokens <n>", "Probe max tokens (best-effort)")
.option("--agent <id>", "Agent id (default: configured default agent)") .option(
.action(async (opts) => { "--agent <id>",
"Agent id to inspect (overrides OPENCLAW_AGENT_DIR/PI_CODING_AGENT_DIR)",
)
.action(async (opts, command) => {
const agent =
resolveOptionFromCommand<string>(command, "agent") ?? (opts.agent as string | undefined);
await runModelsCommand(async () => { await runModelsCommand(async () => {
await modelsStatusCommand( await modelsStatusCommand(
{ {
@ -100,7 +108,7 @@ export function registerModelsCli(program: Command) {
probeTimeout: opts.probeTimeout as string | undefined, probeTimeout: opts.probeTimeout as string | undefined,
probeConcurrency: opts.probeConcurrency as string | undefined, probeConcurrency: opts.probeConcurrency as string | undefined,
probeMaxTokens: opts.probeMaxTokens as string | undefined, probeMaxTokens: opts.probeMaxTokens as string | undefined,
agent: opts.agent as string | undefined, agent,
}, },
defaultRuntime, defaultRuntime,
); );
@ -282,6 +290,10 @@ export function registerModelsCli(program: Command) {
}); });
const auth = models.command("auth").description("Manage model auth profiles"); const auth = models.command("auth").description("Manage model auth profiles");
auth.option("--agent <id>", "Agent id for auth order get/set/clear");
auth.action(() => {
auth.help();
});
auth auth
.command("add") .command("add")
@ -375,12 +387,14 @@ export function registerModelsCli(program: Command) {
.requiredOption("--provider <name>", "Provider id (e.g. anthropic)") .requiredOption("--provider <name>", "Provider id (e.g. anthropic)")
.option("--agent <id>", "Agent id (default: configured default agent)") .option("--agent <id>", "Agent id (default: configured default agent)")
.option("--json", "Output JSON", false) .option("--json", "Output JSON", false)
.action(async (opts) => { .action(async (opts, command) => {
const agent =
resolveOptionFromCommand<string>(command, "agent") ?? (opts.agent as string | undefined);
await runModelsCommand(async () => { await runModelsCommand(async () => {
await modelsAuthOrderGetCommand( await modelsAuthOrderGetCommand(
{ {
provider: opts.provider as string, provider: opts.provider as string,
agent: opts.agent as string | undefined, agent,
json: Boolean(opts.json), json: Boolean(opts.json),
}, },
defaultRuntime, defaultRuntime,
@ -394,12 +408,14 @@ export function registerModelsCli(program: Command) {
.requiredOption("--provider <name>", "Provider id (e.g. anthropic)") .requiredOption("--provider <name>", "Provider id (e.g. anthropic)")
.option("--agent <id>", "Agent id (default: configured default agent)") .option("--agent <id>", "Agent id (default: configured default agent)")
.argument("<profileIds...>", "Auth profile ids (e.g. anthropic:default)") .argument("<profileIds...>", "Auth profile ids (e.g. anthropic:default)")
.action(async (profileIds: string[], opts) => { .action(async (profileIds: string[], opts, command) => {
const agent =
resolveOptionFromCommand<string>(command, "agent") ?? (opts.agent as string | undefined);
await runModelsCommand(async () => { await runModelsCommand(async () => {
await modelsAuthOrderSetCommand( await modelsAuthOrderSetCommand(
{ {
provider: opts.provider as string, provider: opts.provider as string,
agent: opts.agent as string | undefined, agent,
order: profileIds, order: profileIds,
}, },
defaultRuntime, defaultRuntime,
@ -412,12 +428,14 @@ export function registerModelsCli(program: Command) {
.description("Clear per-agent auth order override (fall back to config/round-robin)") .description("Clear per-agent auth order override (fall back to config/round-robin)")
.requiredOption("--provider <name>", "Provider id (e.g. anthropic)") .requiredOption("--provider <name>", "Provider id (e.g. anthropic)")
.option("--agent <id>", "Agent id (default: configured default agent)") .option("--agent <id>", "Agent id (default: configured default agent)")
.action(async (opts) => { .action(async (opts, command) => {
const agent =
resolveOptionFromCommand<string>(command, "agent") ?? (opts.agent as string | undefined);
await runModelsCommand(async () => { await runModelsCommand(async () => {
await modelsAuthOrderClearCommand( await modelsAuthOrderClearCommand(
{ {
provider: opts.provider as string, provider: opts.provider as string,
agent: opts.agent as string | undefined, agent,
}, },
defaultRuntime, defaultRuntime,
); );

View file

@ -6,9 +6,9 @@ import {
} from "../../agents/auth-profiles.js"; } from "../../agents/auth-profiles.js";
import { normalizeProviderId } from "../../agents/model-selection.js"; import { normalizeProviderId } from "../../agents/model-selection.js";
import { loadConfig } from "../../config/config.js"; import { loadConfig } from "../../config/config.js";
import { normalizeAgentId } from "../../routing/session-key.js";
import type { RuntimeEnv } from "../../runtime.js"; import type { RuntimeEnv } from "../../runtime.js";
import { shortenHomePath } from "../../utils.js"; import { shortenHomePath } from "../../utils.js";
import { resolveKnownAgentId } from "./shared.js";
function resolveTargetAgent( function resolveTargetAgent(
cfg: ReturnType<typeof loadConfig>, cfg: ReturnType<typeof loadConfig>,
@ -17,7 +17,7 @@ function resolveTargetAgent(
agentId: string; agentId: string;
agentDir: string; agentDir: string;
} { } {
const agentId = raw?.trim() ? normalizeAgentId(raw.trim()) : resolveDefaultAgentId(cfg); const agentId = resolveKnownAgentId({ cfg, rawAgentId: raw }) ?? resolveDefaultAgentId(cfg);
const agentDir = resolveAgentDir(cfg, agentId); const agentDir = resolveAgentDir(cfg, agentId);
return { agentId, agentDir }; return { agentId, agentDir };
} }

View file

@ -1,5 +1,10 @@
import path from "node:path"; import path from "node:path";
import { resolveAgentDir, resolveDefaultAgentId } from "../../agents/agent-scope.js"; import { resolveOpenClawAgentDir } from "../../agents/agent-paths.js";
import {
resolveAgentDir,
resolveAgentModelFallbacksOverride,
resolveAgentModelPrimary,
} from "../../agents/agent-scope.js";
import { import {
buildAuthHealthSummary, buildAuthHealthSummary,
DEFAULT_OAUTH_WARN_MS, DEFAULT_OAUTH_WARN_MS,
@ -15,6 +20,7 @@ import {
buildModelAliasIndex, buildModelAliasIndex,
parseModelRef, parseModelRef,
resolveConfiguredModelRef, resolveConfiguredModelRef,
resolveDefaultModelForAgent,
resolveModelRefFromString, resolveModelRefFromString,
} from "../../agents/model-selection.js"; } from "../../agents/model-selection.js";
import { CONFIG_PATH, loadConfig } from "../../config/config.js"; import { CONFIG_PATH, loadConfig } from "../../config/config.js";
@ -40,8 +46,12 @@ import {
sortProbeResults, sortProbeResults,
type AuthProbeSummary, type AuthProbeSummary,
} from "./list.probe.js"; } from "./list.probe.js";
import { normalizeAgentId } from "../../routing/session-key.js"; import {
import { DEFAULT_MODEL, DEFAULT_PROVIDER, ensureFlagCompatibility } from "./shared.js"; DEFAULT_MODEL,
DEFAULT_PROVIDER,
ensureFlagCompatibility,
resolveKnownAgentId,
} from "./shared.js";
export async function modelsStatusCommand( export async function modelsStatusCommand(
opts: { opts: {
@ -63,11 +73,19 @@ export async function modelsStatusCommand(
throw new Error("--probe cannot be used with --plain output."); throw new Error("--probe cannot be used with --plain output.");
} }
const cfg = loadConfig(); const cfg = loadConfig();
const resolved = resolveConfiguredModelRef({ const agentId = resolveKnownAgentId({ cfg, rawAgentId: opts.agent });
cfg, const agentDir = agentId ? resolveAgentDir(cfg, agentId) : resolveOpenClawAgentDir();
defaultProvider: DEFAULT_PROVIDER, const agentModelPrimary = agentId ? resolveAgentModelPrimary(cfg, agentId) : undefined;
defaultModel: DEFAULT_MODEL, 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 const modelConfig = cfg.agents?.defaults?.model as
| { primary?: string; fallbacks?: string[] } | { primary?: string; fallbacks?: string[] }
@ -77,11 +95,13 @@ export async function modelsStatusCommand(
| { primary?: string; fallbacks?: string[] } | { primary?: string; fallbacks?: string[] }
| string | string
| undefined; | undefined;
const rawModel = const rawDefaultsModel =
typeof modelConfig === "string" ? modelConfig.trim() : (modelConfig?.primary?.trim() ?? ""); typeof modelConfig === "string" ? modelConfig.trim() : (modelConfig?.primary?.trim() ?? "");
const rawModel = agentModelPrimary ?? rawDefaultsModel;
const resolvedLabel = `${resolved.provider}/${resolved.model}`; const resolvedLabel = `${resolved.provider}/${resolved.model}`;
const defaultLabel = rawModel || resolvedLabel; const defaultLabel = rawModel || resolvedLabel;
const fallbacks = typeof modelConfig === "object" ? (modelConfig?.fallbacks ?? []) : []; const defaultsFallbacks = typeof modelConfig === "object" ? (modelConfig?.fallbacks ?? []) : [];
const fallbacks = agentFallbacksOverride ?? defaultsFallbacks;
const imageModel = const imageModel =
typeof imageConfig === "string" ? imageConfig.trim() : (imageConfig?.primary?.trim() ?? ""); typeof imageConfig === "string" ? imageConfig.trim() : (imageConfig?.primary?.trim() ?? "");
const imageFallbacks = typeof imageConfig === "object" ? (imageConfig?.fallbacks ?? []) : []; const imageFallbacks = typeof imageConfig === "object" ? (imageConfig?.fallbacks ?? []) : [];
@ -95,10 +115,6 @@ export async function modelsStatusCommand(
); );
const allowed = Object.keys(cfg.agents?.defaults?.models ?? {}); const allowed = Object.keys(cfg.agents?.defaults?.models ?? {});
const agentId = opts.agent?.trim()
? normalizeAgentId(opts.agent.trim())
: resolveDefaultAgentId(cfg);
const agentDir = resolveAgentDir(cfg, agentId);
const store = ensureAuthProfileStore(agentDir); const store = ensureAuthProfileStore(agentDir);
const modelsPath = path.join(agentDir, "models.json"); const modelsPath = path.join(agentDir, "models.json");
@ -300,12 +316,21 @@ export async function modelsStatusCommand(
JSON.stringify( JSON.stringify(
{ {
configPath: CONFIG_PATH, configPath: CONFIG_PATH,
...(agentId ? { agentId } : {}),
agentDir, agentDir,
defaultModel: defaultLabel, defaultModel: defaultLabel,
resolvedDefault: resolvedLabel, resolvedDefault: resolvedLabel,
fallbacks, fallbacks,
imageModel: imageModel || null, imageModel: imageModel || null,
imageFallbacks, imageFallbacks,
...(agentId
? {
modelConfig: {
defaultSource: agentModelPrimary ? "agent" : "defaults",
fallbacksSource: agentFallbacksOverride !== undefined ? "agent" : "defaults",
},
}
: {}),
aliases, aliases,
allowed, allowed,
auth: { auth: {
@ -341,7 +366,10 @@ export async function modelsStatusCommand(
} }
const rich = isRich(opts); const rich = isRich(opts);
type ModelConfigSource = "agent" | "defaults";
const label = (value: string) => colorize(rich, theme.accent, value.padEnd(14)); const label = (value: string) => colorize(rich, theme.accent, value.padEnd(14));
const labelWithSource = (value: string, source?: ModelConfigSource) =>
label(source ? `${value} (${source})` : value);
const displayDefault = const displayDefault =
rawModel && rawModel !== resolvedLabel ? `${resolvedLabel} (from ${rawModel})` : resolvedLabel; rawModel && rawModel !== resolvedLabel ? `${resolvedLabel} (from ${rawModel})` : resolvedLabel;
@ -356,32 +384,34 @@ export async function modelsStatusCommand(
)}`, )}`,
); );
runtime.log( runtime.log(
`${label("Default")}${colorize(rich, theme.muted, ":")} ${colorize( `${labelWithSource("Default", agentId ? (agentModelPrimary ? "agent" : "defaults") : undefined)}${colorize(
rich, rich,
theme.success, theme.muted,
displayDefault, ":",
)}`, )} ${colorize(rich, theme.success, displayDefault)}`,
); );
runtime.log( runtime.log(
`${label(`Fallbacks (${fallbacks.length || 0})`)}${colorize(rich, theme.muted, ":")} ${colorize( `${labelWithSource(
`Fallbacks (${fallbacks.length || 0})`,
agentId ? (agentFallbacksOverride !== undefined ? "agent" : "defaults") : undefined,
)}${colorize(rich, theme.muted, ":")} ${colorize(
rich, rich,
fallbacks.length ? theme.warn : theme.muted, fallbacks.length ? theme.warn : theme.muted,
fallbacks.length ? fallbacks.join(", ") : "-", fallbacks.length ? fallbacks.join(", ") : "-",
)}`, )}`,
); );
runtime.log( runtime.log(
`${label("Image model")}${colorize(rich, theme.muted, ":")} ${colorize( `${labelWithSource("Image model", agentId ? "defaults" : undefined)}${colorize(
rich,
imageModel ? theme.accentBright : theme.muted,
imageModel || "-",
)}`,
);
runtime.log(
`${label(`Image fallbacks (${imageFallbacks.length || 0})`)}${colorize(
rich, rich,
theme.muted, theme.muted,
":", ":",
)} ${colorize( )} ${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, rich,
imageFallbacks.length ? theme.accentBright : theme.muted, imageFallbacks.length ? theme.accentBright : theme.muted,
imageFallbacks.length ? imageFallbacks.join(", ") : "-", imageFallbacks.length ? imageFallbacks.join(", ") : "-",

View file

@ -29,8 +29,11 @@ const mocks = vi.hoisted(() => {
return { return {
store, store,
resolveOpenClawAgentDir: vi.fn().mockReturnValue("/tmp/openclaw-agent"),
resolveAgentDir: vi.fn().mockReturnValue("/tmp/openclaw-agent"), resolveAgentDir: vi.fn().mockReturnValue("/tmp/openclaw-agent"),
resolveDefaultAgentId: vi.fn().mockReturnValue("main"), resolveAgentModelPrimary: vi.fn().mockReturnValue(undefined),
resolveAgentModelFallbacksOverride: vi.fn().mockReturnValue(undefined),
listAgentIds: vi.fn().mockReturnValue(["main", "jeremiah"]),
ensureAuthProfileStore: vi.fn().mockReturnValue(store), ensureAuthProfileStore: vi.fn().mockReturnValue(store),
listProfilesForProvider: vi.fn((s: typeof store, provider: string) => { listProfilesForProvider: vi.fn((s: typeof store, provider: string) => {
return Object.entries(s.profiles) return Object.entries(s.profiles)
@ -72,18 +75,16 @@ const mocks = vi.hoisted(() => {
}; };
}); });
vi.mock("../../agents/agent-scope.js", () => ({ vi.mock("../../agents/agent-paths.js", () => ({
resolveAgentDir: mocks.resolveAgentDir, resolveOpenClawAgentDir: mocks.resolveOpenClawAgentDir,
resolveDefaultAgentId: mocks.resolveDefaultAgentId,
})); }));
vi.mock("../../routing/session-key.js", async (importOriginal) => { vi.mock("../../agents/agent-scope.js", () => ({
const actual = await importOriginal<typeof import("../../routing/session-key.js")>(); resolveAgentDir: mocks.resolveAgentDir,
return { resolveAgentModelPrimary: mocks.resolveAgentModelPrimary,
...actual, resolveAgentModelFallbacksOverride: mocks.resolveAgentModelFallbacksOverride,
normalizeAgentId: (id: string) => id.toLowerCase().replace(/\s+/g, "-"), listAgentIds: mocks.listAgentIds,
}; }));
});
vi.mock("../../agents/auth-profiles.js", async (importOriginal) => { vi.mock("../../agents/auth-profiles.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../agents/auth-profiles.js")>(); const actual = await importOriginal<typeof import("../../agents/auth-profiles.js")>();
@ -127,6 +128,7 @@ describe("modelsStatusCommand auth overview", () => {
await modelsStatusCommand({ json: true }, runtime as never); await modelsStatusCommand({ json: true }, runtime as never);
const payload = JSON.parse(String((runtime.log as vi.Mock).mock.calls[0][0])); const payload = JSON.parse(String((runtime.log as vi.Mock).mock.calls[0][0]));
expect(mocks.resolveOpenClawAgentDir).toHaveBeenCalled();
expect(payload.defaultModel).toBe("anthropic/claude-opus-4-5"); expect(payload.defaultModel).toBe("anthropic/claude-opus-4-5");
expect(payload.auth.storePath).toBe("/tmp/openclaw-agent/auth-profiles.json"); expect(payload.auth.storePath).toBe("/tmp/openclaw-agent/auth-profiles.json");
expect(payload.auth.shellEnvFallback.enabled).toBe(true); expect(payload.auth.shellEnvFallback.enabled).toBe(true);
@ -157,23 +159,74 @@ describe("modelsStatusCommand auth overview", () => {
).toBe(true); ).toBe(true);
}); });
it("resolves agent dir from --agent flag", async () => { it("uses agent overrides and reports sources", async () => {
mocks.resolveAgentDir.mockReturnValue("/tmp/openclaw-agent-custom");
const localRuntime = { const localRuntime = {
log: vi.fn(), log: vi.fn(),
error: vi.fn(), error: vi.fn(),
exit: vi.fn(), exit: vi.fn(),
}; };
const originalPrimary = mocks.resolveAgentModelPrimary.getMockImplementation();
const originalFallbacks = mocks.resolveAgentModelFallbacksOverride.getMockImplementation();
const originalAgentDir = mocks.resolveAgentDir.getMockImplementation();
mocks.resolveAgentModelPrimary.mockReturnValue("openai/gpt-4");
mocks.resolveAgentModelFallbacksOverride.mockReturnValue(["openai/gpt-3.5"]);
mocks.resolveAgentDir.mockReturnValue("/tmp/openclaw-agent-custom");
try { try {
await modelsStatusCommand({ json: true, agent: "jeremiah" }, localRuntime as never); await modelsStatusCommand({ json: true, agent: "Jeremiah" }, localRuntime as never);
expect(mocks.resolveAgentDir).toHaveBeenCalledWith(expect.anything(), "jeremiah"); expect(mocks.resolveAgentDir).toHaveBeenCalledWith(expect.anything(), "jeremiah");
const payload = JSON.parse(String((localRuntime.log as vi.Mock).mock.calls[0][0])); const payload = JSON.parse(String((localRuntime.log as vi.Mock).mock.calls[0][0]));
expect(payload.agentId).toBe("jeremiah");
expect(payload.agentDir).toBe("/tmp/openclaw-agent-custom"); expect(payload.agentDir).toBe("/tmp/openclaw-agent-custom");
expect(payload.defaultModel).toBe("openai/gpt-4");
expect(payload.fallbacks).toEqual(["openai/gpt-3.5"]);
expect(payload.modelConfig).toEqual({
defaultSource: "agent",
fallbacksSource: "agent",
});
} finally { } finally {
mocks.resolveAgentDir.mockReturnValue("/tmp/openclaw-agent"); mocks.resolveAgentModelPrimary.mockImplementation(originalPrimary);
mocks.resolveAgentModelFallbacksOverride.mockImplementation(originalFallbacks);
mocks.resolveAgentDir.mockImplementation(originalAgentDir);
} }
}); });
it("labels defaults when --agent has no overrides", async () => {
const localRuntime = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
};
const originalPrimary = mocks.resolveAgentModelPrimary.getMockImplementation();
const originalFallbacks = mocks.resolveAgentModelFallbacksOverride.getMockImplementation();
mocks.resolveAgentModelPrimary.mockReturnValue(undefined);
mocks.resolveAgentModelFallbacksOverride.mockReturnValue(undefined);
try {
await modelsStatusCommand({ agent: "main" }, localRuntime as never);
const output = (localRuntime.log as vi.Mock).mock.calls
.map((call) => String(call[0]))
.join("\n");
expect(output).toContain("Default (defaults)");
expect(output).toContain("Fallbacks (0) (defaults)");
} finally {
mocks.resolveAgentModelPrimary.mockImplementation(originalPrimary);
mocks.resolveAgentModelFallbacksOverride.mockImplementation(originalFallbacks);
}
});
it("throws when agent id is unknown", async () => {
const localRuntime = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
};
await expect(modelsStatusCommand({ agent: "unknown" }, localRuntime as never)).rejects.toThrow(
'Unknown agent id "unknown".',
);
});
it("exits non-zero when auth is missing", async () => { it("exits non-zero when auth is missing", async () => {
const originalProfiles = { ...mocks.store.profiles }; const originalProfiles = { ...mocks.store.profiles };
mocks.store.profiles = {}; mocks.store.profiles = {};

View file

@ -5,11 +5,14 @@ import {
parseModelRef, parseModelRef,
resolveModelRefFromString, resolveModelRefFromString,
} from "../../agents/model-selection.js"; } from "../../agents/model-selection.js";
import { listAgentIds } from "../../agents/agent-scope.js";
import { formatCliCommand } from "../../cli/command-format.js";
import { import {
type OpenClawConfig, type OpenClawConfig,
readConfigFileSnapshot, readConfigFileSnapshot,
writeConfigFile, writeConfigFile,
} from "../../config/config.js"; } from "../../config/config.js";
import { normalizeAgentId } from "../../routing/session-key.js";
export const ensureFlagCompatibility = (opts: { json?: boolean; plain?: boolean }) => { export const ensureFlagCompatibility = (opts: { json?: boolean; plain?: boolean }) => {
if (opts.json && opts.plain) { if (opts.json && opts.plain) {
@ -82,6 +85,22 @@ export function normalizeAlias(alias: string): string {
return trimmed; return trimmed;
} }
export function resolveKnownAgentId(params: {
cfg: OpenClawConfig;
rawAgentId?: string | null;
}): string | undefined {
const raw = params.rawAgentId?.trim();
if (!raw) return undefined;
const agentId = normalizeAgentId(raw);
const knownAgents = listAgentIds(params.cfg);
if (!knownAgents.includes(agentId)) {
throw new Error(
`Unknown agent id "${raw}". Use "${formatCliCommand("openclaw agents list")}" to see configured agents.`,
);
}
return agentId;
}
export { modelKey }; export { modelKey };
export { DEFAULT_MODEL, DEFAULT_PROVIDER }; export { DEFAULT_MODEL, DEFAULT_PROVIDER };