[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) · Issue #48868 · anomalyco/opencode · GitHub
You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[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) #48868
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, API https://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-go provider.
Have the agent run the read tool on a PDF file (e.g. read C:\...\report.pdf). The tool stores its result with state.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.
packages/opencode/src/session/message-v2.ts:147-159 — supportsMediaInToolResult() returns false for 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 user ModelMessage whose content is an array of blocks.
packages/opencode/src/provider/transform.ts:409-445 — unsupportedParts() does not save the case:
The model declares pdf in its input modalities on models.dev (verified: glm-5.3-flash → modalities.input: ["text","image","video","pdf"]), so the file block passes through untouched.
Even when a block is degraded to text, the message content remains an array; @ai-sdk/openai-compatible only collapses arrays to a plain string when there is exactly one text block. The synthetic message always has ≥2 parts.
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-compatible as supporting no media anywhere — do not create the synthetic user message with file parts at all. Instead append a textual marker to the tool output, e.g. [Attached application/pdf: report.pdf] (this pattern already exists in compaction.ts:73-78 and in the stripMedia path).
In transform.ts (unsupportedParts), defense-in-depth: after filtering, collapse any user message whose content array is now all-text blocks back into a plain content: 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 in message-v2.ts so 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!
This issue might be a duplicate of existing issues. Please check:
bug: tool-result media 400s whole request for models without attachment capability #41163: Same underlying path — tool-result media causing provider request failures due to supportsMediaInToolResult logic in message-v2.ts; your issue extends this specifically to PDF attachments on strict OpenAI-compatible gateways via the synthetic user message content array.
This issue might be a duplicate of existing issues. Please check:
bug: tool-result media 400s whole request for models without attachment capability #41163: Same underlying path — tool-result media causing provider request failures due to supportsMediaInToolResult logic in message-v2.ts; your issue extends this specifically to PDF attachments on strict OpenAI-compatible gateways via the synthetic user message content array.
If your issue is distinct enough (422 on user message content array specifically), feel free to clarify how it differs from the above.
Thanks for the triage! I checked the three referenced issues. Mine is related but has a distinct failure surface — happy to consolidate if maintainers prefer, but flagging what is unique here:
vs. #41163 — same supportsMediaInToolResult path, but in my case the tool result itself was correctly degraded to a plain string ("PDF read successfully"). The failure happens one step later: the synthetic user message built at message-v2.ts:380-399 produces a contentarray ([text, file]), and strict OpenAI-compatible gateways reject user-role content arrays entirely:
422 messages.297.user.content.str: Input should be a valid string
Note the model's declared modalities say PDF is supported (glm-5.3-flash → input: ["text","image","video","pdf"]), so unsupportedParts() never degrades the file block — and even if it did, the array would still not collapse to a string (the @ai-sdk/openai-compatible SDK only stringifies single-text-block arrays). A session with a prior PDF read fails permanently on these gateways.
vs. #42263 — that one is about the memory/OOM cost of re-sending base64 attachments each turn. Mine is a hard request rejection (422): the session is bricked, not just heavy. Same data flow, different symptom and different fix locus (message serialization vs. storage/compaction).
vs. #39824 — that one is about media bypassing modality checks. Here the modality check passed (the model does declare PDF support), and that is precisely what sends the file block downstream to a gateway that doesn't implement it regardless of declared capabilities.
Concrete, provider-agnostic fix unique to this report:
In message-v2.ts (extraction logic, lines ~296-313 and ~380-399): treat @ai-sdk/openai-compatible as file-block-incapable — don't create the synthetic user message with file parts; append a textual marker to the tool output instead (pattern already used in compaction.ts:73-78 and the stripMedia path).
In transform.ts (unsupportedParts, lines ~409-445), defense-in-depth: after filtering, collapse any user message whose content array is all-text blocks back into a plain content: string (joined with \n\n).
If the team prefers merging this into #41163 as a tracking issue, fine by me — just noting the 422-on-user-content-array failure mode isn't currently covered by the three referenced reports.
Thanks for the report! OpenCode 2.0 is now out and development is focused on V2, so we're closing issues filed against OpenCode 1. Please upgrade (opencode upgrade --major, details: https://anoma.ly/notes/opencode-2-0), and if this is still relevant on 2.0, reopen this issue or open a new one with your 2.0 version.
Activity
github-actions commentedon Sep 13, 2026
This issue might be a duplicate of existing issues. Please check:
supportsMediaInToolResultlogic inmessage-v2.ts; your issue extends this specifically to PDF attachments on strict OpenAI-compatible gateways via the synthetic user message content array.readtool stored as base64 in history and re-sent on every turn (memory/OOM angle, but same data flow).If your issue is distinct enough (422 on user message content array specifically), feel free to clarify how it differs from the above.
pipaacebedo commentedon Sep 13, 2026
Thanks for the triage! I checked the three referenced issues. Mine is related but has a distinct failure surface — happy to consolidate if maintainers prefer, but flagging what is unique here:
vs. #41163 — same
supportsMediaInToolResultpath, but in my case the tool result itself was correctly degraded to a plain string ("PDF read successfully"). The failure happens one step later: the synthetic user message built atmessage-v2.ts:380-399produces acontentarray ([text, file]), and strict OpenAI-compatible gateways reject user-role content arrays entirely:422 messages.297.user.content.str: Input should be a valid string
Note the model's declared modalities say PDF is supported (
glm-5.3-flash→input: ["text","image","video","pdf"]), sounsupportedParts()never degrades thefileblock — and even if it did, the array would still not collapse to a string (the@ai-sdk/openai-compatibleSDK only stringifies single-text-block arrays). A session with a prior PDFreadfails permanently on these gateways.vs. #42263 — that one is about the memory/OOM cost of re-sending base64 attachments each turn. Mine is a hard request rejection (422): the session is bricked, not just heavy. Same data flow, different symptom and different fix locus (message serialization vs. storage/compaction).
vs. #39824 — that one is about media bypassing modality checks. Here the modality check passed (the model does declare PDF support), and that is precisely what sends the
fileblock downstream to a gateway that doesn't implement it regardless of declared capabilities.Concrete, provider-agnostic fix unique to this report:
message-v2.ts(extraction logic, lines ~296-313 and ~380-399): treat@ai-sdk/openai-compatibleas file-block-incapable — don't create the synthetic user message withfileparts; append a textual marker to the tool output instead (pattern already used incompaction.ts:73-78and thestripMediapath).transform.ts(unsupportedParts, lines ~409-445), defense-in-depth: after filtering, collapse any user message whose content array is all-textblocks back into a plaincontent: string(joined with\n\n).If the team prefers merging this into #41163 as a tracking issue, fine by me — just noting the 422-on-user-content-array failure mode isn't currently covered by the three referenced reports.
thdxr commentedon Oct 11, 2026
Thanks for the report! OpenCode 2.0 is now out and development is focused on V2, so we're closing issues filed against OpenCode 1. Please upgrade (
opencode upgrade --major, details: https://anoma.ly/notes/opencode-2-0), and if this is still relevant on 2.0, reopen this issue or open a new one with your 2.0 version.— from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊