Dans un moteur de recherche sémantique d'animes, j'ai un reranker qui classe vingt-cinq candidats avec un LLM. Il tournait avec temperature: 0 depuis le premier jour, ce qui est l'évidence quand on veut un ordre reproductible. J'ai changé le modèle pour un modèle de raisonnement et l'appel s'est mis à renvoyer une erreur : ce modèle refuse temperature. Il ne l'ignore pas, il le refuse.
Le correctif n'a pas été de supprimer la ligne. Ça a été de la remplacer par reasoning_effort, de le sortir dans une constante avec low par défaut, et de le passer aussi à l'autre endroit où le même modèle était appelé, un script d'ingestion qui synthétise des prémisses de franchise. Trois fichiers, une nouvelle variable d'environnement et un document à mettre à jour. Pour un paramètre.
C'est arrivé six fois en un an et demi, et chaque fois d'une manière différente.
Les quatre types de casse
Après les avoir collectionnées, ce sont toujours les quatre mêmes qui reviennent :
- Le paramètre qui disparaît.
temperaturedans les modèles de raisonnement. Il n'y a pas d'équivalent exact :reasoning_effortn'est pas la même chose, c'est un autre levier. Tu ne migres pas la valeur, tu en décides une nouvelle. - La forme du payload. Passer de Chat Completions à la Responses API pour un parseur de vision, ce n'est pas changer le nom de la méthode.
messagesdevientinput,{"type": "text"}devientinput_text, etimage_urlcesse d'être un objet{url, detail}pour devenir une chaîne avecdetailcomme frère au même niveau. - La forme de la réponse. Dans cette même migration,
response.choices[0].message.contentest devenuresponse.output_text. Et le compteur de tokens a cessé d'être unique : il y a maintenantinput_tokensetoutput_tokensséparément. Ça a obligé à changer la signature de la fonction de-> strà-> tuple[str, int, int]et à les accumuler à la main en récupérant les futures en parallèle. - La qualité change sans prévenir. C'est le pire parce qu'il n'y a pas d'erreur. Le modèle répond, le JSON valide, et les résultats sont moins bons.
Les trois premiers, tu les vois au premier déploiement. Le quatrième, tu le manges pendant des semaines.
OpenRouter et LangChain déplacent le problème
La réaction logique est de mettre une couche par-dessus. Je l'ai fait deux fois.
Dans un SaaS de transcription, j'avais un lib/claude.ts avec un processWithClaude(text, prompt) qui appelait directement le SDK d'Anthropic. Je l'ai supprimé entièrement et remplacé par un singleton AIService contre OpenRouter, avec modelName et temperature comme paramètres venant du FormData de l'endpoint. Gain réel : changer de modèle est devenu envoyer une autre chaîne dans la requête.
Dans une app de chat avec projets et contextes, la couche a été LangChain, avec @langchain/anthropic et @langchain/google-genai pour pouvoir avoir un menu déroulant groupé par fournisseur. C'est là que j'ai appris l'autre moitié : la couche casse aussi. Un commit des premiers jours s'appelle littéralement "update dependencies for @langchain/anthropic and zod to latest versions". Mettre à jour vers latest des abstractions posées sur des APIs qui bougent, c'est échanger un problème que tu comprends contre un que tu ne comprends pas.
Ce que font ces couches est réel mais limité : elles normalisent la forme de l'appel. Elles ne normalisent pas les capacités. Si le modèle A accepte temperature et le B non, la couche te laisse passer les deux paramètres et l'erreur t'arrive quand même, sauf que maintenant depuis l'intérieur d'une bibliothèque, avec la stack trace trois niveaux plus bas.
Le coût d'un changement en plein produit
Le jour où j'ai changé le modèle d'un générateur d'emplois du temps de google/gemini-3-flash-preview à deepseek/deepseek-v3.2, le diff touchait trois fichiers : app/api/generate/route.ts, app/api/adjust-schedule/route.ts et app/api/verify-checks/route.ts. Le même littéral, copié trois fois dans trois JSON.stringify différents. Ce n'est pas la faute du fournisseur, c'est la mienne, et c'est ça le coût réel : quand le nom du modèle est incrusté au point d'appel, migrer est un find-and-replace en priant pour n'en avoir oublié aucun.
Dans un service d'analyse de documents, j'ai essayé de régler ça en centralisant modèles et calcul de coût dans un fichier de constantes. Ça m'a pris trois refactors en trois semaines : un le 18, un autre le lendemain, et un de plus avant. Le nom d'un des commits contient la coquille telle quelle, "reafactor constants", ce qui en dit long sur l'après-midi. La couche de constantes n'a pas pris du premier coup parce que j'essayais de faire entrer deux choses différentes dans la même structure : quel modèle utiliser et combien il coûte.
Comment j'isole l'appel aujourd'hui
Sans cérémonie. Trois règles.
Le modèle n'est jamais un littéral au point d'appel. Toujours une constante lue depuis l'environnement avec une valeur par défaut, du genre RERANK_MODEL = process.env.RERANK_MODEL || "gpt-5.4-mini". Changer de modèle en production ne devrait pas demander un déploiement.
Les paramètres spécifiques au modèle voyagent avec le modèle. Si un modèle a besoin de reasoning_effort et un autre de temperature, c'est de la configuration du modèle, pas de la fonction. Un objet par modèle, pas un paramètre isolé que tu traînes à travers six signatures.
Défense au bord, toujours. Quand j'ai ajouté le timeout aux appels d'OpenAI, j'ai aussi mis ceci dans le batch d'embeddings :
if (embeddings.length !== input.length) {
throw new Error(
`Embedding provider returned ${embeddings.length} vectors for ${input.length} inputs`
);
}
Quatre lignes. Elles transforment un désalignement silencieux entre vecteurs et textes — qui te remplit la base d'embeddings assignés au mauvais anime — en une erreur immédiate. L'AbortSignal.timeout(30_000) du même commit fait pareil pour les appels qui restent pendus.
Ce que je considère stable
Stable : qu'il y a un endpoint HTTP, qu'il accepte du JSON, qu'il renvoie du JSON. Ça s'arrête là.
Pas stable : le nom du modèle, les paramètres d'échantillonnage, la forme du message, la forme de la réponse, l'endroit où arrive le compteur de tokens, le prix, et le fait que le modèle d'aujourd'hui existe encore dans six mois.
Je ne conçois pas pour le modèle que j'utilise. Je conçois pour le jour où il ne sera plus là.
