Чат
OpenAI-совместимый /v1/chat/completions — текст, стриминг SSE, vision (картинки в сообщениях), tools и reasoning. Оплата по токенам, стоимость в usage.cost.
POST /v1/chat/completions
Синхронный OpenAI-совместимый эндпоинт. Работает с любым OpenAI SDK — меняешь base_url и api_key, остальное без изменений. Тело запроса пробрасывается модели целиком («полная мощь по желанию»): стандартные параметры OpenAI поддерживаются как есть.
Параметры
| Поле | Тип | Описание |
|---|---|---|
model |
string, обяз. | ID модели, например souz/gpt-5.4-mini — каталог |
messages |
array, обяз. | Сообщения в формате OpenAI (system / user / assistant / tool) |
stream |
boolean | true → стриминг SSE |
temperature, top_p, max_tokens, stop, … |
— | Стандартные параметры OpenAI, пробрасываются модели |
tools, tool_choice |
— | Вызов инструментов (function calling) — для моделей с поддержкой |
response_format |
object | Например {"type": "json_object"} |
reasoning_effort |
string | low / medium / high — глубина размышлений для thinking-моделей (gpt-5.5, o3, …) |
Сейчас API отклоняет объявленный Content-Length больше 25 МБ (vision-картинки едут внутри
JSON). Это не hard limit для chunked/отсутствующего заголовка; bounded body reader — RSA-27.
Пример
from openai import OpenAI
client = OpenAI(base_url="https://api.souz.ai/v1", api_key="sk-souz-...")
resp = client.chat.completions.create(
model="souz/claude-sonnet-4-6",
messages=[
{"role": "system", "content": "Отвечай кратко."},
{"role": "user", "content": "Объясни, что такое вебхук."},
],
)
print(resp.choices[0].message.content)
print("кредитов:", resp.usage.cost)
В ответе — стандартный объект OpenAI. Дополнительно usage.cost — фактически списанная сумма
в целых микро-USD (1 кредит = 10 000 µUSD), как и все денежные поля API: дешёвый вызов
честно показывает cost: 7, а не округлённый ноль и не целый цент.
Стриминг
stream = client.chat.completions.create(
model="souz/gpt-5.4-mini",
messages=[{"role": "user", "content": "Напиши хокку про API"}],
stream=True,
)
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="")
if chunk.usage: # последний чанк
print("\nкредитов:", chunk.usage.cost)
Поток — стандартный SSE (data: {...}, в конце data: [DONE]). Когда апстрим присылает
финальный usage-чанк, СОЮЗ добавляет в него usage.cost. Если апстрим оборвал поток или не
прислал usage, публичный cost в чанках может отсутствовать; сервис всё равно завершает расчёт
по доступному usage либо оценке уже отданного текста.
Vision — картинки в сообщениях
Модели с пометкой «vision» в каталоге принимают картинки в сообщениях (data URL, как у OpenAI):
resp = client.chat.completions.create(
model="souz/gemini-2.5-flash-lite", # самый дешёвый vision
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "Что на фото?"},
{"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,..."}},
],
}],
)
Reasoning (thinking-модели)
resp = client.chat.completions.create(
model="souz/gpt-5.5",
messages=[{"role": "user", "content": "Сложная задача..."}],
reasoning_effort="high",
)
Токены размышлений учитываются как выходные — это видно в usage.
Ошибки
| HTTP | code |
Когда |
|---|---|---|
| 400 | invalid_request |
Тело не JSON либо нет model/непустого messages |
| 400 | invalid_input / content_blocked |
Терминальный отказ апстрима: неверный вход или модерация |
| 401 | invalid_api_key |
Нет/неверный ключ |
| 402 | insufficient_credits |
Не хватает кредитов |
| 404 | model_not_found |
Неизвестная модель |
| 413 | invalid_request |
Объявленный Content-Length больше 25 МБ |
| 429 | rate_limited |
Превышен лимит запросов (лимиты) |
| 502 | model_unavailable |
Все провайдеры модели доказанно недоступны, либо попытку не удалось подтвердить. Эти случаи имеют один публичный code, поэтому не повторяй чат автоматически — см. ниже |
Формы ошибок зависят от пути (короткий JSON, failed-job, framework/direct-host exceptions) —
точная матрица в разделе Ошибки. Поле message у chat-отказа — наш текст по коду;
ответ апстрима в него не попадает. Логику всё равно строй на code, а не на message.
Неподтверждённая попытка
Если связь с апстримом оборвалась после отправки запроса (таймаут, обрыв соединения,
408 или любой 5xx от шлюза), мы не знаем, выполнил он ответ или нет. В этом случае мы не повторяем запрос
у другого провайдера — иначе ты рискуешь получить двойную работу и мы точно платим дважды. Ты
сразу получаешь 502 model_unavailable, а запрос остаётся неразрешённым у нас внутри.
Что это значит на практике:
- ответа нет, и повторить его автоматически мы не будем;
- резерв кредитов освобождается, только когда апстрим подтвердит, что запрос не был принят или результата нет. Если проверить судьбу нечем, резерв остаётся удержанным до решения оператора;
Idempotency-Keyна chat не действует. Любой ручной повтор после500,502, сетевого обрыва или таймаута — новый вызов со своей ценой и может создать вторую платную работу. Сначала посмотри историю списаний; решение о повторе принимай сам или дедуплицируй его в своей системе.