La maggior parte delle persone non inizia un progetto pensando alla portabilità tra provider. Prendono una chiave dal vendor con la registrazione più semplice, scrivono l'integrazione e rilasciano. Qualche mese dopo leggono un benchmark, o aprono una fattura, o si scontrano con un rate limit, e vogliono provare qualcos'altro. È lì che emerge il dolore, perché hanno incorporato le assunzioni sul primo provider nel layer HTTP.
La soluzione non è una libreria di astrazione. È un formato di trasmissione. L'API chat completions di OpenAI è diventata silenziosamente il protocollo predefinito per comunicare con gli LLM, e una volta che scrivi contro di essa, cambiare provider è per lo più questione di modificare due stringhe.
L'API che tutti hanno copiato
OpenAI ha definito un contratto semplice: invii un POST JSON a /v1/chat/completions, ricevi indietro una stringa choices[0].message.content. La richiesta si presenta così:
curl https://api.openai.com/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-d '{
"model": "gpt-4o",
"messages": [{"role": "user", "content": "Explain DNS in one paragraph"}]
}'Quasi ogni modello importante ora parla questo dialetto. DeepSeek, Qwen, ERNIE, Doubao e gli altri espongono tutti un endpoint che accetta lo stesso body e restituisce la stessa struttura. Questo significa che il tuo codice client non si preoccupa di chi c'è dall'altra parte. Puoi scambiare la stringa model e l'endpoint e nient'altro cambia.
Ecco la stessa chiamata indirizzata a un aggregatore che ospita diversi di questi modelli sotto un unico base URL:
curl https://api.token8341.com/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKENWORKS_API_KEY" \
-d '{
"model": "deepseek-chat",
"messages": [{"role": "user", "content": "Explain DNS in one paragraph"}]
}'Un solo client, molti modelli
In Python di solito istanzi l'SDK OpenAI una volta e passi un nome di modello per richiesta. Se il tuo provider supporta l'API compatibile, imposti base_url una volta e mantieni tutto il resto identico:
from openai import OpenAI
client = OpenAI(
base_url="https://api.token8341.com/v1",
api_key="sk-your-tokenworks-key",
)
def ask(model: str, prompt: str) -> str:
resp = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
)
return resp.choices[0].message.content
# Same function, three different model families.
print(ask("gpt-4o", "Summarize this log for me."))
print(ask("deepseek-chat", "Summarize this log for me."))
print(ask("qwen-max", "Summarize this log for me."))La funzione non cambia. La stringa del modello cambia. Tutto il trucco è qui. Lo streaming funziona allo stesso modo:
stream = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "user", "content": "Write a haiku about databases"}],
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta.content or ""
print(delta, end="")Il function calling, la modalità JSON e gli embeddings viaggiano tutti sulla stessa superficie compatibile, quindi non sei limitato alla semplice chat quando cambi.
Cosa non ti offre il cambio
Voglio essere chiaro sulla parte che non cambia gratuitamente. Il trasporto è portabile; i modelli no.
Modelli diversi rispondono ai prompt in modo diverso. Un system prompt ottimizzato per GPT-4o può rendere meno su un modello di reasoning che vuole una struttura diversa. Le finestre di contesto differiscono, a volte di molto: un modello può accettare 128k token mentre un altro ne accetta 32k. Anche i tokenizer differiscono, quindi lo stesso documento costa un numero diverso di token su ogni modello. La lunghezza massima di output è un'altra manopola che è per-modello, non per-protocollo.
Quindi "cambiare senza modificare il codice" in realtà è "cambiare senza riscrivere il tuo client HTTP". Ti devi comunque un giro di valutazione prima di indirizzare il traffico di produzione verso un nuovo modello. La buona notizia è che l'API compatibile rende quella valutazione economica da eseguire, perché l'harness è solo un ciclo sui nomi dei modelli.
Un pattern pratico
Quello che faccio davvero nei side project è mantenere una piccola tabella di configurazione, non modifiche al codice:
MODELS = {
"fast": "deepseek-chat",
"smart": "qwen-max",
"frontier": "gpt-4o",
}
def run(task, prompt):
return ask(MODELS[task], prompt)Vuoi provare un modello più economico per la corsia fast? Cambia una riga. Vuoi fare fallback su un secondo provider quando il primo è in rate limit? Cattura l'eccezione e ritenta con la stringa del modello successivo. Niente di tutto questo tocca la costruzione della richiesta.
È in questo pattern di fallback che un endpoint multi-modello si guadagna il suo valore. Con un base URL e una chiave puoi fare routing, retry e A/B test tra provider, tutto attraverso le stesse due righe di setup del client che hai scritto il primo giorno.
Stranezze nelle risposte a cui fare attenzione
Il trasporto è compatibile, ma alcune differenze a livello di risposta trapelano comunque. I modelli di reasoning a volte restituiscono un campo extra come reasoning_content accanto al messaggio normale. Il codice che legge choices[0].message.content continua a funzionare comunque, ma potresti voler mostrare quel testo di reasoning in una vista di debug. Anche i messaggi di errore variano: un provider restituisce una stringa utile insufficient_quota, un altro restituisce un 429 nudo senza body. Se stai costruendo dei retry, tratta qualsiasi 429 o 5xx come "fai backoff e ritenta" invece di analizzare testo specifico del vendor.
Niente di tutto questo è un motivo per restare bloccato su un solo provider. È un motivo per mantenere la gestione degli errori generica e la scelta del modello una variabile.
Questa è la posizione di SiCore TokenWorks: un endpoint compatibile con OpenAI su https://api.token8341.com/v1 dove la stringa del modello è l'unica cosa che cambia quando ti muovi tra GPT-4o, Claude, Gemini, DeepSeek, Qwen, ERNIE, Doubao, Spark e Pangu.