unbleep è un'API compatibile con OpenAI, il che in pratica significa che la migrazione è di due righe e non aggiunge dipendenze. Mantieni l'SDK ufficiale, la tua logica di retry, il tuo loop di streaming, il tuo conteggio dei token e i tuoi gestori di errori. Cambiano solo la destinazione delle richieste e la chiave che le autentica. Questo articolo copre lo scambio, e poi le quattro cose che differiscono abbastanza da rompere qualcosa se non le conosci.
Migrare all'API compatibile con OpenAI in due righe
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)Se preferisci non toccare affatto il codice, l'SDK legge entrambi i valori dall'ambiente, quindi basta una modifica di configurazione:
export OPENAI_BASE_URL="https://unbleep.ai/v1"
export OPENAI_API_KEY="ub_live_9f2c..."Quella stessa coppia copre gran parte dell'ecosistema costruito sopra l'SDK — LangChain, LlamaIndex, Instructor, il Vercel AI SDK, qualsiasi cosa esponga un'impostazione per la base URL. Le chiavi hanno un prefisso, così le fughe saltano all'occhio degli scanner di segreti: ub_live_ e ub_test_ addebitano entrambe sullo stesso credito prepagato, alla stessa tariffa per token. Una chiave di test è una credenziale separata e revocabile con un tetto più basso — 15 richieste al minuto invece delle 60 dell'account — non un piano gratuito. Tienile entrambe lato server.
GET /v1/models funziona, quindi gli strumenti che enumerano i modelli per popolare un menu a tendina non hanno bisogno di trattamenti speciali.
Scegliere un modello
Tre tier. Gli id datati — unbleep-250811, unbleep-high-250811, unbleep-mini-250811 — sono accettati come alias, ma oggi risolvono alla stessa build dell'id semplice, e la risposta restituisce l'id semplice. Trattali come una grafia pensata per la compatibilità futura, non come una garanzia di riproducibilità; se una valutazione deve essere ripetibile, registra gli output, non l'id del modello.
| Modello | Contesto | Prezzo in / out per 1M | Note | | --- | --- | --- | --- | | unbleep | 256K | $3.00 / $3.00 | Predefinito. Tier con reasoning. | | unbleep-high | 1M* | $5.00 / $5.00 | Per i job più grandi. Tier con reasoning. | | unbleep-mini | 32K | $1.00 / $1.00 | Economico e veloce. Risponde direttamente, senza traccia di reasoning. |
*Il corpo della richiesta è limitato a 2.000.000 di byte — circa 500k token — quindi una singola chiamata non può davvero riempire la finestra da 1M; qualsiasi cosa più grande torna con un 413 payload_too_large.
Il tetto di 32K su unbleep-mini è quello che coglie di sorpresa chi migra da un modello con contesto da 128K: un prompt che prima ci stava ora viene rifiutato. Se instradi in base al costo, instrada anche in base alla lunghezza.
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
Imposta stream=True e ottieni Server-Sent Events standard: ogni evento è un chat.completion.chunk che trasporta un delta, e lo stream termina con un data: [DONE] letterale. Il tuo loop esistente funziona senza modifiche.
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}
}'I conteggi dei token arrivano comunque, che tu li chieda o no: unbleep richiede sempre l'usage al backend e lo inoltra, e solo un esplicito stream_options: {"include_usage": false} lo rimuove in uscita. In ogni caso lo stream termina con un chunk finale che ha un array choices vuoto — e che trasporta l'oggetto usage popolato, a meno che tu non l'abbia disattivato — ed è per questo che il loop qui sotto controlla choices prima di toccarlo.
Il campo reasoning_content
Questa è l'unica vera aggiunta allo schema. unbleep e unbleep-high ragionano prima di rispondere, e quella catena di pensiero viene restituita in reasoning_content, un campo fratello di content nel messaggio (non-streaming) o nel delta (streaming). I backend a monte non concordano se chiamarlo reasoning o reasoning_content; l'API normalizza a reasoning_content, così gestisci sempre un'unica forma.
Poiché non fa parte dello schema OpenAI, non è nei type stub dell'SDK. I modelli di risposta ammettono campi extra, quindi l'accesso per attributo funziona a runtime — ma leggilo con getattr, così una risposta di mini, che non ha traccia, non solleva un'eccezione:
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)}")Tre conseguenze pratiche:
- I token di reasoning sono token di output. Compaiono in
completion_tokense vengono fatturati alla tariffa di output.usage.completion_tokens_details.reasoning_tokensti dice quanta parte del conto se n'è andata in ragionamento. - Consumano anche
max_tokens. Un limite stretto su un tier con reasoning può essere speso interamente sulla traccia, lasciandoti una risposta troncata o uncontentvuoto. Metti a budget entrambi, oppure disattiva il reasoning. - Disattivalo quando non ti serve. Invia
"thinking": falsetramiteextra_bodyper chiamate brevi o ad alto volume. Anchereasoning_effortpassa attraverso.
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
)Non reinserire mai reasoning_content in un turno successivo come contenuto dell'assistant. È output diagnostico, non cronologia della conversazione, e riprodurlo degrada la risposta successiva.
Errori e le due vere insidie
Gli errori usano l'envelope di OpenAI — {"error": {"type", "code", "message"}} — quindi i tuoi blocchi except esistenti continuano a funzionare. I codici di stato mappano come ti aspetteresti: 401 chiave non valida, 422 bloccato da policy: strict, 429 rate limit, 5xx errore upstream ritentabile.
Il codice di stato che probabilmente non hai mai gestito è il 402, credito esaurito. Gli account sono prepagati, quindi non c'è eccedenza né fattura — le richieste semplicemente si fermano finché non ricarichi. OpenAI segnala l'esaurimento della quota con un 429, il che significa che nella migrazione ingenua la tua logica di backoff ritenta un 402 all'infinito. Trattalo come terminale e fai scattare un alert.
La seconda insidia: system_fingerprint non viene restituito. Identifica il backend di serving, quindi viene rimosso insieme agli altri campi del vendor. Se ci agganci una cache o un controllo di riproducibilità, ti servirà un marcatore di versione tuo: gli id datati dei modelli sono alias della build corrente, non snapshot congelati, quindi non ti diranno quando il backend cambia.
I rate limit tornano come header su ogni risposta — x-ratelimit-limit-requests, x-ratelimit-remaining-requests, x-ratelimit-reset-requests — così un job batch può regolare la propria frequenza di richieste invece di scoprire quel tetto sbattendoci contro. C'è un secondo tetto che gli header non descrivono: al massimo 8 richieste in volo per account, e la nona torna 429 con codice too_many_concurrent_requests. Una chiamata in streaming tiene occupato il suo slot finché lo stream non termina, quindi limita a 8 il tuo pool di worker.
A cosa stai puntando
Vale la pena essere espliciti: i modelli dietro questo endpoint sono abliterated, cioè il loro comportamento di rifiuto è stato rimosso a livello dei pesi. È questo il punto — è un'API per sviluppatori pensata per la ricerca sulla sicurezza, il red teaming e la valutazione, dove un rifiuto è un errore di misura. Significa anche che i consueti guardrail non ci sono a intercettare un prompt sbagliato, quindi mantieni un essere umano responsabile degli output e leggi la policy di uso accettabile. L'uso lecito è a carico tuo.
Ottieni una chiave API — la migrazione è davvero di due righe.