Num buscador semântico de anime tenho um reranker que ordena vinte e cinco candidatos com um LLM. Estava com temperature: 0 desde o primeiro dia, que é o óbvio quando você quer uma ordem reproduzível. Troquei o modelo por um de raciocínio e a chamada começou a devolver erro: esse modelo rejeita temperature. Não ignora, rejeita.
O conserto não foi apagar a linha. Foi substituí-la por reasoning_effort, extraí-lo para uma constante com low como padrão, e passá-lo também pelo outro lugar onde o mesmo modelo era chamado, um script de ingestão que sintetiza premissas de franquia. Três arquivos, uma variável de ambiente nova e um documento para atualizar. Por um parâmetro.
Isso aconteceu seis vezes em um ano e meio, e cada vez de um jeito diferente.
Os quatro tipos de quebra
Depois de colecioná-las, repetem-se sempre as mesmas quatro:
- O parâmetro que desaparece.
temperatureem modelos de raciocínio. Não há equivalente exato:reasoning_effortnão é a mesma coisa, é outra alavanca. Você não migra o valor, decide um novo. - A forma do payload. Passar de Chat Completions para a Responses API num parser de visão não é mudar o nome do método.
messagesvirainput,{"type": "text"}virainput_text, eimage_urldeixa de ser um objeto{url, detail}para ser uma string comdetailcomo irmão no mesmo nível. - A forma da resposta. Nessa mesma migração,
response.choices[0].message.contentvirouresponse.output_text. E o contador de tokens deixou de ser um só: agora háinput_tokenseoutput_tokensseparados. Isso obrigou a mudar a assinatura da função de-> strpara-> tuple[str, int, int]e a acumulá-los na mão ao recolher os futures em paralelo. - A qualidade muda sem avisar. Este é o pior porque não dá erro. O modelo responde, o JSON valida, e os resultados são piores.
As três primeiras você vê no primeiro deploy. A quarta você engole por semanas.
OpenRouter e LangChain movem o problema
A reação lógica é colocar uma camada por cima. Já fiz isso duas vezes.
Num SaaS de transcrição eu tinha um lib/claude.ts com um processWithClaude(text, prompt) chamando direto o SDK da Anthropic. Apaguei inteiro e substituí por um singleton AIService contra o OpenRouter, com modelName e temperature como parâmetros que viajam desde o FormData do endpoint. Ganho real: trocar de modelo virou mandar outra string na requisição.
Num app de chat com projetos e contextos, a camada foi o LangChain, com @langchain/anthropic e @langchain/google-genai para poder ter um dropdown agrupado por provedor. Ali aprendi a outra metade: a camada também quebra. Um commit dos primeiros dias se chama literalmente "update dependencies for @langchain/anthropic and zod to latest versions". Atualizar para latest em abstrações sobre APIs que se mexem é trocar um problema que você entende por um que não entende.
O que essas camadas fazem é real, mas limitado: normalizam a forma da chamada. Não normalizam as capacidades. Se o modelo A aceita temperature e o B não, a camada deixa você passar os dois parâmetros e o erro chega do mesmo jeito, só que agora de dentro de uma biblioteca, com o stack trace três níveis abaixo.
O custo de trocar no meio do produto
No dia em que troquei o modelo de um gerador de horários de google/gemini-3-flash-preview para deepseek/deepseek-v3.2, o diff tocou três arquivos: app/api/generate/route.ts, app/api/adjust-schedule/route.ts e app/api/verify-checks/route.ts. O mesmo literal, copiado três vezes dentro de três JSON.stringify diferentes. Isso não é falha do provedor, é minha, e é o custo real: quando o nome do modelo está incrustado no ponto de chamada, migrar é um find-and-replace e rezar para não esquecer nenhum.
Num serviço de análise de documentos tentei arrumar isso centralizando modelos e cálculo de custo num arquivo de constantes. Custou-me três refactors em três semanas: um no dia 18, outro no dia seguinte, e antes mais um. O nome de um dos commits tem o erro de digitação registrado, "reafactor constants", o que diz bastante sobre aquela tarde. A camada de constantes não pegou de primeira porque eu estava tentando enfiar na mesma estrutura duas coisas distintas: qual modelo usar e quanto custa.
Como isolo a chamada hoje
Sem cerimônia. Três regras.
O modelo nunca é um literal no ponto de chamada. Sempre uma constante lida do ambiente com default, do tipo RERANK_MODEL = process.env.RERANK_MODEL || "gpt-5.4-mini". Trocar de modelo em produção não deveria exigir um deploy.
Os parâmetros específicos do modelo viajam com o modelo. Se um modelo precisa de reasoning_effort e outro de temperature, isso é configuração do modelo, não da função. Um objeto por modelo, não um parâmetro solto que você arrasta por seis assinaturas.
Defesa na borda, sempre. Quando adicionei o timeout às chamadas da OpenAI, coloquei também isto no batch de embeddings:
if (embeddings.length !== input.length) {
throw new Error(
`Embedding provider returned ${embeddings.length} vectors for ${input.length} inputs`
);
}
Quatro linhas. Convertem um desalinhamento silencioso entre vetores e textos — que deixa o banco cheio de embeddings atribuídos ao anime errado — em um erro imediato. O AbortSignal.timeout(30_000) do mesmo commit faz o mesmo com as chamadas penduradas.
O que considero estável
Estável: que existe um endpoint HTTP, que aceita JSON, que devolve JSON. Aí termina.
Não estável: o nome do modelo, os parâmetros de amostragem, a forma da mensagem, a forma da resposta, onde vem o contador de tokens, o preço, e que o modelo de hoje ainda exista daqui a seis meses.
Não projeto para o modelo que uso. Projeto para o dia em que ele deixar de existir.
