mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 17:04:46 +00:00
refactor(cli): lazy-load models runtime
This commit is contained in:
@@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- CLI/models: lazy-load model runtime helpers for command actions so `openclaw models --help` renders without importing the model runtime path.
|
||||
- Security/sandbox: include Windows `USERPROFILE` in the sandbox blocked home roots so credential-bearing binds (such as `.codex`, `.openclaw`, or `.ssh` under the Windows user profile) are denied even when `HOME` points at a different shell home. (#63074) Thanks @luoyanglang.
|
||||
- Models config/auth: stop inferring provider env-var markers from broad `^[A-Z_][A-Z0-9_]*$` strings, and resolve config-backed provider `apiKey` values only through structured env SecretRefs (`secrets.providers[id]` / `secrets.defaults`), so unrelated env vars cannot accidentally become provider credentials. Thanks @sallyom.
|
||||
- Media fetch: skip allocating and buffering the response body for bodyless media responses (HEAD probes and 204-style empty bodies), avoiding wasted heap on streams that carry no payload. Thanks @shakkernerd.
|
||||
|
||||
77
src/cli/models-cli.lazy.test.ts
Normal file
77
src/cli/models-cli.lazy.test.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { Command } from "commander";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("models cli lazy runtime boundary", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.doUnmock("./models-cli.runtime.js");
|
||||
vi.doUnmock("../commands/models/list.status-command.js");
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it("renders help without importing the models runtime", async () => {
|
||||
const runtimeLoaded = vi.fn();
|
||||
vi.doMock("./models-cli.runtime.js", () => {
|
||||
runtimeLoaded();
|
||||
return {
|
||||
defaultRuntime: {},
|
||||
rejectAgentScopedModelWrite: vi.fn(),
|
||||
resolveModelAgentOption: vi.fn(),
|
||||
runModelsCommand: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const { registerModelsCli } = await import("./models-cli.js");
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
program.configureOutput({
|
||||
writeErr: () => {},
|
||||
writeOut: () => {},
|
||||
});
|
||||
registerModelsCli(program);
|
||||
|
||||
await expect(program.parseAsync(["models", "--help"], { from: "user" })).rejects.toMatchObject({
|
||||
exitCode: 0,
|
||||
});
|
||||
expect(runtimeLoaded).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("loads the models runtime for command actions", async () => {
|
||||
const defaultRuntime = {};
|
||||
const modelsStatusCommand = vi.fn().mockResolvedValue(undefined);
|
||||
const runModelsCommand = vi.fn(async (action: () => Promise<void>) => {
|
||||
await action();
|
||||
});
|
||||
const resolveModelAgentOption = vi.fn(() => "poe");
|
||||
const runtimeLoaded = vi.fn();
|
||||
|
||||
vi.doMock("./models-cli.runtime.js", () => {
|
||||
runtimeLoaded();
|
||||
return {
|
||||
defaultRuntime,
|
||||
rejectAgentScopedModelWrite: vi.fn(),
|
||||
resolveModelAgentOption,
|
||||
runModelsCommand,
|
||||
};
|
||||
});
|
||||
vi.doMock("../commands/models/list.status-command.js", () => ({
|
||||
modelsStatusCommand,
|
||||
}));
|
||||
|
||||
const { registerModelsCli } = await import("./models-cli.js");
|
||||
const program = new Command();
|
||||
registerModelsCli(program);
|
||||
|
||||
await program.parseAsync(["models", "status", "--json"], { from: "user" });
|
||||
|
||||
expect(runtimeLoaded).toHaveBeenCalledTimes(1);
|
||||
expect(runModelsCommand).toHaveBeenCalledTimes(1);
|
||||
expect(modelsStatusCommand).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ agent: "poe", json: true }),
|
||||
defaultRuntime,
|
||||
);
|
||||
});
|
||||
});
|
||||
33
src/cli/models-cli.runtime.ts
Normal file
33
src/cli/models-cli.runtime.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { Command } from "commander";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { resolveOptionFromCommand, runCommandWithRuntime } from "./cli-utils.js";
|
||||
import { formatCliCommand } from "./command-format.js";
|
||||
|
||||
export { defaultRuntime };
|
||||
|
||||
export function runModelsCommand(action: () => Promise<void>) {
|
||||
return runCommandWithRuntime(defaultRuntime, action);
|
||||
}
|
||||
|
||||
export function resolveModelAgentOption(
|
||||
command: Command | undefined,
|
||||
opts?: { agent?: unknown },
|
||||
): string | undefined {
|
||||
return (
|
||||
resolveOptionFromCommand<string>(command, "agent") ??
|
||||
(typeof opts?.agent === "string" ? opts.agent : undefined)
|
||||
);
|
||||
}
|
||||
|
||||
export function rejectAgentScopedModelWrite(
|
||||
command: Command,
|
||||
commandName: "set" | "set-image",
|
||||
): void {
|
||||
const agent = resolveOptionFromCommand<string>(command, "agent");
|
||||
if (!agent) {
|
||||
return;
|
||||
}
|
||||
throw new Error(
|
||||
`openclaw models ${commandName} does not support --agent; it only updates global model defaults. Remove --agent, or run ${formatCliCommand("openclaw agents list")} and set the per-agent model in agent config.`,
|
||||
);
|
||||
}
|
||||
@@ -1,22 +1,14 @@
|
||||
import type { Command } from "commander";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { formatDocsLink } from "../terminal/links.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
import { resolveOptionFromCommand, runCommandWithRuntime } from "./cli-utils.js";
|
||||
import { formatCliCommand } from "./command-format.js";
|
||||
|
||||
function runModelsCommand(action: () => Promise<void>) {
|
||||
return runCommandWithRuntime(defaultRuntime, action);
|
||||
}
|
||||
type ModelsCliRuntime = typeof import("./models-cli.runtime.js");
|
||||
|
||||
function rejectAgentScopedModelWrite(command: Command, commandName: "set" | "set-image"): void {
|
||||
const agent = resolveOptionFromCommand<string>(command, "agent");
|
||||
if (!agent) {
|
||||
return;
|
||||
}
|
||||
throw new Error(
|
||||
`openclaw models ${commandName} does not support --agent; it only updates global model defaults. Remove --agent, or run ${formatCliCommand("openclaw agents list")} and set the per-agent model in agent config.`,
|
||||
);
|
||||
async function withModelsRuntime(
|
||||
action: (runtime: ModelsCliRuntime) => Promise<void>,
|
||||
): Promise<void> {
|
||||
const runtime = await import("./models-cli.runtime.js");
|
||||
return runtime.runModelsCommand(() => action(runtime));
|
||||
}
|
||||
|
||||
export function registerModelsCli(program: Command) {
|
||||
@@ -44,7 +36,7 @@ export function registerModelsCli(program: Command) {
|
||||
.option("--json", "Output JSON", false)
|
||||
.option("--plain", "Plain line output", false)
|
||||
.action(async (opts) => {
|
||||
await runModelsCommand(async () => {
|
||||
await withModelsRuntime(async ({ defaultRuntime }) => {
|
||||
const { modelsListCommand } = await import("../commands/models/list.list-command.js");
|
||||
await modelsListCommand(opts, defaultRuntime);
|
||||
});
|
||||
@@ -79,9 +71,8 @@ export function registerModelsCli(program: Command) {
|
||||
"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 withModelsRuntime(async ({ defaultRuntime, resolveModelAgentOption }) => {
|
||||
const agent = resolveModelAgentOption(command, opts);
|
||||
const { modelsStatusCommand } = await import("../commands/models/list.status-command.js");
|
||||
await modelsStatusCommand(
|
||||
{
|
||||
@@ -106,10 +97,11 @@ export function registerModelsCli(program: Command) {
|
||||
.description("Set the default model")
|
||||
.argument("<model>", "Model id or alias")
|
||||
.action(async (model: string, _opts: unknown, command: Command) => {
|
||||
rejectAgentScopedModelWrite(command, "set");
|
||||
await runModelsCommand(async () => {
|
||||
const runtime = await import("./models-cli.runtime.js");
|
||||
runtime.rejectAgentScopedModelWrite(command, "set");
|
||||
await runtime.runModelsCommand(async () => {
|
||||
const { modelsSetCommand } = await import("../commands/models/set.js");
|
||||
await modelsSetCommand(model, defaultRuntime);
|
||||
await modelsSetCommand(model, runtime.defaultRuntime);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -118,10 +110,11 @@ export function registerModelsCli(program: Command) {
|
||||
.description("Set the image model")
|
||||
.argument("<model>", "Model id or alias")
|
||||
.action(async (model: string, _opts: unknown, command: Command) => {
|
||||
rejectAgentScopedModelWrite(command, "set-image");
|
||||
await runModelsCommand(async () => {
|
||||
const runtime = await import("./models-cli.runtime.js");
|
||||
runtime.rejectAgentScopedModelWrite(command, "set-image");
|
||||
await runtime.runModelsCommand(async () => {
|
||||
const { modelsSetImageCommand } = await import("../commands/models/set-image.js");
|
||||
await modelsSetImageCommand(model, defaultRuntime);
|
||||
await modelsSetImageCommand(model, runtime.defaultRuntime);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -133,7 +126,7 @@ export function registerModelsCli(program: Command) {
|
||||
.option("--json", "Output JSON", false)
|
||||
.option("--plain", "Plain output", false)
|
||||
.action(async (opts) => {
|
||||
await runModelsCommand(async () => {
|
||||
await withModelsRuntime(async ({ defaultRuntime }) => {
|
||||
const { modelsAliasesListCommand } = await import("../commands/models/aliases.js");
|
||||
await modelsAliasesListCommand(opts, defaultRuntime);
|
||||
});
|
||||
@@ -145,7 +138,7 @@ export function registerModelsCli(program: Command) {
|
||||
.argument("<alias>", "Alias name")
|
||||
.argument("<model>", "Model id or alias")
|
||||
.action(async (alias: string, model: string) => {
|
||||
await runModelsCommand(async () => {
|
||||
await withModelsRuntime(async ({ defaultRuntime }) => {
|
||||
const { modelsAliasesAddCommand } = await import("../commands/models/aliases.js");
|
||||
await modelsAliasesAddCommand(alias, model, defaultRuntime);
|
||||
});
|
||||
@@ -156,7 +149,7 @@ export function registerModelsCli(program: Command) {
|
||||
.description("Remove a model alias")
|
||||
.argument("<alias>", "Alias name")
|
||||
.action(async (alias: string) => {
|
||||
await runModelsCommand(async () => {
|
||||
await withModelsRuntime(async ({ defaultRuntime }) => {
|
||||
const { modelsAliasesRemoveCommand } = await import("../commands/models/aliases.js");
|
||||
await modelsAliasesRemoveCommand(alias, defaultRuntime);
|
||||
});
|
||||
@@ -170,7 +163,7 @@ export function registerModelsCli(program: Command) {
|
||||
.option("--json", "Output JSON", false)
|
||||
.option("--plain", "Plain output", false)
|
||||
.action(async (opts) => {
|
||||
await runModelsCommand(async () => {
|
||||
await withModelsRuntime(async ({ defaultRuntime }) => {
|
||||
const { modelsFallbacksListCommand } = await import("../commands/models/fallbacks.js");
|
||||
await modelsFallbacksListCommand(opts, defaultRuntime);
|
||||
});
|
||||
@@ -181,7 +174,7 @@ export function registerModelsCli(program: Command) {
|
||||
.description("Add a fallback model")
|
||||
.argument("<model>", "Model id or alias")
|
||||
.action(async (model: string) => {
|
||||
await runModelsCommand(async () => {
|
||||
await withModelsRuntime(async ({ defaultRuntime }) => {
|
||||
const { modelsFallbacksAddCommand } = await import("../commands/models/fallbacks.js");
|
||||
await modelsFallbacksAddCommand(model, defaultRuntime);
|
||||
});
|
||||
@@ -192,7 +185,7 @@ export function registerModelsCli(program: Command) {
|
||||
.description("Remove a fallback model")
|
||||
.argument("<model>", "Model id or alias")
|
||||
.action(async (model: string) => {
|
||||
await runModelsCommand(async () => {
|
||||
await withModelsRuntime(async ({ defaultRuntime }) => {
|
||||
const { modelsFallbacksRemoveCommand } = await import("../commands/models/fallbacks.js");
|
||||
await modelsFallbacksRemoveCommand(model, defaultRuntime);
|
||||
});
|
||||
@@ -202,7 +195,7 @@ export function registerModelsCli(program: Command) {
|
||||
.command("clear")
|
||||
.description("Clear all fallback models")
|
||||
.action(async () => {
|
||||
await runModelsCommand(async () => {
|
||||
await withModelsRuntime(async ({ defaultRuntime }) => {
|
||||
const { modelsFallbacksClearCommand } = await import("../commands/models/fallbacks.js");
|
||||
await modelsFallbacksClearCommand(defaultRuntime);
|
||||
});
|
||||
@@ -218,7 +211,7 @@ export function registerModelsCli(program: Command) {
|
||||
.option("--json", "Output JSON", false)
|
||||
.option("--plain", "Plain output", false)
|
||||
.action(async (opts) => {
|
||||
await runModelsCommand(async () => {
|
||||
await withModelsRuntime(async ({ defaultRuntime }) => {
|
||||
const { modelsImageFallbacksListCommand } =
|
||||
await import("../commands/models/image-fallbacks.js");
|
||||
await modelsImageFallbacksListCommand(opts, defaultRuntime);
|
||||
@@ -230,7 +223,7 @@ export function registerModelsCli(program: Command) {
|
||||
.description("Add an image fallback model")
|
||||
.argument("<model>", "Model id or alias")
|
||||
.action(async (model: string) => {
|
||||
await runModelsCommand(async () => {
|
||||
await withModelsRuntime(async ({ defaultRuntime }) => {
|
||||
const { modelsImageFallbacksAddCommand } =
|
||||
await import("../commands/models/image-fallbacks.js");
|
||||
await modelsImageFallbacksAddCommand(model, defaultRuntime);
|
||||
@@ -242,7 +235,7 @@ export function registerModelsCli(program: Command) {
|
||||
.description("Remove an image fallback model")
|
||||
.argument("<model>", "Model id or alias")
|
||||
.action(async (model: string) => {
|
||||
await runModelsCommand(async () => {
|
||||
await withModelsRuntime(async ({ defaultRuntime }) => {
|
||||
const { modelsImageFallbacksRemoveCommand } =
|
||||
await import("../commands/models/image-fallbacks.js");
|
||||
await modelsImageFallbacksRemoveCommand(model, defaultRuntime);
|
||||
@@ -253,7 +246,7 @@ export function registerModelsCli(program: Command) {
|
||||
.command("clear")
|
||||
.description("Clear all image fallback models")
|
||||
.action(async () => {
|
||||
await runModelsCommand(async () => {
|
||||
await withModelsRuntime(async ({ defaultRuntime }) => {
|
||||
const { modelsImageFallbacksClearCommand } =
|
||||
await import("../commands/models/image-fallbacks.js");
|
||||
await modelsImageFallbacksClearCommand(defaultRuntime);
|
||||
@@ -276,14 +269,14 @@ export function registerModelsCli(program: Command) {
|
||||
.option("--set-image", "Set agents.defaults.imageModel to the first image selection", false)
|
||||
.option("--json", "Output JSON", false)
|
||||
.action(async (opts) => {
|
||||
await runModelsCommand(async () => {
|
||||
await withModelsRuntime(async ({ defaultRuntime }) => {
|
||||
const { modelsScanCommand } = await import("../commands/models/scan.js");
|
||||
await modelsScanCommand(opts, defaultRuntime);
|
||||
});
|
||||
});
|
||||
|
||||
models.action(async (opts) => {
|
||||
await runModelsCommand(async () => {
|
||||
await withModelsRuntime(async ({ defaultRuntime }) => {
|
||||
const { modelsStatusCommand } = await import("../commands/models/list.status-command.js");
|
||||
await modelsStatusCommand(
|
||||
{
|
||||
@@ -309,9 +302,8 @@ export function registerModelsCli(program: Command) {
|
||||
.option("--agent <id>", "Agent id (default: configured default agent)")
|
||||
.option("--json", "Output JSON", false)
|
||||
.action(async (opts, command) => {
|
||||
const agent =
|
||||
resolveOptionFromCommand<string>(command, "agent") ?? (opts.agent as string | undefined);
|
||||
await runModelsCommand(async () => {
|
||||
await withModelsRuntime(async ({ defaultRuntime, resolveModelAgentOption }) => {
|
||||
const agent = resolveModelAgentOption(command, opts);
|
||||
const { modelsAuthListCommand } = await import("../commands/models/auth-list.js");
|
||||
await modelsAuthListCommand(
|
||||
{
|
||||
@@ -328,10 +320,8 @@ export function registerModelsCli(program: Command) {
|
||||
.command("add")
|
||||
.description("Interactive auth helper (provider auth or paste token)")
|
||||
.action(async (command) => {
|
||||
const agent =
|
||||
resolveOptionFromCommand<string>(command, "agent") ??
|
||||
resolveOptionFromCommand<string>(auth, "agent");
|
||||
await runModelsCommand(async () => {
|
||||
await withModelsRuntime(async ({ defaultRuntime, resolveModelAgentOption }) => {
|
||||
const agent = resolveModelAgentOption(command) ?? resolveModelAgentOption(auth);
|
||||
const { modelsAuthAddCommand } = await import("../commands/models/auth.js");
|
||||
await modelsAuthAddCommand({ agent }, defaultRuntime);
|
||||
});
|
||||
@@ -344,8 +334,8 @@ export function registerModelsCli(program: Command) {
|
||||
.option("--method <id>", "Provider auth method id")
|
||||
.option("--set-default", "Apply the provider's default model recommendation", false)
|
||||
.action(async (opts, command) => {
|
||||
const agent = resolveOptionFromCommand<string>(command, "agent");
|
||||
await runModelsCommand(async () => {
|
||||
await withModelsRuntime(async ({ defaultRuntime, resolveModelAgentOption }) => {
|
||||
const agent = resolveModelAgentOption(command);
|
||||
const { modelsAuthLoginCommand } = await import("../commands/models/auth.js");
|
||||
await modelsAuthLoginCommand(
|
||||
{
|
||||
@@ -365,8 +355,8 @@ export function registerModelsCli(program: Command) {
|
||||
.option("--provider <name>", "Provider id")
|
||||
.option("--yes", "Skip confirmation", false)
|
||||
.action(async (opts, command) => {
|
||||
const agent = resolveOptionFromCommand<string>(command, "agent");
|
||||
await runModelsCommand(async () => {
|
||||
await withModelsRuntime(async ({ defaultRuntime, resolveModelAgentOption }) => {
|
||||
const agent = resolveModelAgentOption(command);
|
||||
const { modelsAuthSetupTokenCommand } = await import("../commands/models/auth.js");
|
||||
await modelsAuthSetupTokenCommand(
|
||||
{
|
||||
@@ -389,8 +379,8 @@ export function registerModelsCli(program: Command) {
|
||||
"Optional expiry duration (e.g. 365d, 12h). Stored as absolute expiresAt.",
|
||||
)
|
||||
.action(async (opts, command) => {
|
||||
const agent = resolveOptionFromCommand<string>(command, "agent");
|
||||
await runModelsCommand(async () => {
|
||||
await withModelsRuntime(async ({ defaultRuntime, resolveModelAgentOption }) => {
|
||||
const agent = resolveModelAgentOption(command);
|
||||
const { modelsAuthPasteTokenCommand } = await import("../commands/models/auth.js");
|
||||
await modelsAuthPasteTokenCommand(
|
||||
{
|
||||
@@ -409,8 +399,8 @@ export function registerModelsCli(program: Command) {
|
||||
.description("Login to GitHub Copilot via GitHub device flow (TTY required)")
|
||||
.option("--yes", "Overwrite existing profile without prompting", false)
|
||||
.action(async (opts, command) => {
|
||||
const agent = resolveOptionFromCommand<string>(command, "agent");
|
||||
await runModelsCommand(async () => {
|
||||
await withModelsRuntime(async ({ defaultRuntime, resolveModelAgentOption }) => {
|
||||
const agent = resolveModelAgentOption(command);
|
||||
const { modelsAuthLoginCommand } = await import("../commands/models/auth.js");
|
||||
await modelsAuthLoginCommand(
|
||||
{
|
||||
@@ -433,9 +423,8 @@ export function registerModelsCli(program: Command) {
|
||||
.option("--agent <id>", "Agent id (default: configured default agent)")
|
||||
.option("--json", "Output JSON", false)
|
||||
.action(async (opts, command) => {
|
||||
const agent =
|
||||
resolveOptionFromCommand<string>(command, "agent") ?? (opts.agent as string | undefined);
|
||||
await runModelsCommand(async () => {
|
||||
await withModelsRuntime(async ({ defaultRuntime, resolveModelAgentOption }) => {
|
||||
const agent = resolveModelAgentOption(command, opts);
|
||||
const { modelsAuthOrderGetCommand } = await import("../commands/models/auth-order.js");
|
||||
await modelsAuthOrderGetCommand(
|
||||
{
|
||||
@@ -455,9 +444,8 @@ export function registerModelsCli(program: Command) {
|
||||
.option("--agent <id>", "Agent id (default: configured default agent)")
|
||||
.argument("<profileIds...>", "Auth profile ids (e.g. anthropic:default)")
|
||||
.action(async (profileIds: string[], opts, command) => {
|
||||
const agent =
|
||||
resolveOptionFromCommand<string>(command, "agent") ?? (opts.agent as string | undefined);
|
||||
await runModelsCommand(async () => {
|
||||
await withModelsRuntime(async ({ defaultRuntime, resolveModelAgentOption }) => {
|
||||
const agent = resolveModelAgentOption(command, opts);
|
||||
const { modelsAuthOrderSetCommand } = await import("../commands/models/auth-order.js");
|
||||
await modelsAuthOrderSetCommand(
|
||||
{
|
||||
@@ -476,9 +464,8 @@ export function registerModelsCli(program: Command) {
|
||||
.requiredOption("--provider <name>", "Provider id (e.g. anthropic)")
|
||||
.option("--agent <id>", "Agent id (default: configured default agent)")
|
||||
.action(async (opts, command) => {
|
||||
const agent =
|
||||
resolveOptionFromCommand<string>(command, "agent") ?? (opts.agent as string | undefined);
|
||||
await runModelsCommand(async () => {
|
||||
await withModelsRuntime(async ({ defaultRuntime, resolveModelAgentOption }) => {
|
||||
const agent = resolveModelAgentOption(command, opts);
|
||||
const { modelsAuthOrderClearCommand } = await import("../commands/models/auth-order.js");
|
||||
await modelsAuthOrderClearCommand(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user