fix(gateway): accept file-only input on /v1/responses (parity with image-only)#93011
Conversation
|
Codex review: needs maintainer review before merge. Reviewed June 14, 2026, 12:00 PM ET / 16:00 UTC. Summary PR surface: Source +17, Tests +172. Total +189 across 4 files. Reproducibility: yes. Source inspection shows file-only input_file content is extracted into extraSystemPrompt, but current main leaves prompt.message empty and the /v1/responses guard returns 400 before the agent runs. Review metrics: none identified. Stored data model Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Next step before merge
Security Review detailsBest possible solution: Land the prompt-builder placeholder extension with its focused Responses regression tests after normal exact-head merge gates pass. Do we have a high-confidence way to reproduce the issue? Yes. Source inspection shows file-only input_file content is extracted into extraSystemPrompt, but current main leaves prompt.message empty and the /v1/responses guard returns 400 before the agent runs. Is this the best way to solve the issue? Yes. Extending the existing active-turn placeholder pattern from the merged image-only fix is the narrow prompt-boundary solution and preserves the empty-input 400 behavior. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 2e240e772b83. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +17, Tests +172. Total +189 across 4 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
|
Maintainer follow-up pushed in Verification:
Known gap: live external file ingestion was not exercised; focused Gateway server behavior and CI routing are covered. |
…age-only) (openclaw#93011) * fix(gateway): accept file-only input on /v1/responses (parity with image-only) * fix(gateway): preserve file-only model context --------- Co-authored-by: Vincent Koc <[email protected]>
…age-only) (openclaw#93011) * fix(gateway): accept file-only input on /v1/responses (parity with image-only) * fix(gateway): preserve file-only model context --------- Co-authored-by: Vincent Koc <[email protected]>
…age-only) (openclaw#93011) * fix(gateway): accept file-only input on /v1/responses (parity with image-only) * fix(gateway): preserve file-only model context --------- Co-authored-by: Vincent Koc <[email protected]>
…age-only) (openclaw#93011) * fix(gateway): accept file-only input on /v1/responses (parity with image-only) * fix(gateway): preserve file-only model context --------- Co-authored-by: Vincent Koc <[email protected]>
Summary
A
/v1/responsesturn whose only content is aninput_file(a document or PDF upload, with the instruction carried ininstructionsor with no text at all) is rejected with400 Missing user message in input., even though the file is fully parsed and folded into the agent's system prompt. The byte-equivalent image-only turn returns200.This is the file-only sibling that the image-only fix (#92488) left open: that change taught the OpenResponses prompt builder to substitute a placeholder for an image-only active user turn, but not for a file-only one.
Root cause
The OpenResponses prompt builder only knows about text and images:
For a file-only turn
extractTextContentreturns""andhasImageContentisfalse, soprompt.messagecomes back empty. The handler guard then rejects it:The file is already extracted into
fileContextsand merged intoextraSystemPrompt(together withinstructions) before this guard runs, so the content exists, the turn just never reaches the agent. Theinstructionsfield cannot satisfy the guard because it lands in the system prompt, not inprompt.message.This mirrors the image-only bug exactly: chat completions guards
!prompt.message && images.length === 0, while responses guards!prompt.messagealone and relies on the prompt builder to supply a placeholder.Fix
Teach the prompt builder about file-only turns, the same way it already handles image-only turns:
An active file-only user turn now gets a non-empty placeholder, so it clears the
!prompt.messageguard and runs with the extracted file content attached viaextraSystemPrompt(and any rendered images viaimages). The empty-input case (no text, no image, no file) still returns400.Why not one-sided
buildAgentPromptis the single chokepoint for the responses prompt, so both the streaming and non-streaming/v1/responsespaths are covered by this one change (they shareconst prompt = buildAgentPrompt(payload.input)and the same!prompt.messageguard).extraSystemPrompt) and a PDF that renders to images (content goes toimagesplus a context marker). Both arrive as aninput_filepart, sohasFileContentmatches regardless of how the file extracts. Both are exercised by the regression tests below.input_filehandling insrc/gateway/openai-http.ts), so there is no sibling endpoint to change. The new placeholder is therefore responses-local, unlikeIMAGE_ONLY_USER_MESSAGEwhich stays shared because both endpoints use it.Verification
node scripts/run-vitest.mjs src/gateway/openresponses-parity.test.ts-> file-only placeholder, file-with-text, and empty-input unit cases pass.node scripts/run-vitest.mjs src/gateway/openresponses-http.test.ts-> 87 passed, including the new file-only document e2e case.node scripts/run-vitest.mjs src/gateway/openresponses-http.file-only.test.ts-> file-only PDF-rendered-to-images turn returns 200 with images forwarded.npx oxfmt --check+node scripts/run-oxlint.mjs --tsconfig config/tsconfig/oxlint.core.jsonon the changed files -> clean.pnpm tsgo:core+pnpm tsgo:core:test-> clean.Real behavior proof
Behavior addressed: a
/v1/responsesturn whose onlyinputcontent is aninput_filedocument (instruction supplied viainstructions) now returns 200 and reaches the agent with the file content attached, instead of being rejected with400 Missing user message in input..Real environment tested: the real gateway
/v1/responsesHTTP handler running on a local server with OpenResponses enabled, driven over HTTP withfetch; the same file-only document request was sent against the pre-patch build and the patched build (the agent runtime call was stubbed at its boundary so the request resolves without a live model, exactly as in the image-only path).Exact steps or command run after this patch: start the gateway HTTP server locally with OpenResponses enabled, then
POST /v1/responseswithinstructions: "Summarize the attached document."and a singleinput_file(base64text/plain,filename: doc.txt) as the only content of the user message; capture the HTTP status, the response body, and whether the agent command was invoked, before and after the patch.Evidence after fix:
Observed result after fix: the identical file-only document request that previously returned
400 Missing user message in input.and never invoked the agent now returns200, invokes the agent with a non-empty message, and forwards the extracted file content in the system prompt.What was not tested: a live end-to-end run against a real upstream model provider (the agent runtime was stubbed at its boundary to keep the proof hermetic); the PDF-renders-to-images path is covered by
openresponses-http.file-only.test.tswith the file-render boundary stubbed rather than a real scanned PDF.Sibling of #92488 (image-only
/v1/responsesfix).