400 invalid_request_error: невалидный JSON и обязательные поля
Ошибка 400 означает, что сервер разобрал запрос и нашёл в нём проблему ещё до обращения к модели: тело не JSON, не объект, или не хватает обязательного поля. OpenAI API называет это invalid_request_error, go2llm — кодом invalid_request. И там и там это ошибка формы запроса, а не денег, ключа или модели: инференс не запускается вовсе, соответственно ничего не резервируется и не списывается.
Страница составлена 2026-09-27, обновлена 2026-09-27.
Как выглядит ошибка
На /v1/messages и /v1/chat/completions go2llm отвечает статусом 400, но на самом первом сбое — теле, которое вообще не разбирается как JSON, — это не стандартный конверт ошибки, а голая строка в поле error:
{
"error": "invalid json"
}
Если тело — валидный JSON, но не объект (например, массив или строка верхнего уровня), это уже конверт с type и message, но короче обычного — без code и request_id:
{
"error": {
"type": "invalid_request",
"message": "request body must be a JSON object"
}
}
OpenAI API на отсутствующем обязательном поле возвращает похожую по духу, но иначе оформленную ошибку:
{
"error": {
"message": "'messages' is a required property",
"type": "invalid_request_error",
"param": "messages",
"code": null
}
}
У OpenAI поле code для этого класса ошибок часто пустое, а суть проблемы описывает message и param — какое именно поле не понравилось серверу.
Причины
1. Отсутствует обязательное поле — но сам шлюз проверяет только одно
Для POST /v1/messages обязательны model, max_tokens и messages; для POST /v1/chat/completions — model и messages, max_tokens там необязателен. Сам go2llm до всего остального проверяет только model: отсутствующая или неизвестная модель проваливает поиск в каталоге и возвращается как 404 model_not_found (см. 404 model_not_found), а не как invalid_request этой страницы. Отсутствие messages или max_tokens шлюз вообще не проверяет — тело уходит поставщику модели как есть, и статус с формой ответа зависят от него, а не от кода, который определяет сам go2llm.
2. Тело не является валидным JSON
Незакрытая кавычка, висящая запятая после последнего элемента массива, или тело собрано склейкой строк без экранирования. На /v1/messages и /v1/chat/completions — собственных endpoint'ах этой страницы — это тот самый конверт из примера выше, голая строка {"error": "invalid json"}, всегда, независимо от конкретной синтаксической причины. Формулировка request body must be valid JSON реальна, но принадлежит другим endpoint'ам — /v1/responses, /v1/messages/count_tokens, /v1/images/generations — которые заворачивают тот же сбой в привычный конверт error.type/error.message.
3. Тело — не JSON-объект
Отправлен голый массив сообщений вместо объекта с полем messages, или строка вместо объекта — например, когда клиентский код сериализует не тот уровень структуры. Сообщение: request body must be a JSON object.
4. Поле имеет неверный тип — и отказывает поставщик, а не шлюз
messages пришёл строкой вместо массива, max_tokens — строкой вместо числа, temperature — вне допустимого диапазона. Ни одного из этих типов шлюз перед отправкой не проверяет, поэтому отказ приходит от поставщика: его статус и его формулировка, переданные с кодом вида upstream_http_400, а слова вендора — в error.upstream_message, если они дошли. Рассчитывать, что это придёт собственным invalid_request шлюза, нельзя.
5. В тело скопирован параметр не из того endpoint
Частый источник — код или пример скопирован из документации соседнего протокола или соседнего endpoint. previous_response_id, store и background относятся только к POST /v1/responses и на нём дают отдельные, более конкретные коды — 400 stateful_responses_unsupported и background_unsupported, а не общий invalid_request. Похожим образом изображение или PDF в messages дают unsupported_content_type, а не invalid_request — разбор рядом: 400 unsupported_content_type.
Как починить по шагам
Шаг 1. Проверьте, что тело — валидный JSON, до отправки, а не по факту ошибки:
cat request-body.json | python3 -m json.tool > /dev/null && echo "JSON валиден"
Шаг 2. Проверьте наличие обязательных полей для нужного endpoint:
def assert_required_fields(body: dict, endpoint: str) -> None:
required = {
"messages": ("model", "max_tokens", "messages"),
"chat.completions": ("model", "messages"),
}[endpoint]
missing = [f for f in required if f not in body]
if missing:
raise ValueError(f"в теле не хватает полей: {missing}")
Шаг 3. Собранное тело отправьте и, если 400 всё же пришёл, прочитайте ошибку целиком. Мест, где искать, три, и они не взаимозаменяемы: при битом JSON error — это просто строка invalid json; при отказе собственных проверок шлюза словами объясняет error.message; при отказе самой модели error.message — общая фраза шлюза, а слова вендора, если они пришли, лежат в error.upstream_message. Команда ниже разбирает все три случая:
curl -s https://go2llm.tech/v1/chat/completions \
-H "Authorization: Bearer $GO2LLM_API_KEY" \
-H "content-type: application/json" \
-d @request-body.json | python3 -c "import sys,json; e=json.load(sys.stdin)['error']; print(e if isinstance(e, str) else (e.get('upstream_message') or e.get('message')))"
Шаг 4. Пример корректного минимального тела на каждый протокол:
curl 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-haiku-4-5","max_tokens":120,"messages":[{"role":"user","content":"Ответь одним словом: работает"}]}'
import os
from openai import OpenAI
client = OpenAI(api_key=os.environ["GO2LLM_API_KEY"], base_url="https://go2llm.tech/v1")
response = client.chat.completions.create(
model="gpt-5.6-luna",
messages=[{"role": "user", "content": "Ответь: OK"}],
)
print(response.choices[0].message.content)
Оба тела содержат ровно те поля, которых для своего протокола достаточно — max_tokens обязателен у Anthropic Messages и необязателен у OpenAI Chat Completions.
Как избежать в будущем
Валидируйте тело схемой на своей стороне, а не полагайтесь на ответ сервера как на единственную проверку — это особенно важно, если запросы собирает библиотека или фреймворк автоматически: LangChain и подобные инструменты иногда добавляют в тело поля, специфичные для одного вендора, которые второй не примет. Держите отдельные, минимальные шаблоны запроса под /v1/messages и под /v1/chat/completions вместо одного общего конструктора тела — протоколы Anthropic и OpenAI различаются по обязательным полям, и общий конструктор рано или поздно потеряет что-то одно для одного из них.
400 не имеет смысла ретраить: go2llm вернёт тот же ответ, пока тело не изменится, и повтор только тратит лимит 20 RPM на ключ впустую.
Связанные ошибки
- 400 unsupported_content_type — нетекстовый блок в теле текстового запроса, отдельный код от общего
invalid_request. - 400 stateful_responses_unsupported —
previous_response_idиstoreна/v1/responses. - 404 model_not_found — само тело валидно, но модель не найдена в каталоге.
- Справочник всех кодов go2llm и docs/errors.
FAQ
Чем invalid_request у go2llm отличается от invalid_request_error у OpenAI? По сути ничем — это одна и та же категория «сервер разобрал тело и нашёл проблему формы». Отличается только буквальное имя кода: у go2llm он короче, invalid_request, у OpenAI — invalid_request_error, и структура JSON вокруг него своя у каждого вендора.
Можно ли по HTTP-статусу 400 понять точную причину? Нет, под одним статусом 400 у go2llm живут несколько разных кодов: invalid_request, unsupported_content_type, model_protocol_mismatch, stateful_responses_unsupported, background_unsupported, idempotency_key_required. Ветвиться в коде нужно по error.code, а не по статусу.
Списываются ли деньги за запрос с 400? Нет. Проверка тела происходит до вызова модели, поэтому резерв и списание не наступают вовсе.
Почему сообщение об ошибке не называет конкретное поле? Для базовых проверок JSON (валидность, тип верхнего уровня) go2llm сообщает только характер проблемы, а конкретная формулировка зависит от endpoint'а: /v1/messages и /v1/chat/completions отвечают на невалидный JSON голой строкой invalid json, а на тело не-объект — request body must be a JSON object; /v1/responses, /v1/messages/count_tokens и /v1/images/generations заворачивают невалидный JSON в request body must be valid JSON. Для более специфичных проверок, например нетекстовых блоков, сообщение уже включает точный путь до поля — так устроен код unsupported_content_type.
Получить ключ и сделать первый запрос за 3 минуты → go2llm.tech. Формат обоих протоколов — в документации, цены моделей — на странице тарифов.