Esempi pratici di chat
Programmi completi eseguibili per conversazioni, roleplay, streaming nell'interfaccia e chiamate a strumenti, in Python, Node.js e curl.
Ogni esempio in questa pagina è un programma intero: imposta due variabili d'ambiente, copialo ed eseguilo. Tutti chiamano POST https://api.spicyapi.ai/v1/chat/completions con le librerie ufficiali OpenAI, quindi lo stesso codice funziona con ogni modello di testo nel catalogo. Testo e streaming spiega il protocollo; questa pagina mostra come costruirvi le funzionalità più comuni.
Prima di iniziare
1. Metti la chiave API in una variabile d'ambiente, così non devi mai incollarla nel codice:
export SPICY_API_KEY="sk-spicy-paste-your-key-here"2. Scegli un modello di testo dal catalogo aggiornato e conserva il suo ID in SPICY_MODEL. Ogni esempio lo legge. Questo comando elenca i modelli di testo attualmente utilizzabili dalla tua chiave (richiede jq):
curl -s "https://api.spicyapi.ai/api/v1/models?modality=text" \
-H "Authorization: Bearer $SPICY_API_KEY" \
| jq -r '.data.items[] | select(.enabled and .available) | .model'
export SPICY_MODEL="MODEL_ID_FROM_CATALOG" # replace with one line from the list aboveGli ID dei modelli di testo hanno la forma publisher/model/chat. MODEL_ID_FROM_CATALOG è soltanto un segnaposto: non inviarlo mai letteralmente.
3. Controlla cosa accetta quel modello. I modelli differiscono: alcuni accettano temperature, altri no; reasoning_effort ha valori diversi a seconda del modello; soltanto alcuni accettano immagini. I campi di campionamento non supportati, come temperature o frequency_penalty, vengono ignorati e indicati nell'header di risposta X-Spicy-Ignored-Params. Sono gestiti anche i valori predefiniti comuni dei client: stop viene ignorato se non supportato; un n: 1 non supportato significa una sola risposta; un livello di ragionamento riconosciuto viene mappato al livello supportato più vicino, quando necessario. L'header X-Spicy-Adjusted-Params indica le impostazioni adattate. I campi funzionali non supportati, come tools o response_format, continuano a produrre 400. Questo comando mostra i campi, i valori di reasoning_effort e i tipi di contenuto dei messaggi accettati dal modello:
curl -s "https://api.spicyapi.ai/api/v1/models/$SPICY_MODEL" \
-H "Authorization: Bearer $SPICY_API_KEY" \
| jq '.data.inputSchema.properties | {
fields: keys,
reasoning_effort: .reasoning_effort.enum,
content_parts: [.messages.items.properties.content.items.oneOf[]?.properties.type.const]
}'Per una panoramica rapida di tutti i modelli di testo, GET /v1/models?modality=text restituisce supported_parameters, context_length e max_output_tokens di ciascuno; elenca soltanto i nomi dei campi, quindi usa il comando sopra quando ti servono i valori consentiti.
4. Installa la libreria OpenAI per il tuo linguaggio: pip install openai per Python oppure npm install openai per Node.js 18 o successivo. Salva i programmi Node.js con estensione .mjs (usano await al livello principale) ed eseguili con node file.mjs.
Lascia spazio al ragionamento
Molti modelli di testo ragionano prima di rispondere e questo ragionamento conta in max_tokens. Gli esempi omettono max_tokens, così ciascun modello usa il proprio valore predefinito. Se lo imposti, lascia un margine ampio (qualche migliaio di token): un valore troppo piccolo può essere consumato interamente dal ragionamento, facendo fallire la richiesta con un messaggio che indica di aumentare max_tokens o ridurre reasoning_effort. Un errore di questo tipo non viene addebitato.
Conversazione a più turni
L'API non conserva memoria fra le chiamate. Una conversazione è un elenco di messaggi che tu conservi e reinvii per intero a ogni turno: prima il messaggio system, poi messaggi user e assistant alternati. Dopo ogni risposta, aggiungila all'elenco come messaggio assistant.
Ogni messaggio reinviato viene fatturato di nuovo come input, quindi rimuovi i turni più vecchi quando la conversazione diventa lunga. Mantieni sempre il messaggio system e rimuovi i turni in coppie utente/assistente, in modo che la cronologia inizi ancora con un messaggio user.
import os
from openai import OpenAI
client = OpenAI(api_key=os.environ["SPICY_API_KEY"], base_url="https://api.spicyapi.ai/v1")
MODEL = os.environ["SPICY_MODEL"]
KEEP = 20 # messages kept after the system prompt; an even number keeps user/assistant pairs together
history = [{"role": "system", "content": "You are a patient cooking coach. Keep every answer under 80 words."}]
while True:
try:
user_text = input("you> ").strip()
except EOFError: # Ctrl-D
break
if user_text in ("", "quit", "exit"):
break
history.append({"role": "user", "content": user_text})
reply = client.chat.completions.create(model=MODEL, messages=history)
answer = reply.choices[0].message.content or ""
print(f"coach> {answer}\n")
history.append({"role": "assistant", "content": answer})
# The API remembers nothing between calls: resend the conversation every turn,
# and trim it so the prompt (and what you pay for it) stops growing.
history = history[:1] + history[1:][-KEEP:]Il modello risponde all'ultimo messaggio usando tutto ciò che lo precede: chiedigli in seguito quali ingredienti hai menzionato e lo sa. I modelli di ragionamento restituiscono anche reasoning_content accanto a content; per una normale conversazione non serve reinviarlo.
Roleplay con una scheda personaggio
Un personaggio è un messaggio system scritto con cura. Poiché l'API non conserva memoria, la scheda personaggio accompagna ogni richiesta: è anche ciò che mantiene coerente il personaggio al cinquantesimo turno. Una buona scheda risponde a sei domande:
| Parte | Cosa scrivere | Esempio |
|---|---|---|
| Identità | Chi è il personaggio, in due o tre fatti che ne influenzano il comportamento | Una detective in pensione che ora gestisce un faro |
| Voce | Come parla: lunghezza delle frasi, vocabolario, abitudini, modo di rivolgersi all'utente | Misurata, nota piccoli dettagli ad alta voce, chiama l'utente «amico» |
| Mondo | L'ambientazione e uno o due fatti su cui ruota la storia | Un villaggio di pescatori degli anni Venti dove la campana di una nave affondata suona a mezzanotte |
| Limiti | Cosa farà e non farà il personaggio e come uscire dalla storia | Resta nell'ambientazione; risponde brevemente fuori personaggio a (OOC: ...); mantiene la storia adatta a un pubblico generale |
| Narrazione | Persona narrativa, tempo verbale e modo di distinguere i dialoghi | Terza persona, passato; battute fra virgolette |
| Formato | Lunghezza e conclusione di ogni risposta | Al massimo 120 parole, con due o tre scelte numerate alla fine |
Due abitudini rendono naturale il roleplay. Inserisci la battuta iniziale del personaggio nella cronologia come messaggio assistant, così la prima cosa che l'utente vede è già nel personaggio. E termina ogni risposta con una domanda o una scelta, così l'utente sa sempre come continuare; nella narrativa interattiva, le scelte numerate permettono di rispondere con una sola cifra.
Una navigatrice di un dirigibile fantasy. La risposta arriva in streaming, parola per parola.
import os
from openai import OpenAI
client = OpenAI(api_key=os.environ["SPICY_API_KEY"], base_url="https://api.spicyapi.ai/v1")
MODEL = os.environ["SPICY_MODEL"]
CARD = {
"name": "Captain Ilsa Varn",
"identity": "Navigator of the airship Kestrel. Forty years old, calm under pressure, dry sense of humour, fiercely loyal to her crew.",
"voice": "Short, vivid sentences full of wind and weather. Calls the user 'deckhand'. No modern slang.",
"world": "Islands drift on warm updrafts above an endless sea of cloud. Storms here are alive and hungry, and maps go out of date within a week.",
"boundaries": (
"Stay in character and inside this world. If the user asks about the real world, react as Ilsa would: puzzled but polite. "
"If the user writes (OOC: ...), step out of the story for one short, plain answer, then return. "
"Keep the story suitable for a general audience."
),
"narration": "Describe the scene in the second person ('You feel the deck tilt...'). Put Ilsa's spoken lines in quotation marks.",
"format": "At most 120 words. End every reply with a question or a choice, so the user always knows how to continue.",
}
system_prompt = "\n".join(
[f"You are {CARD['name']}, a character in an interactive story."]
+ [f"{key.capitalize()}: {value}" for key, value in CARD.items() if key != "name"]
)
history = [
{"role": "system", "content": system_prompt},
{"role": "assistant", "content": "\"Welcome aboard, deckhand. Storm wall to the north, a lost island to the east. Which way do we steer?\""},
]
print(history[-1]["content"])
while True:
try:
user_text = input("\nyou> ").strip()
except EOFError: # Ctrl-D
break
if user_text in ("", "quit", "exit"):
break
history.append({"role": "user", "content": user_text})
answer = ""
stream = client.chat.completions.create(model=MODEL, messages=history, stream=True)
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
piece = chunk.choices[0].delta.content
answer += piece
print(piece, end="", flush=True)
print()
history.append({"role": "assistant", "content": answer})Le stesse sei parti funzionano per qualsiasi genere. Il computer di un'astronave di fantascienza, un compagno che aiuta con la routine quotidiana o il narratore di un'avventura testuale richiedono soltanto una scheda diversa:
You are Juniper, a friendly running coach inside a fitness app.
Identity: a former trail runner, upbeat and practical, never judgemental.
Voice: warm and brief; uses the user's name when they give it; celebrates small wins.
World: the user's own week. Remember goals and aches they mention earlier in this conversation.
Boundaries: give general fitness encouragement only; for pain, injury or medical questions, suggest seeing a professional. Stay in character; answer (OOC: ...) plainly.
Narration: first person, present tense, talking directly to the user.
Format: at most 80 words; end with one concrete suggestion for today.Per storie lunghe, mantieni la scheda breve e stabile; quando la cronologia cresce, sostituisci i turni più vecchi con una breve nota «La storia finora» in un secondo messaggio system, invece di lasciar crescere il prompt senza limiti.
Trasmettere in streaming nella tua interfaccia
Lo streaming mostra la risposta mentre viene scritta, rendendo reattiva l'interfaccia di chat. Sei regole coprono ogni modello nel catalogo:
- Richiedi l'utilizzo se vuoi i conteggi dei token. Gli stream seguono le regole OpenAI: senza
stream_options.include_usage, nessun frammento contieneusage. Con"include_usage": true, arriva un frammento aggiuntivo poco prima di[DONE]con l'usagefinale e una listachoicesvuota, e ogni altro frammento hausage: null. - Controlla
choicesprima di leggerechoices[0]. Ogni frammento tranne quello finale di utilizzo contiene almeno una scelta, quindi conta soltanto quando richiedi l'utilizzo; ma il controllo non costa nulla e mantiene sicuro il ciclo in entrambi i casi. - Il ragionamento è separato. I modelli di ragionamento trasmettono i propri pensieri in
delta.reasoning_contente la risposta indelta.content. Mostra il primo attenuato, compresso oppure non mostrarlo; soltanto il secondo è la risposta. finish_reason: "length"significa risposta interrotta. Il modello ha raggiuntomax_tokens. Aumentalo oppure riducireasoning_effort.- Gli errori possono arrivare durante lo stream. Un errore dopo l'avvio dello streaming arriva come evento con un oggetto
errore le librerie OpenAI lo sollevano come eccezione. Una richiesta fallita di questo tipo non viene addebitata. Se il modello aveva già finito la risposta quando la connessione si è interrotta, non ricevi alcun errore: lo stream termina normalmente e la risposta viene addebitata come qualsiasi altra. Dettagli: Completamento, errori e disconnessioni. - Fermarsi prima non ferma l'addebito. Chiudere la connessione non ferma il modello; paghi ciò che esso riporta. Riduci invece
max_tokens(Cosa paghi quando uno stream si ferma prima).
Un renderer per terminale che segue tutte e sei le regole: ragionamento attenuato, risposta a colori e una riga finale con i conteggi dei token.
import os
from openai import OpenAI
client = OpenAI(api_key=os.environ["SPICY_API_KEY"], base_url="https://api.spicyapi.ai/v1")
stream = client.chat.completions.create(
model=os.environ["SPICY_MODEL"],
messages=[{"role": "user", "content": "Write a four-line poem about a cat who lives in a lighthouse."}],
stream=True,
stream_options={"include_usage": True},
)
finish_reason = None
usage = None
thinking_shown = False
for chunk in stream:
# With include_usage, only the last chunk before [DONE] carries usage; it has no choices.
if chunk.usage:
usage = chunk.usage
if not chunk.choices:
continue
choice = chunk.choices[0]
thinking = getattr(choice.delta, "reasoning_content", None) # reasoning models only
if thinking:
thinking_shown = True
print(f"\033[2m{thinking}\033[0m", end="", flush=True) # dimmed
if choice.delta.content:
if thinking_shown:
print("\n")
thinking_shown = False
print(choice.delta.content, end="", flush=True)
if choice.finish_reason:
finish_reason = choice.finish_reason
print()
if finish_reason == "length":
print("[stopped at max_tokens: raise it, or lower reasoning_effort]")
if usage:
print(f"[{usage.prompt_tokens} prompt tokens, {usage.completion_tokens} completion tokens]")Chiamate a strumenti
Gli strumenti permettono al modello di chiedere al programma di fare qualcosa (tirare dadi, cercare un ordine, leggere un calendario), poi di rispondere usando il risultato. Il ciclo è sempre lo stesso:
- Invia i messaggi insieme a
tools, un elenco di funzioni descritte con JSON Schema. - Se la risposta contiene
tool_calls, aggiungi quel messaggio dell'assistente esattamente come è arrivato, esegui personalmente ogni chiamata e aggiungi un messaggiotoolper chiamata con iltool_call_idcorrispondente. - Invia nuovamente tutto. Ripeti finché una risposta non contiene
tool_calls: quella è la risposta finale.
Tratta gli argomenti come input non attendibili: analizzali, controllali e restituisci un errore come risultato dello strumento invece di interrompere il programma. Limita il numero di turni, così un modello confuso non può proseguire all'infinito. Ogni modello di testo nel catalogo attualmente accetta tools; controlla i valori di tool_choice prima di forzare uno strumento specifico, perché alcuni modelli accettano soltanto auto e none.
import json
import os
import random
from openai import OpenAI
client = OpenAI(api_key=os.environ["SPICY_API_KEY"], base_url="https://api.spicyapi.ai/v1")
MODEL = os.environ["SPICY_MODEL"]
def roll_dice(count, sides):
rolls = [random.randint(1, sides) for _ in range(count)]
return {"rolls": rolls, "total": sum(rolls)}
LOCAL_TOOLS = {"roll_dice": roll_dice}
TOOLS = [{
"type": "function",
"function": {
"name": "roll_dice",
"description": "Roll dice for a tabletop game and return every roll and the total.",
"parameters": {
"type": "object",
"properties": {
"count": {"type": "integer", "minimum": 1, "maximum": 10, "description": "How many dice to roll"},
"sides": {"type": "integer", "enum": [4, 6, 8, 10, 12, 20], "description": "Sides on each die"},
},
"required": ["count", "sides"],
},
},
}]
messages = [
{"role": "system", "content": "You are the game master of a fantasy adventure. Whenever the rules call for a roll, use roll_dice; never invent a result. A roll of 12 or more on a d20 succeeds."},
{"role": "user", "content": "I try to pick the lock on the old tower door. Roll a d20 for me."},
]
for _ in range(5): # a hard stop, so a confused model cannot loop forever
reply = client.chat.completions.create(model=MODEL, messages=messages, tools=TOOLS)
message = reply.choices[0].message
# Send the assistant turn back exactly as it came, tool calls included.
messages.append(message.model_dump(exclude_none=True))
if not message.tool_calls:
print(message.content)
break
for call in message.tool_calls:
tool = LOCAL_TOOLS.get(call.function.name)
try:
result = tool(**json.loads(call.function.arguments or "{}")) if tool else {"error": "unknown tool"}
except (TypeError, ValueError) as error: # the model sent arguments the tool cannot use
result = {"error": str(error)}
print(f"[tool] {call.function.name}({call.function.arguments}) -> {result}")
messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})La prima risposta termina con finish_reason: "tool_calls" e content impostato su null; quella finale termina con "stop". I modelli di ragionamento possono includere anche reasoning_content nel turno dell'assistente: conservalo quando reinvii il turno, come negli esempi che aggiungono il messaggio senza modificarlo. Quando trasmetti in streaming una risposta con chiamata a strumenti, la chiamata arriva in parti; uniscile per index come descritto in Conversazioni e chiamate a strumenti.
Output JSON
Quando il programma richiede dati anziché prosa, chiedi JSON con response_format sui modelli il cui inputSchema lo elenca e descrivi i campi nel prompt. Analizza sempre la risposta in modo difensivo: una risposta interrotta da max_tokens non è JSON valido.
import json
import os
from openai import OpenAI
client = OpenAI(api_key=os.environ["SPICY_API_KEY"], base_url="https://api.spicyapi.ai/v1")
reply = client.chat.completions.create(
model=os.environ["SPICY_MODEL"],
messages=[
{"role": "system", "content": "Reply with a JSON object only, with the keys title (string), genre (string) and hook (one sentence)."},
{"role": "user", "content": "Invent a cosy mystery novel set in a mountain bakery."},
],
response_format={"type": "json_object"},
)
try:
book = json.loads(reply.choices[0].message.content)
print(book["title"], "-", book["hook"])
except (json.JSONDecodeError, KeyError, TypeError):
print("Not the JSON we asked for:", reply.choices[0].message.content)Su un modello che non elenca response_format, ometti il campo e affidati soltanto al prompt di sistema; il blocco try sopra conta ancora di più in quel caso.
Quando qualcosa non funziona
Gli errori vengono restituiti nel formato OpenAI, {"error": {"message", "type", "param", "code"}}, quindi le librerie OpenAI sollevano le consuete eccezioni. Basa la logica sullo stato HTTP e su error.code; mostra error.message alle persone perché indica cosa cambiare. Quando l'errore riguarda un campo, param lo identifica come percorso con punti, per esempio temperature o messages.1.content.0.
| Stato | error.code | Cosa è successo | Cosa fare |
|---|---|---|---|
| 400 | unsupported_parameter | Il modello non accetta questo campo (per esempio logprobs: is not a supported field) oppure questo tipo di parte di messaggio (per esempio una parte image_url su un modello solo testuale). I campi di campionamento come temperature non vengono rifiutati: vengono ignorati e indicati in X-Spicy-Ignored-Params | Rimuovilo. Controlla i campi del modello come in Prima di iniziare |
| 400 | invalid_request | Un campo manca o ha un valore non accettato, per esempio reasoning_effort: must be one of: "low", "high", "max"; oppure il modello non è di chat (param è model) | Cambia il valore indicato nel messaggio oppure scegli un modello il cui ID termini con /chat |
| 400 | content_rejected | La politica sui contenuti del modello ha rifiutato la richiesta | Riformulala; inviare di nuovo lo stesso input comporta un altro rifiuto |
| 401 | invalid_api_key | La chiave manca, è scritta male o è revocata | Controlla SPICY_API_KEY; le chiavi iniziano con sk-spicy- |
| 402 | insufficient_balance, spend_cap_exceeded | Il saldo o un limite di spesa non copre l'importo bloccato per questa richiesta | Aggiungi fondi, aumenta il limite oppure riduci max_tokens (vedi sotto) |
| 404 | model_not_found | Nessun modello ha questo ID (param è model). Il messaggio mostra la struttura dell'ID e, se esiste, il modello di chat utilizzabile più simile | Copia l'ID esatto dal catalogo aggiornato |
| 404 | not_found | Il modello esiste ma non può essere chiamato adesso | Scegli un altro modello dal catalogo aggiornato |
| 409 | conflict | Un Idempotency-Key è stato riutilizzato con un corpo di richiesta diverso | Invia ogni nuovo messaggio con una nuova chiave; consulta Chiavi di idempotenza sugli endpoint di testo |
| 429 | rate_limit_exceeded | Troppe richieste | Attendi e riprova; se è presente un header Retry-After, indica quanti secondi |
| 503 | upstream_failed, model_unavailable | Il modello ha avuto un errore o non è temporaneamente disponibile. Nessun addebito | Riprova più tardi con un nuovo Idempotency-Key |
Perché una richiesta può fallire con 402 anche se il saldo sembra sufficiente: prima dell'esecuzione di una richiesta di testo, viene bloccato un importo che copre i messaggi e l'intero max_tokens di output ai prezzi del modello (il max_tokens predefinito se lo ometti). Paghi soltanto i token effettivamente usati e il resto dell'importo bloccato viene liberato al termine, ma l'intero blocco deve essere coperto dal saldo disponibile prima dell'avvio. Un max_tokens realistico riduce l'importo bloccato. Consulta Fatturazione.
Trovare il costo di una chiamata
Ogni richiesta accettata viene eseguita come attività e la risposta la identifica nell'header X-Spicy-Task-Id, con o senza streaming. Leggi l'header, poi cerca l'attività: cost è finale quando settled è true. Una richiesta rifiutata prima dell'accettazione (per esempio un errore di validazione) non crea attività e non ha quell'header.
import os
from openai import OpenAI
client = OpenAI(api_key=os.environ["SPICY_API_KEY"], base_url="https://api.spicyapi.ai/v1")
raw = client.chat.completions.with_raw_response.create(
model=os.environ["SPICY_MODEL"],
messages=[{"role": "user", "content": "Say hello in one sentence."}],
)
reply = raw.parse() # the usual ChatCompletion object
print(reply.choices[0].message.content)
print("task:", raw.headers.get("x-spicy-task-id"))Con uno stream, l'header è nella risposta prima del primo frammento; with_raw_response in Python e .withResponse() in Node.js funzionano allo stesso modo con stream: true. Le voci di GET /api/v1/jobs contengono anche il requestId della chiamata che le ha create, cioè l'header di risposta X-Request-Id.
Continua a leggere
Testo e streaming · Migrare da OpenAI o OpenRouter · Catalogo dei modelli e schemi · Fatturazione · Errori
Testo e streaming
Chiama i modelli di testo nel formato ufficiale OpenAI, Anthropic o Google Gemini, e gestisci correttamente conversazioni, strumenti, stream e costi.
Migrare da OpenAI o OpenRouter
Collega il codice OpenAI esistente a SpicyAPI: URL di base, chiave, ID del modello, differenze nei campi e impostazioni OpenRouter da rimuovere.

