Texte et streaming
Appeler les modèles de texte au format officiel OpenAI, Anthropic ou Google Gemini, et gérer correctement conversations, outils, flux et coûts.
Les modèles de texte répondent dans la même requête HTTP : il n’y a pas de tâche à interroger. Vous pouvez attendre la réponse complète, ou la recevoir en streaming, c’est-à-dire morceau par morceau pendant que le modèle l’écrit encore, comme une application de chat qui affiche une réponse au fil de la frappe. Les morceaux diffusés arrivent sous forme de Server-Sent Events (SSE) : des lignes de texte brut qui commencent par data:, envoyées les unes après les autres sur une connexion qui reste ouverte jusqu’à ce que la réponse soit complète.
Choisir un modèle de texte disponible
Les exemples shell de cette page sont écrits pour un terminal macOS ou Linux et utilisent jq, un petit outil JSON en ligne de commande (brew install jq sous macOS, sudo apt install jq sous Debian ou Ubuntu). Sous Windows, exécutez-les dans WSL, ou utilisez plutôt les exemples avec les SDK Python.
Placez d’abord votre clé API dans une variable d’environnement, c’est-à-dire une valeur nommée que votre terminal transmet aux commandes qu’il exécute : vous n’avez ainsi jamais à coller la clé dans une commande.
export SPICY_API_KEY="sk-spicy-paste-your-key-here"Listez les modèles de texte que cette clé peut appeler en ce moment :
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'Vous devez voir un identifiant de modèle par ligne. Copiez-en un et placez-le dans SPICY_MODEL ; tous les exemples ci-dessous le lisent :
export SPICY_MODEL="MODEL_ID_FROM_CATALOG" # remplacez par une ligne de la liste ci-dessusMODEL_ID_FROM_CATALOG n’est qu’un emplacement réservé : ne l’envoyez jamais tel quel. Avant de compter sur les outils, les images, les réglages de raisonnement ou la sortie structurée, lisez l’inputSchema de ce modèle via GET /api/v1/models/{model} : ces fonctionnalités ne marchent que si le schéma les liste. La compatibilité des protocoles ne donne pas toutes les capacités à tous les modèles.
Le même identifiant de modèle fonctionne avec chacun des protocoles ci-dessous, quel que soit l’éditeur du modèle : vous pouvez appeler le modèle d’un autre éditeur au format Anthropic, ou un modèle qui n’est pas de Google au format Gemini.
Protocoles pris en charge
Tous ces points de terminaison partagent la même clé API et le même catalogue de modèles, ainsi que les mêmes limites de débit, la même validation des paramètres et la même facturation ; seuls les formats de requête et de réponse diffèrent. Utilisez celui que parlent déjà votre code ou votre SDK.
| Protocole | Point de terminaison | En-tête de la clé API |
|---|---|---|
| OpenAI Chat Completions | POST /v1/chat/completions | Authorization: Bearer |
| OpenAI Responses | POST /v1/responses | Authorization: Bearer |
| Anthropic Messages | POST /v1/messages | x-api-key ou Authorization: Bearer |
| Google Gemini | POST /v1beta/models/{model}:generateContent | x-goog-api-key ou Authorization: Bearer |
| Google Gemini (streaming) | POST /v1beta/models/{model}:streamGenerateContent | x-goog-api-key ou Authorization: Bearer |
| Flux natif à prix confirmé | POST /api/v1/jobs/stream | Authorization: Bearer |
Sur /v1 et /v1beta, les réponses et les erreurs suivent leur propre protocole et ne portent pas l’enveloppe native {code,msg,data,request_id} : ne les analysez donc pas avec un client qui ne lit que body.data. Le format Gemini place l’identifiant du modèle dans le chemin de l’URL ; les barres obliques de l’identifiant restent telles quelles et n’ont pas besoin d’être encodées.
N’envoyez la clé que dans un en-tête de requête. x-api-key et x-goog-api-key ne fonctionnent que sur /v1 et /v1beta ; l’API native /api/v1 n’accepte que Authorization: Bearer. Un paramètre de requête ?key= n’est jamais accepté : une fois qu’une clé se retrouve dans une URL, elle traîne dans l’historique du navigateur, les serveurs proxy et les journaux d’accès. Les SDK officiels envoient tous la clé dans un en-tête : ils ne sont pas concernés.
Utiliser un SDK officiel
Changez seulement l’URL de base et la clé ; tout le reste fonctionne comme d’habitude avec chaque SDK.
| SDK | URL de base |
|---|---|
OpenAI (openai) | https://api.spicyapi.ai/v1 |
Anthropic (anthropic) | https://api.spicyapi.ai |
Google GenAI (google-genai) | https://api.spicyapi.ai |
Les SDK Anthropic et Google ajoutent eux-mêmes /v1 ou /v1beta : leur URL de base ne contient donc pas de chemin de version. L’URL de base du SDK OpenAI, elle, doit inclure /v1. Les trois exemples Python ci-dessous lisent SPICY_API_KEY dans l’environnement, ainsi qu’un SPICY_MODEL choisi dans le catalogue en direct.
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)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)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)La couche de compatibilité facture au prix en vigueur à l’acceptation et ne prend pas de devis
/v1 et /v1beta facturent le prix en vigueur au moment où la requête est acceptée. Ils n’acceptent ni quoteId ni expectedCost : ce chemin n’a donc aucun plafond par appel, et une boucle d’agent qui s’emballe continue de dépenser.
Il existe deux façons de le borner, et la plupart des intégrations ont besoin des deux :
- Un appel à la fois : demandez un devis avec le point natif
POST /api/v1/jobs/quote, puis soumettezPOST /api/v1/jobs/streamavec lequoteIdet l’expectedCostrenvoyés. Si le prix a changé entre-temps, l’acceptation échoue avec40901au lieu de facturer plus que ce que vous aviez accepté. Voir Flux natif à prix confirmé. - Tout ce que dépense cette clé : fixez les plafonds de dépense quotidien, mensuel et cumulé de la clé. Ils s’appliquent partout, y compris ici, et un dépassement renvoie
40202au lieu d’une facture plus lourde. Voir Authentification.
Clés d’idempotence sur les points de terminaison de texte
Un en-tête Idempotency-Key empêche qu’une nouvelle tentative réseau soit facturée deux fois. Sur les points de terminaison de texte, il ne permet pas de récupérer une réponse perdue, car la réponse vous est diffusée et n’est pas stockée :
- Renvoyer une clé dont la requête s’est terminée, avec le même corps, renvoie
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) et ne facture rien de plus. L’enregistrement de la tâche ne contient pas non plus le texte. - Si cette requête antérieure a échoué, renvoyer la même clé avec le même corps renvoie la même erreur qu’alors, et ne facture rien.
- La renvoyer avec un corps différent renvoie
409.
En pratique : envoyez une nouvelle clé avec chaque nouveau message (par exemple $(uuidgen) dans un shell, uuid.uuid4() en Python), et ne réutilisez une clé que pour renvoyer exactement la même requête après une erreur réseau, en acceptant que la réponse à cette requête soit irrécupérable. Si vous voulez obtenir de nouveau une réponse, envoyez la requête avec une nouvelle clé et payez-la.
Dans les SDK, passez la clé à chaque appel, jamais sur l’objet client : avec une seule clé pour tout le client, chaque message après le premier serait refusé avec 409 pendant 24 heures.
import os, uuid
# SDK OpenAI (client de l'exemple OpenAI ci-dessus). Le SDK Anthropic accepte
# le même argument extra_headers sur 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 et types de l'exemple Google GenAI ci-dessus) :
# http_options dans la config propre à l'appel
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())}),
),
)Les SDK OpenAI et Anthropic réessaient d’eux-mêmes après un dépassement de délai, avec les mêmes en-têtes. Si une telle nouvelle tentative atteint une requête que nous avions déjà acceptée, le SDK lève une erreur 400 (bad request) avec le message ci-dessus : la première tentative a été traitée et facturée, et sa réponse est perdue.
Utiliser les modèles SpicyAPI depuis le client en ligne de commande Grok
Cette configuration fait envoyer à SpicyAPI les requêtes du client en ligne de commande de xAI ; le client dialogue dès lors avec les modèles du catalogue SpicyAPI, et non avec le point de terminaison propre à xAI. La famille de modèles Grok publiée au catalogue en fait partie : vous l’appelez par son identifiant de modèle sur SpicyAPI, comme n’importe quel autre modèle. Le client peut faire pointer un modèle personnalisé vers n’importe quel point de terminaison compatible OpenAI. Ajoutez l’entrée ci-dessous à ~/.grok/config.toml ; env_key donne le nom de la variable d’environnement que lit le client, si bien que la clé elle-même n’est jamais écrite dans le fichier.
[model.spicyapi]
model = "MODEL_ID_FROM_CATALOG"
base_url = "https://api.spicyapi.ai/v1"
name = "SpicyAPI"
env_key = "SPICY_API_KEY"
[models]
default = "spicyapi"Remplacez MODEL_ID_FROM_CATALOG par un identifiant exact de modèle de texte tiré du catalogue en direct, de la forme <publisher>/<model>/<task> ; ne laissez jamais l’emplacement réservé en place. Lancez ensuite grok -m spicyapi, ou changez de modèle en cours de session avec /model spicyapi. Le client parle Chat Completions : la correspondance des paramètres, les fins de flux et les formes d’erreur décrites ci-dessous s’appliquent donc sans changement.
Correspondance des paramètres entre protocoles
Les champs propres à chaque protocole sont d’abord convertis vers les noms de paramètres canoniques de la plateforme, puis validés par rapport à l’inputSchema du modèle choisi. Un paramètre que le modèle ne prend pas en charge renvoie 400 ; il n’est jamais ignoré en silence pendant que la requête est facturée normalement. Le tableau ci-dessous ne montre que la correspondance, et « — » signifie que le protocole n’a pas de champ équivalent. C’est l’inputSchema de chaque modèle qui définit s’il accepte un paramètre et quelles valeurs il autorise.
| Paramètre canonique | Chat Completions | Responses | Messages | Gemini |
|---|---|---|---|---|
messages | messages | instructions, input | system, messages | systemInstruction, contents |
max_tokens | max_tokens, max_completion_tokens | max_output_tokens | max_tokens | generationConfig.maxOutputTokens |
temperature | temperature | temperature | temperature | generationConfig.temperature |
top_p | top_p | top_p | top_p | generationConfig.topP |
tools | tools | tools (type function) | tools (outils personnalisés) | tools[].functionDeclarations |
tool_choice | tool_choice | tool_choice | tool_choice | toolConfig.functionCallingConfig |
response_format | response_format | text.format | — | generationConfig.responseMimeType plus responseSchema ou responseJsonSchema |
reasoning_effort | reasoning_effort | reasoning.effort | — | generationConfig.thinkingConfig |
Les champs absents du tableau sont eux aussi convertis en paramètres canoniques et validés par le modèle ; tout ce qui manque dans son inputSchema renvoie 400. Par exemple, seuls certains modèles listent seed dans leur inputSchema ; sur les autres, seed dans Chat et generationConfig.seed dans Gemini renvoient 400. Les réglages d’échantillonnage et les séquences d’arrêt font exception : sur un modèle qui ne les accepte pas, ils sont ignorés (nommés dans l’en-tête de réponse X-Spicy-Ignored-Params), et une valeur hors de ce que le modèle accepte est remplacée par la valeur acceptée la plus proche (nommée dans X-Spicy-Adjusted-Params). Cela couvre temperature, top_p et les pénalités, stop dans Chat, stop_sequences dans Messages, generationConfig.stopSequences dans Gemini et les niveaux de reasoning_effort. Lisez le schéma du modèle que vous avez choisi plutôt que de supposer un champ d’un autre modèle.
Quelques exceptions :
- Les champs qui n’ont de sens que pour la plateforme d’origine du protocole sont ignorés et n’influent pas sur la génération :
user,metadataetstoredans Chat ;user,metadata,storeetincludedans Responses ;metadatadans Messages. - Responses ne prend pas en charge
previous_response_id; envoyez l’historique complet dansinput. safetySettingsest accepté par Gemini mais n’a aucun effet.cachedContentet les outils côté serveur commegoogleSearchetcodeExecutionne sont pas pris en charge et renvoient400. Passez les images eninlineData(base64) ou enfileData.fileUri(une URL https) ; c’est l’inputSchemadu modèle qui définit s’il accepte les images.
Envoyer un flux Chat
Avec SPICY_API_KEY et SPICY_MODEL définis comme ci-dessus, ces commandes écrivent la requête dans un fichier puis diffusent la réponse. Remplacez le prompt d’exemple selon vos besoins, et validez les paramètres du modèle par rapport au schéma actuel avant l’envoi. Chaque exécution envoie une Idempotency-Key neuve, car chaque exécution est un nouveau message.
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 affiche chaque morceau dès son arrivée, et --max-time 120 abandonne au bout de deux minutes. Vous devez voir arriver une à une des lignes comme celles ci-dessous, puis enfin data: [DONE]. La réponse est répartie entre les morceaux delta.content ; assemblez-les dans l’ordre pour obtenir le texte complet. Le dernier morceau avant [DONE] porte 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 est fourni avec macOS ; sous Linux, il fait partie du paquet uuid-runtime.
Envoyer une requête Gemini
Cet exemple utilise les mêmes SPICY_API_KEY et SPICY_MODEL, et là encore une nouvelle Idempotency-Key : c’est une requête différente de l’exemple Chat, et réutiliser cette clé renverrait 409. L’identifiant du modèle figure dans l’URL : le corps de la requête ne contient donc pas de champ model. Entourez l’URL de guillemets doubles et écrivez la variable sous la forme ${SPICY_MODEL}, pour que le shell ne prenne pas le deux-points qui la suit pour un modificateur de variable.
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.jsonSi vous n’avez pas besoin du streaming, remplacez :streamGenerateContent?alt=sse par :generateContent et lisez la réponse dans candidates[0].content.parts[].text ; la consommation se trouve dans usageMetadata. Sans alt=sse, une requête en streaming renvoie un unique tableau JSON écrit progressivement ; les SDK officiels ajoutent alt=sse pour vous.
Flux natif à prix confirmé
POST /api/v1/jobs/stream est le seul point de terminaison de texte qui accepte un devis : il peut donc refuser de s’exécuter si le prix a changé. Le corps de la requête a la même forme que pour createTask : model plus un input dont les champs viennent de l’inputSchema du modèle (pour les modèles de texte, messages et les autres paramètres canoniques du tableau ci-dessus).
# 1. Écrivez la requête une seule fois.
jq -n --arg model "$SPICY_MODEL" '{
model: $model,
input: {messages: [{role: "user", content: "Explain a rainbow in one sentence."}]}
}' > native-request.json
# 2. Demandez-en le devis. Cela ne crée rien et ne facture rien.
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. Dans les cinq minutes, ajoutez quoteId et expectedCost à la requête inchangée, puis diffusez-la.
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.jsonL’étape 2 affiche code: 200 et le prix en dollars américains ; tout autre code signifie que le devis a échoué, et msg en donne la raison. Si l’appel de flux échoue avant l’envoi du moindre texte — par exemple avec 40901 parce que le prix ou la requête a changé, ou avec 40201 pour un solde insuffisant —, vous recevez le JSON habituel {code,msg,data,request_id} et rien n’est mis de côté. Une fois le streaming commencé, un échec termine le flux avec event: error et data: {"code":"stream_interrupted"}, et rien n’est facturé pour cette requête. Les modèles d’image, de vidéo et d’audio renvoient 503 ici ; utilisez createTask pour eux.
La réponse arrive sous forme de lignes SSE data:. Contrairement à /v1/chat/completions, ce point de terminaison ne garantit pas un format d’événement unique pour tous les modèles : testez donc votre analyseur avec le modèle choisi. Si vous avez besoin d’un format unique et stable, utilisez un point de terminaison /v1 et bornez les dépenses avec les plafonds de la clé.
Envoyer des images, de la vidéo et de l’audio à un modèle de texte
Certains modèles de texte lisent des images en plus du texte, et quelques-uns lisent aussi la vidéo et l’audio. Vous envoyez les médias avec une question, et la réponse reste du texte. Ce qu’un modèle accepte est écrit dans son schéma ; le protocole ne le sous-entend pas. Vérifiez le modèle que vous avez choisi :
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 liste indique les parties de contenu que le modèle accepte. ["text","image_url"] signifie texte et images ; un modèle qui lit aussi la vidéo et l’audio ajoute video_url, audio_url et input_audio. Une liste vide signifie texte seul. Envoyer une partie que le modèle ne liste pas renvoie 400 et ne facture rien.
Pour envoyer des médias, faites du content du message une liste de parties plutôt qu’une chaîne — une partie text pour la question et une partie par fichier :
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.jsonChaque type de média a sa propre partie :
| Partie | Forme | Origine du fichier |
|---|---|---|
| Image | {"type": "image_url", "image_url": {"url": "…"}} | Une URL HTTPS publique, un fichier téléversé (spicy://f/…) ou une petite URI data: |
| Vidéo | {"type": "video_url", "video_url": {"url": "…"}} | Un fichier téléversé (spicy://f/…) ou une URL HTTPS publique |
| Fichier audio | {"type": "audio_url", "audio_url": {"url": "spicy://f/…"}} | Un fichier téléversé uniquement |
| Audio en ligne | {"type": "input_audio", "input_audio": {"data": "<base64>", "format": "wav"}} | Du base64 dans la requête, wav ou mp3 |
La manière de téléverser un fichier et d’obtenir son URI spicy://f/… est décrite dans Médias ; les fichiers téléversés sont conservés un jour. Quelques règles découlent de la façon dont chaque type atteint le modèle :
- Les images peuvent être une URL HTTPS publique qui renvoie le fichier image lui-même (et non une page qui l’affiche), un fichier téléversé ou une URI
data:dans les limites en ligne. JPEG et PNG fonctionnent sur tous les modèles qui acceptent les images. - La vidéo et l’audio fonctionnent mieux en téléversement. Nous lisons le fichier téléversé et le transmettons au modèle dans la requête ; la vidéo et l’audio téléversés d’une même requête ne peuvent donc totaliser que 14 Mo au maximum — vidéo MP4 ou WebM, audio MP3 ou WAV. Un total plus gros renvoie
400et ne facture rien. Une URL vidéo publique est transmise telle quelle, comme un lien, et ne fonctionne que si le fournisseur du modèle peut la télécharger. - L’audio en ligne (
input_audio) suit le format OpenAI et doit tenir dans le corps de requête de 2 Mio ; il convient donc aux clips courts. Téléversez tout ce qui est plus long et envoyez-le enaudio_url.
Plusieurs fichiers peuvent figurer dans un même message, chacun dans sa propre partie, et les messages précédents de l’historique peuvent aussi contenir des médias.
Les autres protocoles transportent les images sous leur propre forme, et toutes atteignent le modèle de manière identique. Les parties vidéo et audio passent par Chat Completions et le flux natif.
| Protocole | Partie image |
|---|---|
| Chat Completions | {"type": "image_url", "image_url": {"url": "…"}} |
| Responses | {"type": "input_image", "image_url": "…"} |
| Anthropic Messages | {"type": "image", "source": {"type": "url", "url": "…"}}, ou une source base64 |
| Gemini | inlineData (base64) ou fileData.fileUri (une URL HTTPS) |
Ce que cela coûte. Les médias sont comptés comme jetons d’entrée au tarif d’entrée habituel du modèle ; il n’y a pas de prix distinct par fichier. Une image ajoute généralement de quelques centaines à quelques milliers de jetons selon le modèle et la taille de l’image ; la vidéo et l’audio augmentent avec leur durée. Le bloc usage de la réponse indique le nombre exact.
Ce qui est retenu à l’avance. Avant l’exécution d’une requête, nous retenons une borne supérieure et libérons la différence au règlement. Pour une requête dont les seuls médias sont des images que vous avez téléversées ou envoyées en ligne, la borne ajoute une marge fixe par image. Pour une requête qui contient de la vidéo ou de l’audio, ou une image fournie sous forme d’URL publique, la borne est dimensionnée sur toute la limite d’entrée du modèle, car sa taille ne peut pas être connue à l’avance : le montant retenu peut donc être bien supérieur au montant finalement facturé. Téléversez vos images pour garder la retenue faible, gardez un solde suffisant pour cette retenue, ou chiffrez d’abord la requête avec POST /api/v1/jobs/quote pour la voir.
Dans le Playground. Sur un modèle qui accepte les médias, la zone de message affiche un bouton de pièce jointe. Vous pouvez aussi coller une capture d’écran ou déposer des fichiers sur la zone ; chaque message accepte jusqu’à quatre pièces jointes, et la vidéo et l’audio qu’elles contiennent peuvent totaliser 14 Mo. Les modèles qui ne lisent que du texte n’affichent aucun bouton.
Conversations et appels d’outils
Chat Completions
- Sans streaming : lisez
choices[0].message. - En streaming : traitez
choices[].deltaun événement SSE à la fois, et non paquet réseau par paquet réseau. Assemblez lestool_callsd’après leurindex, et ne validez et n’exécutez un appel qu’une fois sesargumentscomplets. - Gardez le message assistant dans l’historique, et renvoyez chaque résultat dans un message
toolavec letool_call_idcorrespondant.
Responses
- Envoyez l’historique complet dans
input;previous_response_idest refusé. - Renvoyez les résultats des fonctions sous forme d’éléments
function_call_outputavec lecall_idcorrespondant.
Messages
- Utilisez des blocs de contenu
text,tool_useettool_resultplutôt que la forme de message de Chat.
Gemini
contentsalterne entre deux rôles,useretmodel.- Quand le modèle veut appeler un outil, il renvoie une partie
functionCall. Après avoir exécuté l’outil, placez le résultat dans une partiefunctionResponseet renvoyez-le dans le contenuusersuivant, avec le mêmenameque l’appel.
Fin de réponse, erreurs et déconnexions
| Protocole | Fin normale | Consommation |
|---|---|---|
| Chat Completions | data: [DONE] | usage dans le dernier morceau |
| Responses | Événement response.completed | response.usage dans cet événement |
| Messages | message_stop après message_delta | usage dans message_delta |
| Gemini | Dernier événement avec finishReason ; il n’y a pas de [DONE] | usageMetadata |
Vérifiez le statut HTTP et le Content-Type avant de lire un flux ; les erreurs reviennent dans le JSON propre à chaque protocole :
| Protocole | Corps de l’erreur |
|---|---|
| Chat Completions, Responses | {"error": {"message", "type", "param", "code"}} |
| Messages | {"type": "error", "error": {"type", "message"}} |
| Gemini | {"error": {"code", "message", "status"}} |
Les codes de statut HTTP sont les mêmes pour les quatre protocoles. Le champ status de Gemini reprend les noms de statut de Google : 400 correspond à INVALID_ARGUMENT, 401 à UNAUTHENTICATED, 402 à FAILED_PRECONDITION, 403 à PERMISSION_DENIED, 404 à NOT_FOUND, 409 à ABORTED, 429 à RESOURCE_EXHAUSTED, 500 à INTERNAL, 503 à UNAVAILABLE et 504 à DEADLINE_EXCEEDED. Faites dépendre votre code du code de statut et du type d’erreur, jamais du texte du message.
Si un problème survient après le début du streaming, Chat et Gemini envoient un événement qui porte un objet d’erreur, Responses envoie un événement d’erreur et Messages envoie event: error. Votre client doit gérer ces événements d’erreur, ainsi que les dépassements de délai et une connexion qui se ferme sans le marqueur de fin normal. Si le flux passe par votre propre proxy, désactivez-y la mise en tampon des réponses, sinon les morceaux arrivent par paquets.
Ce que vous payez quand un flux s’arrête trop tôt
- Vous vous déconnectez (onglet fermé, requête interrompue, délai d’attente dépassé côté client) : se déconnecter n’arrête pas la facturation. Vous êtes facturé pour la consommation que déclare le modèle, qui pour beaucoup de modèles couvre la réponse entière — y compris le texte que vous n’avez jamais reçu —, et jamais plus que le montant mis de côté à l’acceptation de la requête. Le texte non reçu ne peut pas être récupéré plus tard. Pour dépenser moins, réduisez
max_tokensplutôt que de couper la connexion. - Le modèle ou notre service échoue en cours de flux (vous recevez un événement d’erreur, ou le flux se termine sans son marqueur de fin) : la requête est marquée comme échouée et rien n’est facturé.
- Le modèle vous a envoyé du texte ou du raisonnement, puis sa réponse a été jugée vide ou refusée : ce que vous avez déjà reçu est facturé selon la consommation que déclare le modèle, jamais plus que le montant mis de côté, et le flux se termine normalement. Ce n’est que si rien du tout ne vous est parvenu que la requête est intégralement remboursée.
- La requête dépasse son échéance côté serveur : elle se termine en échec et rien n’est facturé.
Délais d’expiration et streaming
Sans streaming, la réponse arrive d’un seul bloc une fois prête, et les réponses longues comme les raisonnements longs prennent du temps : des modèles comme Grok, GLM, DeepSeek Pro et Claude Opus peuvent réfléchir plusieurs minutes avant d’écrire quoi que ce soit, surtout avec un reasoning_effort élevé. La périphérie réseau placée devant l’API ferme une connexion sur laquelle rien n’est arrivé pendant 100 secondes environ : les requêtes des deux types sont donc maintenues ouvertes pendant que le modèle travaille.
- Une requête sans streaming encore en cours après 30 secondes environ commence sa réponse plus tôt. Vous recevez le statut
200,Content-Type: application/jsonet l’en-têteX-Spicy-Keepalive: 1, puis un saut de ligne toutes les 15 secondes, puis le corps JSON. JSON accepte des espaces avant une valeur : les analyseurs standard (json.loads,JSON.parse,response.json()) et les SDK officiels lisent donc le corps tel quel. Une requête qui se termine plus tôt reçoit une réponse normale, avec son vrai code de statut et sans cet en-tête. - Passé ce point, une erreur se lit dans le corps, pas dans le statut. Le
200est déjà parti : le corps contient l’objet d’erreur habituel du protocole (voir le tableau ci-dessus), avec le même code et le même message ; au format Gemini,error.codegarde le statut HTTP que l’erreur aurait eu. QuandX-Spicy-Keepaliveest présent, vérifiez que le corps ne contient pas d’objeterroravant de lire la réponse : les SDK officiels ne traitent pas un200comme une erreur. Une requête échouée n’est pas facturée. - Le streaming garde lui aussi la connexion ouverte. Tant que le modèle travaille, une réponse en streaming transporte des lignes de maintien de connexion (des commentaires
: keep-aliveen SSE, des lignes vides au format Gemini), si bien que la connexion n’est jamais inactive, quelle que soit la durée de réflexion du modèle. Votre analyseur doit les ignorer, comme l’exige le format SSE ; les SDK officiels le font déjà. - Préférez
stream: truepour tout ce qui risque d’être long. Vous voyez la réponse à mesure qu’elle s’écrit, et une erreur arrive sous forme d’événement d’erreur que les SDK comprennent. Chat Completions, Responses et Messages acceptentstream: true; au format Gemini, appelez:streamGenerateContent?alt=sseau lieu de:generateContent. Un corps modifié constitue une nouvelle requête : envoyez-la avec un nouvelIdempotency-Key, car réutiliser l’ancien renvoie409. - Une requête coupée par la périphérie réseau n’est pas facturée. Si rien ne vous parvient pendant 100 secondes environ et que la connexion se ferme, la requête apparaît dans Console → Journal et dans
GET /api/v1/jobscomme échouée, avec l’errorCodetimeoutet un message qui explique ce qui s’est passé. Si vous la renvoyez avec le mêmeIdempotency-Key, ce message vous revient comme erreur. - Vos propres limites de temps s’appliquent toujours. Un délai total dans votre client HTTP, ou dans un proxy que vous gérez, met fin lui aussi à une requête, et dès que quelque chose vous est parvenu — y compris une ligne de maintien de connexion —, cela compte comme une déconnexion de votre part (voir Ce que vous payez quand un flux s’arrête trop tôt). Les SDK OpenAI officiels attendent jusqu’à 10 minutes par défaut ; les exemples curl de cette page abandonnent au bout de 2 minutes, augmentez donc
--max-timepour les raisonnements longs.
Pour activer le streaming dans les SDK OpenAI et dans curl :
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()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 --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.jsonL’exemple Node.js utilise await au niveau supérieur : exécutez-le donc comme module ES (par exemple en l’enregistrant sous stream.mjs). La commande curl envoie le fichier chat-request.json de Envoyer un flux Chat, qui contient déjà stream: true ; avec --max-time 900, la réponse peut prendre jusqu’à 15 minutes.
Devis et facturation
Pour une confirmation explicite du coût, utilisez le flux natif avec un devis. /v1 et /v1beta n’acceptent pas les éléments de devis ; ils appliquent le prix en vigueur à l’acceptation.
Les appels de texte ne renvoient pas d’identifiant de tâche, et un marqueur de fin de texte ne prouve pas le montant final. Pour savoir ce qu’un appel a coûté, listez les tâches récentes avec la même clé API — GET /api/v1/jobs?model=<model>&from=<YYYY-MM-DD> — et faites la correspondance avec createdAt, ou regardez dans Console → Journal. Ne considérez cost comme définitif que lorsque settled vaut true. Voir Tâches asynchrones (historique et reprise des tâches) et Facturation.
reasoning_tokens est déjà inclus dans completion_tokens : ne l’ajoutez pas une seconde fois.
Autres formes de requête
Ces exemples ne décrivent que les enveloppes des protocoles. Remplacez MODEL_ID_FROM_CATALOG par un modèle en direct qui prend en charge les champs montrés ; n’envoyez jamais l’emplacement réservé lui-même.
{
"model": "MODEL_ID_FROM_CATALOG",
"input": "Explain a rainbow in one sentence.",
"stream": false
}{
"model": "MODEL_ID_FROM_CATALOG",
"max_tokens": 256,
"messages": [{"role": "user", "content": "Explain a rainbow in one sentence."}],
"stream": false
}Pour aller plus loin
Devis et compatibilité des protocoles · Idempotence · Facturation · Intégration en production · Dépannage et reprise
Devis et compatibilité des protocoles
Connaître le prix exact d’une requête avant de payer, la soumettre à ce prix, reprendre la main après une réponse perdue et savoir comment les API compatibles pour le texte et la vidéo sont facturées.
Recettes de chat
Programmes complets en Python, Node.js et curl pour les conversations, le roleplay, le streaming dans votre interface et les appels d’outils.

