Чат

.md

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, сетевого обрыва или таймаута — новый вызов со своей ценой и может создать вторую платную работу. Сначала посмотри историю списаний; решение о повторе принимай сам или дедуплицируй его в своей системе.