Skip to content

zai: PDF attachments always fail with 400 [1210] — opencode sends file_data, Z.AI accepts PDF only via file_url/file_id #49237

Closed as not planned

Description

@jlinazasoro

Summary

Reading any PDF with the read tool permanently fails a zai session. OpenCode maps application/pdf attachments to the OpenAI-compatible file.file_data (base64 data-URI) transport, which is the one PDF transport Z.AI's /paas/v4/chat/completions does not accept. The request is rejected with HTTP 400 code 1210 and the turn dies.

glm-5.3-flash reads PDFs fine — via file_url or file_id. Only file_data is rejected. So this is a transport-mapping defect, not a provider limitation.

Same class as #44486 (OpenAI Responses, input_file.file_data in tool-result replay), which was fixed.

Environment

  • opencode version: 1.18.31 (Homebrew, /usr/local/Cellar/opencode/1.18.31)
  • OS: macOS 15.7.9, x86_64, Darwin 24.6.0
  • Terminal: WarpTerminal v0.2026.09.09.08.26.stable_02, shell /bin/zsh
  • Provider / model: zai / glm-5.3-flash (variant high)
  • Active plugins: none (~/.config/opencode/plugin does not exist; config is {"$schema": ...} only)
  • Provider mapping (models.json): npm: @ai-sdk/openai-compatible, api: https://api.z.ai/api/paas/v4

Reproduction

  1. opencode with zai/glm-5.3-flash.
  2. Ask it to read any local PDF, so the read tool attaches it.
  3. The next assistant turn fails:
level=ERROR message="stream error" providerID=zai modelID=glm-5.3-flash
  error.error="AI_APICallError: Failed to parse the file. Please check its accessibility and format."

The tool part itself succeeds (output: "PDF read successfully", attachments[0].mime = application/pdf). The failure happens when that attachment is hoisted into the synthetic user message and converted for the provider.

Root cause

MessageV2.toModelMessages hoists tool-result media into a synthetic user message. The H() allow-predicate only returns true for @ai-sdk/anthropic, @ai-sdk/openai, @ai-sdk/amazon-bedrock*, @ai-sdk/google-vertex/anthropic, @ai-sdk/xai (images) and @ai-sdk/google (gemini-3). For @ai-sdk/openai-compatible it returns false, so the PDF becomes a file part on a user message.

@ai-sdk/openai-compatible then hardcodes:

if (part.mediaType === "application/pdf") {
  return { type: "file", file: {
    filename: part.filename ?? "document.pdf",
    file_data: `data:application/pdf;base64,${convert(part.data)}`
  }}
}

Z.AI rejects exactly that shape for PDFs.

Evidence — transport matrix

Direct calls to https://api.z.ai/api/paas/v4/chat/completions, model: glm-5.3-flash, no opencode in the circuit:

Transport Result
file.file_data = data:application/pdf;base64,… (what opencode sends) 400 1210 "Failed to parse the file."
file.file_data, raw base64, no data-URI prefix 400 1210
file.file_data, with/without filename 400 1210
file.file_url = https://cdn.bigmodel.cn/static/demo/demo1.pdf 200 — PDF read correctly
file.file_id from POST /paas/v4/files purpose=user_data 200 — PDF read correctly
file.file_id from purpose=agent 400 1210
same bytes rendered to PNG via image_url 200

Controls that rule out alternative explanations:

  • Not the file. A 615-byte PDF I generated locally fails identically; the same bytes as image_url return 200.
  • Not the account/key/network. Success and failure alternate on the same key, endpoint and TLS path within the same minute.
  • Not thinking. Adding thinking:{type:"enabled"} with reasoning_effort low/high/max does not change the outcome. (Note: thinking:disabled does return 1210, with a different message — "This model always engages in thinking and cannot be disabled" — see [Bug] OpenCode Go intermittently returns 400 [1210] "thinking cannot be disabled" and MissingSessionID when routing omen-alpha requests #47872. 1210 is a generic "invalid parameter" bucket; the discriminator is the message field.)
  • Not max_tokens/temperature. image_url + max_tokens:512 + temperature:0.6 returns 200.

What the spec says

Z.AI's OpenAPI for POST /paas/v4/chat/completions (VisionMultimodalContentItem → File) documents three members of file, and attaches the supported-format list only to file_url:

  • file_url — "Only GLM-5.3-Flash, GLM-4.6V, GLM-4.5V supported. Supports formats such as pdf, txt, word, jsonl, xlsx, pptx"
  • file_id — "The ID returned by the File Upload API, only GLM-5.3-Flash supported"
  • file_data — "Base64 file content in the format data:<MIME>;base64,<BASE64_DATA>" — no format list

The docs' own "File Visual Example" uses file_url with a .pdf on glm-5.3-flash. So PDF-over-file_data was never promised; opencode's JSON is schema-conformant but picks the wrong transport.

Upload endpoint: POST /paas/v4/files, multipart, purpose enum user_data | agent. Only user_data produces a file_id the chat endpoint accepts for PDF.

Minimal repro

export ZAI_API_KEY=...   # https://z.ai/manage-apikey/apikey-list
python3 - <<'EOF'
import base64, json, os, urllib.request
KEY = os.environ["ZAI_API_KEY"]
# any valid PDF
pdf = base64.b64encode(open("small.pdf","rb").read()).decode()
body = {"model":"glm-5.3-flash","stream":False,"messages":[{"role":"user","content":[
    {"type":"file","file":{"filename":"document.pdf",
                           "file_data":"data:application/pdf;base64,"+pdf}},
    {"type":"text","text":"What is this?"}]}]}
req = urllib.request.Request("https://api.z.ai/api/paas/v4/chat/completions",
    data=json.dumps(body).encode(),
    headers={"Authorization":"Bearer "+KEY,"Content-Type":"application/json"})
try:
    print(urllib.request.urlopen(req, timeout=120).status)
except urllib.error.HTTPError as e:
    print(e.code, e.read().decode())
# -> 400 {"error":{"code":"1210","message":"Failed to parse the file. ..."}}
EOF

Swap the content part for {"type":"file","file":{"file_url":"https://cdn.bigmodel.cn/static/demo/demo1.pdf"}} to get a 200 against the same key and model.

Suggested fix

For zai (and any provider whose API is api.z.ai/api/paas/v4), don't emit file_data for application/pdf. Either:

  1. Upload the attachment to POST /paas/v4/files with purpose=user_data and send {"type":"file","file":{"file_id": "<id>"}} — works for local files, which is the read-tool case; or
  2. Send file_url when the attachment already has a reachable URL.

Option 1 needs a small cache keyed by content hash so a PDF is not re-uploaded on every step of the agent loop.

Worth noting for whoever picks this up: the file/image_url mutual exclusion in the same message — "Not support passing both the file and image_url or video_url parameters at the same time" — means a turn mixing a PDF and a screenshot will fail regardless of transport.

I'm happy to send a PR if the approach sounds right.

Activity

  1. github-actions commented on Sep 15, 2026

    @github-actions
    Contributor

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

  2. jlinazasoro commented on Sep 15, 2026

    @jlinazasoro
    Author

    Cross-linking — the duplicate bot was right, and the scope is wider than I framed it.

    I ran those two attachments through the transport matrix (comment). Both contain zero embedded images, which disproves the "PDF contains an image" hypothesis in that thread, and both flip from 400 1210 to a correct 200 when moved from file_data to file_id (purpose=user_data).

    One open variable I cannot explain: #49028 reports that one PDF was accepted. I could not reproduce any file_data PDF succeeding, including a 615-byte image-free one. Flagging it so nobody treats my model as complete.

    Happy for maintainers to consolidate these however is most useful — including closing this one in favour of #49028 or #48868. I kept the write-up here rather than duplicating it across three threads.

    One further data point relevant to whether file_id is an acceptable fix, since PDF ingestion quality matters for anyone relying on it: on a two-column registry document (body text plus a left margin column of annotations), the file_id route returns the body in correct reading order with no margin contamination, and its output matches, word for word, an independent vision pass over the same page rendered to PNG. Naïve pdftotext on the same page interleaves the two columns. So Z.AI's server-side ingestion is not a naïve text extraction, and file_id preserves document fidelity.

  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