← Todos os posts

Migrando da OpenAI em duas linhas

20 ago 2026 · 7 min de leitura · api, migration

"OpenAI-compatible" costuma significar "quase compatível, e você descobre onde não é durante o deploy". Então vale ser específico sobre o que exatamente muda quando você aponta um cliente existente para a unbleep — e sobre os dois lugares onde há de fato uma diferença.

A migração em si é o que promete ser: base_url e api_key. O SDK, os tipos, o tratamento de erros, o streaming e o seu código de retry continuam iguais.

As duas linhas

python
import os

from openai import OpenAI

# Before
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

# After -- same SDK, same call sites, same response objects.
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": "Say hello."}],
)
print(resp.choices[0].message.content)

Se você não quer nem tocar no código, o SDK da OpenAI lê as duas coisas do ambiente. Isso funciona bem para migrar um serviço já em produção atrás de uma flag:

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

# Any script that constructs OpenAI() with no arguments now talks to unbleep.
python -c "
from openai import OpenAI
print(OpenAI().chat.completions.create(
    model='unbleep',
    messages=[{'role':'user','content':'ping'}],
).choices[0].message.content)
"

As chaves carregam prefixo — ub_live_… para produção e ub_test_… para desenvolvimento local. As duas são cobradas do mesmo crédito pré-pago, à mesma tarifa por token: a de teste é apenas uma credencial separada e revogável, com um teto menor (15 requisições/minuto em vez das 60 da conta). Não é um tier gratuito. O prefixo existe para que um vazamento seja óbvio para secret scanners.

Os três modelos

Passe um destes ids como model. Os ids datados (unbleep-250811, unbleep-high-250811, unbleep-mini-250811) também são aceitos, mas hoje resolvem para o mesmo build do id sem data — e a resposta devolve sempre o id sem data. Trate-os como grafia à prova de futuro, não como garantia de reprodutibilidade: se um experimento precisa ser repetível, guarde as saídas, não o id do modelo.

| Modelo | Contexto | Preço in/out por 1M | Para quê | |---|---|---|---| | unbleep | 256K | $3.00 | O padrão. Uso geral, tier de raciocínio. | | unbleep-high | 1M* | $5.00 | Jobs grandes — codebases inteiras, transcrições longas, análise multi-documento. | | unbleep-mini | 32K | $1.00 | Barato e rápido, para alto volume. Responde direto, sem etapa de raciocínio. |

*O corpo da requisição é limitado a 2.000.000 bytes — cerca de 500 mil tokens —, então uma única chamada não chega a preencher a janela de 1M; acima disso a resposta é 413 payload_too_large.

Input e output custam o mesmo em todos os tiers, o que simplifica a estimativa: some tudo e multiplique. A cobrança é em micro-USD inteiros, arredondada para cima, então não há erro de ponto flutuante acumulando ao longo de milhares de chamadas.

O teto de 32K do unbleep-mini é o que pega quem migra de um modelo com contexto de 128K: um prompt que cabia antes passa a ser rejeitado. Se você roteia por custo, roteie por tamanho também.

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"

Streaming

Idêntico à OpenAI: stream=True devolve Server-Sent Events, cada evento é um chat.completion.chunk com um delta, e o fluxo termina com um literal data: [DONE]. Seu loop de streaming atual não muda.

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}
  }'

Você recebe a contagem de tokens pedindo ou não: a unbleep sempre solicita o usage ao backend e o repassa, e só um stream_options: {"include_usage": false} explícito o remove na saída. De um jeito ou de outro, o fluxo termina com um chunk final de choices vazio — carregando o objeto usage preenchido, a menos que você tenha desligado — e é por isso que o loop abaixo checa choices antes de mexer nele. Indexar choices[0] direto quebra ali.

python
stream = client.chat.completions.create(
    model="unbleep-high",
    messages=[{"role": "user", "content": "Threat-model a public S3 bucket."}],
    stream=True,
)

for chunk in stream:
    if not chunk.choices:      # final usage-only chunk carries no choices
        continue
    delta = chunk.choices[0].delta
    if delta.content:
        print(delta.content, end="", flush=True)

O campo reasoning_content

Aqui está a única adição real ao schema. Os tiers de raciocínio (unbleep e unbleep-high) retornam, além de content, um campo reasoning_content com a cadeia de raciocínio. O unbleep-mini não é um modelo de raciocínio — ele responde direto e o campo simplesmente não aparece.

Três coisas que importam na prática:

O nome é normalizado. Backends diferentes chamam esse campo de coisas diferentes — alguns usam reasoning, outros o reasoning_content que virou padrão de facto desde o DeepSeek. A API sempre expõe reasoning_content, qualquer que seja o nome upstream, então você não precisa de um branch por modelo.

O SDK não tipa esse campo. Como reasoning_content não faz parte do schema oficial da OpenAI, os modelos Pydantic do openai-python não o declaram nos type stubs — ele chega em model_extra. Como esses modelos aceitam campos extras, delta.reasoning_content funciona em runtime — mas o campo só existe quando o modelo produziu traço (o unbleep-mini nunca produz). Leia com getattr(delta, "reasoning_content", None) ou pelo dicionário de extras:

python
stream = client.chat.completions.create(
    model="unbleep",
    messages=[{"role": "user", "content": "Why does TCP need a three-way handshake?"}],
    stream=True,
)

thinking = False
for chunk in stream:
    if not chunk.choices:
        continue
    delta = chunk.choices[0].delta
    # Not in the OpenAI schema, so the SDK parks it in `model_extra`
    # rather than exposing it as a typed attribute.
    thought = (delta.model_extra or {}).get("reasoning_content")
    if thought:
        if not thinking:
            print("\n--- reasoning ---")
            thinking = True
        print(thought, end="", flush=True)
    elif delta.content:
        if thinking:
            print("\n--- answer ---")
            thinking = False
        print(delta.content, end="", flush=True)

O mesmo vale fora do streaming: resp.choices[0].message.model_extra["reasoning_content"].

Raciocínio é output pago. Os tokens de reasoning_content contam como completion tokens, do mesmo jeito que em qualquer provedor de modelo de raciocínio. Se você está estimando custo pelo tamanho da resposta visível, vai subestimar. Para trabalho de alto volume onde a cadeia não interessa, o unbleep-mini sai mais barato duas vezes: preço por token menor e nenhum token de raciocínio.

Esses tokens também consomem max_tokens: um limite apertado num tier de raciocínio pode ser gasto inteiro no traço, e o que sobra é uma resposta truncada ou um content vazio. Reserve orçamento para os dois — ou desligue o raciocínio quando não precisar dele. Mande "thinking": false via extra_body em chamadas curtas ou de alto volume; reasoning_effort também passa direto.

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
)

Nunca realimente reasoning_content num turno seguinte como conteúdo do assistant. É saída de diagnóstico, não histórico de conversa, e repeti-la degrada a resposta seguinte.

Erros e rate limits

Mesmo envelope da OpenAI, então o seu try/except existente já cobre tudo. O único código novo a tratar é o 402:

| Status | Significado | |---|---| | 401 | Chave ausente ou inválida | | 402 | Crédito esgotado — faça um top-up para continuar | | 422 | Bloqueado por policy: strict | | 429 | Rate limit — aplique backoff | | 5xx | Erro upstream — seguro re-tentar com backoff |

Falhas transitórias do upstream (um modelo "at capacity", 429 e 5xx) já são re-tentadas internamente antes de o primeiro byte chegar até você, e essa re-tentativa nunca cobra duas vezes. Os headers x-ratelimit-limit-requests, -remaining-requests e -reset-requests vêm em toda resposta.

Um último parâmetro que não existe na OpenAI: policy (off por padrão, research, strict). off e research respondem exatamente igual — research apenas grava o valor na sua linha de uso, para o seu próprio relatório. Só strict faz triagem: contrasta o texto com a blocklist do serviço (mantida por nós, igual para todo mundo, sem blocklist por conta) e devolve 422 em caso de match. Como não está no schema do SDK, mande via extra_body, senão ele é descartado na serialização:

python
resp = client.chat.completions.create(
    model="unbleep",
    messages=[{"role": "user", "content": "…"}],
    extra_body={"policy": "research"},
)

Antes de subir

O que muda de verdade no comportamento é o modelo não recusar — e isso desloca o julgamento para a sua camada. A política de uso delimita o terreno em detalhe. Se você expõe essa saída para usuários finais, a checagem prática é banal e vale fazer antes do deploy: rotule a superfície, tenha alguém responsável pelo output, e não trate "sem filtro" como sinônimo de "sem revisão".

Se o seu código já chama Chat Completions, pegue uma chave e troque as duas linhas — dá para medir a diferença na primeira chamada.