La plupart des gens ne commencent pas un projet en pensant à la portabilité entre fournisseurs. Ils prennent une clé chez le fournisseur le plus simple pour s'inscrire, écrivent l'intégration et livrent. Quelques mois plus tard, ils lisent un benchmark, ou ouvrent une facture, ou se heurtent à une limite de débit, et ils veulent essayer autre chose. C'est là que la douleur apparaît, parce qu'ils ont intégré des hypothèses sur le premier fournisseur dans la couche HTTP.
La solution n'est pas une bibliothèque d'abstraction. C'est un format d'échange. L'API de complétions de chat d'OpenAI est discrètement devenue le protocole par défaut pour communiquer avec les LLM, et une fois que vous écrivez contre elle, changer de fournisseur revient essentiellement à modifier deux chaînes de caractères.
L'API que tout le monde a copiée
OpenAI a défini un contrat simple : vous envoyez du JSON en POST à /v1/chat/completions, et vous recevez en retour une chaîne choices[0].message.content. La requête ressemble à ceci :
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"}]
}'Presque tous les grands modèles parlent désormais ce dialecte. DeepSeek, Qwen, ERNIE, Doubao et les autres exposent tous un point de terminaison qui accepte le même corps de requête et renvoie la même structure. Cela signifie que votre code client ne se soucie pas de qui se trouve à l'autre bout. Vous pouvez échanger la chaîne model et le point de terminaison, et rien d'autre ne change.
Voici le même appel pointé vers un agrégateur qui héberge plusieurs de ces modèles sous une seule URL de base :
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 seul client, plusieurs modèles
En Python, vous instanciez normalement le SDK OpenAI une seule fois et passez un nom de modèle par requête. Si votre fournisseur prend en charge l'API compatible, vous définissez base_url une fois et gardez tout le reste identique :
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 fonction ne change pas. La chaîne du modèle change. C'est toute l'astuce. Le streaming fonctionne de la même manière :
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="")L'appel de fonctions, le mode JSON et les embeddings passent tous par la même surface compatible, donc vous n'êtes pas limité au simple chat lorsque vous changez de fournisseur.
Ce que le changement ne vous apporte pas
Je veux être clair sur la partie qui ne change pas gratuitement. Le transport est portable ; les modèles ne le sont pas.
Différents modèles réagissent différemment aux prompts. Un prompt système ajusté pour GPT-4o peut sous-performer sur un modèle de raisonnement qui attend une structure différente. Les fenêtres de contexte diffèrent, parfois considérablement : un modèle peut accepter 128k tokens tandis qu'un autre en accepte 32k. Les tokenizers diffèrent aussi, donc le même document coûte un nombre de tokens différent sur chaque modèle. La longueur maximale de sortie est un autre paramètre qui est propre à chaque modèle, pas au protocole.
Donc « changer sans modifier le code » signifie en réalité « changer sans réécrire votre client HTTP ». Vous vous devez quand même une session d'évaluation avant de diriger le trafic de production vers un nouveau modèle. La bonne nouvelle, c'est que l'API compatible rend cette évaluation peu coûteuse à exécuter, parce que le harnais n'est qu'une boucle sur des noms de modèles.
Un patron pratique
Ce que je fais réellement dans mes projets annexes, c'est garder une petite table de configuration, pas des modifications de code :
MODELS = {
"fast": "deepseek-chat",
"smart": "qwen-max",
"frontier": "gpt-4o",
}
def run(task, prompt):
return ask(MODELS[task], prompt)Vous voulez essayer un modèle moins cher pour la voie fast ? Changez une ligne. Vous voulez basculer vers un second fournisseur quand le premier est limité en débit ? Attrapez l'exception et réessayez avec la chaîne de modèle suivante. Rien de tout cela ne touche à la construction de la requête.
C'est dans ce patron de repli qu'un point de terminaison multi-modèles justifie sa valeur. Avec une seule URL de base et une seule clé, vous pouvez router, réessayer et faire des tests A/B entre fournisseurs, le tout via les deux lignes de configuration client que vous avez écrites le premier jour.
Particularités de réponse à surveiller
Le transport est compatible, mais quelques différences au niveau des réponses transparaissent encore. Les modèles de raisonnement renvoient parfois un champ supplémentaire comme reasoning_content à côté du message normal. Le code qui lit choices[0].message.content continue de fonctionner quoi qu'il arrive, mais vous voudrez peut-être afficher ce texte de raisonnement dans une vue de débogage. Les messages d'erreur varient aussi : un fournisseur renvoie une chaîne utile insufficient_quota, un autre renvoie un simple 429 sans corps. Si vous construisez des mécanismes de réessai, traitez tout 429 ou 5xx comme « attendre et réessayer » plutôt que d'analyser un texte spécifique à un fournisseur.
Rien de tout cela n'est une raison de rester verrouillé sur un seul fournisseur. C'est une raison de garder votre gestion d'erreurs générique et votre choix de modèle variable.
C'est la position que défend SiCore TokenWorks : un point de terminaison compatible OpenAI à https://api.token8341.com/v1 où la chaîne du modèle est la seule chose qui change lorsque vous passez entre GPT-4o, Claude, Gemini, DeepSeek, Qwen, ERNIE, Doubao, Spark et Pangu.