diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fca76efcc1..2a55b7342bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Cron/Telegram: preserve explicit `:topic:` delivery targets over stale session-derived thread IDs when isolated cron announces to Telegram forum topics. Carries forward #59069; refs #49704 and #43808. Thanks @roytong9. +- CLI/model probes: reject empty or whitespace-only `infer model run --prompt` values before calling local providers or the Gateway, so smoke checks do not spend provider calls on invalid turns. Fixes #73185. Thanks @iot2edge. - Gateway/media: route text-only `chat.send` image offloads through media-understanding fields so `agents.defaults.imageModel` can describe WebChat attachments instead of leaving only an opaque `media://inbound` marker. Fixes #72968. Thanks @vorajeeah. - CLI/onboarding: infer image input for common custom-provider vision model IDs, ask only for unknown models, and keep `--custom-image-input`/`--custom-text-input` overrides so vision-capable proxies do not get saved as text-only configs. Fixes #51869. Thanks @Antsoldier1974. - Models/OpenAI Codex: stop listing or resolving unsupported `openai-codex/gpt-5.4-mini` rows through Codex OAuth, keep stale discovery rows suppressed with a clear API-key-route hint, and leave direct `openai/gpt-5.4-mini` available. Fixes #73242. Thanks @0xCyda. diff --git a/docs/cli/infer.md b/docs/cli/infer.md index 9203bad6b64..722f9a7fd35 100644 --- a/docs/cli/infer.md +++ b/docs/cli/infer.md @@ -159,6 +159,7 @@ openclaw infer model run --local --model openai/gpt-4.1 --prompt "Reply with exa Notes: - Local `model run` is the narrowest CLI smoke for provider/model/auth health because it sends only the supplied prompt to the selected model. +- `model run --prompt` must contain non-whitespace text; empty prompts are rejected before local providers or the Gateway are called. - Local `model run` exits non-zero when the provider returns no text output, so unreachable local providers and empty completions do not look like successful probes. - Use `model run --gateway` when you need to test Gateway routing, agent-runtime setup, or Gateway-managed provider state instead of the lean local completion path. - `model auth login`, `model auth logout`, and `model auth status` manage saved provider auth state. diff --git a/src/cli/capability-cli.test.ts b/src/cli/capability-cli.test.ts index 09510657a6d..e473a972a9a 100644 --- a/src/cli/capability-cli.test.ts +++ b/src/cli/capability-cli.test.ts @@ -437,6 +437,26 @@ describe("capability cli", () => { expect(mocks.runtime.writeJson).not.toHaveBeenCalled(); }); + it.each(["", " ", "\n\t"])( + "rejects empty model run prompts before local dispatch (%j)", + async (prompt) => { + await expect( + runRegisteredCli({ + register: registerCapabilityCli as (program: Command) => void, + argv: ["capability", "model", "run", "--prompt", prompt, "--json"], + }), + ).rejects.toThrow("exit 1"); + + expect(mocks.runtime.error).toHaveBeenCalledWith( + expect.stringContaining("--prompt cannot be empty or whitespace-only."), + ); + expect(mocks.prepareSimpleCompletionModelForAgent).not.toHaveBeenCalled(); + expect(mocks.completeWithPreparedSimpleCompletionModel).not.toHaveBeenCalled(); + expect(mocks.callGateway).not.toHaveBeenCalled(); + expect(mocks.runtime.writeJson).not.toHaveBeenCalled(); + }, + ); + it("runs gateway model probes without chat-agent prompt policy or tools", async () => { await runRegisteredCli({ register: registerCapabilityCli as (program: Command) => void, @@ -455,6 +475,21 @@ describe("capability cli", () => { ); }); + it("rejects empty model run prompts before gateway dispatch", async () => { + await expect( + runRegisteredCli({ + register: registerCapabilityCli as (program: Command) => void, + argv: ["capability", "model", "run", "--prompt", " ", "--gateway", "--json"], + }), + ).rejects.toThrow("exit 1"); + + expect(mocks.runtime.error).toHaveBeenCalledWith( + expect.stringContaining("--prompt cannot be empty or whitespace-only."), + ); + expect(mocks.callGateway).not.toHaveBeenCalled(); + expect(mocks.runtime.writeJson).not.toHaveBeenCalled(); + }); + it("defaults tts status to gateway transport", async () => { await runRegisteredCli({ register: registerCapabilityCli as (program: Command) => void, diff --git a/src/cli/capability-cli.ts b/src/cli/capability-cli.ts index a79ad3164d8..ecbb033e3b4 100644 --- a/src/cli/capability-cli.ts +++ b/src/cli/capability-cli.ts @@ -577,6 +577,13 @@ function collectModelRunText(content: Array<{ type: string; text?: string }>): s .trim(); } +function requireModelRunPrompt(value: unknown): string { + if (typeof value !== "string" || normalizeOptionalString(value) === undefined) { + throw new Error("--prompt cannot be empty or whitespace-only."); + } + return value; +} + async function runModelRun(params: { prompt: string; model?: string; @@ -1487,6 +1494,7 @@ export function registerCapabilityCli(program: Command) { .option("--json", "Output JSON", false) .action(async (opts) => { await runCommandWithRuntime(defaultRuntime, async () => { + const prompt = requireModelRunPrompt(opts.prompt); const transport = resolveTransport({ local: Boolean(opts.local), gateway: Boolean(opts.gateway), @@ -1494,7 +1502,7 @@ export function registerCapabilityCli(program: Command) { defaultTransport: "local", }); const result = await runModelRun({ - prompt: String(opts.prompt), + prompt, model: opts.model as string | undefined, transport, });