anomalyco / anomalyco/opencode
zai: PDF attachments always fail with 400 [1210] — opencode sends `file_data`, Z.AI accepts PDF only via `file_url`/`file_id`
@kitlangton is already working on this.
Since Sep 15, 2026.
- Dominant language
- TypeScript
- Stars
- 209k
- Forks
- 27.5k
- PR merge metrics
- PR metrics pending
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 #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. ..."}}
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:
- 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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.