Torna al blog

API e SDKGuida

Ottenere JSON valido da un'API LLM: response_format e nuovi tentativi

Una richiesta Python copiabile che restituisce JSON analizzabile, un wrapper json.loads che gestisce risposte vuote o troncate, e una checklist del perché il JSON si rompe.

Di CheapTokens TeamPubblicato il 6 min di lettura

Un terminale che mostra una richiesta di completamento chat con response_format json_object
In questa pagina
  1. Il compito: un'email in ingresso, una riga in uscita
  2. Perché l'esempio conta più del flag
  3. Analizza in sicurezza: vuoto, troncato o semplicemente rotto
  4. Controlla la forma, non solo la sintassi
  5. Quanto costa un batch di email
  6. Verifica rapida

Per ottenere JSON da un modello di chat, fai tre cose: passa response_format={"type": "json_object"}, inserisci la parola "json" e un esempio della struttura nel prompt, e imposta max_tokens abbastanza alto per l'intero oggetto. Poi non fidarti mai ciecamente della risposta: controlla che non sia vuota, verifica finish_reason, e solo allora chiama json.loads.

Questa è la versione breve. Il resto di questa guida è uno script completo per un compito molto comune: estrarre campi da un'email. Gli esempi usano l'URL di base https://api.cheaptokens.dev/v1 e l'id del modello deepseek-ai/DeepSeek-V4-Flash-0731 (documentazione).

Il compito: un'email in ingresso, una riga in uscita#

Supponiamo che il tuo script legga email di supporto e debba trasformare ciascuna in un record: mittente, data, importo e cosa vuole il mittente. Prendi questa:

Ciao, sono Anna Petrova di Lumen Ltd. Il 14 ottobre ho pagato la fattura #3381 per 1.250 EUR, ma la ricevuta la mostra ancora come non pagata. Per favore risolvi entro venerdì.

Vuoi questo indietro, e nient'altro:

json
{"sender": "Anna Petrova", "date": "2026-10-14", "amount": 1250, "currency": "EUR", "action": "fix receipt status"}

Un semplice prompt "estrai i campi" può restituirti una frase amichevole, un blocco di codice, o entrambi. Il tuo json.loads si blocca al primo backtick. Questo è il problema che response_format e un buon prompt risolvono insieme.

La stessa richiesta in Python e curl

Python

python
import json
import os

from openai import OpenAI

client = OpenAI(
    base_url="https://api.cheaptokens.dev/v1",
    api_key=os.environ["CHEAPTOKENS_API_KEY"],
)
MODEL = "deepseek-ai/DeepSeek-V4-Flash-0731"

SYSTEM = """Extract fields from the email and answer with json only.
Example of the json format:
{"sender": "Jane Roe", "date": "2026-01-31", "amount": 99.5, "currency": "USD", "action": "short phrase"}
Use null for a field that is not in the email."""

resp = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": SYSTEM},
        {"role": "user", "content": email_text},
    ],
    response_format={"type": "json_object"},
    max_tokens=1000,
    temperature=0,
)
print(resp.choices[0].message.content)

curl

bash
curl https://api.cheaptokens.dev/v1/chat/completions \
  -H "Authorization: Bearer $CHEAPTOKENS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "deepseek-ai/DeepSeek-V4-Flash-0731",
    "messages": [
      {"role": "system", "content": "Extract fields from the email and answer with json only. Example: {\"sender\": \"Jane Roe\", \"amount\": 99.5}"},
      {"role": "user", "content": "Hi, this is Anna Petrova from Lumen Ltd. I paid invoice #3381 for 1,250 EUR..."}
    ],
    "response_format": {"type": "json_object"},
    "max_tokens": 1000
  }'

La documentazione dell'API DeepSeek per JSON Output (consultata il 27 ottobre 2026; la pagina non ha data di pubblicazione) indica le regole che questa richiesta segue: response_format impostato a json_object, la parola "json" da qualche parte nel prompt di sistema o utente, e un esempio della struttura target. Senza l'esempio il modello deve indovinare i nomi dei tuoi campi, e li indovinerà diversamente alla prossima email.

Perché l'esempio conta più del flag#

response_format chiede all'API di restituire un oggetto JSON. Non dice al modello quale oggetto. Immagina di dire a un collega "mandami un foglio di calcolo" senza specificare le colonne. Ottieni un foglio di calcolo, ma non il tuo.

L'esempio nel prompt è la riga di intestazione delle colonne. Due abitudini lo fanno funzionare meglio:

  • Mostra la forma reale, inclusi i tipi: 1250 è un numero, "2026-10-14" è una stringa in un formato data fisso.
  • Di' cosa fare quando un campo manca (null sopra). Altrimenti il modello inventa un valore per riempire il vuoto.

Tratta response_format come un forte suggerimento, non una garanzia. I modelli a volte restituiscono comunque una stringa vuota, o avvolgono l'oggetto in prosa. Ecco perché esiste la parte successiva.

Analizza in sicurezza: vuoto, troncato o semplicemente rotto#

Tre fallimenti si presentano in pratica, e ciascuno lascia una traccia diversa:

  • Contenuto vuoto. La documentazione DeepSeek dice che la modalità JSON può occasionalmente restituire contenuto vuoto e suggerisce di riformulare il prompt. Dal tuo lato sembra che message.content sia "" o None, e json.loads("") sollevi Expecting value: line 1 column 1 (char 0).
  • Troncato. max_tokens è esaurito a metà dell'oggetto. finish_reason è length, e il testo termina a metà stringa. Maggiori informazioni in perché una risposta AI si interrompe a metà frase.
  • Non JSON. Una frase prima dell'oggetto, o un blocco di codice attorno ad esso.
json.loads su una risposta vuota solleva JSONDecodeError
Come appare una risposta vuota in modalità JSON in Python

La soluzione per il primo e il terzo è un nuovo tentativo con un prompt più fermo. Per il secondo, un nuovo tentativo con lo stesso limite non cambia nulla, quindi il wrapper solleva un'eccezione e ti dice di aumentare max_tokens:

python
def ask_json(email_text: str, retries: int = 1) -> dict:
    user = email_text
    for attempt in range(retries + 1):
        resp = client.chat.completions.create(
            model=MODEL,
            messages=[
                {"role": "system", "content": SYSTEM},
                {"role": "user", "content": user},
            ],
            response_format={"type": "json_object"},
            max_tokens=1000,
            temperature=0,
        )
        choice = resp.choices[0]
        if choice.finish_reason == "length":
            raise ValueError("JSON cut off by max_tokens: raise the limit")
        text = (choice.message.content or "").strip()
        if text:
            try:
                return json.loads(text)
            except json.JSONDecodeError:
                pass
        # empty or not JSON: ask again, more firmly
        user = email_text + "\n\nReply with one json object only, in the format of the example."
    raise ValueError("no valid JSON after retry")

Un nuovo tentativo è sufficiente per uno script. Se fallisce due volte, registra l'id dell'email e vai avanti; una terza richiesta identica raramente aiuta.

Abbina le coppie

Quale sintomo indica quale causa?

  • finish_reason è "length"max_tokens è troppo piccolo per l'oggetto
  • content è una stringa vuotala modalità JSON non ha restituito nulla; riformula il prompt e riprova
  • JSONDecodeError su un carattere che è un backtickil modello ha avvolto il JSON in un blocco di codice
  • I nomi dei campi differiscono tra le emailil prompt non ha un esempio della struttura

Controlla la forma, non solo la sintassi#

json.loads prova solo che il testo è JSON valido. Non prova che abbia i campi che hai richiesto. Aggiungi un controllo economico subito dopo l'analisi:

python
REQUIRED = {"sender", "date", "amount", "currency", "action"}

data = ask_json(email_text)
missing = REQUIRED - data.keys()
if missing:
    raise ValueError(f"missing fields: {sorted(missing)}")

Da qui il dict va direttamente in una riga di database o in un foglio di calcolo. Se hai bisogno che il modello chiami le tue funzioni invece di riempire un record fisso, quello è uno strumento diverso: vedi function calling in Python.

Il percorso dall'email al record verificato: prompt con esempio, json_object, analisi, controllo dei campi
L'intero percorso in cinque passi

Quanto costa un batch di email#

Un'estrazione breve è il tipo economico di richiesta: qualche centinaio di token in ingresso, qualche decina in uscita. Supponi 1.000 email al giorno a circa 400 token di input (prompt, esempio ed email) e circa 80 token di output (il JSON). Su DeepSeek-V4-Flash sono $0.014 al giorno, e la tabella mostra lo stesso carico sugli altri modelli:

Modello Per richiesta 1000 richieste OpenRouter, 1000 richieste
GLM-5.3-Flash $0.000011 $0.0112 $0.0226
DeepSeek-V4-Flash $0.000014 $0.014 $0.0315
MiniMax-M2.7 $0.000032 $0.032 $0.216

400 token di input e 80 token di output per richiesta. I prezzi per 1M di token sono attuali e cambiano insieme alle nostre tariffe; la colonna OpenRouter mostra i prezzi di riferimento per lo stesso carico di lavoro.

In totale, con input e output contati insieme, DeepSeek-V4-Flash risulta più economico del prezzo di riferimento di OpenRouter per questo carico. Consente anche fino a 32.768 token di output per risposta, quindi un lungo elenco di voci non verrà troncato come accadrebbe con un limite piccolo, e il prezzo è lo stesso a qualsiasi ora. GLM-5.3-Flash (zai-org/GLM-5.3-Flash) è un'altra opzione allo stesso indirizzo API: fino a 8.192 token di output, che sono più che sufficienti per un record per email. I prezzi attuali sono sulla pagina dei prezzi; la pagina del modello DeepSeek ha i dettagli per il modello usato qui.

Verifica rapida#

Verifica rapida

Il tuo JSON è troncato a metà di una stringa. Cosa guardi per primo?

Mostra risposta

Risposta: finish_reason e max_tokens

Un oggetto troncato termina con finish_reason impostato a length. Il limite è stato raggiunto, quindi aumenta max_tokens; cambiare la temperatura o l'indirizzo non aiuta.

Se non hai ancora inviato una richiesta, inizia con la tua prima richiesta API, poi torna e sostituisci con il codice sopra.