OpenAI gateway · Claude / Codex / Antigravity
Шлюз заперт в сетевом контуре и слушает только адрес veth: наружу из контура ведёт единственный маршрут — в Contour, он же выбирает страну. Панель живёт отдельным процессом и открыта в локальную сеть.
claude-code/sonnet:high └────┬───┘ └──┬─┘ └┬─┘ бэкенд модель уровень размышлений
Без суффикса модель не думает — отвечает сразу.
Суффиксы: :low, :medium, :high,
:xhigh, :max, а :none — то же,
что без суффикса. Какие уровни поддерживает конкретная модель, видно
на вкладке «Модели».
Уровень стоит в имени модели не случайно: так он попадает в журнал и в статистику. По записи «claude-code/sonnet:low» видно, как ход выполнялся на самом деле, — поля в теле запроса такой отметки не оставляют.
curl http://10.200.0.2:4110/v1/chat/completions \
-H "Authorization: Bearer ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-code/haiku",
"messages": [{"role": "user", "content": "Привет"}]
}'
from openai import OpenAI
client = OpenAI(
base_url="http://10.200.0.2:4110/v1",
api_key="ВАШ_КЛЮЧ",
)
answer = client.chat.completions.create(
model="claude-code/opus:high",
messages=[{"role": "user", "content": "Привет"}],
)
print(answer.choices[0].message.content)
# ход мысли приезжает отдельным полем
print(answer.choices[0].message.reasoning)
import OpenAI from 'openai';
const client = new OpenAI({
baseURL: 'http://10.200.0.2:4110/v1',
apiKey: 'ВАШ_КЛЮЧ',
});
const answer = await client.chat.completions.create({
model: 'claude-code/sonnet',
messages: [{ role: 'user', content: 'Привет' }],
});
console.log(answer.choices[0].message.content);
Проще всего — суффиксом в имени модели. Но есть и поля в теле, для
клиентов, которые имя модели не трогают: reasoning_effort
как у OpenAI и объект reasoning как у OpenRouter.
{
"model": "claude-code/sonnet",
"messages": [{"role": "user", "content": "Задача посложнее"}],
"reasoning": {
"effort": "high",
"max_tokens": 6000,
"enabled": true,
"exclude": false
}
}
Старшинство: суффикс в имени модели → reasoning →
reasoning_effort. Исключения два: enabled: false
и max_tokens сильнее суффикса — они задают не глубину,
а сам режим.
{
"choices": [{
"message": {
"role": "assistant",
"content": "Ответ, который видит пользователь",
"reasoning": "Ход мысли модели — показывать отдельно"
}
}],
"usage": {
"completion_tokens_details": {"reasoning_tokens": 881}
}
}
Ход мысли едет полем reasoning, а не внутри
content: иначе клиент не отличил бы рассуждения от
ответа и показал бы всё вперемешку. В потоке для них свои кадры —
delta.reasoning.
Точный потолок задать нельзя. Нынешние модели думают
adaptive и фиксированный бюджет игнорируют: запрос с
max_tokens: 6000 давал ноль токенов на размышления, тогда
как :max — 755. Поэтому число переводится в уровень
усилия: до 2000 — low, до 8000 — medium, до 16000 — high, дальше max.
Модель сама решает, думать ли: на лёгком вопросе она честно
ответит нулём токенов даже при :max. Уровень задаёт
потолок глубины, а не обязанность.
У Codex размышления полностью не выключаются: низший работающий
уровень — low, и без суффикса используется он, а текст
рассуждений просто не возвращается.
У Antigravity так же: уровни — какие есть у модели в
agy models (у Gemini — low, medium и high), без
суффикса — низший. Текст рассуждений шлюз читает из файла разговора
agy: у Gemini это их пересказ, и приходит он целым
куском после каждого вызова модели, а не по мере набора.
stream = client.chat.completions.create(
model="claude-code/sonnet:high",
messages=[{"role": "user", "content": "Привет"}],
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta
if getattr(delta, "reasoning", None):
print("[думает]", delta.reasoning, end="")
if delta.content:
print(delta.content, end="")
Формат обычный: кадры chat.completion.chunk и
data: [DONE] в конце. У claude-code текст идёт по мере
появления — и в цикле инструментов тоже: придерживается только сам
вызов, текст перед ним уходит сразу. У codex так же, когда он
запущен постоянным app-server (по умолчанию); в запасном режиме
codex exec — одним кадром. У antigravity — по частям.
Расход едет в закрывающем кадре; отдельным кадром он приедет по
stream_options.include_usage.
Пока ход молчит дольше 10 секунд, поток будится комментарием
: ping раз в 15 секунд — иначе прокси с простоем в
минуту рвут соединение. Клиенты SSE такие строки пропускают. Ошибка,
случившаяся после этого, приходит кадром с кодом и
[DONE], а не HTTP-статусом.
Формат OpenAI целиком: объявляешь функции, получаешь
tool_calls, возвращаешь результаты сообщениями с ролью
tool. Работает у всех бэкендов.
{
"model": "claude-code/sonnet",
"messages": [{"role": "user", "content": "Какая погода в Томске?"}],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Возвращает погоду в городе",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
}
}
}]
}
Ответ придёт с finish_reason: "tool_calls". Дальше
присылаешь тот же диалог плюс сообщение с результатом —
{"role": "tool", "tool_call_id": "…", "content": "…"} —
и получаешь финальный ответ.
Несколько вызовов сразу поддерживаются: если модель зовёт
инструменты параллельно, они приедут одним массивом
tool_calls, и результаты можно вернуть все вместе —
по сообщению с ролью tool на каждый
tool_call_id, в одном запросе.
Результат может нести картинку — например, снимок экрана после
действия: content массивом из text и
image_url с data:-строкой, как у сообщения
пользователя. Модель увидит её картинкой внутри результата, а ход
продолжится в той же сессии. Отдельная реплика со снимком после
результата так не умеет: её некуда положить, и разговор
собирается заново. Системные сообщения за результатами
допускаются — модель получит их ролью system сразу после
результата, с приставкой «PostToolUse:… hook additional context:».
У Codex на app-server картинка в результате тоже доходит картинкой,
а указания после результата пока собирают разговор заново; в
режиме codex exec картинка становится пометкой.
У Antigravity картинку больше ~4 КБ agy выносит в файл,
и модель открывает её сама встроенным read_file — это
лишний вызов модели на снимок. Указания после результата
дописываются в конец самого результата: системным сообщением
agy их в этот момент не передаёт.
Числа кругов шлюз не ограничивает. «Ожидание результата инструмента» — сколько сессия ждёт тебя на КАЖДОМ круге, удерживая процесс в памяти; общую длину работы это не ограничивает.
{
"model": "claude-code/sonnet",
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": "Что на картинке?"},
{"type": "image_url",
"image_url": {"url": "data:image/png;base64,iVBOR..."}}
]
}]
}
Число картинок шлюз не ограничивает, размер одной — 8 МБ, меняется в настройках. Пределы провайдера: 100 картинок в запросе у моделей с окном 200 тыс., 600 у остальных; тело запроса до 32 МБ — и у провайдера, и у шлюза. Ссылки не скачиваются, только вложенный base64: иначе шлюз стал бы открытым прокси, через который можно тянуть что угодно откуда угодно.
curl http://10.200.0.2:4110/v1/images/generations \
-H "Authorization: Bearer ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{
"model": "codex/gpt-5.6-luna",
"prompt": "кот в скафандре"
}'
Ответ — data[].b64_json. Только бэкенд codex: у него
рисование встроено инструментом. Занимает заметно дольше чата —
считай десятки секунд.
Два разных способа, и путать их не стоит.
{
"model": "claude-code/sonnet",
"messages": [{"role": "user", "content": "Перечисли три пункта"}],
"stop": ["
4.", "КОНЕЦ"]
}
Для голосового помощника нужен второй способ: перебили — закрыл соединение, и агент перестаёт говорить. Раньше он договаривал в никуда и тратил окно подписки; теперь обрыв гасит ход.
Оговорка про stop: провайдер умеет обрывать сам, но
Agent SDK такую опцию не пробрасывает, поэтому обрезаем у себя.
Ответ приходит правильный, а вот токены за отброшенный хвост
списываются — модель договаривает до конца.
{
"model": "claude-code/sonnet",
"messages": [
{"role": "developer", "content": "Отвечай коротко."},
{"role": "user", "name": "Иван", "content": "Когда отгрузка?"},
{"role": "assistant", "content": "В четверг."},
{"role": "user", "name": "Пётр", "content": "А оплата?"},
{"role": "system", "content": "Контекст на этот ход: счёт оплачен."},
{"role": "user", "name": "Иван", "content": "Подтверди Петру."}
]
}
Роли раскладываются по-настоящему, а не склеиваются в один текст: прошлый ответ модель видит как свой ход. Системные сообщения действуют с любого места переписки — этим удобно подкладывать свежий контекст под каждый вопрос.
Поле name у формата Claude своего места не имеет,
поэтому имя переносится в начало текста реплики. Без него разные
люди под ролью user слились бы в одного собеседника.
У Antigravity роли раскладываются так же, но agy
оборачивает их по-своему: системный промпт — в
<System Prompt>, реплику — в
<USER_REQUEST> с текущим временем, системное
сообщение — в <SYSTEM_MESSAGE> с пометкой, что
это не слова пользователя. Ход без реплики пользователя запускается
репликой (no content) — как у Claude.
X-Conversation-Id: conv-8f2a
Необязательный заголовок: один и тот же номер на все запросы одного разговора, включая круги инструментов. С ним шлюз не угадывает разговор по последним репликам, а продолжает сессию, только если всё начало присланной истории совпало с ней точно. Изменилось прежнее сообщение — например, история сжата в выжимку — и сессия собирается заново из присланного.
Заголовком, а не полем тела: незнакомое поле часть провайдеров
отклоняет, а заголовок — никто. До 128 символов: латиница, цифры
и ._:-. Работает, когда в настройках включено
продолжение разговоров.
Retry-After скажет, через сколько секунд повторять
Ошибки приходят в формате OpenAI:
{"error": {"code": "…", "message": "…", "type": "…"}}.
Тип следует из статуса: authentication_error,
permission_error, not_found_error,
rate_limit_error, для 5xx — api_error.
Тело каждого вызова, включая упавшие, лежит в журнале на вкладке
«Обзор» — вместе с текстом ошибки.
Команда повторного входа — выполнить в терминале сервера.