Чат

POST /v1/chat/completions — контракт OpenAI Chat Completions, в том числе с "stream": true. Итог вызова (статус и цена, без текста) — GET /v1/jobs/{id} с id ответа; карточка с токенами и временем — GET /v1/usage/{id} тем же ключом. Поля — схема ChatCompletionRequest.

curl https://api.souz.ai/v1/chat/completions \
  -H "Authorization: Bearer $SOUZ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "gpt-5-nano", "messages": [{"role": "user", "content": "Привет!"}], "routing": "fast"}'

Поля запроса

  • Поля, которых нет в схеме, уходят модели как есть; что модель читает — её карточка.

  • Значения известных полей проверяются до списания, ошибка — 400 invalid_request, в detail[] поле и что подойдёт:

    • temperature — от 0 до 2, top_p — от 0 до 1; если в params карточки у поля свой диапазон, действует он;
    • reasoning_effort и reasoning.effort — одна из ступеней none, minimal, low, medium, high, xhigh, max; ступени модели — в params карточки. Ступень, которой нет в карточке, канал получает ближайшей ступенью карточки не ниже запрошенной, а если такой нет — высшей; канал, который подбирает ступень под модель сам, получает её как есть и применяет своё правило соответствия;
    • max_tokens, max_completion_tokens и n — целые от 1; max_tokens и max_completion_tokens — не меньше min из params (у GPT-6 — 16);
    • имена полей — с учётом регистра: Max_Tokens или N — 400 с {"path": "N", "reason": "unsupported_field"}.
    JSON
    {"path": "temperature", "reason": "out_of_range", "allowed": ["at most 2"]}
  • tools, картинка, файл, звук и видео в messages[].content и response_format: json_schema уходят только туда, где их понимают; если модель этого не умеет — 400 capability_mismatch до списания, в detail[] названо поле.

  • tool_choice: "required" и {"type": "function", "function": {"name": "…"}} исполняют не все каналы: такой запрос идёт только по тем, что умеют. Не умеет ни один — 400 capability_mismatch до списания, {"path": "tool_choice", "reason": "unsupported_value"}; с "auto" модель решает сама.

  • Параметр, который канал не поддерживает, этим каналом игнорируется: temperature, top_p, top_k, min_p, seed, штрафы, logprobs, logit_bias, stop и подобные; что учитывает канал — capabilities.supported_parameters. tools, tool_choice, response_format и verbosity идут только к каналам, которые их учитывают; не учитывает ни один — 400 capability_mismatch до списания с полем в detail[].

  • response_format: {"type": "json_object"} у моделей OpenAI требует слова «json» в сообщениях; без него — 400 invalid_request с {"path": "response_format", "reason": "incompatible_value"}.

  • Тип картинки в data URL определяется по её байтам: подпись image/jpeg у PNG не мешает.

  • Веб-поиск ни одна модель не исполняет: web_search_options (и {}) или плагин {"id": "web"} в plugins — 400 capability_mismatch до списания с полем в detail[].

  • service_tier, provider, models, route, transforms и остальные plugins (file-parser и другие) игнорируются: канал и запасные каналы выбирают Авто-роутинг и routing_options, PDF модель получает сама.

  • Общие поля Souz API: routing, max_price_multiplier, routing_options, store.

Claude Haiku 5.5: мышление и кэш

Модель claude-haiku-5.5 принимает reasoning_effort со значениями none, low, medium, high, xhigh, max; умолчание — medium. none отключает мышление. tools поддерживает tool_choice: "auto" и "none"; принудительный или именованный выбор функции у доступных каналов не поддерживается. Изображения и PDF передаются стандартными частями сообщений.

Промпт можно закэшировать на 5 минут или 1 час. cache_control передаётся внутри текстового блока без изменений, например:

JSON
{
  "model": "claude-haiku-5.5",
  "reasoning_effort": "medium",
  "max_tokens": 4096,
  "messages": [{
    "role": "user",
    "content": [{
      "type": "text",
      "text": "Длинный документ и вопрос по нему",
      "cache_control": {"type": "ephemeral", "ttl": "1h"}
    }]
  }]
}

Повторный запрос с тем же префиксом может прочитать кэш. Запись и чтение оплачиваются отдельно; для промпта свыше 100 тысяч токенов действует тариф длинного контекста. Итоговое списание — по фактическому использованию. stream: true включает потоковый ответ, stream_options: {"include_usage": true} возвращает итоговый расход токенов в потоке.

Файлы, звук и видео

Части messages[].content — как у OpenAI Chat Completions, SDK работает без правок.

ЧастьФормаКарточка модели
файл (PDF){"type": "file", "file": {"filename": "doc.pdf", "file_data": "data:application/pdf;base64,…"}}capabilities.file_input
звук{"type": "input_audio", "input_audio": {"data": "<base64>", "format": "wav"}}, wav или mp3capabilities.audio_input
видео{"type": "video_url", "video_url": {"url": "https://…/clip.mp4"}} или data:video/mp4;base64,…capabilities.video_input
  • Файл и звук — в теле, base64; видео — ссылкой или data URL. Ссылка должна открываться без авторизации; подойдёт url файла из POST /v1/files. Если модель не смогла скачать ссылку — 400 invalid_input с {"path": "messages", "reason": "unreachable", "allowed": ["data URL"]}: передайте вложение data URL.
  • Модель без нужного входа — 400 capability_mismatch до списания, в detail[] — {"path": "messages", "reason": "unsupported_value"}. Какие каналы примут вход — превью с file_input, audio_input, video_input.
  • Картинка data URL больше предела канала уходит только каналам, которые её примут. Не примет ни один — 400 capability_mismatch до списания с {"path": "messages", "reason": "too_large", "allowed": ["at most <N> base64 bytes per image"]}, где N — наибольший предел каналов модели. Уменьшите картинку или передайте её ссылкой.
  • Файл, звук и кадры видео модель считает во входных токенах; у части моделей токены звука дороже текстовых. Итог — по факту выполнения.
  • Тело запроса — до 25 МиБ; больше — 413 с {"path": "body", "reason": "too_large", "allowed": ["at most 26214400 bytes"]}. Крупное вложение загрузите через POST /v1/files и передайте его url или передайте ссылкой https://….
Python
import base64

pdf = base64.b64encode(open("contract.pdf", "rb").read()).decode()
chat = client.chat.completions.create(
    model="gemini-3.8-flash",
    messages=[{"role": "user", "content": [
        {"type": "text", "text": "Перечисли сроки из договора."},
        {"type": "file", "file": {"filename": "contract.pdf",
                                  "file_data": f"data:application/pdf;base64,{pdf}"}},
    ]}],
)

Поток

"stream": true — text/event-stream по контракту OpenAI. Заголовки приходят с первым содержательным чанком; ошибка до него — обычный JSON со статусом. После 15 секунд тишины — пока модель думает до первого токена или между чанками — поток пишет комментарий : ping; SDK и разбор SSE его пропускают. Если поток уже открыт пингом, ошибка до первого чанка приходит событием data: {"error": {…}} с тем же телом, без data: [DONE]. stream_options: {"include_usage": true} — последний чанк с пустым choices, usage и usage.cost. Без флага usage в потоке нет, у каждого чанка есть choices[0]; списание — в GET /v1/jobs/{id}. Поток, оборвавшийся без usage, бесплатен.

stream = client.chat.completions.create(
    model="gpt-5-nano",
    messages=[{"role": "user", "content": "Привет!"}],
    stream=True,
    stream_options={"include_usage": True},
)
for chunk in stream:
    if chunk.choices:
        print(chunk.choices[0].delta.content or "", end="")

Ответ

usage.cost и usage.currency — списано, channel — канал запроса. Ожидание ответа — до 14 минут; дольше — 502 generation_failed (в потоке — обрыв без usage), без списания.

SDK OpenAI и Anthropic ждут ответа 10 минут и после таймаута сами повторяют запрос. Обрыв вызов не отменяет: без ключа каждый повтор — новый вызов модели и новое списание. Передавайте свой Idempotency-Key на каждый вызов: повтор с тем же ключом и тем же телом дожидается первого вызова и получает его ответ (и поток), списание одно. Тот же ключ с другим телом — 409 idempotency_key_conflict. Ответ под ключом хранится сутки.

Python
chat = client.chat.completions.create(
    model="gpt-5-nano",
    messages=[{"role": "user", "content": "Привет!"}],
    extra_headers={"Idempotency-Key": "order-1842-summary"},
)

Вход длиннее, чем принимает модель, или слишком большой max_tokens — 400 с полем в detail[] (messages с reason: "too_long" или max_tokens); тело больше, чем принимает любой канал модели, — messages с reason: "too_large"; пустой messages — тоже с полем.

Ответ, остановленный фильтром содержания (finish_reason: "content_filter"), несёт в refusal нашу фразу, а не текст фильтра; в потоке refusal приходит одним куском в чанке с finish_reason.