400 unsupported_content_type: why images and PDFs get rejected on text endpoints
A 400 unsupported_content_type means a non-text block — an image, audio, video, or PDF — made it into a text request's body. go2llm checks the body before it ever reaches a model and answers with something like Only text input is supported; rejected image at messages.0.content.1.source, pointing straight at the field that broke it. This is a gateway limit, not a model limit: Anthropic's own claude-opus-5 accepts images just fine.
Written 2026-09-11, updated 2026-09-11.
What it looks like
{
"error": {
"type": "unsupported_content_type",
"code": "unsupported_content_type",
"message": "Only text input is supported; rejected image at messages.0.content.1.source"
}
}
Reproduced with an image block in Anthropic's format:
curl -i https://go2llm.tech/v1/messages \
-H "x-api-key: $GO2LLM_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "claude-sonnet-5",
"max_tokens": 256,
"messages": [{"role":"user","content":[
{"type":"text","text":"What is in this image?"},
{"type":"image","source":{"type":"base64","media_type":"image/png","data":"iVBORw0KGgo="}}
]}]
}'
An image_url block in OpenAI's format on POST /v1/chat/completions gets the same result, and so does a {"type":"document","source":{...}} block carrying a PDF.
Two neighboring 400s share the status but not the code: invalid_request (the body isn't valid JSON, or isn't a JSON object — messages read request body must be valid JSON and request body must be a JSON object) and model_protocol_mismatch (the model was called through the wrong endpoint).
Why this happens
A text request carries an image block. go2llm doesn't take images, PDFs, audio, video, or arbitrary binary on the text endpoints POST /v1/messages and POST /v1/chat/completions. The check walks the body recursively and stops at the first non-text field it finds, so the message always names exactly one path — even if the request has three images in it.
A PDF arrived as a document block. Anthropic's Messages API supports {"type":"document"} blocks carrying a base64 PDF. go2llm rejects these under the same unsupported_content_type code, with a path like messages.2.content.0.source.
A library attached the block for you. LangChain, LlamaIndex, and similar frameworks can auto-attach a screenshot, a PDF page, or an image pulled from a RAG index. Nobody on your team wrote that attachment into the request, but it's there. The path in error.message shows exactly which message and block index carries it.
An image-generation model was called on the wrong endpoint. gpt-image-2, nano-banana-2, nano-banana-pro, gemini-3.1-flash-image, and grok-imagine-image are served only on POST /v1/images/generations. Calling one through POST /v1/chat/completions gives 400 model_protocol_mismatch instead — check GET /v1/models for the right endpoint.
The image endpoint got something other than JSON. POST /v1/images/generations only accepts content-type: application/json. multipart/form-data gets a 415, not a 400, with code unsupported_content_type and message image generation requires application/json.
The body is over 20 MiB. Anything larger comes back as a 413 with code request_too_large and message request body exceeds 20 MiB. Base64 inflates a file by roughly a third, so a 16 MB PDF can cross the limit once it's encoded.
Fixing the request
Read the path in error.message first — messages.0.content.1.source means the first message, second content block.
Then pull the text out of the file before you send anything. For a PDF, that's a local step:
import pypdf
def pdf_to_text(path: str, max_chars: int = 120_000) -> str:
reader = pypdf.PdfReader(path)
text = "\n\n".join((page.extract_text() or "") for page in reader.pages)
return text[:max_chars]
Send the extracted text instead of the file, and the request goes through:
import os
from anthropic import Anthropic
client = Anthropic(base_url="https://go2llm.tech", api_key=os.environ["GO2LLM_API_KEY"])
msg = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
messages=[{"role": "user", "content": f"Summarize this document:\n\n{pdf_to_text('doc.pdf')}"}],
)
print(msg.usage) # cost_usd is on the response, same place as any other call
Add a check on your own side so a bad request never leaves your machine:
MEDIA_TYPES = {"image", "image_url", "document", "audio", "input_audio", "video", "file"}
def assert_text_only(messages):
for i, m in enumerate(messages):
content = m.get("content")
if isinstance(content, str):
continue
for j, block in enumerate(content or []):
kind = block.get("type")
if kind in MEDIA_TYPES:
raise ValueError(f"go2llm only accepts text: messages.{i}.content.{j} = {kind}")
If image generation is what you actually need, use its own endpoint with a required Idempotency-Key header (8–128 printable ASCII characters):
curl https://go2llm.tech/v1/images/generations \
-H "Authorization: Bearer $GO2LLM_API_KEY" \
-H "Idempotency-Key: project-20260911-image-001" \
-H "content-type: application/json" \
-d '{"model":"gpt-image-2","prompt":"Minimalist diagram of an API gateway","n":1,"size":"1024x1024","quality":"standard","response_format":"url"}'
Keeping this from happening again
Split the pipeline at design time: your code extracts text from files, and the model only ever sees text. It's cheaper in tokens, reproducible, and doesn't depend on which model happens to be serving the request today.
Run the body through assert_text_only before you send it — the check costs microseconds and saves a request out of the 20 RPM per-key limit, which matters most for frameworks that assemble your messages for you.
Don't retry a 400. go2llm returns the same answer until the body itself changes; unlike a 429 or a 502, backoff here just burns your rate limit for nothing.
If image generation is a real part of your flow, keep the Idempotency-Key in mind: reusing one key with a different body gives a 409 idempotency_conflict, and a media call that comes back ambiguous_upstream_result should never be auto-retried — the provider may already have accepted it, and a retry risks charging you twice.
Related errors
- 404 model_not_found — the model was called through the wrong endpoint, or the ID itself is wrong.
- 402 insufficient_balance — a large text request runs into the balance instead.
- 429 rate_limit — the 20 RPM ceiling, exceeded.
- The full error reference.
FAQ
Is image input on the roadmap for go2llm? We don't publish timelines for that. If it matters for your work, tell @go2llm_support and we'll answer and update this page. As of 2026-09-11, go2llm's text endpoints only take text, and image generation runs through its own POST /v1/images/generations.
Why does the message only name one path when there are several images? The check stops at the first non-text block it finds and returns that path. Fix it, resend, and the next request will point at the next one — running your own check over the whole body first saves you the round trips.
How is 400 unsupported_content_type different from a 415? The 400 is for a non-text block inside a text request's body. The 415 with the same code comes from POST /v1/images/generations when its content-type header isn't application/json.
What's the largest request body go2llm accepts? 20 MiB. Anything larger gets a 413 with code request_too_large and message request body exceeds 20 MiB.
Get a key and make your first request in 3 minutes → go2llm.tech. The full list of endpoints and models is in the docs, and per-model details are on the pricing page.