Repository navigation
Description
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(varianthigh) - Active plugins: none (
~/.config/opencode/plugindoes not exist; config is{"$schema": ...}only) - Provider mapping (
models.json):npm: @ai-sdk/openai-compatible,api: https://api.z.ai/api/paas/v4
Reproduction
opencodewithzai/glm-5.3-flash.- Ask it to read any local PDF, so the
readtool attaches it. - 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_urlreturn 200. - Not the account/key/network. Success and failure alternate on the same key, endpoint and TLS path within the same minute.
- Not
thinking. Addingthinking:{type:"enabled"}withreasoning_effortlow/high/maxdoes not change the outcome. (Note:thinking:disableddoes return1210, 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.1210is a generic "invalid parameter" bucket; the discriminator is themessagefield.) - Not
max_tokens/temperature.image_url+max_tokens:512+temperature:0.6returns 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 formatdata:<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. ..."}}
EOFSwap 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:
- Upload the attachment to
POST /paas/v4/fileswithpurpose=user_dataand send{"type":"file","file":{"file_id": "<id>"}}— works for local files, which is theread-tool case; or - Send
file_urlwhen 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
github-actions commentedon Sep 15, 2026
This issue might be a duplicate of existing issues. Please check:
@ai-sdk/openai-compatiblesendsfile_datafor PDFs in synthetic user messages; strict OpenAI-compatible gateways (including Z.AI) reject the request and brick the sessionjlinazasoro commentedon Sep 15, 2026
Cross-linking — the duplicate bot was right, and the scope is wider than I framed it.
file_datamapping breaks theopencode-go/ zen gateway with a 422 (messages.N.user.content.str). So the condition is not "provider iszai" but "OpenAI-compatible provider that rejects PDFs overfile_data". Worth folding into the fix design.zai, with two repro PDFs attached.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
1210to a correct 200 when moved fromfile_datatofile_id(purpose=user_data).One open variable I cannot explain: #49028 reports that one PDF was accepted. I could not reproduce any
file_dataPDF 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_idis 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), thefile_idroute 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ïvepdftotexton the same page interleaves the two columns. So Z.AI's server-side ingestion is not a naïve text extraction, andfile_idpreserves document fidelity.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 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊