diff --git a/CHANGELOG.md b/CHANGELOG.md index c4242fd7826..16ce400c773 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Auto-reply: honor explicit `silentReply.direct: "allow"` for clean empty or reasoning-only direct chat turns while keeping the default direct-chat empty-response guard conservative. Fixes #74409. Thanks @jesuskannolis. +- OpenAI Codex: send a non-empty Responses input item when a Codex turn only has systemPrompt-backed instructions, avoiding ChatGPT backend 400s from `input: []`. Fixes #73820. Thanks @woodhouse-bot. - Ollama: normalize provider-prefixed tool-call names at the native stream boundary so Kimi/Ollama calls such as `functions.exec` dispatch as `exec` instead of missing configured tools. Fixes #74487. Thanks @afurm and @carreipeia. - Security/audit: resolve configured model aliases before model-tier and small-parameter checks, so alias-based GPT-5/Codex configs no longer report false weak-model warnings. Fixes #74455. Thanks @blaspat. - CLI/agent: isolate Gateway-timeout embedded fallback runs under explicit `gateway-fallback-*` sessions so accepted Gateway runs cannot race transcript locks or replace the routed conversation session. Fixes #62981. Thanks @HemantSudarshan. diff --git a/src/agents/openai-transport-stream.test.ts b/src/agents/openai-transport-stream.test.ts index 13bf1a7e7f3..d3f377310cf 100644 --- a/src/agents/openai-transport-stream.test.ts +++ b/src/agents/openai-transport-stream.test.ts @@ -1031,6 +1031,40 @@ describe("openai transport stream", () => { expect(params.service_tier).toBe("auto"); }); + it("adds minimal user input for Codex responses when only the system prompt is present", () => { + const params = buildOpenAIResponsesParams( + { + id: "gpt-5.4", + name: "GPT-5.4", + api: "openai-codex-responses", + provider: "openai-codex", + baseUrl: "https://chatgpt.com/backend-api", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 200000, + maxTokens: 8192, + } satisfies Model<"openai-codex-responses">, + { + systemPrompt: `Stable prefix${SYSTEM_PROMPT_CACHE_BOUNDARY}Dynamic suffix`, + messages: [], + tools: [], + } as never, + undefined, + ) as { + input?: Array<{ role?: string; content?: Array<{ type?: string; text?: string }> }>; + instructions?: string; + }; + + expect(params.instructions).toBe("Stable prefix\nDynamic suffix"); + expect(params.input).toEqual([ + { + role: "user", + content: [{ type: "input_text", text: " " }], + }, + ]); + }); + it("does not infer high reasoning when Pi passes thinking off", () => { const params = buildOpenAIResponsesParams( { diff --git a/src/agents/openai-transport-stream.ts b/src/agents/openai-transport-stream.ts index 805c31a5a93..7214f9564b5 100644 --- a/src/agents/openai-transport-stream.ts +++ b/src/agents/openai-transport-stream.ts @@ -53,6 +53,7 @@ import { transformTransportMessages } from "./transport-message-transform.js"; import { mergeTransportMetadata, sanitizeTransportPayloadText } from "./transport-stream-shared.js"; const DEFAULT_AZURE_OPENAI_API_VERSION = "2024-12-01-preview"; +const OPENAI_CODEX_RESPONSES_EMPTY_INPUT_TEXT = " "; const log = createSubsystemLogger("openai-transport"); type BaseStreamOptions = { @@ -876,6 +877,22 @@ function buildOpenAICodexResponsesInstructions(context: Context): string | undef return sanitizeTransportPayloadText(stripSystemPromptCacheBoundary(context.systemPrompt)); } +function ensureOpenAICodexResponsesInput(messages: ResponseInput, context: Context): void { + if (messages.length > 0 || !context.systemPrompt) { + return; + } + const text = buildOpenAICodexResponsesInstructions(context); + if (!text) { + throw new Error( + "OpenAI Codex Responses requires non-empty input when only systemPrompt is provided.", + ); + } + messages.push({ + role: "user", + content: [{ type: "input_text", text: OPENAI_CODEX_RESPONSES_EMPTY_INPUT_TEXT }], + }); +} + export function buildOpenAIResponsesParams( model: Model, context: Context, @@ -892,6 +909,9 @@ export function buildOpenAIResponsesParams( new Set(["openai", "openai-codex", "opencode", "azure-openai-responses"]), { includeSystemPrompt: !isCodexResponses, supportsDeveloperRole }, ); + if (isCodexResponses) { + ensureOpenAICodexResponsesInput(messages, context); + } const cacheRetention = resolveCacheRetention(options?.cacheRetention); const payloadPolicy = resolveOpenAIResponsesPayloadPolicy(model, { storeMode: "disable",