provider: PDF attachment bricks session on GLM models (upstream json_parse_error at messages[N]) · Issue #53809 · anomalyco/opencode · GitHub
Skip to content

provider: PDF attachment bricks session on GLM models (upstream json_parse_error at messages[N]) #53809

Open

Description

@David-Birk

Summary

When the read tool opens a PDF, OpenCode attaches the PDF as a file part to the conversation history. On the opencode provider's GLM models (e.g. glm-5.3-flash), the upstream gateway rejects the next request with HTTP 400 json_parse_error. Because the unsupported part stays in history, every subsequent request fails — even plain "continue" prompts — permanently bricking the session. /compact, which drops the attachment from context, recovers it.

Environment

  • opencode version: v2.0.24 (latest)
  • OS: Arch Linux, kernel 7.2.9-arch1-1 (x86_64)
  • Terminal: kitty (TERM=xterm-256color, COLORTERM=truecolor)
  • Shell: /usr/bin/bash
  • Install/channel: latest
  • Active plugins: none found in config
  • Provider / model: opencode / glm-5.3-flash

Reproduction

  1. Start a new session with provider opencode, model glm-5.3-flash.

  2. Ask the agent to inspect any PDF file with the read tool (e.g. a generated report).

  3. The tool call completes and the PDF is attached to the tool result.

  4. The next model request fails with HTTP 400:

    Upstream request failed: [json_parse_error] Invalid JSON data: Failed to deserialize
    the JSON body into the target type: messages[N]: data did not match any variant of
    untagged enum MessageContent at line 1 column <offset>
    

    N is the index of the message carrying the PDF part.

  5. Send anything afterwards — even the literal prompt continue — and the identical error repeats on every request. The session is unusable from that point on.

  6. Run /compact (or start a new session). Once the PDF part is out of context, the session works again.

Reproducibility: deterministic. Hit twice today in two independent sessions, 100% failure rate after the PDF part enters history; 6 failed requests total.

Server log evidence (abridged, session IDs shortened):

  • Session A: read on a ~39 KB invoice PDF at 22:10:39 local, then 4×
    level=ERROR "Failed to drain Session" cause="AI.Error: Upstream request failed: [json_parse_error] ... messages[136]: ... at line 1 column 349405" (22:10:45–22:12:13), including for the prompt continue.
  • Session B: same PDF read, 2× ... messages[51]: ... at line 1 column 163865 (22:17:02, 22:17:33); a compaction entry at 22:18:27, session functional afterwards.

Expected Behavior

  • OpenCode should normalize PDF tool results into a content type the target provider actually supports (e.g. page images or extracted text), or omit them for providers/upstreams that don't accept file parts.
  • Independently, a single unsupported part should not permanently brick a session: on a 400 of this shape, OpenCode could sanitize/drop the offending part and retry.

Actual Behavior

Upstream rejects every request with json_parse_error once a PDF part is in the history; the session cannot be continued at all until compaction removes the part.

Additional Context

  • The error text comes from the upstream gateway (z.ai/GLM, Rust/serde-style message), not from OpenCode's own JSON handling — OpenCode is sending a content shape the upstream can't deserialize.
  • The byte offsets in the two variants (~164 KB and ~350 KB) are consistent with the position of the PDF-bearing message inside a long request body.
  • Workaround: /compact or a new session; avoid read on PDFs with this provider/model.
  • Possibly related to how file parts are mapped for models without native file-part support.

Activity

  1. opencode-agent commented on Oct 7, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report. I reproduced the OpenCode side of this on v2.0.24 and on the latest v2 branch (8587f817), using a simulated model. After read opens a PDF, the tool result is just PDF read successfully. The PDF goes in a separate user message as an OpenAI Chat file part:

    {"role":"user","content":[{"type":"file","file":{"filename":"report.pdf","file_data":"data:application/pdf;base64,..."}}]}

    That message is sent again on every later request, including a plain continue. This matches the messages[N] index in your error and why only /compact recovers the session.

    I couldn't confirm the json_parse_error itself, because that comes from the OpenCode Zen gateway and I don't have provider access here. A maintainer with Zen access will need to check whether the gateway accepts this file part for glm-5.3-flash.

    Related issues (closest first):

    For now, /compact or a new session works around it, as you found.

  2. pipaacebedo commented on Oct 11, 2026

    @pipaacebedo

    Here is a small, non-destructive CLI tool that repairs opencode sessions that get stuck after an agent reads a PDF file while using strict OpenAI-compatible providers (e.g. the opencode-go / zen gateway). Written in pure Python (3.8+, stdlib only).

    Related upstream issue: anomalyco/opencode#48868

    When the read tool runs on a PDF, opencode stores the result in its database with a base64 attachment:

    { "type": "tool", "tool": "read", "state": { "output": "PDF read successfully", "attachments": [{ "type": "file", "mime": "application/pdf", "url": "data:application/pdf;base64,..." }] } }

    On every subsequent turn, the session provider-isolation layer re-injects that media as a synthetic user message whose content is an array of blocks (message-v2.ts:380-399). Strict OpenAI-compatible gateways only accept content: string for user messages, so every request fails with:

    422  messages.N.user.content.str: Input should be a valid string
    400  Upstream request failed: [400] Request validation failed
    

    The session is permanently bricked: every retry replays the same broken history.

    For the affected session (or a single part), it:

    1. Backs up each affected row to fix-backups/part_<id>.json (the full JSON of the part, a few hundred KB — the 6+ GB database file is never copied).
    2. Patches the stored part so state.attachments becomes [] inside a single atomic transaction. The text output ("PDF read successfully"), metadata and tool input are kept untouched. The model already ingested the PDF text while working; only the base64 binary that breaks history replay is discarded.
    3. Verifies the result and prints a summary.

    opencode does not need to be closed: SQLite WAL mode safely supports the concurrent writer. The -wal / -shm files require no special handling.

    Requirements

    • Python 3.8+ (standard library only)
    • Works on Windows, macOS and Linux (arrow-key menu needs an ANSI-capable terminal; Windows Terminal, cmd and most POSIX terminals are fine)

    Usage

    Run the interactive wizard (recommended):

    python fix-opencode-db.py
    
    1. Type a search text (or press Enter for the 40 most recent sessions).
    2. Pick a session with the arrow keys + Enter.
    3. Review the detected parts (tool, file path, size) and confirm with s.
    4. Done — reopen the session in opencode.

    Non-interactive commands (for scripting / automation):

    Command Description
    python fix-opencode-db.py list [text] List sessions (date, id, title) matching text; marks [PDF xN]
    python fix-opencode-db.py fix <ses_... | prt_...> Backup + patch + verify for the whole session or one part
    python fix-opencode-db.py verify <ses_... | prt_...> Check current state
    python fix-opencode-db.py restore <ses_... | prt_...> Roll back rows to their exact original state
    --db <path> Point at a database in a non-default location

    Backups live in fix-backups/ next to the script, with a manifest.json that maps each part id to its session for session-level restores.

    Safety notes

    • Non-destructive by design: one-row transactions, strict JSON validation before
      writing, abort on any mismatch.
    • Always creates a row backup first; restore puts everything back byte-for-byte.
    • The database file itself is never copied, moved or rewritten by bulk operations.
    • Any non-PDF attachments (images) are left untouched.
    • Running fix on a session with no PDF attachments is a safe no-op.

    Troubleshooting

    Symptom Fix
    Could not find opencode.db Pass --db "C:\path\to\opencode.db"
    Session still fails after fix Run verify; if it prints OK, the failure is likely a different issue — report it upstream
    Need to undo python fix-opencode-db.py restore ses_... (type RESTORE to confirm)

    Root cause (for the curious)

    • packages/opencode/src/tool/read.ts:306-324 — read returns PDFs as base64 attachments.
    • packages/opencode/src/session/message-v2.ts:147-159 — supportsMediaInToolResult() returns false for @ai-sdk/openai-compatible, so media is extracted into a synthetic user message (lines 380-399) with array content.
    • packages/opencode/src/provider/transform.ts:409-445 — unsupportedParts() passes the file block through when the model declares PDF support, and never collapses all-text content arrays back to strings, which the OpenAI-compatible SDK requires.

    Fixes to upstream are discussed in the linked issue.

    Disclaimer

    This tool edits opencode's local SQLite database directly. Use at your own risk.
    It performs strict validation and per-row backups before any change, and no database files are deleted at any point.

    Script

    fix-opencode-db.py

  3. amitnarw commented on Oct 14, 2026

    @amitnarw

    Hit this too, on v2.0.28 (latest). Windows 11, ollama-cloud / glm-5.3-flash.
    Read a ~400KB PDF with the read tool, worked fine, model even answered questions about it. From the next message onwards every single request failed with invalid message format (ref: ...) = a different ref each time, so at first I thought it was random. Restart didn't help, session was dead. Even "say hi" wouldn't go through.
    Dug into the session db and found the read tool had stuffed the entire PDF as base64 into the history. Every new message resends that history, GLM chokes on it, session never recovers.
    Fixed it by hand by editing the stored message and swapped the PDF blob with a short text note pointing to the file on disk. Worked again right away, lost nothing except the blob. Same history reads fine on a non-GLM model btw, so it's specifically GLM failing on the PDF part.
    Also: the error text here is different from the json_parse_error in the main report (I'm on ollama-cloud, not the opencode provider), so anyone googling "invalid message format" should find this thread.

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