I guasti delle API di AI sono diversi dai guasti delle API tradizionali. Una risposta 200 non significa che la tua generazione sia riuscita. Un campo null in content non è sempre un errore. E lo stesso prompt che funzionava ieri potrebbe fallire oggi perché un provider ha aggiornato la sua policy sui contenuti.
Questa guida spiega come leggere gli errori delle API di AI, cosa significa realmente ciascuna modalità di errore e come costruire una gestione degli errori che ti dica cosa si è rotto invece di limitarsi a dire che qualcosa si è rotto.
Nota: I nomi dei modelli come gpt-5.4 e gpt-5.4-mini usati in questo articolo sono identificatori di piattaforma di CometAPI. Funzionano solo tramite https://api.cometapi.com/v1 — non direttamente tramite le API di OpenAI o Anthropic. Vedi l’elenco completo dei modelli.
Perché il debugging delle API di AI è più difficile rispetto al debugging delle API tradizionali
Con una tipica API REST, un 200 significa successo e un 4xx significa che hai sbagliato qualcosa. Le API di AI aggiungono una terza categoria: soft failures — risposte che restituiscono 200 ma non contengono contenuto utilizzabile.
Tre cose possono andare storte:
- Guasto hard — errore HTTP (4xx, 5xx). La richiesta non è stata completata.
- Guasto soft — HTTP 200, ma
finish_reasonècontent_filterolength, oppurecontentènull. - Guasto silenzioso — HTTP 200, il contenuto sembra a posto, ma l’output è errato in un modo che puoi cogliere solo a livello applicativo.
La maggior parte della gestione degli errori copre solo il caso 1. I casi 2 e 3 sono dove si annidano la maggior parte dei bug in produzione.
Comprendere il formato della risposta di errore
L’endpoint delle completions testuali restituisce una struttura di errore coerente:
{ "error": { "message": "human-readable description (often includes request id)", "type": "comet_api_error", "param": "the_problematic_parameter_or_null", "code": "error_code_or_null" }}
Gli endpoint di immagini e video restituiscono formati di errore differenti — analizza sempre il body grezzo della risposta invece di assumere una struttura fissa tra endpoint.
Il campo message di solito ti dice esattamente cosa non va. Il campo param ti dice quale parametro l’ha causato. Registra sempre entrambi.
Conosci il significato di ciascun codice di stato HTTP
| Stato | Significato | Causa comune | Correzione |
|---|---|---|---|
| 400 | Richiesta non valida | Modello mancante, parametro sbagliato per questo modello | Controlla error.param nella risposta |
| 401 | Non autorizzato | Chiave API errata o mancante | Verifica il formato Authorization: Bearer |
| 429 | Rate limited | Troppe richieste | Backoff esponenziale (vedi Passo 4) |
| 500 | Errore del server | Problema lato provider o body della richiesta malformato | Riprova con backoff; controlla il formato della richiesta |
| 504 | Timeout del gateway | Il provider ha impiegato troppo tempo | Riprova; considera un modello più veloce |
Fonte**: Documentazione CometAPI chat completions
La distinzione tra 400 e 500 è importante per la logica di retry. Un 400 significa che la tua richiesta è errata — ripetere la stessa richiesta non aiuterà. Un 500 o 504 significa che il server ha avuto un problema — ha senso riprovare.
Controlla finish_reason — il campo più trascurato
Una risposta 200 con finish_reason: "content_filter" significa che la generazione è stata bloccata. Il campo content sarà null o vuoto. Se non lo controlli, la tua app restituirà silenziosamente il nulla.
| finish_reason | Significato | Cosa fare | Correzione |
|---|---|---|---|
| stop | Completamento normale | Niente — questo è un successo | Controlla error.param nella risposta |
| length | Raggiunto il limite di token | Aumenta max_tokens o accorcia il prompt | Verifica il formato Authorization: Bearer |
| content_filter | Bloccato dalla policy di sicurezza | Riformula il prompt; evita nomi/argomenti specifici | Backoff esponenziale (vedi Passo 4) |
| tool_calls | Il modello ha chiamato uno strumento invece di restituire testo | Gestisci la chiamata allo strumento; il contenuto sarà null | Riprova con backoff; controlla il formato della richiesta |
| 504 | Timeout del gateway | Il provider ha impiegato troppo tempo | Riprova; considera un modello più veloce |
Fonte**: Documentazione CometAPI chat completions
import osimport loggingfrom openai import OpenAI, APIStatusError, APIConnectionError, APITimeoutErrorfrom dotenv import load_dotenvload_dotenv()api_key = os.environ.get("COMETAPI_KEY")if not api_key: raise ValueError("COMETAPI_KEY is not set")client = OpenAI( base_url="https://api.cometapi.com/v1", api_key=api_key,)def safe_complete(messages: list, model: str = "gpt-5.4-mini", **kwargs) -> dict: """ Complete a chat request with full error and finish_reason handling. Returns {"content": str, "finish_reason": str, "tool_calls": list | None} Raises on API errors. """ try: response = client.chat.completions.create( model=model, messages=messages, **kwargs ) except APIStatusError as e: error_body = {} try: error_body = e.response.json().get("error", {}) except Exception: pass logging.error( f"API error status={e.status_code} " f"message={error_body.get('message')} " f"param={error_body.get('param')}" ) raise except (APIConnectionError, APITimeoutError) as e: logging.error(f"Network/timeout error: {e}") raise choice = response.choices[0] finish_reason = choice.finish_reason if finish_reason == "content_filter": raise ValueError( f"Generation blocked by content filter. " f"Model: {model}. Rephrase the prompt." ) if finish_reason == "length": used = response.usage.completion_tokens if response.usage else "unknown" logging.warning(f"Output truncated at token limit. Used {used} tokens.") # Return structured result so callers can handle tool_calls explicitly return { "content": choice.message.content or "", "finish_reason": finish_reason, "tool_calls": choice.message.tool_calls, }# Usageresult = safe_complete( messages=[{"role": "user", "content": "Summarize this article: [text]"}], model="gpt-5.4-mini")if result["finish_reason"] == "tool_calls": # Handle tool call — content will be empty print("Model wants to call a tool:", result["tool_calls"])else: print(result["content"])
Rileva i guasti silenziosi a livello applicativo
I guasti silenziosi sono i più difficili da individuare. L’API restituisce 200, finish_reason è stop, ma l’output è semanticamente errato. Puoi rilevarli solo a livello applicativo.
Pattern comuni:
def validate_completion(result: dict, task: str) -> str: """ Application-layer validation for silent failures. Raises ValueError if the output doesn't meet basic expectations. """ content = result["content"].strip() # Empty output that isn't a tool call if not content and result["finish_reason"] != "tool_calls": raise ValueError(f"Empty output for task '{task}' with finish_reason='{result['finish_reason']}'") # Task-specific checks if task == "classify": valid_labels = {"positive", "negative", "neutral"} if content.lower() not in valid_labels: logging.warning( f"Unexpected classification output: '{content}'. " f"Expected one of {valid_labels}. " f"Model may have returned explanation instead of label." ) if task == "json_extract": import json try: json.loads(content) except json.JSONDecodeError: raise ValueError( f"Expected JSON output but got: '{content[:100]}...'. " f"Try adding 'respond with valid JSON only' to the prompt, " f"or use response_format={{\"type\": \"json_object\"}}." ) if task == "summarize" and len(content.split()) < 10: logging.warning( f"Suspiciously short summary ({len(content.split())} words). " f"Check if the input was too short or the model misunderstood the task." ) return content# Full flow with silent failure detectionresult = safe_complete( messages=[{"role": "user", "content": "Classify as positive/negative/neutral: 'Great product!'"}], model="claude-haiku-4-5")label = validate_completion(result, task="classify")
I guasti silenziosi solitamente derivano da una delle tre cause: il prompt è ambiguo, il modello ha ignorato le istruzioni di formattazione oppure l’input era troppo corto/lungo per il compito. Registrare l’output completo quando la validazione fallisce è il modo più rapido per diagnosticare quale delle tre sia.
Aggiungi backoff esponenziale per i rate limit
Gli errori di rate limit (429) sono temporanei. La risposta corretta è attendere e riprovare con ritardi crescenti — una pratica standard per qualsiasi API con rate limit:
import timeimport randomfrom openai import RateLimitErrordef complete_with_retry( messages: list, model: str = "gpt-5.4-mini", max_retries: int = 3, **kwargs) -> dict: """Retry on rate limits and server errors with exponential backoff.""" last_error = None for attempt in range(max_retries): try: return safe_complete(messages, model=model, **kwargs) except APIStatusError as e: if e.status_code < 500: raise # 4xx: don't retry, request is wrong last_error = e except RateLimitError as e: last_error = e except (APIConnectionError, APITimeoutError) as e: last_error = e if attempt < max_retries - 1: wait = (2 ** attempt) + random.random() # jitter prevents thundering herd logging.warning(f"Attempt {attempt + 1} failed. Waiting {wait:.1f}s before retry.") time.sleep(wait) raise RuntimeError(f"All {max_retries} attempts failed") from last_error
Non riprovare su 400 o 401 — sono errori del client che non si risolvono da soli.
Debug degli errori di generazione di immagini
La generazione di immagini ha modalità di errore proprie oltre ai consueti errori HTTP:
import base64import requestsdef generate_image_safe(prompt: str, model: str = "dall-e-3") -> dict: """ Generate an image with full error handling. Returns {"url": str | None, "bytes": bytes | None, "blocked": bool} """ api_key = os.environ.get("COMETAPI_KEY") if not api_key: raise ValueError("COMETAPI_KEY is not set") BASE64_MODELS = {"gpt-image-2", "qwen-image"} headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" } payload = {"model": model, "prompt": prompt, "size": "1024x1024"} if model in BASE64_MODELS: payload["output_format"] = "png" else: payload["response_format"] = "url" try: response = requests.post( "https://api.cometapi.com/v1/images/generations", json=payload, headers=headers, timeout=60 ) response.raise_for_status() except requests.exceptions.HTTPError as e: logging.error(f"Image generation HTTP error: {e.response.status_code} {e.response.text}") raise except requests.exceptions.Timeout: logging.error("Image generation timed out after 60s") raise data = response.json().get("data", []) if not data: logging.warning("Image generation returned empty data — prompt may have been filtered.") return {"url": None, "bytes": None, "blocked": True} item = data[0] if "revised_prompt" in item: logging.info(f"Provider revised prompt to: {item['revised_prompt']}") if "url" in item: return {"url": item["url"], "bytes": None, "blocked": False} return { "url": None, "bytes": base64.b64decode(item["b64_json"]), "blocked": False }
Problemi specifici delle immagini da tenere d’occhio:
| Sintomo | Causa | Correzione |
|---|---|---|
| Array data vuoto | Prompt filtrato | Controlla revised_prompt; riformula |
| Errore response_format su GPT Image 2 | Parametro non supportato | Usa output_format invece |
| Errore n > 1 su Qwen Image | Limitazione del modello | Effettua richieste in loop |
| L’URL restituisce 403 in seguito | URL scaduto | Scarica immediatamente dopo la generazione |
Fonte**: Documentazione CometAPI image generation
Debug degli errori di generazione video
La generazione video fallisce in modo diverso perché è asincrona. Inizializza le variabili di stato prima del loop in modo che il messaggio di timeout sia sempre ben formato:
def submit_and_poll_video( prompt: str, model: str = "veo3-fast", max_wait: int = 600) -> str: """Submit video task and poll to completion. Returns video URL.""" api_key = os.environ.get("COMETAPI_KEY") if not api_key: raise ValueError("COMETAPI_KEY is not set") headers = {"Authorization": f"Bearer {api_key}"} try: response = requests.post( "https://api.cometapi.com/v1/videos", headers=headers, files={ "prompt": (None, prompt), "model": (None, model), "size": (None, "16x9") }, timeout=30 ) response.raise_for_status() except requests.exceptions.HTTPError as e: logging.error(f"Video submit failed: {e.response.status_code} {e.response.text}") raise task_id = response.json()["id"] logging.info(f"Video task submitted: {task_id}") poll_url = f"https://api.cometapi.com/v1/videos/{task_id}" elapsed = 0 interval = 10 status = "unknown" # initialize before loop progress = 0 # initialize before loop while elapsed < max_wait: try: poll_response = requests.get(poll_url, headers=headers, timeout=30) poll_response.raise_for_status() except requests.exceptions.HTTPError as e: logging.error(f"Poll request failed: {e.response.status_code}") raise result = poll_response.json() status = result.get("status", "unknown") progress = result.get("progress", 0) logging.info(f"Task {task_id}: status={status} progress={progress}%") if status == "succeeded": return result["output"][0] elif status in ("failed", "cancelled"): error_detail = result.get("error", "no error detail returned") raise RuntimeError(f"Video task {task_id} failed: {error_detail}") time.sleep(interval) elapsed += interval raise TimeoutError( f"Video task {task_id} did not complete within {max_wait}s. " f"Last status: {status}, progress: {progress}%" )
Problemi specifici dei video:
| Sintomo | Causa | Correzione |
|---|---|---|
Task bloccato in queued per 10+ min | Carico del server | Riprova con un modello diverso |
| failed senza dettagli errore | Prompt filtrato o errore del modello | Riformula il prompt |
| L’URL del video restituisce 403 | URL scaduto | Scarica immediatamente |
| task_not_exist al primo poll di Runway | Il task è ancora in inizializzazione (comportamento documentato da CometAPI) | Attendi 5s e riprova |
| Kling restituisce "succeed" non "succeeded" | L’API di Kling usa una stringa di stato non standard | Gestisci entrambe nella logica di polling |
Fonte**: Documentazione CometAPI video generation**, Documentazione Kling Video
Versione Node.js
import OpenAI from 'openai';const apiKey = process.env.COMETAPI_KEY;if (!apiKey) throw new Error('COMETAPI_KEY is not set');const client = new OpenAI({ baseURL: 'https://api.cometapi.com/v1', apiKey,});async function safeComplete(messages, model = 'gpt-5.4-mini', options = {}) { let response; try { response = await client.chat.completions.create({ model, messages, ...options }); } catch (err) { if (err.status && err.status < 500) { console.error(`Client error ${err.status}: ${err.message}`); } else { console.error(`Server/network error: ${err.message}`); } throw err; } const choice = response.choices[0]; const finishReason = choice.finish_reason; if (finishReason === 'content_filter') { throw new Error(`Generation blocked by content filter. Model: ${model}`); } if (finishReason === 'length') { console.warn(`Output truncated. Used ${response.usage?.completion_tokens ?? 'unknown'} tokens.`); } return { content: choice.message.content ?? '', finishReason, toolCalls: choice.message.tool_calls ?? null, };}async function completeWithRetry(messages, model = 'gpt-5.4-mini', maxRetries = 3) { let lastError; for (let attempt = 0; attempt < maxRetries; attempt++) { try { return await safeComplete(messages, model); } catch (err) { // Don't retry 4xx client errors if (err.status && err.status < 500) throw err; lastError = err; if (attempt < maxRetries - 1) { const wait = (2 ** attempt + Math.random()) * 1000; console.warn(`Attempt ${attempt + 1} failed. Retrying in ${(wait / 1000).toFixed(1)}s`); await new Promise(r => setTimeout(r, wait)); } } } throw new Error(`All ${maxRetries} attempts failed: ${lastError?.message}`);}// Usageconst result = await safeComplete( [{ role: 'user', content: 'Classify as positive/negative/neutral: "Great product!"' }], 'claude-haiku-4-5');if (result.finishReason === 'tool_calls') { console.log('Tool call requested:', result.toolCalls);} else { console.log(result.content);}
Una checklist di debugging
Quando una generazione fallisce e non sai da dove iniziare:
Per la generazione di testo:
- La chiave API è impostata e nel formato
Authorization: Bearer <key>? - Il
finish_reasonè diverso dastop? - Il
contentè null? Verifica sefinish_reasonètool_calls - L’output è stato troncato? Controlla
finish_reason: "length"eusage.completion_tokens - L’errore è un 4xx (correggi la richiesta) o 5xx (riprova)?
- L’output supera la tua validazione a livello applicativo? (guasto silenzioso)
Per la generazione di immagini:
- L’array
dataè vuoto? (content filter) - Hai usato
response_formatsu GPT Image 2? (non supportato — usaoutput_format) - Hai impostato
n > 1su Qwen Image? (non supportato) - Hai scaricato l’immagine prima che l’URL scadesse?
Per la generazione video:
- Il task è bloccato in
queued? (prova un modello diverso) - Hai controllato il campo
errornella risposta del task fallito? - Hai scaricato il video prima che l’URL scadesse?
- Stai gestendo sia
"succeed"(Kling) sia"succeeded"(Veo, Runway)?
Domande frequenti
D: La mia richiesta restituisce 200 ma non c’è contenuto. Cosa è successo?
Controlla finish_reason. Se è content_filter, la generazione è stata bloccata — la richiesta è riuscita ma l’output è stato soppresso. Se è tool_calls, il modello ha chiamato uno strumento invece di restituire testo, e content è null per progettazione. Se finish_reason è stop ma il contenuto è comunque vuoto, è un guasto silenzioso — registra la risposta completa e controlla il prompt.
D: Come faccio a sapere se il mio prompt viene filtrato?
Per il testo: controlla finish_reason === "content_filter". Per le immagini: verifica se l’array data è vuoto. Per i video: controlla se il task arriva a stato failed poco dopo la sottomissione senza dettagli di errore. In tutti i casi, prova a riformulare il prompt in modo più neutro.
D: Quando dovrei riprovare una richiesta fallita?
Riprova con 429 e 5xx usando il backoff esponenziale. Non riprovare con 4xx — una richiesta errata non si corregge da sola. L’eccezione è 401 se stai ruotando le chiavi API.
D: Che cos’è il backoff esponenziale e perché è importante?
Invece di riprovare immediatamente, aspetti sempre più a lungo: 1s, 2s, 4s. Aggiungere jitter casuale (+ random.random()) impedisce che più client riprovino in sincronia. È una pratica standard per qualsiasi API con rate limit — non è specifica di CometAPI.
D: Il task video è bloccato in queued da 10 minuti. È fallito?
Non necessariamente — le code possono accumularsi sotto carico. Attendi fino alla tua soglia max_wait, poi solleva un TimeoutError e riprova con un modello diverso. Registra l’ID del task così puoi controllarne lo stato manualmente se necessario.
D: Come intercetto i guasti silenziosi?
I guasti silenziosi richiedono validazione a livello applicativo — l’API non ti dirà che l’output è semanticamente errato. Controlla che l’output rispetti il formato atteso (JSON valido, etichetta prevista, lunghezza minima). Registra l’output completo quando la validazione fallisce. Le cause più comuni sono prompt ambigui, istruzioni di formato ignorate o input troppo corti o troppo lunghi per il compito.
