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)

Open
#48,868 2 comments 0 reactions 1 assignee View on GitHub

@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, 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-324read returns PDFs as base64 attachments ("PDF read successfully" + state.attachments).
  2. packages/opencode/src/session/message-v2.ts:147-159supportsMediaInToolResult() 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-445unsupportedParts() does not save the case:
    • The model declares pdf in its input modalities on models.dev (verified: glm-5.3-flashmodalities.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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.