Skip to content

fix(gateway): accept file-only input on /v1/responses (parity with image-only)#93011

Merged
vincentkoc merged 2 commits into
openclaw:mainfrom
yetval:fix/responses-file-only-input
Jun 15, 2026
Merged

fix(gateway): accept file-only input on /v1/responses (parity with image-only)#93011
vincentkoc merged 2 commits into
openclaw:mainfrom
yetval:fix/responses-file-only-input

Conversation

@yetval

@yetval yetval commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Summary

A /v1/responses turn whose only content is an input_file (a document or PDF upload, with the instruction carried in instructions or with no text at all) is rejected with 400 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 returns 200.

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:

// src/gateway/openresponses-prompt.ts (before)
function hasImageContent(content) {
  return typeof content !== "string" && content.some((p) => p.type === "input_image");
}
const body =
  content ||
  (item.role === "user" && i === activeUserMessageIndex && hasImageContent(item.content)
    ? IMAGE_ONLY_USER_MESSAGE
    : "");

For a file-only turn extractTextContent returns "" and hasImageContent is false, so prompt.message comes back empty. The handler guard then rejects it:

// src/gateway/openresponses-http.ts
if (!prompt.message) {
  sendJson(res, 400, { error: { message: "Missing user message in `input`.", ... } });
  return true;
}

The file is already extracted into fileContexts and merged into extraSystemPrompt (together with instructions) before this guard runs, so the content exists, the turn just never reaches the agent. The instructions field cannot satisfy the guard because it lands in the system prompt, not in prompt.message.

This mirrors the image-only bug exactly: chat completions guards !prompt.message && images.length === 0, while responses guards !prompt.message alone 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:

function hasFileContent(content) {
  return typeof content !== "string" && content.some((p) => p.type === "input_file");
}

function placeholderForActiveTurn(content) {
  if (hasImageContent(content)) return IMAGE_ONLY_USER_MESSAGE;
  if (hasFileContent(content)) return FILE_ONLY_USER_MESSAGE;
  return "";
}

const body =
  content ||
  (item.role === "user" && i === activeUserMessageIndex
    ? placeholderForActiveTurn(item.content)
    : "");

An active file-only user turn now gets a non-empty placeholder, so it clears the !prompt.message guard and runs with the extracted file content attached via extraSystemPrompt (and any rendered images via images). The empty-input case (no text, no image, no file) still returns 400.

Why not one-sided

  • buildAgentPrompt is the single chokepoint for the responses prompt, so both the streaming and non-streaming /v1/responses paths are covered by this one change (they share const prompt = buildAgentPrompt(payload.input) and the same !prompt.message guard).
  • It covers both file extraction outcomes: a text-extractable document (content goes to extraSystemPrompt) and a PDF that renders to images (content goes to images plus a context marker). Both arrive as an input_file part, so hasFileContent matches regardless of how the file extracts. Both are exercised by the regression tests below.
  • Chat completions does not extract files at all (no input_file handling in src/gateway/openai-http.ts), so there is no sibling endpoint to change. The new placeholder is therefore responses-local, unlike IMAGE_ONLY_USER_MESSAGE which 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.json on the changed files -> clean.
  • pnpm tsgo:core + pnpm tsgo:core:test -> clean.

Real behavior proof

Behavior addressed: a /v1/responses turn whose only input content is an input_file document (instruction supplied via instructions) now returns 200 and reaches the agent with the file content attached, instead of being rejected with 400 Missing user message in input..

Real environment tested: the real gateway /v1/responses HTTP handler running on a local server with OpenResponses enabled, driven over HTTP with fetch; 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/responses with instructions: "Summarize the attached document." and a single input_file (base64 text/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:

# BEFORE (origin/main, file-only document turn)
POST /v1/responses  ->  400
{"error":{"message":"Missing user message in `input`.","type":"invalid_request_error"}}
agent invoked:                 false
agent message:                 null
file content in system prompt: false

# AFTER (this patch, identical request)
POST /v1/responses  ->  200
{"id":"resp_117b83dd-acef-4630-8ac2-65a20d7e5786","object":"response","created_a...
agent invoked:                 true
agent message:                 "User sent file(s) with no text."
file content in system prompt: true

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 returns 200, 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.ts with the file-render boundary stubbed rather than a real scanned PDF.

Sibling of #92488 (image-only /v1/responses fix).

@openclaw-barnacle openclaw-barnacle Bot added gateway Gateway runtime size: M proof: supplied External PR includes structured after-fix real behavior proof. labels Jun 14, 2026
@clawsweeper

clawsweeper Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed June 14, 2026, 12:00 PM ET / 16:00 UTC.

Summary
The PR teaches the OpenResponses prompt builder to emit a non-empty placeholder for active file-only user turns and adds gateway regression coverage for text-extracted and image-rendered file inputs.

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
Persistent data-model change detected: serialized state: src/gateway/openresponses-http.file-only.test.ts. Confirm migration or upgrade compatibility proof before merge.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🐚 platinum hermit
Patch quality: 🐚 platinum hermit
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • none.

Next step before merge

  • No automated repair is needed; the PR is a focused implementation with no narrow defect found in this review.

Security
Cleared: The diff only changes gateway prompt logic and tests; it does not alter dependencies, workflows, permissions, secrets, package resolution, or code-execution surfaces.

Review details

Best 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 changes

Label changes:

  • add P2: The PR addresses a normal gateway API bug with limited blast radius in /v1/responses file-only inputs.
  • add proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes before/after local gateway HTTP output showing the same file-only request changing from 400 to 200 and reaching the agent boundary.
  • add rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🐚 platinum hermit and patch quality is 🐚 platinum hermit.
  • add status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (live_output): The PR body includes before/after local gateway HTTP output showing the same file-only request changing from 400 to 200 and reaching the agent boundary.

Label justifications:

  • P2: The PR addresses a normal gateway API bug with limited blast radius in /v1/responses file-only inputs.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🐚 platinum hermit and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (live_output): The PR body includes before/after local gateway HTTP output showing the same file-only request changing from 400 to 200 and reaching the agent boundary.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes before/after local gateway HTTP output showing the same file-only request changing from 400 to 200 and reaching the agent boundary.
Evidence reviewed

PR surface:

Source +17, Tests +172. Total +189 across 4 files.

View PR surface stats
Area Files Added Removed Net
Source 1 23 6 +17
Tests 3 173 1 +172
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 4 196 7 +189

What I checked:

  • Current main prompt gap: Current main extracts only text/image placeholder content in buildAgentPrompt, so an active user message containing only input_file content produces an empty prompt message. (src/gateway/openresponses-prompt.ts:57, 2e240e772b83)
  • Current main HTTP guard: The OpenResponses handler extracts files before building the prompt, merges fileContexts into extraSystemPrompt, and then returns 400 when prompt.message is empty. (src/gateway/openresponses-http.ts:685, 2e240e772b83)
  • File extraction contract: input_file sources are decoded, MIME-validated, and returned as text and/or rendered images before the Responses prompt guard runs. (src/media/input-files.ts:384, 2e240e772b83)
  • Downstream non-empty message requirement: Agent command preparation still rejects an empty message, so a guard-only relaxation would not be a sufficient fix. (src/agents/agent-command.ts:611, 2e240e772b83)
  • Sibling endpoint behavior: Chat Completions already scopes a media-only placeholder to the active user turn, which supports using the same prompt-boundary shape for Responses file-only input. (src/gateway/openai-http.ts:670, 2e240e772b83)
  • PR implementation and tests: The diff adds a file-only placeholder branch in openresponses-prompt.ts and regression tests for prompt-builder, text-extracted file, and PDF-rendered-to-images paths. (src/gateway/openresponses-prompt.ts:6, f62b9c0656b4)

Likely related people:

  • s554097550: Authored the merged image-only /v1/responses placeholder fix that this PR extends to file-only input. (role: recent feature contributor; confidence: high; commits: b72634f56de8, fea5e51de020; files: src/gateway/openresponses-prompt.ts, src/gateway/openresponses-http.ts)
  • vincentkoc: Merged the image-only Responses fix and appears repeatedly in live history for OpenAI-compatible gateway/media paths. (role: merger and recent adjacent gateway contributor; confidence: high; commits: b72634f56de8, ff334600d547; files: src/gateway/openresponses-prompt.ts, src/gateway/openresponses-http.ts, src/media/input-files.ts)
  • steipete: Live history shows repeated OpenResponses, gateway, and media-helper work, including foundational OpenResponses phase support and recent gateway documentation/refactors. (role: foundational and recent gateway contributor; confidence: high; commits: afca9540bf82, f1bdc91b64d4, 3ad7049cba9d; files: src/gateway/openresponses-http.ts, src/gateway/openresponses-prompt.ts, src/media/input-files.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

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
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. P2 Normal backlog priority with limited blast radius. labels Jun 14, 2026
@vincentkoc vincentkoc self-assigned this Jun 14, 2026
@openclaw-barnacle openclaw-barnacle Bot removed the proof: sufficient ClawSweeper judged the real behavior proof convincing. label Jun 14, 2026
@vincentkoc

Copy link
Copy Markdown
Member

Maintainer follow-up pushed in 92a5df0d11: empty extracted files now remain model-visible, and the real Gateway HTTP test is routed through the serialized server lane.

Verification:

  • node scripts/run-vitest.mjs src/gateway/openresponses-http.server.file-only.test.ts test/scripts/ci-node-test-plan.test.ts
  • fresh autoreview: clean, no accepted/actionable findings

Known gap: live external file ingestion was not exercised; focused Gateway server behavior and CI routing are covered.

@vincentkoc
vincentkoc merged commit 02acb0b into openclaw:main Jun 15, 2026
163 of 164 checks passed
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jun 15, 2026
…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]>
wangmiao0668000666 pushed a commit to wangmiao0668000666/openclaw that referenced this pull request Jun 17, 2026
…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]>
crh-code pushed a commit to crh-code/openclaw that referenced this pull request Jun 18, 2026
…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]>
badgerbees pushed a commit to badgerbees/openclaw that referenced this pull request Jul 8, 2026
…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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gateway Gateway runtime P2 Normal backlog priority with limited blast radius. proof: supplied External PR includes structured after-fix real behavior proof. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. size: M status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants