From b902d86318e587d80c2734fe14637ce8c0f418a9 Mon Sep 17 00:00:00 2001 From: Edionwheels Date: Wed, 6 May 2026 16:24:56 +0800 Subject: [PATCH] fix(cli): pass instructions for local openai-codex model probes (#76470) * fix infer model run codex instructions * docs changelog for codex model probe fix * fix codex model probe instructions only * docs: note codex model probe instruction shim * chore: rerun proof gate --------- Co-authored-by: Le LI Co-authored-by: Peter Steinberger --- CHANGELOG.md | 1 + docs/cli/infer.md | 3 +- src/cli/capability-cli.test.ts | 87 ++++++++++++++++++++++++++++++++-- src/cli/capability-cli.ts | 14 +++++- 4 files changed, 99 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index feee97ab764..ffcac7995ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -112,6 +112,7 @@ Docs: https://docs.openclaw.ai - Google Meet/Voice Call: wait longer before playing PIN-derived Twilio DTMF for Meet dial-in prompts and retire stale delegated phone sessions instead of reusing completed calls. - Onboard/channels: recover externalized channel plugins from stale `channels.` config by falling back to `ensureChannelSetupPluginInstalled` via the trusted catalog when the plugin is missing on disk, so leftover `appId`/token entries no longer dead-end onboard with " plugin not available." (#78328) Thanks @sliverp. - Codex/app-server: forward the OpenClaw workspace bootstrap block through Codex `developerInstructions` instead of `config.instructions`, so persona/style guidance reaches the behavior-shaping app-server lane. Fixes #77363. Thanks @lonexreb. +- CLI/infer: pass minimal instructions to local `openai-codex/*` model probes and surface provider error details when `infer model run` returns no text. Fixes #76464. Thanks @lilesjtu. - Dependencies: override transitive `ip-address` to `10.2.0` so the runtime lockfile no longer includes the vulnerable `10.1.0` build flagged by Dependabot alert 109. Thanks @vincentkoc. - Feishu: hydrate missing native topic starter thread IDs before session routing so first turns and follow-ups stay in the same topic session. Fixes #78262. Thanks @joeyzenghuan. - LINE: reject `dmPolicy: "open"` configs without wildcard `allowFrom` so webhook DMs fail validation instead of being acknowledged and silently blocked before inbound processing. Fixes #78316. diff --git a/docs/cli/infer.md b/docs/cli/infer.md index 3c41b7ae068..a16fc1e7226 100644 --- a/docs/cli/infer.md +++ b/docs/cli/infer.md @@ -164,7 +164,8 @@ openclaw infer model run --local --model ollama/qwen2.5vl:7b --prompt "Describe 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. +- Local `model run` is the narrowest CLI smoke for provider/model/auth health because, for non-Codex providers, it sends only the supplied prompt to the selected model. +- `openai-codex/*` local probes are the narrow exception: OpenClaw adds a minimal system instruction so the Codex Responses transport can populate its required `instructions` field, without adding full agent context, tools, memory, or session transcript. - Local `model run --file` keeps that lean path and attaches image content directly to the single user message. Common image files such as PNG, JPEG, and WebP work when their MIME type is detected as `image/*`; unsupported or unrecognized files fail before the provider is called. - `model run --file` is best when you want to test the selected multimodal text model directly. Use `infer image describe` when you want OpenClaw's image-understanding provider selection and default image-model routing. - The selected model must support image input; text-only models may reject the request at the provider layer. diff --git a/src/cli/capability-cli.test.ts b/src/cli/capability-cli.test.ts index 6de317155fb..cb651bb51f1 100644 --- a/src/cli/capability-cli.test.ts +++ b/src/cli/capability-cli.test.ts @@ -406,16 +406,21 @@ describe("capability cli", () => { ); expect(mocks.completeWithPreparedSimpleCompletionModel).toHaveBeenCalledWith( expect.objectContaining({ - context: { + context: expect.objectContaining({ messages: [ expect.objectContaining({ role: "user", content: "hello", }), ], - }, + }), }), ); + const calls = mocks.completeWithPreparedSimpleCompletionModel.mock.calls as unknown as Array< + [{ context?: { systemPrompt?: string } }] + >; + const call = calls[0]?.[0]; + expect(call.context).not.toHaveProperty("systemPrompt"); }); it("passes image files to local model probes", async () => { @@ -438,7 +443,7 @@ describe("capability cli", () => { expect(mocks.completeWithPreparedSimpleCompletionModel).toHaveBeenCalledWith( expect.objectContaining({ - context: { + context: expect.objectContaining({ messages: [ expect.objectContaining({ role: "user", @@ -448,9 +453,14 @@ describe("capability cli", () => { ], }), ], - }, + }), }), ); + const calls = mocks.completeWithPreparedSimpleCompletionModel.mock.calls as unknown as Array< + [{ context?: { systemPrompt?: string } }] + >; + const call = calls[0]?.[0]; + expect(call.context).not.toHaveProperty("systemPrompt"); expect(mocks.runtime.writeJson).toHaveBeenCalledWith( expect.objectContaining({ inputs: [ @@ -463,6 +473,55 @@ describe("capability cli", () => { ); }); + it("adds minimal instructions only for openai-codex local model probes", async () => { + mocks.prepareSimpleCompletionModelForAgent.mockResolvedValueOnce({ + selection: { + provider: "openai-codex", + modelId: "gpt-5.5", + agentDir: "/tmp/agent", + }, + model: { + provider: "openai-codex", + id: "gpt-5.5", + api: "openai-codex-responses", + maxTokens: 128, + }, + auth: { + apiKey: "codex-app-server", + source: "codex-app-server", + mode: "token", + }, + } as never); + + await runRegisteredCli({ + register: registerCapabilityCli as (program: Command) => void, + argv: [ + "capability", + "model", + "run", + "--model", + "openai-codex/gpt-5.5", + "--prompt", + "hello", + "--json", + ], + }); + + expect(mocks.completeWithPreparedSimpleCompletionModel).toHaveBeenCalledWith( + expect.objectContaining({ + context: expect.objectContaining({ + systemPrompt: "You are a personal assistant running inside OpenClaw.", + messages: [ + expect.objectContaining({ + role: "user", + content: "hello", + }), + ], + }), + }), + ); + }); + it("passes image files to gateway model probes as attachments", async () => { const tempInput = path.join(os.tmpdir(), `openclaw-model-run-gateway-image-${Date.now()}.png`); await fs.writeFile(tempInput, Buffer.from(PNG_1X1_BASE64, "base64")); @@ -547,6 +606,26 @@ describe("capability cli", () => { expect(mocks.runtime.writeJson).not.toHaveBeenCalled(); }); + it("surfaces provider errors when local model probes return no text output", async () => { + mocks.completeWithPreparedSimpleCompletionModel.mockResolvedValueOnce({ + content: [], + stopReason: "error", + errorMessage: '{"detail":"Instructions are required"}', + } as never); + + await expect( + runRegisteredCli({ + register: registerCapabilityCli as (program: Command) => void, + argv: ["capability", "model", "run", "--prompt", "hello", "--json"], + }), + ).rejects.toThrow("exit 1"); + + expect(mocks.runtime.error).toHaveBeenCalledWith( + expect.stringContaining('{"detail":"Instructions are required"}'), + ); + expect(mocks.runtime.writeJson).not.toHaveBeenCalled(); + }); + it("rejects local Codex provider probes before simple-completion dispatch", async () => { mocks.prepareSimpleCompletionModelForAgent.mockResolvedValueOnce({ selection: { diff --git a/src/cli/capability-cli.ts b/src/cli/capability-cli.ts index 06bafa9742e..b6a7168937d 100644 --- a/src/cli/capability-cli.ts +++ b/src/cli/capability-cli.ts @@ -90,6 +90,7 @@ import { collectOption } from "./program/helpers.js"; type CapabilityTransport = "local" | "gateway"; const IMAGE_OUTPUT_FORMATS = ["png", "jpeg", "webp"] as const; const IMAGE_BACKGROUNDS = ["transparent", "opaque", "auto"] as const; +const LOCAL_MODEL_RUN_SYSTEM_PROMPT = "You are a personal assistant running inside OpenClaw."; type CapabilityMetadata = { id: string; @@ -659,11 +660,17 @@ async function runModelRun(params: { 'The codex provider is served by the Codex app-server agent runtime, not the local simple-completion transport. Use an openai/ ref with agents.defaults.agentRuntime.id: "codex", run through the gateway, or use /codex commands.', ); } + const localModelRunSystemPrompt = + prepared.selection.provider === "openai-codex" || + prepared.model.api === "openai-codex-responses" + ? LOCAL_MODEL_RUN_SYSTEM_PROMPT + : undefined; const result = await completeWithPreparedSimpleCompletionModel({ model: prepared.model, auth: prepared.auth, cfg, context: { + ...(localModelRunSystemPrompt ? { systemPrompt: localModelRunSystemPrompt } : {}), messages: [ { role: "user", @@ -681,8 +688,13 @@ async function runModelRun(params: { }); const text = collectModelRunText(result.content); if (!text) { + const providerErrorMessage = (result as { errorMessage?: unknown }).errorMessage; + const detail = + typeof providerErrorMessage === "string" && providerErrorMessage.trim() + ? `: ${providerErrorMessage.trim()}` + : ""; throw new Error( - `No text output returned for provider "${prepared.selection.provider}" model "${prepared.selection.modelId}".`, + `No text output returned for provider "${prepared.selection.provider}" model "${prepared.selection.modelId}"${detail}.`, ); } return {