spicyapiDocumentation
Texte et chat
Contenu principal

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-dessus

MODEL_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.

ProtocolePoint de terminaisonEn-tête de la clé API
OpenAI Chat CompletionsPOST /v1/chat/completionsAuthorization: Bearer
OpenAI ResponsesPOST /v1/responsesAuthorization: Bearer
Anthropic MessagesPOST /v1/messagesx-api-key ou Authorization: Bearer
Google GeminiPOST /v1beta/models/{model}:generateContentx-goog-api-key ou Authorization: Bearer
Google Gemini (streaming)POST /v1beta/models/{model}:streamGenerateContentx-goog-api-key ou Authorization: Bearer
Flux natif à prix confirméPOST /api/v1/jobs/streamAuthorization: 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.

SDKURL 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.

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)

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 soumettez POST /api/v1/jobs/stream avec le quoteId et l’expectedCost renvoyés. Si le prix a changé entre-temps, l’acceptation échoue avec 40901 au 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 40202 au 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.

Idempotency-Key par appel
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.

~/.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"

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 canoniqueChat CompletionsResponsesMessagesGemini
messagesmessagesinstructions, inputsystem, messagessystemInstruction, contents
max_tokensmax_tokens, max_completion_tokensmax_output_tokensmax_tokensgenerationConfig.maxOutputTokens
temperaturetemperaturetemperaturetemperaturegenerationConfig.temperature
top_ptop_ptop_ptop_pgenerationConfig.topP
toolstoolstools (type function)tools (outils personnalisés)tools[].functionDeclarations
tool_choicetool_choicetool_choicetool_choicetoolConfig.functionCallingConfig
response_formatresponse_formattext.format—generationConfig.responseMimeType plus responseSchema ou responseJsonSchema
reasoning_effortreasoning_effortreasoning.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, metadata et store dans Chat ; user, metadata, store et include dans Responses ; metadata dans Messages.
  • Responses ne prend pas en charge previous_response_id ; envoyez l’historique complet dans input.
  • safetySettings est accepté par Gemini mais n’a aucun effet. cachedContent et les outils côté serveur comme googleSearch et codeExecution ne sont pas pris en charge et renvoient 400. Passez les images en inlineData (base64) ou en fileData.fileUri (une URL https) ; c’est l’inputSchema du 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.json

Si 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.json

L’é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.json

Chaque type de média a sa propre partie :

PartieFormeOrigine 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 400 et 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 en audio_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.

ProtocolePartie 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
GeminiinlineData (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[].delta un événement SSE à la fois, et non paquet réseau par paquet réseau. Assemblez les tool_calls d’après leur index, et ne validez et n’exécutez un appel qu’une fois ses arguments complets.
  • Gardez le message assistant dans l’historique, et renvoyez chaque résultat dans un message tool avec le tool_call_id correspondant.

Responses

  • Envoyez l’historique complet dans input ; previous_response_id est refusé.
  • Renvoyez les résultats des fonctions sous forme d’éléments function_call_output avec le call_id correspondant.

Messages

  • Utilisez des blocs de contenu text, tool_use et tool_result plutôt que la forme de message de Chat.

Gemini

  • contents alterne entre deux rôles, user et model.
  • 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 partie functionResponse et renvoyez-le dans le contenu user suivant, avec le même name que l’appel.

Fin de réponse, erreurs et déconnexions

ProtocoleFin normaleConsommation
Chat Completionsdata: [DONE]usage dans le dernier morceau
ResponsesÉvénement response.completedresponse.usage dans cet événement
Messagesmessage_stop après message_deltausage dans message_delta
GeminiDernier é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 :

ProtocoleCorps 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_tokens plutô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/json et l’en-tête X-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 200 est 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.code garde le statut HTTP que l’erreur aurait eu. Quand X-Spicy-Keepalive est présent, vérifiez que le corps ne contient pas d’objet error avant de lire la réponse : les SDK officiels ne traitent pas un 200 comme 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-alive en 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: true pour 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 acceptent stream: true ; au format Gemini, appelez :streamGenerateContent?alt=sse au lieu de :generateContent. Un corps modifié constitue une nouvelle requête : envoyez-la avec un nouvel Idempotency-Key, car réutiliser l’ancien renvoie 409.
  • 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/jobs comme échouée, avec l’errorCode timeout et un message qui explique ce qui s’est passé. Si vous la renvoyez avec le même Idempotency-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-time pour les raisonnements longs.

Pour activer le streaming dans les SDK OpenAI et dans 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’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.

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
}

Pour aller plus loin

Devis et compatibilité des protocoles · Idempotence · Facturation · Intégration en production · Dépannage et reprise

Sur cette page