"Compatible con OpenAI" se usa con mucha soltura y casi nunca significa lo mismo. En la práctica hay tres niveles: el que acepta el mismo JSON pero devuelve otra forma, el que devuelve la forma correcta pero rompe en streaming o en tool calls, y el que un SDK sin modificar no distingue del original. unbleep apunta al tercero: POST /v1/chat/completions con el esquema de OpenAI en la request y en la response, incluidos los eventos SSE y la envoltura de errores.
Lo que sigue es la migración completa, con lo que cambia y —sobre todo— lo que no.
Los dos cambios
from openai import OpenAI
client = OpenAI(
base_url="https://unbleep.ai/v1", # was: the default OpenAI endpoint
api_key="ub_live_9f2c...", # keys are prefixed: ub_live_ / ub_test_
)
resp = client.chat.completions.create(
model="unbleep",
messages=[
{"role": "system", "content": "You are a security analyst. Be terse."},
{"role": "user", "content": "Summarise this advisory in five bullets."},
],
temperature=0.2,
max_tokens=600,
)
print(resp.choices[0].message.content)Eso es todo. El resto de tu código —reintentos, timeouts, parsing, tipos del SDK, la capa de observabilidad que ya escribiste— sigue igual, porque choices[0].message.content, finish_reason y usage conservan sus nombres y su semántica.
Si prefieres no tocar el código, el SDK lee ambos valores del entorno, así que basta con un cambio de configuración:
export OPENAI_BASE_URL="https://unbleep.ai/v1"
export OPENAI_API_KEY="ub_live_9f2c..."Ese mismo par cubre buena parte del ecosistema construido sobre el SDK —LangChain, LlamaIndex, Instructor, el Vercel AI SDK, cualquier cosa que exponga un ajuste de base URL.
Las keys llevan prefijo a propósito: ub_live_ y ub_test_ facturan igual contra tu crédito prepago, a la misma tarifa por token. La de test es solo una credencial aparte y revocable, con un techo más bajo —15 peticiones/minuto en vez de las 60 de la cuenta—, no un tier gratuito. El prefijo hace que un secret scanner reconozca una filtración sin reglas a medida.
Y el mismo request en curl, útil para verificar red y credenciales antes de tocar la aplicación:
curl https://unbleep.ai/v1/chat/completions \
-H "Authorization: Bearer ub_live_9f2c..." \
-H "Content-Type: application/json" \
-d '{
"model": "unbleep",
"messages": [{"role": "user", "content": "ping"}],
"max_tokens": 32
}'Los modelos
Los IDs son nuestros, no del modelo open-weight que hay debajo. Es deliberado: podemos mejorar los pesos sin que tu código se entere.
unbleep— 256K de contexto. El default: uso general, evaluación, análisis por muestra.unbleep-high— 1M de ventana anunciada. Los trabajos grandes: repositorios completos, transcripciones largas, análisis multi-documento. Un aviso: el cuerpo de la petición está limitado a 2.000.000 bytes —unos 500k tokens—, así que una sola llamada no llega a llenar la ventana de 1M; por encima recibes un 413payload_too_large.unbleep-mini— 32K. Barato y rápido, para volumen alto y llamadas simples.
El alias sin fecha siempre apunta al build más reciente. Los ids fechados —unbleep-250811, unbleep-high-250811, unbleep-mini-250811— se aceptan, pero hoy resuelven al mismo build que el id sin fecha y la respuesta devuelve el id corto. Son una grafía a prueba de futuro, no una garantía de reproducibilidad: si una evaluación tiene que repetirse, guarda las salidas. GET /v1/models lista lo disponible en el mismo formato que espera tu SDK.
Streaming
Con stream=True la respuesta es text/event-stream: una secuencia de objetos chat.completion.chunk con un delta, cerrada por un literal data: [DONE]. Es exactamente lo que tu cliente ya parsea, así que el bucle no cambia:
stream = client.chat.completions.create(
model="unbleep",
messages=[{"role": "user", "content": "Walk me through the analysis."}],
stream=True,
)
for chunk in stream:
if not chunk.choices: # el chunk final solo trae usage
continue
print(chunk.choices[0].delta.content or "", end="", flush=True)Dos detalles operativos que ahorran una tarde de depuración. Primero: si tienes un proxy propio delante, desactiva el buffering en esa ruta, o los chunks se acumularán y el streaming dejará de serlo sin ningún error visible. Segundo: los contadores de tokens llegan por defecto —el stream termina con un chunk que trae choices vacío y usage poblado—, así que no hace falta pedirlos; si no los quieres, quítalos con stream_options={"include_usage": False}. Ese chunk final es la razón de comprobar choices antes de indexarlo.
reasoning_content
Esta es la única adición que probablemente quieras usar. Los tiers de razonamiento —unbleep y unbleep-high— piensan antes de responder, y esa traza se expone en un campo aparte, reasoning_content, junto a content. Sigue la convención de DeepSeek, y la normalizamos siempre a ese nombre aunque el backend la llame de otra forma. unbleep-mini no razona: responde directo y no emite el campo.
En streaming el campo llega en el delta, lo que permite mostrar el razonamiento en un panel separado mientras la respuesta se escribe:
stream = client.chat.completions.create(
model="unbleep-high",
messages=[{"role": "user", "content": "Why does this stack trace point at the wrong frame?"}],
stream=True,
)
for chunk in stream:
if not chunk.choices: # el chunk final solo trae usage
continue
delta = chunk.choices[0].delta
# `reasoning_content` is the chain of thought; keep it out of the user-facing
# transcript and out of anything you feed back as context.
if getattr(delta, "reasoning_content", None):
print(delta.reasoning_content, end="", flush=True)
if delta.content:
print(delta.content, end="", flush=True)Tres cosas que conviene tener claras antes de mandarlo a producción:
- La traza se factura como tokens de salida. Aparece desglosada en
usage.completion_tokens_details.reasoning_tokens. Si tu max_tokens es ajustado, el razonamiento se come el presupuesto de la respuesta.
- Se puede apagar. Manda
"thinking": falsevíaextra_bodypara que una pregunta corta gaste
su presupuesto en contestar y no en pensar. reasoning_effort también viaja al modelo si el tier lo soporta.
- No la reinyectes.
reasoning_contentno es parte del historial de la conversación; devolverlo
como mensaje previo degrada el turno siguiente. Guárdalo para auditoría o para tu UI, no para el contexto.
Desde el SDK, el flag viaja en extra_body:
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
)Con un límite así de corto en un tier de razonamiento, la traza puede gastarse todo el presupuesto y dejarte una respuesta truncada o un content vacío. Presupuesta para las dos cosas, o apaga el razonamiento.
Lo que sí es distinto
Poco, pero conviene saberlo:
policy— parámetro nuestro:off(default, sin filtro),research(responde exactamente igual queoff; el valor solo queda registrado en tu línea de uso para tus propios informes, no aplica ningún filtro ni etiquetado extra) ystrict(contrasta el texto del mensaje con la blocklist del servicio —que mantenemos nosotros y es la misma para todos los que se acogen; no hay blocklist por cuenta— y devuelve 422 al coincidir). Nunca se reenvía al modelo.- Errores — misma envoltura
{"error": {...}}de OpenAI, así que tu manejo actual funciona sin
cambios. Códigos nuevos: 402 (crédito agotado) y 422 (bloqueado por policy: strict). 429 y 5xx se reintentan con backoff, como siempre.
- Rate limits — los headers estándar
x-ratelimit-limit-requests,
x-ratelimit-remaining-requests y x-ratelimit-reset-requests vienen en cada respuesta.
- Parámetros desconocidos — solo relayamos campos de una allowlist explícita (samplers incluidos:
top_k, min_p, repetition_penalty, además de tools, response_format, seed). Un campo fuera de la lista se descarta en silencio en lugar de romper la llamada; si un parámetro te importa, verifica su efecto con una llamada de prueba.
Checklist de migración
- Cambia
base_urlyapi_key; deja el SDK como está. - Mapea tus nombres de modelo a
unbleep/unbleep-high/unbleep-mini; los ids fechados se aceptan pero resuelven al mismo build, así que no los uses como garantía de reproducibilidad. - Corre tu suite con una
ub_test_antes de mover tráfico real. - Agrega
402y422al manejo de errores. - Decide qué haces con
reasoning_content: mostrarlo, guardarlo o apagarlo.
El modelo responde sin rechazos reflejos; la responsabilidad por lo que construyes encima sigue siendo tuya, y las líneas están en la política de uso aceptable.
Crea tu cuenta, genera una key de test y haz la primera llamada antes de terminar el café.