← Все посты

OpenAI-совместимый API как drop-in замена — миграция в две строки

20 авг 2026 · 5 мин чтения · api, migration

unbleep — это OpenAI-совместимый API, что на практике означает: миграция занимает две строки и не требует новых зависимостей. Вы оставляете официальный SDK, свою логику ретраев, свой цикл стриминга, свой учёт токенов и свои обработчики ошибок. Меняется только то, куда уходят запросы и какой ключ их аутентифицирует. В этом посте — сама замена, а затем четыре отличия, достаточно существенные, чтобы что-нибудь сломать, если о них не знать.

Миграция на OpenAI-совместимый API в две строки

python
import os

from openai import OpenAI

client = OpenAI(
    base_url="https://unbleep.ai/v1",
    api_key=os.environ["UNBLEEP_API_KEY"],
)

resp = client.chat.completions.create(
    model="unbleep",
    messages=[{"role": "user", "content": "Summarise this incident report."}],
)
print(resp.choices[0].message.content)

Если код трогать совсем не хочется — SDK читает оба значения из окружения, так что достаточно изменить конфигурацию:

bash
export OPENAI_BASE_URL="https://unbleep.ai/v1"
export OPENAI_API_KEY="ub_live_9f2c..."

Та же пара переменных покрывает большую часть экосистемы, построенной поверх SDK, — LangChain, LlamaIndex, Instructor, Vercel AI SDK, всё, где есть настройка base URL. У ключей есть префикс, чтобы утечки бросались в глаза сканерам секретов: и ub_live_, и ub_test_ списывают с одного и того же предоплаченного кредита по одной и той же ставке за токен. Тестовый ключ — это отдельные отзываемые учётные данные с более низким потолком — 15 запросов в минуту вместо 60 у аккаунта, — а не бесплатный тариф. Оба держите на стороне сервера.

GET /v1/models работает, поэтому инструменты, которые перечисляют модели, чтобы заполнить выпадающий список, в особой обработке не нуждаются.

Выбор модели

Три уровня. Датированные идентификаторы — unbleep-250811, unbleep-high-250811, unbleep-mini-250811 — принимаются как алиасы, но сегодня они разрешаются в ту же сборку, что и короткий id, и в ответе возвращается короткий id. Считайте их совместимым на будущее способом записи, а не гарантией воспроизводимости; если оценка должна быть повторяемой, сохраняйте выходы, а не идентификатор модели.

| Модель | Контекст | Цена вход / выход за 1M | Примечания | | --- | --- | --- | --- | | unbleep | 256K | $3.00 / $3.00 | По умолчанию. Уровень с рассуждениями. | | unbleep-high | 1M* | $5.00 / $5.00 | Самые большие задачи. Уровень с рассуждениями. | | unbleep-mini | 32K | $1.00 / $1.00 | Дёшево и быстро. Отвечает сразу, без трассы рассуждений. |

*Тело запроса ограничено 2 000 000 байт — примерно 500k токенов, — так что одним вызовом заполнить окно в 1M на деле нельзя; всё, что больше, возвращается с 413 payload_too_large.

Потолок в 32K у unbleep-mini — тот самый, на который натыкаются мигрирующие с модели с контекстом 128K: промпт, который раньше помещался, теперь будет отклонён. Если вы маршрутизируете по стоимости, маршрутизируйте и по длине.

python
def pick_model(prompt_chars: int) -> str:
    """~4 chars/token is a deliberate under-estimate; leave room for the completion."""
    est_tokens = prompt_chars // 4
    if est_tokens < 24_000:
        return "unbleep-mini"
    return "unbleep" if est_tokens < 200_000 else "unbleep-high"

Стриминг

Установите stream=True — и получите стандартные Server-Sent Events: каждое событие — это chat.completion.chunk с delta, а поток завершается буквальным data: [DONE]. Ваш существующий цикл работает без изменений.

bash
curl -N https://unbleep.ai/v1/chat/completions \
  -H "Authorization: Bearer $UNBLEEP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "unbleep",
    "messages": [{"role": "user", "content": "Explain the residual stream."}],
    "stream": true,
    "stream_options": {"include_usage": true}
  }'

Счётчики токенов вы получаете независимо от того, просили ли о них: unbleep всегда запрашивает usage у бэкенда и передаёт его дальше, и только явный stream_options: {"include_usage": false} вырезает его на выходе. В любом случае поток заканчивается одним финальным чанком с пустым массивом choices — и с заполненным объектом usage, если вы от него не отказались, — поэтому цикл ниже проверяет choices, прежде чем к нему обращаться.

Поле reasoning_content

Это единственное настоящее дополнение к схеме. unbleep и unbleep-high думают, прежде чем ответить, и эта цепочка рассуждений возвращается в reasoning_content — соседе content в message (без стриминга) или в delta (со стримингом). Upstream-бэкенды расходятся в том, называть ли его reasoning или reasoning_content; API нормализует всё к reasoning_content, так что вы всегда имеете дело с одной формой.

Поскольку поле не входит в схему OpenAI, его нет в type stubs SDK. Модели ответа допускают дополнительные поля, так что доступ через атрибут работает во время выполнения — но читайте его через getattr, чтобы ответ от mini, у которого трассы нет, не выбрасывал исключение:

python
import sys

stream = client.chat.completions.create(
    model="unbleep",
    messages=[{"role": "user", "content": "Why did this detection rule misfire?"}],
    stream=True,
    stream_options={"include_usage": True},
)

usage = None
for chunk in stream:
    if not chunk.choices:          # final usage-only chunk
        usage = chunk.usage
        continue
    delta = chunk.choices[0].delta
    thought = getattr(delta, "reasoning_content", None)
    if thought:                    # trace to stderr, answer to stdout
        sys.stderr.write(thought)
    if delta.content:
        sys.stdout.write(delta.content)

if usage:
    details = usage.completion_tokens_details
    print(f"\nreasoning tokens: {getattr(details, 'reasoning_tokens', 0)}")

Три практических следствия:

python
resp = client.chat.completions.create(
    model="unbleep",
    messages=[{"role": "user", "content": "phishing or benign?"}],
    max_tokens=4,
    extra_body={"thinking": False},   # spend the budget on the answer, not the trace
)

Никогда не подавайте reasoning_content обратно в следующий ход как содержимое assistant. Это диагностический вывод, а не история диалога, и его повторная подача ухудшает следующий ответ.

Ошибки и два настоящих подводных камня

Ошибки используют конверт OpenAI — {"error": {"type", "code", "message"}}, — так что ваши существующие блоки except продолжают работать. Коды статусов сопоставляются ожидаемо: 401 — плохой ключ, 422 — заблокировано policy: strict, 429 — ограничение частоты, 5xx — повторяемая ошибка upstream.

Статус, который вы, вероятно, никогда не обрабатывали, — 402, кредит исчерпан. Аккаунты предоплаченные, так что нет ни перерасхода, ни счёта на оплату — запросы просто останавливаются, пока вы не пополните баланс. OpenAI сигнализирует об исчерпании квоты через 429, а значит, при наивной миграции ваша логика backoff будет ретраить 402 бесконечно. Считайте его терминальным и настройте на него алерт.

Второй подводный камень: system_fingerprint не возвращается. Он идентифицирует обслуживающий бэкенд, поэтому вырезается вместе с остальными вендорскими полями. Если вы строите на нём ключ кэша или проверку воспроизводимости, понадобится собственный маркер версии: датированные идентификаторы моделей — это алиасы текущей сборки, а не замороженные снимки, так что о смене бэкенда они вам не скажут.

Лимиты частоты приходят заголовками в каждом ответе — x-ratelimit-limit-requests, x-ratelimit-remaining-requests, x-ratelimit-reset-requests, — так что batch-задача может дозировать частоту запросов, а не обнаруживать потолок, упёршись в него. Есть и второй потолок, которого заголовки не описывают: не более 8 запросов в полёте на аккаунт, а девятый возвращается с 429 и кодом too_many_concurrent_requests. Стриминговый вызов удерживает свой слот, пока поток не завершится, так что ограничьте собственный пул воркеров восемью.

На что вы указываете

Стоит сказать прямо: модели за этим эндпоинтом — abliterated, то есть их поведение отказа удалено на уровне весов. В этом и смысл — это API для разработчиков, нацеленный на исследования безопасности, red-teaming и оценку, где отказ является ошибкой измерения. Это также значит, что привычных защитных фильтров, которые перехватили бы плохой промпт, здесь нет, — так что держите человека ответственным за результаты и прочитайте политику допустимого использования. Законность использования — на вас.

Получите API-ключ — миграция действительно занимает две строки.