unbleep — это OpenAI-совместимый API, что на практике означает: миграция занимает две строки и не требует новых зависимостей. Вы оставляете официальный SDK, свою логику ретраев, свой цикл стриминга, свой учёт токенов и свои обработчики ошибок. Меняется только то, куда уходят запросы и какой ключ их аутентифицирует. В этом посте — сама замена, а затем четыре отличия, достаточно существенные, чтобы что-нибудь сломать, если о них не знать.
Миграция на OpenAI-совместимый API в две строки
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 читает оба значения из окружения, так что достаточно изменить конфигурацию:
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: промпт, который раньше помещался, теперь будет отклонён. Если вы маршрутизируете по стоимости, маршрутизируйте и по длине.
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]. Ваш существующий цикл работает без изменений.
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, у которого трассы нет, не выбрасывал исключение:
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)}")Три практических следствия:
- Токены рассуждений — это выходные токены. Они входят в
completion_tokensи тарифицируются по ставке за выход.usage.completion_tokens_details.reasoning_tokensпоказывает, какая часть счёта ушла на размышления. - Они же расходуют
max_tokens. Жёсткий лимит на уровне с рассуждениями может целиком уйти на трассу, оставив вам обрезанный ответ или пустойcontent. Закладывайте бюджет на оба — или отключайте размышления. - Отключайте, когда не нужно. Для коротких или массовых вызовов передавайте
"thinking": falseчерезextra_body.reasoning_effortтоже пробрасывается.
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-ключ — миграция действительно занимает две строки.