anomalyco / anomalyco/opencode
[Bug] 422 "messages.N.user.content.str: Input should be a valid string" when replaying a session containing a PDF tool result on OpenAI-compatible providers (opencode-go / zen gateway)
@neriousy is already working on this.
Since Sep 13, 2026.
- Dominant language
- TypeScript
- Stars
- 209k
- Forks
- 27.5k
- PR merge metrics
- PR metrics pending
Description
Description
Description
When a session contains a tool result from the read tool executed on a PDF file, replaying that session against an OpenAI-compatible provider (in my case opencode-go / GLM-5.3-Flash via the zen/go gateway) fails deterministically with a 422 validation error from the gateway. The session becomes permanently stuck: every subsequent message fails until the PDF attachment is manually removed from the local database.
Environment
- opencode version: 1.18.22
- Provider:
opencode-go(@ai-sdk/openai-compatible, APIhttps://opencode.ai/zen/go/v1) - Model:
glm-5.3-flash - OS: Windows 10
Steps to reproduce
- Start a session with a model served through the
opencode-goprovider. - Have the agent run the
readtool on a PDF file (e.g.read C:\...\report.pdf). The tool stores its result withstate.attachments = [{ type: "file", mime: "application/pdf", url: "data:application/pdf;base64,..." }]. - Send any follow-up message so the full history is replayed to the provider.
Actual result
The request fails with HTTP 422 and the session is bricked (retries produce the same error):
APIError: Error from provider (Console Go): Upstream request failed: invalid_request_error Input should be a valid string
statusCode: 422
responseBody: {"error":{"param":"messages.297.user.content.str","type":"invalid_request_error","message":"Error from provider (Console Go): Upstream request failed: invalid_request_error Input should be a valid string"}}
url: https://opencode.ai/zen/go/v1/chat/completions
messages.297 corresponds exactly to the synthetic user message described below. The gateway only accepts content: string for user role messages, but the history contains a user message whose content is an array of blocks including a file block with the base64 PDF.
Root cause analysis (verified against source)
The chain is:
packages/opencode/src/tool/read.ts:306-324—readreturns PDFs as base64 attachments ("PDF read successfully"+state.attachments).packages/opencode/src/session/message-v2.ts:147-159—supportsMediaInToolResult()returnsfalsefor npm@ai-sdk/openai-compatible, so the PDF is extracted from the tool result (lines 296-313) and re-injected as a synthetic user message containing[text part, file part](lines 380-399).convertToModelMessages()produces a userModelMessagewhosecontentis an array of blocks.packages/opencode/src/provider/transform.ts:409-445—unsupportedParts()does not save the case:- The model declares
pdfin its input modalities on models.dev (verified:glm-5.3-flash→modalities.input: ["text","image","video","pdf"]), so thefileblock passes through untouched. - Even when a block is degraded to text, the message
contentremains an array;@ai-sdk/openai-compatibleonly collapses arrays to a plain string when there is exactly onetextblock. The synthetic message always has ≥2 parts.
- The model declares
So the extraction workaround assumes the provider accepts media in user messages as content arrays — true for Anthropic/Google/xAI SDKs, but not for strict OpenAI-compatible gateways like zen/go that only accept content: string on user messages. Result: any session with a prior PDF read fails forever on these providers, regardless of the model's declared capabilities.
Note: the experimental native runtime path fails earlier and client-side with "OpenAI Chat does not support media type application/pdf" (packages/llm/src/protocols/openai-chat.ts + shared.ts, IMAGE_MIMES doesn't include PDF) — same underlying gap.
Workaround applied (for other users hitting this)
With opencode closed, remove the PDF attachment from the stored tool part so the history replays as plain string content:
-- locate the offending part (a tool part with attachments mime application/pdf)
-- then rewrite its data JSON setting state.attachments = []
After clearing state.attachments for that single part, the session works again.
Suggested fixes (maintainer's choice)
- In
message-v2.ts(extraction logic): treat@ai-sdk/openai-compatibleas supporting no media anywhere — do not create the synthetic user message withfileparts at all. Instead append a textual marker to the tool output, e.g.[Attached application/pdf: report.pdf](this pattern already exists incompaction.ts:73-78and in thestripMediapath). - In
transform.ts(unsupportedParts), defense-in-depth: after filtering, collapse any user message whose content array is now all-textblocks back into a plaincontent: string(joined with\n\n). This fixes the "everything degraded to text but still an array" case for every strict OpenAI-compatible gateway. - Optionally move the
model.capabilities.input[modality]check into the extraction step inmessage-v2.tsso unsupported media is never extracted into a synthetic message that must be degraded later.
Happy to provide the full DB row structure, logs, or test repro if useful. Thank you!
Plugins
No response
OpenCode version
1.18.22
Screenshot and/or share link
No response
Operating System
Windows 11 HSL
Terminal
Windows Terminal
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.