Cambiare backend al giudice LLM: da Anthropic SDK a OpenRouter
Ho riscritto un sistema di validazione LLM da SDK Anthropic a SDK OpenAI puntato a OpenRouter. Dieci volte meno costo, qualche gotcha Windows, un path di rollback semplice.
Ho passato la settimana a riscrivere il giudice LLM di un sistema di catalogazione interno. Quattro moduli brand-knowledge-mcp distinti (uno per ogni catalogo di componenti gestito) usavano l'SDK ufficiale Anthropic per chiamare Haiku 4.5 con tool_choice forzato sullo schema di validazione. Funzionava bene, ma la fattura saliva e cominciava a pesare anche sullo sviluppo locale.
Il refactor è stato sostituire l'SDK Anthropic con l'SDK OpenAI puntato a OpenRouter (base_url=https://openrouter.ai/api/v1), modello default openai/gpt-4.1-mini. Il differenziale di costo è circa dieci volte (input 0.40 contro 1.00 dollari per milione di token, con caching automatico OpenAI sopra i 1024 token). Ma il vero motivo per accettare il pattern non è il prezzo: è la possibilità di switchare modello via variabile d'ambiente senza toccare codice, che vuol dire poter confrontare GPT, Claude e DeepSeek nello stesso giro.
Tre cose tecniche che vale la pena annotare.
Schema enforcement diverso. OpenAI non supporta tool_choice="required" sull'output JSON come fa Anthropic in modo pulito. La sostituta è response_format={"type": "json_schema", ...} con extra_body={"provider": {"require_parameters": True}} per OpenRouter. Output identico (un dizionario validato), strada diversa.
Path di rollback preservato. Le settings hanno un campo llm_backend con due valori: openai_compat (default) e anthropic_native (legacy). Il giorno in cui un brand peggiora qualitativamente con gpt-4.1-mini, basta una variabile d'ambiente per tornare al giudice Haiku 4.5 solo per quel modulo. Niente flag globali, niente codice morto.
Il gotcha Windows. L'orchestratore di test cross-brand lanciava i quattro moduli in subprocess separati (per liberare la memoria di SBERT fra brand) e leggeva stdout via PIPE. Su Windows un subprocess che fa molto output e un parent che fa anche sys.stdout.write per echo crea un deadlock silenzioso: il PIPE si riempie, write bloccante, child fermo, parent fermo. La soluzione è stata far scrivere il subprocess direttamente sul file di log (stdout=open(log, "w")) e tenere il monitoring esterno via tail. Niente PIPE intermedio, niente blocco.
Lo smoke test cross-brand finale: 4 brand su 4 verdi, costo per chiamata 0.0014 dollari, latenza media 22 secondi (in linea con Haiku 4.5).