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 <leli@LedeMacBook-Air.local>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Edionwheels
2026-05-06 16:24:56 +08:00
committed by GitHub
parent 3e04755874
commit b902d86318
4 changed files with 99 additions and 6 deletions

View File

@@ -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.<id>` 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 "<channel> 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.

View File

@@ -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.

View File

@@ -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: {

View File

@@ -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/<model> 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 {