Una risposta HTTP 200 con zero parole dentro. Secondo la documentazione Anthropic, è così che Claude Sonnet 5.5 dice di no: nessun codice di errore, un campo content vuoto e stop_reason: "refusal". Il modello è uscito il 28 settembre 2026 e, come riporta TechCrunch, è il primo Sonnet soggetto agli stessi filtri di sicurezza informatica di Fable e Opus. Chi cambia il model ID nel plugin per risparmiare eredita anche i rifiuti, e un codice che controlla solo lo stato HTTP li legge come successo.
Perché il plugin non se ne accorge
Il codice tipico di un’integrazione WordPress fatta in fretta ha due controlli: is_wp_error() sulla chiamata e codice di risposta uguale a 200. Poi prende il primo blocco di content e lo salva come bozza, meta description o riepilogo. Con un rifiuto entrambi i controlli passano, perché la chiamata è riuscita. È il modello ad aver deciso di non rispondere. L’array content però è vuoto, quindi il plugin scrive una stringa vuota o, a seconda di come è scritto, un avviso PHP nel log e niente nel post. Nessuno se ne accorge finché un cliente non trova la pagina senza testo.
Il rischio non riguarda solo chi fa cose strane. Tra le categorie di rifiuto la documentazione elenca cyber, e precisa che anche il lavoro di sicurezza legittimo può farla scattare. Un’automazione che riassume i log di Wordfence o classifica le segnalazioni di vulnerabilità dei plugin è esattamente quel tipo di lavoro. È lo stesso principio del controllo sul JSON prima di scrivere in WordPress: la risposta si valida per quello che contiene, non per come è arrivata.
Il controllo su stop_reason, prima di leggere il testo
La correzione sta in poche righe. Si legge stop_reason prima del testo, si tratta il rifiuto come un errore con nome e si prende solo il testo dei blocchi di tipo text, invece di dare per scontato che il primo blocco sia quello giusto:
$data = json_decode( wp_remote_retrieve_body( $res ), true );
$stop = $data['stop_reason'] ?? '';
if ( 'refusal' === $stop ) {
$cat = $data['stop_details']['category'] ?? 'nessuna';
return new WP_Error( 'ai_refusal', 'Rifiuto del modello, categoria: ' . $cat );
}
if ( 'end_turn' !== $stop ) {
return new WP_Error( 'ai_incompleta', 'Risposta non conclusa: ' . $stop );
}
$testo = '';
foreach ( $data['content'] ?? array() as $blocco ) {
if ( 'text' === ( $blocco['type'] ?? '' ) ) {
$testo .= $blocco['text'];
}
}
if ( '' === trim( $testo ) ) {
return new WP_Error( 'ai_vuota', 'Il modello ha restituito testo vuoto' );
}
Il secondo controllo copre anche max_tokens, cioè una risposta troncata che altrimenti finirebbe pubblicata a metà. Se l’integrazione usa i tool, tool_use va aggiunto ai valori ammessi. La categoria finisce nel messaggio d’errore perché serve a chi legge il log: stop_details contiene anche una spiegazione testuale, ma Anthropic avverte che quel testo non è stabile e va mostrato, non interpretato dal codice. Quando il rifiuto non rientra in nessuna categoria, category vale null ed è un valore normale: per questo nel codice c’è il ripiego su «nessuna».
Cosa fare quando il rifiuto arriva
Ci sono due strade documentate. La più semplice, in beta sulla sola API Claude, è la fallback lato server: si passa fallbacks: "default" con l’header beta server-side-fallback-2026-07-01 e l’API riprova da sola la richiesta rifiutata sul modello che Anthropic raccomanda per quella categoria. Se per la categoria non esiste un modello raccomandato, il rifiuto resta. Non funziona sulla Message Batches API né su Amazon Bedrock, Google Cloud e Microsoft Foundry: lì il nuovo tentativo va scritto a mano o con il middleware degli SDK.
Per una PMI che usa l’API per un solo flusso, spesso basta meno: segnare l’articolo come da rivedere, notificare chi lo gestisce e non pubblicare. Anche il costo va messo in conto. Un rifiuto cyber arrivato prima di qualsiasi output non viene fatturato, ma conta comunque nei limiti di richieste al minuto. E se il rifiuto arriva a metà di una risposta in streaming, la parte già ricevuta va buttata: la documentazione la considera incompleta.
L’errore da evitare, e perché il cambio di modello è un deploy
L’errore è trattare il rifiuto come un guasto passeggero e rimettere in coda la stessa richiesta, come si fa con un timeout. Stesso prompt, stesso modello, stesso filtro: il risultato non cambia, e ogni giro consuma la quota di richieste che serve al resto del sito. Peggio ancora è far riscrivere il prompt a un secondo passaggio automatico finché il filtro non tace. Un’automazione non deve imparare ad aggirare un controllo di sicurezza di cui nessuno ha letto il motivo.
La mia opinione è che il listino abbia fatto passare in secondo piano la parte che conta. Sonnet 5.5 costa 2 dollari per milione di token in ingresso e 10 in uscita, e Anthropic lo dichiara più veloce del 30% rispetto a Sonnet 5. Buoni motivi, ma il model ID nuovo cambia anche il comportamento del sistema, non solo la fattura. Chi ha scelto tra un LLM e un modello tipizzato lo sa già: un LLM può rispondere in modi che il codice non prevede. In pratica, prima di cambiare il model ID si aggiunge il controllo su stop_reason, poi si fanno girare venti casi reali del proprio flusso e si conta quanti tornano come refusal. Se sono zero, si passa al modello nuovo. Se non lo sono, si sa quale categoria li blocca prima che se ne accorga un cliente.

