spicyapiDocumentazione
Testo e chat
Contenuto principale

Testo e streaming

Chiama i modelli di testo nel formato ufficiale OpenAI, Anthropic o Google Gemini, e gestisci correttamente conversazioni, strumenti, stream e costi.

I modelli di testo rispondono nella stessa richiesta HTTP: non c’è nessuna attività su cui fare polling. Puoi aspettare la risposta completa oppure riceverla in streaming, cioè a piccoli pezzi mentre il modello sta ancora scrivendo, come quando un’app di chat mostra la risposta che si compone lettera dopo lettera. I pezzi arrivano come Server-Sent Events (SSE): semplici righe di testo che iniziano con data:, inviate una dopo l’altra su una connessione che resta aperta finché la risposta non è completa.

Scegli un modello di testo disponibile

Gli esempi di shell di questa pagina sono scritti per un terminale macOS o Linux e usano jq, un piccolo strumento a riga di comando per il JSON (brew install jq su macOS, sudo apt install jq su Debian o Ubuntu). Su Windows, eseguili in WSL oppure usa gli esempi con gli SDK Python.

Per prima cosa metti la tua chiave API in una variabile d’ambiente: un valore con un nome che il terminale passa ai comandi che esegue, così non devi mai incollare la chiave dentro un comando.

export SPICY_API_KEY="sk-spicy-paste-your-key-here"

Elenca i modelli di testo che questa chiave può chiamare in questo momento:

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'

Dovresti vedere un ID di modello per riga. Copiane uno e tienilo in SPICY_MODEL; tutti gli esempi qui sotto lo leggono da lì:

export SPICY_MODEL="MODEL_ID_FROM_CATALOG"   # sostituiscilo con una riga dell'elenco qui sopra

MODEL_ID_FROM_CATALOG è solo un segnaposto: non inviarlo mai così com’è. Prima di contare su strumenti, immagini, impostazioni di ragionamento o output strutturato, leggi l’inputSchema di quel modello da GET /api/v1/models/{model}: queste funzioni sono disponibili solo se lo schema le elenca. La compatibilità di protocollo non dà a ogni modello tutte le capacità.

Lo stesso ID di modello funziona con qualsiasi protocollo tra quelli qui sotto, indipendentemente da chi ha creato il modello: puoi chiamare il modello di un altro produttore nel formato Anthropic, o un modello che non è di Google nel formato Gemini.

Protocolli supportati

Tutti questi endpoint condividono la stessa chiave API e lo stesso catalogo dei modelli, oltre agli stessi limiti di frequenza, alla stessa convalida dei parametri e alla stessa fatturazione; cambiano solo i formati di richiesta e di risposta. Usa quello che il tuo codice o il tuo SDK parla già.

ProtocolloEndpointIntestazione della chiave API
OpenAI Chat CompletionsPOST /v1/chat/completionsAuthorization: Bearer
OpenAI ResponsesPOST /v1/responsesAuthorization: Bearer
Anthropic MessagesPOST /v1/messagesx-api-key o Authorization: Bearer
Google GeminiPOST /v1beta/models/{model}:generateContentx-goog-api-key o Authorization: Bearer
Google Gemini (streaming)POST /v1beta/models/{model}:streamGenerateContentx-goog-api-key o Authorization: Bearer
Stream nativo con prezzo confermatoPOST /api/v1/jobs/streamAuthorization: Bearer

Su /v1 e /v1beta, risposte ed errori seguono ciascuno il proprio protocollo e non hanno la struttura di risposta nativa {code,msg,data,request_id}, quindi non analizzarli con un client che legge solo body.data. Il formato Gemini mette l’ID del modello nel percorso dell’URL; le barre dell’ID restano come sono e non vanno codificate.

Invia la chiave solo in un’intestazione della richiesta. x-api-key e x-goog-api-key funzionano solo su /v1 e /v1beta; l’API nativa /api/v1 accetta soltanto Authorization: Bearer. Un parametro di query ?key= non viene mai accettato: una chiave finita in un URL resta nella cronologia del browser, nei server proxy e nei log di accesso. Gli SDK ufficiali inviano tutti la chiave in un’intestazione, quindi non sono interessati.

Usare un SDK ufficiale

Cambia solo l’URL di base e la chiave; tutto il resto funziona come fa normalmente ciascun SDK.

SDKURL di base
OpenAI (openai)https://api.spicyapi.ai/v1
Anthropic (anthropic)https://api.spicyapi.ai
Google GenAI (google-genai)https://api.spicyapi.ai

Gli SDK Anthropic e Google aggiungono da soli /v1 o /v1beta, quindi il loro URL di base non contiene il percorso della versione; l’URL di base dell’SDK OpenAI, invece, deve includere /v1. Tutti e tre gli esempi Python qui sotto leggono SPICY_API_KEY dall’ambiente, insieme a un SPICY_MODEL scelto dal catalogo in tempo reale.

OpenAI
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": "user", "content": "Explain a rainbow in one sentence."}],
)
print(reply.choices[0].message.content)
Anthropic
import os
import anthropic

client = anthropic.Anthropic(api_key=os.environ["SPICY_API_KEY"], base_url="https://api.spicyapi.ai")
message = client.messages.create(
    model=os.environ["SPICY_MODEL"],
    max_tokens=256,
    messages=[{"role": "user", "content": "Explain a rainbow in one sentence."}],
)
print(message.content[0].text)
Google GenAI
import os
from google import genai
from google.genai import types

client = genai.Client(
    api_key=os.environ["SPICY_API_KEY"],
    http_options=types.HttpOptions(base_url="https://api.spicyapi.ai"),
)
response = client.models.generate_content(
    model=os.environ["SPICY_MODEL"],
    contents="Explain a rainbow in one sentence.",
)
print(response.text)

Il livello di compatibilità fissa il prezzo all’accettazione e non accetta preventivi

/v1 e /v1beta addebitano il prezzo in vigore quando la richiesta viene accettata. Non accettano quoteId né expectedCost, quindi su questo percorso non c’è un tetto alla singola chiamata: un ciclo di un agente che sfugge al controllo continua a spendere.

Ci sono due modi per porre un limite, e la maggior parte delle integrazioni li vuole entrambi:

  • Una chiamata alla volta: chiedi il preventivo con l’endpoint nativo POST /api/v1/jobs/quote, poi invia POST /api/v1/jobs/stream con il quoteId e l’expectedCost restituiti. Se nel frattempo il prezzo è cambiato, l’accettazione fallisce con 40901 invece di addebitarti più di quanto avevi accettato. Vedi Stream nativo con prezzo confermato.
  • Tutto ciò che questa chiave spende: imposta i limiti di spesa giornaliero, mensile e complessivo della chiave. Valgono su ogni superficie, compresa questa, e quando vengono superati restituiscono 40202 invece di un conto più alto. Vedi Autenticazione.

Chiavi di idempotenza sugli endpoint di testo

Un’intestazione Idempotency-Key impedisce che un nuovo tentativo dopo un problema di rete ti venga addebitato due volte. Sugli endpoint di testo, però, non ti permette di riavere una risposta andata persa, perché la risposta ti arriva in streaming e non viene archiviata:

  • Se reinvii una chiave la cui richiesta è stata completata, con lo stesso corpo, ricevi 400 (This Idempotency-Key was already used for a completed request, and its answer cannot be replayed. Use a new Idempotency-Key to send a new request) e non ti viene addebitato altro. Nemmeno il record dell’attività conterrà il testo.
  • Se quella richiesta precedente era fallita, reinviare la stessa chiave con lo stesso corpo restituisce lo stesso errore di allora, senza alcun addebito.
  • Se la reinvii con un corpo diverso, ricevi 409.

Quindi: invia una chiave nuova con ogni nuovo messaggio (per esempio $(uuidgen) in una shell, uuid.uuid4() in Python), e riusa una chiave solo quando reinvii esattamente la stessa richiesta dopo un errore di rete, sapendo che la risposta a quella richiesta non si può recuperare. Se vuoi di nuovo una risposta, invia la richiesta con una chiave nuova e pagala.

Negli SDK, passa la chiave a ogni singola chiamata, mai sull’oggetto client: con una sola chiave per tutto il client, ogni messaggio dopo il primo verrebbe rifiutato con 409 per 24 ore.

Idempotency-Key per singola chiamata
import os, uuid

# SDK OpenAI (il client dell'esempio OpenAI qui sopra). L'SDK Anthropic accetta
# lo stesso argomento extra_headers in client.messages.create().
reply = client.chat.completions.create(
    model=os.environ["SPICY_MODEL"],
    messages=[{"role": "user", "content": "Explain a rainbow in one sentence."}],
    extra_headers={"Idempotency-Key": str(uuid.uuid4())},
)

# SDK Google GenAI (client e types dell'esempio Google GenAI qui sopra):
# http_options dentro la config della singola chiamata
response = client.models.generate_content(
    model=os.environ["SPICY_MODEL"],
    contents="Explain a rainbow in one sentence.",
    config=types.GenerateContentConfig(
        http_options=types.HttpOptions(headers={"Idempotency-Key": str(uuid.uuid4())}),
    ),
)

Gli SDK OpenAI e Anthropic ripetono da soli le richieste andate in timeout, con le stesse intestazioni. Se uno di questi nuovi tentativi raggiunge una richiesta che avevamo già accettato, l’SDK solleva un errore 400 (bad request) con il messaggio riportato sopra: il primo tentativo è stato elaborato e addebitato, e la sua risposta è persa.

Usare i modelli SpicyAPI dal client a riga di comando Grok

Questa configurazione fa sì che il client a riga di comando di xAI invii le sue richieste a SpicyAPI: da quel momento dialoga con i modelli del catalogo SpicyAPI e non con l’endpoint di xAI. Tra questi c’è anche la famiglia di modelli Grok pubblicata nel catalogo: la richiami con il suo ID modello su SpicyAPI, come qualsiasi altro modello. Il client può puntare un modello personalizzato verso qualsiasi endpoint compatibile con OpenAI. Aggiungi la voce qui sotto a ~/.grok/config.toml; env_key indica il nome della variabile d’ambiente che il client legge, così la chiave vera e propria non finisce mai nel file.

~/.grok/config.toml
[model.spicyapi]
model = "MODEL_ID_FROM_CATALOG"
base_url = "https://api.spicyapi.ai/v1"
name = "SpicyAPI"
env_key = "SPICY_API_KEY"

[models]
default = "spicyapi"

Sostituisci MODEL_ID_FROM_CATALOG con l’ID esatto di un modello di testo preso dal catalogo in tempo reale, nella forma <publisher>/<model>/<task>; non lasciare mai il segnaposto al suo posto. Poi avvia con grok -m spicyapi, oppure cambia modello durante una sessione con /model spicyapi. Il client parla Chat Completions, quindi la mappatura dei parametri, le chiusure degli stream e le forme degli errori descritte più sotto valgono senza modifiche.

Mappatura dei parametri tra i protocolli

I campi di ciascun protocollo vengono prima convertiti nei nomi di parametro canonici della piattaforma, poi convalidati rispetto all’inputSchema del modello scelto. Un parametro che il modello non supporta restituisce 400: non viene mai scartato in silenzio mentre la richiesta viene addebitata come al solito. La tabella qui sotto mostra solo la mappatura, e «—» significa che il protocollo non ha un campo equivalente. Se un dato modello accetta un parametro, e con quali valori, lo stabilisce l’inputSchema di quel modello.

Parametro canonicoChat CompletionsResponsesMessagesGemini
messagesmessagesinstructions, inputsystem, messagessystemInstruction, contents
max_tokensmax_tokens, max_completion_tokensmax_output_tokensmax_tokensgenerationConfig.maxOutputTokens
temperaturetemperaturetemperaturetemperaturegenerationConfig.temperature
top_ptop_ptop_ptop_pgenerationConfig.topP
toolstoolstools (tipo function)tools (strumenti personalizzati)tools[].functionDeclarations
tool_choicetool_choicetool_choicetool_choicetoolConfig.functionCallingConfig
response_formatresponse_formattext.format—generationConfig.responseMimeType più responseSchema o responseJsonSchema
reasoning_effortreasoning_effortreasoning.effort—generationConfig.thinkingConfig

Anche i campi che non compaiono nella tabella vengono convertiti in parametri canonici e convalidati dal modello; tutto ciò che manca dal suo inputSchema restituisce 400. Per esempio, solo alcuni modelli elencano seed nel proprio inputSchema; sugli altri, seed di Chat e generationConfig.seed di Gemini restituiscono 400. Le impostazioni di campionamento e le sequenze di stop sono l’eccezione: su un modello che non le accetta vengono ignorate (indicate nell’header di risposta X-Spicy-Ignored-Params), e un valore fuori da ciò che il modello accetta viene cambiato in quello accettato più vicino (indicato in X-Spicy-Adjusted-Params). Ciò vale per temperature, top_p e le penalità, per stop di Chat, stop_sequences di Messages, generationConfig.stopSequences di Gemini e per i livelli di reasoning_effort.

Qualche eccezione:

  • I campi che hanno significato solo per la piattaforma del protocollo stesso vengono ignorati e non influiscono sulla generazione: user, metadata e store in Chat; user, metadata, store e include in Responses; metadata in Messages.
  • Responses non supporta previous_response_id; invia la cronologia completa in input.
  • safetySettings di Gemini viene accettato ma non ha alcun effetto. cachedContent e gli strumenti lato server come googleSearch e codeExecution non sono supportati e restituiscono 400. Passa le immagini come inlineData (base64) o fileData.fileUri (un URL https); se un modello accetta immagini lo stabilisce il suo inputSchema.

Inviare uno stream Chat

Con SPICY_API_KEY e SPICY_MODEL impostate come sopra, questo esempio scrive la richiesta in un file e riceve la risposta in streaming. Sostituisci il prompt di esempio come preferisci, e convalida i parametri del modello rispetto allo schema attuale prima di inviare. Ogni esecuzione invia una Idempotency-Key nuova, perché ogni esecuzione è un nuovo messaggio.

jq -n --arg model "$SPICY_MODEL" '{
  model: $model,
  messages: [{role: "user", content: "Explain a rainbow in one sentence."}],
  stream: true
}' > chat-request.json

curl --fail-with-body --no-buffer --max-time 120 \
  https://api.spicyapi.ai/v1/chat/completions \
  -H "Authorization: Bearer $SPICY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  --data-binary @chat-request.json

--no-buffer stampa ogni pezzo appena arriva, e --max-time 120 rinuncia dopo due minuti. Dovresti vedere arrivare, una dopo l’altra, righe come quelle qui sotto, e alla fine data: [DONE]. La risposta è distribuita nei pezzi delta.content: uniscili in ordine per ottenere il testo completo. L’ultimo blocco prima di [DONE] contiene usage.

data: {"id":"chatcmpl-req_01k...","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"A rainbow"}}]}

data: {"id":"chatcmpl-req_01k...","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":" forms when"}}]}

data: [DONE]

uuidgen è già incluso in macOS; su Linux arriva con il pacchetto uuid-runtime.

Inviare una richiesta Gemini

Qui si usano gli stessi SPICY_API_KEY e SPICY_MODEL, e di nuovo una Idempotency-Key nuova: è una richiesta diversa da quella dell’esempio Chat, quindi riusare quella chiave restituirebbe 409. L’ID del modello va nell’URL, quindi il corpo della richiesta non contiene un campo model. Racchiudi l’URL tra virgolette doppie e scrivi la variabile come ${SPICY_MODEL}, così la shell non interpreta i due punti che la seguono come un modificatore della variabile.

jq -n '{
  systemInstruction: {parts: [{text: "Answer in one sentence."}]},
  contents: [{role: "user", parts: [{text: "Explain a rainbow."}]}],
  generationConfig: {temperature: 0.7, maxOutputTokens: 256}
}' > gemini-request.json

curl --fail-with-body --no-buffer --max-time 120 \
  "https://api.spicyapi.ai/v1beta/models/${SPICY_MODEL}:streamGenerateContent?alt=sse" \
  -H "x-goog-api-key: $SPICY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  --data-binary @gemini-request.json

Se non ti serve lo streaming, sostituisci :streamGenerateContent?alt=sse con :generateContent e leggi la risposta da candidates[0].content.parts[].text; il consumo è in usageMetadata. Senza alt=sse, una richiesta in streaming restituisce un unico array JSON scritto un po’ alla volta; gli SDK ufficiali aggiungono alt=sse per te.

Stream nativo con prezzo confermato

POST /api/v1/jobs/stream è l’unico endpoint di testo che accetta un preventivo, quindi può rifiutarsi di partire se il prezzo è cambiato. Il corpo della richiesta ha la stessa forma di quello di createTask: model più un input i cui campi vengono dall’inputSchema del modello (per i modelli di testo, messages e gli altri parametri canonici della tabella qui sopra).

# 1. Scrivi la richiesta una sola volta.
jq -n --arg model "$SPICY_MODEL" '{
  model: $model,
  input: {messages: [{role: "user", content: "Explain a rainbow in one sentence."}]}
}' > native-request.json

# 2. Chiedi il preventivo. Non crea nulla e non addebita nulla.
QUOTE=$(curl -s https://api.spicyapi.ai/api/v1/jobs/quote \
  -H "Authorization: Bearer $SPICY_API_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @native-request.json)
echo "$QUOTE" | jq '{code, msg, estimatedCost: .data.estimatedCost, maxCharge: .data.maxCharge, expiresAt: .data.expiresAt}'

# 3. Entro cinque minuti, aggiungi quoteId ed expectedCost alla richiesta invariata e avvia lo stream.
jq --argjson quote "$QUOTE" \
  '. + {quoteId: $quote.data.quoteId, expectedCost: $quote.data.estimatedCost}' \
  native-request.json > native-confirmed.json

curl --fail-with-body --no-buffer --max-time 120 \
  https://api.spicyapi.ai/api/v1/jobs/stream \
  -H "Authorization: Bearer $SPICY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  --data-binary @native-confirmed.json

Il passo 2 stampa code: 200 e il prezzo in dollari statunitensi; qualsiasi altro code significa che il preventivo non è riuscito, e msg spiega perché. Se la chiamata allo stream fallisce prima che venga inviato del testo, per esempio con 40901 perché il prezzo o la richiesta sono cambiati, o con 40201 per saldo insufficiente, ricevi il normale JSON {code,msg,data,request_id} e non viene accantonato nulla. Una volta iniziato lo streaming, un errore chiude lo stream con event: error e data: {"code":"stream_interrupted"}, e per quella richiesta non viene addebitato nulla. I modelli di immagini, video e audio qui restituiscono 503; per quelli usa createTask.

La risposta arriva come righe SSE data:. A differenza di /v1/chat/completions, questo endpoint non garantisce lo stesso formato di evento per tutti i modelli, quindi prova il tuo parser con il modello che hai scelto; se ti serve un unico formato stabile, usa un endpoint /v1 e limita la spesa con i limiti della chiave.

Inviare immagini, video e audio a un modello di testo

Alcuni modelli di testo leggono le immagini oltre al testo, e alcuni leggono anche video e audio. Invii i contenuti multimediali insieme a una domanda e la risposta resta testo. Ciò che un modello accetta è scritto nel suo schema, non lo lascia intendere il protocollo. Controlla il modello che hai scelto:

curl -s "https://api.spicyapi.ai/api/v1/models/$SPICY_MODEL" \
  -H "Authorization: Bearer $SPICY_API_KEY" \
  | jq -c '[.data.inputSchema.properties.messages.items.properties.content.items.oneOf[]?.properties.type.const]'

La lista indica le parti di contenuto che il modello accetta. ["text","image_url"] significa testo e immagini; un modello che legge anche video e audio elenca pure video_url, audio_url e input_audio. Una lista vuota significa solo testo. Se invii una parte che il modello non elenca ottieni 400 e non viene addebitato nulla.

Per inviare contenuti multimediali, trasforma il content del messaggio in una lista di parti invece di una stringa: una parte text per la domanda e una parte per ogni file:

jq -n --arg model "$SPICY_MODEL" '{
  model: $model,
  stream: true,
  messages: [{
    role: "user",
    content: [
      {type: "text", text: "What is in this picture?"},
      {type: "image_url", image_url: {url: "https://example.com/photo.jpg"}}
    ]
  }]
}' > image-request.json

curl --fail-with-body --no-buffer --max-time 120 \
  https://api.spicyapi.ai/v1/chat/completions \
  -H "Authorization: Bearer $SPICY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  --data-binary @image-request.json

Ogni tipo di contenuto ha la sua parte:

ParteFormaDa dove arriva il file
Immagine{"type": "image_url", "image_url": {"url": "…"}}Un URL HTTPS pubblico, un file caricato (spicy://f/…) o un piccolo URI data:
Video{"type": "video_url", "video_url": {"url": "…"}}Un file caricato (spicy://f/…) o un URL HTTPS pubblico
File audio{"type": "audio_url", "audio_url": {"url": "spicy://f/…"}}Solo un file caricato
Audio inline{"type": "input_audio", "input_audio": {"data": "<base64>", "format": "wav"}}Base64 nella richiesta, wav o mp3

Come caricare un file e ottenere il suo URI spicy://f/… è descritto in Media; i caricamenti durano un giorno. Alcune regole derivano da come ogni tipo raggiunge il modello:

  • Le immagini possono essere un URL HTTPS pubblico che restituisce il file immagine vero e proprio (non una pagina che lo mostra), un file caricato o un URI data: entro i limiti inline. JPEG e PNG funzionano su ogni modello che accetta immagini.
  • Video e audio funzionano meglio come caricamenti. Leggiamo il file caricato e lo passiamo al modello dentro la richiesta, quindi video e audio caricati in una stessa richiesta possono totalizzare al massimo 14 MB: video MP4 o WebM, audio MP3 o WAV. Un totale maggiore restituisce 400 e non addebita nulla. Un URL video pubblico viene invece inoltrato come link e funziona solo se il provider del modello riesce a scaricarlo.
  • L’audio inline (input_audio) segue il formato OpenAI e deve stare nel corpo della richiesta da 2 MiB, quindi si adatta a clip brevi; carica qualsiasi cosa più lunga e inviala come audio_url.

In un messaggio possono andare più file, ciascuno come parte a sé, e anche i messaggi precedenti della cronologia possono contenere media.

Gli altri protocolli trasportano le immagini nella propria forma, e tutti raggiungono il modello in modo identico. Le parti video e audio vengono inviate tramite Chat Completions e lo stream nativo.

ProtocolloParte immagine
Chat Completions{"type": "image_url", "image_url": {"url": "…"}}
Responses{"type": "input_image", "image_url": "…"}
Anthropic Messages{"type": "image", "source": {"type": "url", "url": "…"}}, oppure una sorgente base64
GeminiinlineData (base64) oppure fileData.fileUri (un URL HTTPS)

Quanto costa. I contenuti multimediali vengono contati come token di input alla normale tariffa di input del modello; non esiste un prezzo separato per file. Un’immagine di solito aggiunge da qualche centinaio a qualche migliaio di token, a seconda del modello e delle dimensioni dell’immagine; video e audio crescono con la loro durata. Il blocco usage della risposta riporta il numero esatto.

Cosa viene trattenuto in anticipo. Prima che una richiesta venga eseguita trattieniamo un limite massimo e rilasciamo la differenza al momento del regolamento. Per una richiesta i cui unici contenuti multimediali sono immagini che hai caricato o inviato inline, il limite aggiunge una quota fissa per ogni immagine. Per una richiesta che contiene video o audio, o un’immagine indicata come URL pubblico, il limite è dimensionato sull’intero limite di input del modello, perché la sua dimensione non si può conoscere in anticipo: l’importo trattenuto può quindi essere molto superiore all’addebito finale. Carica le immagini per mantenere piccola la trattenuta, tieni un saldo sufficiente per la trattenuta, oppure calcola prima la richiesta con POST /api/v1/jobs/quote per vederla.

Nel Playground. Su un modello che accetta contenuti multimediali, la casella del messaggio mostra un pulsante per allegare. Puoi anche incollare uno screenshot o trascinare file sulla casella; ogni messaggio accetta fino a quattro allegati, e video e audio tra questi possono totalizzare 14 MB. I modelli che leggono solo testo non mostrano alcun pulsante.

Conversazioni e chiamate agli strumenti

Chat Completions

  • Senza streaming: leggi choices[0].message.
  • In streaming: elabora choices[].delta un evento SSE alla volta, non un pacchetto di rete alla volta. Ricomponi tool_calls in base a index, e convalida ed esegui una chiamata solo quando i suoi arguments sono completi.
  • Tieni il messaggio dell’assistente nella cronologia, e restituisci ogni risultato in un messaggio tool con il tool_call_id corrispondente.

Responses

  • Invia la cronologia completa in input; previous_response_id viene rifiutato.
  • Restituisci i risultati delle funzioni come elementi function_call_output con il call_id corrispondente.

Messages

  • Usa i blocchi di contenuto text, tool_use e tool_result invece della forma dei messaggi di Chat.

Gemini

  • I contents si alternano tra due ruoli, user e model.
  • Quando il modello vuole chiamare uno strumento, restituisce una parte functionCall. Dopo aver eseguito lo strumento, racchiudi il risultato in una parte functionResponse e rimandalo nel contenuto user successivo, usando lo stesso name della chiamata.

Fine, errori e disconnessioni

ProtocolloFine normaleConsumo
Chat Completionsdata: [DONE]usage nell’ultimo blocco
ResponsesEvento response.completedresponse.usage in quell’evento
Messagesmessage_stop dopo message_deltausage in message_delta
GeminiUltimo evento con finishReason; non c’è [DONE]usageMetadata

Controlla lo stato HTTP e il Content-Type prima di leggere uno stream; gli errori tornano nel JSON proprio di ciascun protocollo:

ProtocolloCorpo dell’errore
Chat Completions, Responses{"error": {"message", "type", "param", "code"}}
Messages{"type": "error", "error": {"type", "message"}}
Gemini{"error": {"code", "message", "status"}}

I codici di stato HTTP sono gli stessi in tutti e quattro i protocolli. Lo status di Gemini usa i nomi di stato di Google: 400 è INVALID_ARGUMENT, 401 è UNAUTHENTICATED, 402 è FAILED_PRECONDITION, 403 è PERMISSION_DENIED, 404 è NOT_FOUND, 409 è ABORTED, 429 è RESOURCE_EXHAUSTED, 500 è INTERNAL, 503 è UNAVAILABLE e 504 è DEADLINE_EXCEEDED. Nel codice prendi decisioni in base al codice di stato e al tipo di errore, mai in base al testo del messaggio.

Se qualcosa fallisce dopo l’inizio dello streaming, Chat e Gemini inviano un evento che contiene un oggetto di errore, Responses invia un evento di errore e Messages invia event: error. Il tuo client deve gestire questi eventi di errore, così come i timeout e una connessione che si chiude senza il normale segnale di fine. Se fai passare lo stream attraverso un tuo proxy, disattiva lì il buffering delle risposte, altrimenti i pezzi arrivano a raffiche.

Che cosa paghi quando uno stream si ferma prima del tempo

  • Ti disconnetti tu (chiudi la scheda, interrompi la richiesta, scatta un timeout del client): disconnettersi non ferma l’addebito. Ti viene addebitato il consumo riportato dal modello, che per molti modelli copre l’intera risposta, compreso il testo che non hai ricevuto, e mai più dell’importo accantonato quando la richiesta è stata accettata. Il testo che non hai ricevuto non si può recuperare in seguito. Per spendere meno, abbassa max_tokens invece di disconnetterti.
  • Il modello o il nostro servizio falliscono a metà stream (ricevi un evento di errore, oppure lo stream termina senza il suo segnale di fine): la richiesta viene segnata come fallita e non viene addebitato nulla.
  • Il modello ti ha inviato testo o ragionamento, poi la sua risposta è stata giudicata vuota o rifiutata: quello che hai già ricevuto viene addebitato in base al consumo riportato dal modello, mai più dell’importo accantonato, e lo stream termina normalmente. Solo quando non ti è arrivato proprio nulla l’importo viene restituito per intero.
  • La richiesta supera la sua scadenza lato server: termina come fallita e non viene addebitato nulla.

Timeout e streaming

Senza streaming la risposta arriva tutta insieme quando è pronta, e le risposte lunghe e i ragionamenti lunghi richiedono tempo: modelli come Grok, GLM, DeepSeek Pro e Claude Opus possono pensare per minuti prima di scrivere qualcosa, soprattutto con un reasoning_effort alto. Il bordo di rete davanti all’API chiude una connessione su cui non arriva nulla per circa 100 secondi, quindi le richieste di entrambi i tipi restano aperte mentre il modello lavora.

  • Una richiesta senza streaming ancora in corso dopo circa 30 secondi inizia la risposta in anticipo. Ricevi lo stato 200, Content-Type: application/json e l’header X-Spicy-Keepalive: 1, poi un a capo ogni 15 secondi e infine il body JSON. JSON ammette spazi bianchi prima di un valore, quindi i parser standard (json.loads, JSON.parse, response.json()) e gli SDK ufficiali leggono il body senza modifiche. Una richiesta che finisce prima riceve una risposta normale, con il suo vero codice di stato e senza questo header.
  • Da quel momento un errore sta nel body, non nello stato. Lo stato 200 è già partito, quindi il body contiene il solito oggetto di errore del protocollo (quello della tabella sopra), con lo stesso codice e lo stesso messaggio; nel formato Gemini, error.code conserva lo stato HTTP che l’errore avrebbe avuto. Quando è presente X-Spicy-Keepalive, controlla se il body contiene un oggetto error prima di leggere la risposta: gli SDK ufficiali non trattano un 200 come errore. Una richiesta fallita non viene addebitata.
  • Anche lo streaming tiene aperta la connessione. Mentre il modello sta ancora lavorando, una risposta in streaming trasporta righe di keep-alive (commenti : keep-alive in SSE, righe vuote nel formato Gemini), così la connessione non resta mai inattiva, per quanto a lungo pensi il modello. Il tuo parser deve ignorarle, come richiede il formato SSE; gli SDK ufficiali lo fanno già.
  • Per tutto ciò che potrebbe durare a lungo, preferisci stream: true. Vedi la risposta mentre viene scritta, e un errore arriva come evento di errore che gli SDK capiscono. Chat Completions, Responses e Messages accettano stream: true; nel formato Gemini, chiama :streamGenerateContent?alt=sse invece di :generateContent. Un body diverso è una nuova richiesta, quindi inviala con una nuova Idempotency-Key: riusare quella vecchia restituisce 409.
  • Una richiesta interrotta dal bordo di rete non viene addebitata. Se per circa 100 secondi non ti arriva nulla e la connessione si chiude, la richiesta compare in Console → Log e in GET /api/v1/jobs come fallita, con errorCode timeout e un messaggio che spiega cosa è successo. Se la rimandi con la stessa Idempotency-Key, ricevi quel messaggio come errore.
  • I tuoi limiti di tempo valgono comunque. Un timeout totale nel tuo client HTTP, o in un proxy che gestisci, chiude anche una richiesta, e una volta che ti è arrivato qualcosa (compresa una riga di keep-alive), conta come una tua disconnessione (vedi Che cosa paghi quando uno stream si ferma prima del tempo). Gli SDK ufficiali di OpenAI aspettano fino a 10 minuti per impostazione predefinita; gli esempi curl di questa pagina si arrendono dopo 2 minuti, quindi alza --max-time per i ragionamenti lunghi.

Ecco come attivare lo streaming negli SDK di OpenAI e in curl:

OpenAI Python
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": "Explain a rainbow in one sentence."}],
    stream=True,
)
for chunk in stream:
    if chunk.choices and chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end="", flush=True)
print()
OpenAI Node.js
import OpenAI from "openai";

const client = new OpenAI({ apiKey: process.env.SPICY_API_KEY, baseURL: "https://api.spicyapi.ai/v1" });
const stream = await client.chat.completions.create({
  model: process.env.SPICY_MODEL,
  messages: [{ role: "user", content: "Explain a rainbow in one sentence." }],
  stream: true,
});
for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}
process.stdout.write("\n");
curl
curl --fail-with-body --no-buffer --max-time 900 \
  https://api.spicyapi.ai/v1/chat/completions \
  -H "Authorization: Bearer $SPICY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  --data-binary @chat-request.json

L’esempio Node.js usa await al livello superiore, quindi eseguilo come modulo ES (per esempio salvandolo come stream.mjs). Il comando curl invia il chat-request.json di Inviare uno stream Chat, che imposta già stream: true; con --max-time 900 la risposta può durare fino a 15 minuti.

Preventivi e fatturazione

Per una conferma esplicita del costo, usa lo stream nativo con un preventivo. /v1 e /v1beta non accettano le credenziali di un preventivo: applicano il prezzo al momento dell’accettazione.

Le chiamate di testo non restituiscono un ID attività, e un segnale di fine testo non prova l’addebito definitivo. Per sapere quanto è costata una chiamata, elenca le attività recenti con la stessa chiave API (GET /api/v1/jobs?model=<model>&from=<YYYY-MM-DD>) e trova quella giusta in base a createdAt, oppure guarda in Console → Log. Considera cost definitivo solo quando settled è true. Vedi Attività asincrone e Fatturazione.

reasoning_tokens è già incluso in completion_tokens: non sommarlo due volte.

Altre forme di richiesta

Questi esempi descrivono solo le strutture dei protocolli. Sostituisci MODEL_ID_FROM_CATALOG con un modello disponibile nel catalogo in tempo reale che supporti i campi mostrati; non inviare mai il segnaposto in sé.

POST /v1/responses
{
  "model": "MODEL_ID_FROM_CATALOG",
  "input": "Explain a rainbow in one sentence.",
  "stream": false
}
POST /v1/messages
{
  "model": "MODEL_ID_FROM_CATALOG",
  "max_tokens": 256,
  "messages": [{"role": "user", "content": "Explain a rainbow in one sentence."}],
  "stream": false
}

Continua a leggere

Preventivi e compatibilità di protocollo · Idempotenza · Fatturazione · Integrazione in produzione · Diagnosi e ripristino

In questa pagina