[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
Skip to content

[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

Closed as not planned

Description

@pipaacebedo

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, API https://opencode.ai/zen/go/v1)
  • Model: glm-5.3-flash
  • OS: Windows 10

Steps to reproduce

  1. Start a session with a model served through the opencode-go provider.
  2. 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,..." }].
  3. 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:

  1. packages/opencode/src/tool/read.ts:306-324 — read returns PDFs as base64 attachments ("PDF read successfully" + state.attachments).
  2. 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).
  3. convertToModelMessages() produces a user ModelMessage whose content is an array of blocks.
  4. 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)

  1. 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).
  2. 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.
  3. 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!

Plugins

No response

OpenCode version

1.18.22

Screenshot and/or share link

No response

Operating System

Windows 11 HSL

Terminal

Windows Terminal

Activity

  1. github-actions commented on Sep 13, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    If your issue is distinct enough (422 on user message content array specifically), feel free to clarify how it differs from the above.

  2. pipaacebedo commented on Sep 13, 2026

    @pipaacebedo
    Author

    This issue might be a duplicate of existing issues. Please check:

    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 content array ([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:

    1. 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).
    2. 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.

  3. thdxr commented on Oct 11, 2026

    @thdxr
    Member

    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 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions