fix(cli): reject missing repeatable inference inputs (#130608)

This commit is contained in:
Peter Steinberger
2026-08-26 19:55:41 -07:00
committed by GitHub
parent 59fc8fc03f
commit d3d43eb6cf
4 changed files with 41 additions and 6 deletions
+1
View File
@@ -101,6 +101,7 @@ A good infer-based skill maps common user intents to the right subcommand, inclu
- Use `--json` when the output feeds another command or script; text output otherwise.
- Use `--provider` or `--model provider/model` to pin a specific backend.
- `image edit` and `image describe-many` require at least one `--file`; `embedding create` requires at least one `--text`. Repeat the flag for multiple inputs. Omitting it is a usage error, not an empty successful result, and no inference request is sent.
- Use `model run --thinking <level>` for a one-shot thinking/reasoning override: `off`, `minimal`, `low`, `medium`, `high`, `adaptive`, `xhigh`, or `max`.
- For `image describe`, `audio transcribe`, and `video describe`, `--model` must use the form `<provider/model>`.
- For `image describe`, `--file` accepts local paths and HTTP(S) URLs; remote URLs go through the normal media-fetch SSRF policy.
+37 -3
View File
@@ -568,6 +568,32 @@ vi.mock("../plugins/web-search-providers.runtime.js", () => ({
}));
describe("capability cli", () => {
it.each(
[
{ args: ["image", "edit", "--prompt", "crop the image"], option: "--file <path>" },
{ args: ["image", "describe-many"], option: "--file <path>" },
{ args: ["embedding", "create"], option: "--text <text>" },
].flatMap(({ args, option }) =>
["infer", "capability"].map((root) => ({ args, option, root })),
),
)("rejects missing required repeatable input for $root $args", async ({ root, args, option }) => {
const argv = [root, ...args, "--json"];
const program = new Command().exitOverride().configureOutput({ writeErr: () => {} });
await registerCapabilityCli(program, ["node", "openclaw", ...argv]);
await expect(
program.parseAsync(argv, { from: "user" }).then(() => undefined),
).rejects.toMatchObject({
code: "commander.missingMandatoryOptionValue",
message: `error: required option '${option}' not specified`,
});
expect(mocks.resolveCommandConfigWithSecrets).not.toHaveBeenCalled();
expect(mocks.generateImage).not.toHaveBeenCalled();
expect(mocks.describeImageFile).not.toHaveBeenCalled();
expect(mocks.createEmbeddingProvider).not.toHaveBeenCalled();
expect(mocks.runtime.writeJson).not.toHaveBeenCalled();
});
afterEach(() => {
vi.unstubAllGlobals();
vi.unstubAllEnvs();
@@ -2417,6 +2443,8 @@ describe("capability cli", () => {
"edit",
"--file",
inputPath,
"--file",
inputPath,
"--prompt",
"make three variants",
"--count",
@@ -2425,6 +2453,7 @@ describe("capability cli", () => {
);
expect(firstImageGenerationCall()?.count).toBe(3);
expect(firstImageGenerationCall()?.inputImages).toHaveLength(2);
});
it("rejects unsupported image output format and background hints", async () => {
@@ -3678,15 +3707,20 @@ describe("capability cli", () => {
);
it("uses only embedding providers for embedding creation", async () => {
await runCapability("embedding", "create", "--text", "hello", "--json");
await runCapability("embedding", "create", "--text", "hello", "--text", "world", "--json");
expect(firstEmbeddingProviderCall()?.provider).toBe("auto");
expect(firstEmbeddingProviderCall()?.fallback).toBe("none");
expect(firstJsonOutput()?.capability).toBe("embedding.create");
expect(firstJsonOutput()?.provider).toBe("openai");
expect(firstJsonOutput()?.model).toBe("text-embedding-3-small");
expect(firstJsonOutput()).toMatchObject({ outputs: [{ embedding: [0.1, 0.2] }] });
expect(mocks.embedBatch).toHaveBeenCalledWith(["hello"], { inputType: "document" });
expect(firstJsonOutput()).toMatchObject({
outputs: [
{ text: "hello", embedding: [0.1, 0.2] },
{ text: "world", embedding: [0.1, 0.2] },
],
});
expect(mocks.embedBatch).toHaveBeenCalledWith(["hello", "world"], { inputType: "document" });
expect(closeEmbeddingProviderMock).toHaveBeenCalledTimes(1);
});
+1 -1
View File
@@ -111,7 +111,7 @@ export function registerEmbeddingCapabilityCommands(capability: Command): void {
embedding
.command("create")
.description("Create embeddings")
.requiredOption("--text <text>", "Input text", collectOption, [])
.requiredOption("--text <text>", "Input text", collectOption)
.option("--provider <id>", "Provider id")
.option("--model <provider/model>", "Model override")
.option(
+2 -2
View File
@@ -366,7 +366,7 @@ export function registerImageCapabilityCommands(capability: Command): void {
image
.command("edit")
.description("Edit images with one or more input files")
.requiredOption("--file <path>", "Input file", collectOption, [])
.requiredOption("--file <path>", "Input file", collectOption)
.requiredOption("--prompt <text>", "Prompt text"),
).action(async (opts, command) => {
await runCommandWithRuntime(defaultRuntime, async () => {
@@ -410,7 +410,7 @@ export function registerImageCapabilityCommands(capability: Command): void {
image
.command("describe-many")
.description("Describe multiple image files")
.requiredOption("--file <path>", "Image file", collectOption, [])
.requiredOption("--file <path>", "Image file", collectOption)
.option("--prompt <text>", "Prompt hint")
.option("--model <provider/model>", "Model override")
.option("--timeout-ms <ms>", "Provider request timeout in milliseconds")