400 unsupported_content_type: изображения и PDF в текстовом endpoint
Ошибка 400 unsupported_content_type возникает, когда в тело текстового запроса попал нетекстовый блок — изображение, аудио, видео или PDF. go2llm проверяет тело до вызова модели и отвечает сообщением вида Only text input is supported; rejected image at messages.0.content.1.source, прямо указывая путь к проблемному полю. Это ограничение шлюза, а не модели: сама claude-opus-5 у Anthropic изображения принимает.
Страница составлена 2026-09-11, обновлена 2026-09-11.
Как выглядит ошибка
go2llm возвращает статус 400 и тело с точным путём до нарушающего поля:
{
"error": {
"type": "unsupported_content_type",
"code": "unsupported_content_type",
"message": "Only text input is supported; rejected image at messages.0.content.1.source"
}
}
Воспроизводится запросом с картинкой в Anthropic-формате:
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":"Что на картинке?"},
{"type":"image","source":{"type":"base64","media_type":"image/png","data":"iVBORw0KGgo="}}
]}]
}'
Тот же результат даст image_url в OpenAI-формате на POST /v1/chat/completions и блок {"type":"document","source":{...}} с PDF.
Рядом живут три соседние ошибки с тем же статусом 400 и другими кодами: invalid_request (тело не является валидным JSON или не является объектом, сообщения request body must be valid JSON и request body must be a JSON object) и model_protocol_mismatch (модель вызвана не через свой endpoint).
Причины 400 unsupported_content_type
1. В текстовом запросе есть блок изображения
go2llm не принимает изображения, PDF, аудио, видео и произвольный бинарь на вход в текстовые endpoint POST /v1/messages и POST /v1/chat/completions. Проверка обходит тело рекурсивно и останавливается на первом нетекстовом поле, поэтому в сообщении всегда ровно один путь — даже если картинок в запросе три.
2. PDF пришёл как document-блок
Anthropic Messages API поддерживает блоки {"type":"document"} с PDF в base64. go2llm такие блоки отклоняет с тем же кодом unsupported_content_type, указывая путь вида messages.2.content.0.source.
3. Библиотека добавила мультимодальный блок сама
Фреймворки уровня LangChain и LlamaIndex умеют автоматически прикреплять к сообщению вложение — скриншот, страницу PDF или изображение из RAG-индекса. Разработчик кода вложения не писал, но в теле запроса оно есть. Путь из error.message показывает точный индекс сообщения и блока.
4. Модель генерации изображений вызвана через текстовый endpoint
gpt-image-2, nano-banana-2, nano-banana-pro, gemini-3.1-flash-image и grok-imagine-image обслуживаются только на POST /v1/images/generations. Вызов через POST /v1/chat/completions даёт 400 с кодом model_protocol_mismatch и требует взять правильный endpoint из GET /v1/models.
5. На image-endpoint пришёл не JSON
POST /v1/images/generations принимает только content-type: application/json. multipart/form-data даёт статус 415 с кодом unsupported_content_type и сообщением image generation requires application/json — обратите внимание, статус здесь 415, а не 400.
6. Тело больше 20 MiB
Запрос свыше 20 MiB отклоняется со статусом 413 и кодом request_too_large, сообщение — request body exceeds 20 MiB. Base64-кодирование раздувает файл примерно на 33%, поэтому PDF на 16 MB превышает лимит уже после кодирования.
Как починить по шагам
Шаг 1. Прочитайте путь из error.message — он указывает точное место: messages.0.content.1.source означает первое сообщение, второй блок контента.
Шаг 2. Извлеките текст из файла до отправки. Для PDF это делается локально:
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]
Шаг 3. Отправьте только текст — запрос пройдёт:
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"Сделай выжимку документа:\n\n{pdf_to_text('doc.pdf')}"}],
)
print(msg.usage) # cost_usd — фактическая стоимость
При цене claude-sonnet-5 $0.33 за 1M входных токенов документ на 120 000 символов (примерно 40 000 токенов) обойдётся около $0.013 на вход.
Шаг 4. Добавьте предпроверку тела на своей стороне, чтобы не тратить запрос:
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 примет только текст: messages.{i}.content.{j} = {kind}")
Шаг 5. Если нужна именно генерация изображений — используйте свой endpoint с обязательным заголовком Idempotency-Key (8–128 печатных ASCII-символов):
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":"Минималистичная схема API-шлюза","n":1,"size":"1024x1024","quality":"standard","response_format":"url"}'
Как избежать 400 в будущем
Разделяйте пайплайны на этапе проектирования: извлечение текста из файлов делает ваш код, LLM получает готовый текст. Это дешевле по токенам, воспроизводимее и не зависит от того, какая модель обслуживает запрос сегодня.
Ставьте проверку тела до отправки — функция assert_text_only выше стоит микросекунды и экономит один запрос из лимита 20 RPM на ключ. Особенно это важно для фреймворков, которые собирают сообщения за вас.
Не ретрайте 400: go2llm вернёт тот же ответ, пока тело не изменится. В отличие от 429 и 502, экспоненциальный backoff здесь бесполезен и только жжёт лимит.
Если генерация изображений в вашем сценарии обязательна, помните про Idempotency-Key: повторное использование одного ключа с другим телом даёт 409 idempotency_conflict, а media-запрос с результатом ambiguous_upstream_result автоматически повторять нельзя — поставщик мог его принять, и повтор приведёт к двойному списанию.
Связанные ошибки
- 404 model_not_found — модель вызвана не через свой endpoint или ID написан неверно.
- 402 insufficient_balance — большой текстовый запрос упирается в баланс.
- 429 rate_limit — превышен лимит 20 RPM.
- Справочник всех кодов и docs/errors.
FAQ
Планируется ли приём изображений на вход в go2llm? Планы по срокам мы публично не анонсируем — если такая поддержка нужна вам в работе, напишите в @go2llm_support, ответим и дополним страницу. На 2026-09-11 текстовые endpoint go2llm принимают только текст, а генерация изображений работает через отдельный POST /v1/images/generations.
Почему в сообщении указан только один путь, если картинок несколько? Проверка go2llm останавливается на первом найденном нетекстовом блоке и возвращает его путь. После исправления первого блока повторный запрос укажет следующий, поэтому лучше прогнать тело своей предпроверкой целиком.
Чем 400 unsupported_content_type отличается от 415? Статус 400 возвращается для нетекстового блока внутри тела текстового запроса. Статус 415 с тем же кодом возвращает POST /v1/images/generations, если заголовок content-type не равен application/json.
Какой максимальный размер тела запроса? 20 MiB. Превышение даёт 413 с кодом request_too_large и сообщением request body exceeds 20 MiB.
Получить ключ и сделать первый запрос за 3 минуты → go2llm.tech. Полный список endpoint и моделей — в документации, цены — на странице тарифов.