spicyapiDokumentation
Text und Chat
Hauptinhalt

Text und Streaming

Textmodelle im offiziellen Format von OpenAI, Anthropic oder Google Gemini aufrufen und Unterhaltungen, Tools, Streams und Kosten richtig behandeln.

Textmodelle antworten in derselben HTTP-Anfrage: Es gibt keinen Task, den Sie pollen müssten. Sie können auf die gesamte Antwort warten oder sie streamen, also den Text in kleinen Stücken empfangen, während das Modell noch schreibt, so wie eine Chat-App eine Antwort nach und nach eintippt. Gestreamte Stücke kommen als Server-Sent Events (SSE) an: einfache Textzeilen, die mit data: beginnen und nacheinander über eine Verbindung gesendet werden, die offen bleibt, bis die Antwort vollständig ist.

Ein verfügbares Textmodell auswählen

Die Shell-Beispiele auf dieser Seite sind für ein Terminal unter macOS oder Linux geschrieben und verwenden jq, ein kleines Kommandozeilenwerkzeug für JSON (brew install jq unter macOS, sudo apt install jq unter Debian oder Ubuntu). Unter Windows führen Sie sie in WSL aus oder verwenden stattdessen die Beispiele mit den Python-SDKs.

Legen Sie zuerst Ihren API-Schlüssel in einer Umgebungsvariable ab: einem benannten Wert, den Ihr Terminal an die Befehle weitergibt, die es ausführt, sodass Sie den Schlüssel nie in einen Befehl einfügen müssen:

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

Listen Sie die Textmodelle auf, die dieser Schlüssel gerade aufrufen kann:

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'

Sie sollten eine Modell-ID pro Zeile sehen. Kopieren Sie eine davon und legen Sie sie in SPICY_MODEL ab; jedes Beispiel unten liest diesen Wert:

export SPICY_MODEL="MODEL_ID_FROM_CATALOG"   # durch eine Zeile aus der Liste oben ersetzen

MODEL_ID_FROM_CATALOG ist nur ein Platzhalter; senden Sie ihn nie wörtlich. Bevor Sie sich auf Tools, Bilder, Reasoning-Einstellungen oder strukturierte Ausgabe verlassen, lesen Sie das inputSchema des Modells über GET /api/v1/models/{model}: Diese Funktionen stehen nur zur Verfügung, wenn das Schema sie aufführt. Protokollkompatibilität verleiht nicht jedem Modell jede Fähigkeit.

Dieselbe Modell-ID funktioniert mit jedem der Protokolle unten, egal, wer das Modell entwickelt hat: Sie können das Modell eines anderen Anbieters im Anthropic-Format aufrufen oder ein Modell, das nicht von Google stammt, im Gemini-Format.

Unterstützte Protokolle

Alle diese Endpunkte teilen sich denselben API-Schlüssel und denselben Modellkatalog, dazu dieselben Rate Limits, dieselbe Parameterprüfung und dieselbe Abrechnung; nur die Formate von Anfrage und Antwort unterscheiden sich. Verwenden Sie das Format, das Ihr vorhandener Code oder Ihr SDK bereits spricht.

ProtokollEndpunktHeader für den API-Schlüssel
OpenAI Chat CompletionsPOST /v1/chat/completionsAuthorization: Bearer
OpenAI ResponsesPOST /v1/responsesAuthorization: Bearer
Anthropic MessagesPOST /v1/messagesx-api-key oder Authorization: Bearer
Google GeminiPOST /v1beta/models/{model}:generateContentx-goog-api-key oder Authorization: Bearer
Google Gemini (Streaming)POST /v1beta/models/{model}:streamGenerateContentx-goog-api-key oder Authorization: Bearer
Nativer Stream mit bestätigtem PreisPOST /api/v1/jobs/streamAuthorization: Bearer

Unter /v1 und /v1beta folgen Antworten und Fehler ihrem jeweiligen Protokoll und tragen keinen nativen Envelope {code,msg,data,request_id}; parsen Sie sie also nicht mit einem Client, der nur body.data liest. Das Gemini-Format schreibt die Modell-ID in den URL-Pfad; Schrägstriche in der ID bleiben, wie sie sind, und müssen nicht kodiert werden.

Senden Sie den Schlüssel ausschließlich in einem Anfrage-Header. x-api-key und x-goog-api-key funktionieren nur unter /v1 und /v1beta; das native /api/v1 akzeptiert nur Authorization: Bearer. Ein Query-Parameter ?key= wird nie akzeptiert: Steht ein Schlüssel erst einmal in einer URL, bleibt er im Browserverlauf, auf Proxy-Servern und in Zugriffslogs hängen. Die offiziellen SDKs senden den Schlüssel alle in einem Header und sind davon nicht betroffen.

Ein offizielles SDK verwenden

Ändern Sie nur die Basis-URL und den Schlüssel; alles andere funktioniert so, wie Sie es vom jeweiligen SDK gewohnt sind.

SDKBasis-URL
OpenAI (openai)https://api.spicyapi.ai/v1
Anthropic (anthropic)https://api.spicyapi.ai
Google GenAI (google-genai)https://api.spicyapi.ai

Die SDKs von Anthropic und Google hängen /v1 bzw. /v1beta selbst an, ihre Basis-URL hat deshalb keinen Versionspfad; die Basis-URL des OpenAI-SDK muss /v1 enthalten. Alle drei Python-Beispiele unten lesen SPICY_API_KEY aus der Umgebung, dazu ein SPICY_MODEL, das Sie aus dem Live-Katalog gewählt haben.

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)

Die Kompatibilitätsschicht legt den Preis bei der Annahme fest und nimmt keine Kostenschätzung an

/v1 und /v1beta berechnen den Preis, der bei Annahme der Anfrage gilt. Sie nehmen weder quoteId noch expectedCost an; auf diesem Weg gibt es also keine Obergrenze für einen einzelnen Aufruf, und eine außer Kontrolle geratene Agentenschleife gibt immer weiter Geld aus.

Es gibt zwei Wege, das zu begrenzen, und die meisten Integrationen brauchen beide:

  • Ein einzelner Aufruf: Holen Sie mit dem nativen POST /api/v1/jobs/quote eine Kostenschätzung ein und senden Sie dann POST /api/v1/jobs/stream mit den zurückgegebenen Werten quoteId und expectedCost. Hat sich der Preis dazwischen geändert, scheitert die Annahme mit 40901, statt mehr zu berechnen, als Sie zugestimmt haben. Siehe Nativer Stream mit bestätigtem Preis.
  • Alles, was dieser Schlüssel ausgibt: Setzen Sie Tages-, Monats- und Gesamtlimit des Schlüssels. Sie gelten für jeden Zugangsweg, auch für diesen, und eine Überschreitung liefert 40202 statt einer höheren Rechnung. Siehe Authentifizierung.

Idempotenzschlüssel bei Textendpunkten

Ein Header Idempotency-Key verhindert, dass eine Wiederholung nach einem Netzwerkfehler doppelt berechnet wird. Bei Textendpunkten bringt er Ihnen eine verlorene Antwort nicht zurück, denn die Antwort wird Ihnen gestreamt und nicht gespeichert:

  • Senden Sie einen Idempotenzschlüssel, dessen Anfrage abgeschlossen ist, mit demselben Body erneut, erhalten Sie 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), und es wird nichts zusätzlich berechnet. Auch der Task-Datensatz enthält den Text nicht.
  • Ist diese frühere Anfrage fehlgeschlagen, liefert das erneute Senden mit demselben Idempotenzschlüssel und demselben Body denselben Fehler wie damals, und es wird nichts berechnet.
  • Senden Sie ihn mit einem anderen Body erneut, erhalten Sie 409.

Deshalb gilt: Senden Sie mit jeder neuen Nachricht einen neuen Idempotenzschlüssel (zum Beispiel $(uuidgen) in einer Shell, uuid.uuid4() in Python), und verwenden Sie einen Idempotenzschlüssel nur dann erneut, wenn Sie nach einem Netzwerkfehler genau dieselbe Anfrage noch einmal senden, im Wissen, dass sich die Antwort auf diese Anfrage nicht wiederherstellen lässt. Möchten Sie erneut eine Antwort, senden Sie die Anfrage mit einem neuen Idempotenzschlüssel und bezahlen dafür.

Übergeben Sie den Idempotenzschlüssel in den SDKs pro Aufruf, niemals am Client-Objekt: Mit einem einzigen Idempotenzschlüssel für den ganzen Client würde jede Nachricht nach der ersten 24 Stunden lang mit 409 abgelehnt:

Idempotency-Key pro Aufruf
import os, uuid

# OpenAI-SDK (client aus dem OpenAI-Beispiel oben). Das Anthropic-SDK nimmt
# dasselbe Argument extra_headers bei client.messages.create() an.
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())},
)

# Google-GenAI-SDK (client und types aus dem Google-GenAI-Beispiel oben):
# http_options in der config des jeweiligen Aufrufs
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())}),
    ),
)

Die SDKs von OpenAI und Anthropic wiederholen Timeouts von sich aus mit denselben Headern. Erreicht eine solche Wiederholung eine Anfrage, die wir bereits angenommen hatten, wirft das SDK einen Fehler 400 (Bad Request) mit der Meldung oben: Der erste Versuch wurde verarbeitet und berechnet, und seine Antwort ist verloren.

SpicyAPI-Modelle im Grok-Kommandozeilen-Client verwenden

So richten Sie den Kommandozeilen-Client von xAI so ein, dass er seine Anfragen an SpicyAPI sendet; danach spricht er mit den Modellen aus dem SpicyAPI-Katalog statt mit dem eigenen Endpunkt von xAI. Dazu gehört auch die im Katalog veröffentlichte Grok-Modellfamilie: Sie rufen sie wie jedes andere Modell über ihre Modell-ID bei SpicyAPI auf. Der Client kann ein eigenes Modell auf jeden OpenAI-kompatiblen Endpunkt richten. Fügen Sie den Eintrag unten zu ~/.grok/config.toml hinzu; env_key nennt die Umgebungsvariable, die der Client liest, sodass der Schlüssel selbst nie in der Datei steht.

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

Ersetzen Sie MODEL_ID_FROM_CATALOG durch eine exakte Textmodell-ID aus dem Live-Katalog in der Form <publisher>/<model>/<task>; lassen Sie den Platzhalter nie stehen. Starten Sie dann mit grok -m spicyapi, oder wechseln Sie innerhalb einer Sitzung mit /model spicyapi. Der Client spricht Chat Completions, deshalb gelten die unten beschriebene Parameterzuordnung, die Stream-Enden und die Fehlerformen unverändert.

Parameterzuordnung zwischen den Protokollen

Felder auf Protokollebene werden zuerst in die kanonischen Parameternamen der Plattform umgewandelt und dann gegen das inputSchema des gewählten Modells geprüft. Ein Parameter, den das Modell nicht unterstützt, liefert 400; er wird nie stillschweigend verworfen, während die Anfrage ganz normal berechnet wird. Die Tabelle zeigt nur die Zuordnung; „—“ bedeutet, dass das Protokoll kein entsprechendes Feld hat. Ob ein bestimmtes Modell einen Parameter annimmt und welche Werte es erlaubt, legt das inputSchema dieses Modells fest.

Kanonischer ParameterChat CompletionsResponsesMessagesGemini
messagesmessagesinstructions, inputsystem, messagessystemInstruction, contents
max_tokensmax_tokens, max_completion_tokensmax_output_tokensmax_tokensgenerationConfig.maxOutputTokens
temperaturetemperaturetemperaturetemperaturegenerationConfig.temperature
top_ptop_ptop_ptop_pgenerationConfig.topP
toolstoolstools (Typ function)tools (eigene Tools)tools[].functionDeclarations
tool_choicetool_choicetool_choicetool_choicetoolConfig.functionCallingConfig
response_formatresponse_formattext.format—generationConfig.responseMimeType plus responseSchema oder responseJsonSchema
reasoning_effortreasoning_effortreasoning.effort—generationConfig.thinkingConfig

Felder, die nicht in der Tabelle stehen, werden ebenfalls in kanonische Parameter umgewandelt und vom Modell geprüft; alles, was in seinem inputSchema fehlt, liefert 400. Zum Beispiel führen nur einige Modelle seed in ihrem inputSchema; bei den übrigen liefern seed in Chat und generationConfig.seed in Gemini 400. Sampling-Einstellungen und Stop-Sequenzen sind die Ausnahme: Nimmt ein Modell sie nicht an, werden sie ignoriert (im Antwort-Header X-Spicy-Ignored-Params genannt), und ein Wert außerhalb dessen, was das Modell annimmt, wird auf den nächstliegenden zulässigen Wert geändert (in X-Spicy-Adjusted-Params genannt). Das betrifft temperature, top_p und die Penalties, stop in Chat, stop_sequences in Messages, generationConfig.stopSequences in Gemini sowie die Stufen von reasoning_effort. Lesen Sie das Schema des gewählten Modells, statt ein Feld eines anderen Modells vorauszusetzen.

Einige Ausnahmen:

  • Felder, die nur für die eigene Plattform des Protokolls eine Bedeutung haben, werden ignoriert und beeinflussen die Generierung nicht: user, metadata und store in Chat; user, metadata, store und include in Responses; metadata in Messages.
  • Responses unterstützt previous_response_id nicht; senden Sie den vollständigen Verlauf in input.
  • safetySettings in Gemini wird angenommen, hat aber keine Wirkung. cachedContent und serverseitige Tools wie googleSearch und codeExecution werden nicht unterstützt und liefern 400. Übergeben Sie Bilder als inlineData (Base64) oder fileData.fileUri (eine https-URL); ob ein Modell Bilder annimmt, legt sein inputSchema fest.

Einen Chat-Stream senden

Sind SPICY_API_KEY und SPICY_MODEL wie oben gesetzt, schreibt dieses Beispiel die Anfrage in eine Datei und streamt die Antwort. Ersetzen Sie den Beispiel-Prompt nach Bedarf, und prüfen Sie die Modellparameter vor dem Senden gegen das aktuelle Schema. Jeder Lauf sendet einen frischen Idempotency-Key, denn jeder Lauf ist eine neue Nachricht.

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 gibt jedes Stück aus, sobald es ankommt, und --max-time 120 gibt nach zwei Minuten auf. Sie sollten Zeilen wie die folgenden nacheinander eintreffen sehen und zum Schluss data: [DONE]. Die Antwort ist auf die delta.content-Stücke verteilt; fügen Sie sie der Reihe nach zusammen, um den vollständigen Text zu erhalten. Das letzte Stück vor [DONE] enthält 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 ist unter macOS vorinstalliert; unter Linux gehört es zum Paket uuid-runtime.

Eine Gemini-Anfrage senden

Dieses Beispiel verwendet dieselben Werte SPICY_API_KEY und SPICY_MODEL und wieder einen neuen Idempotency-Key: Es ist eine andere Anfrage als das Chat-Beispiel, und dessen Idempotenzschlüssel erneut zu verwenden, würde 409 liefern. Die Modell-ID steht in der URL, deshalb enthält der Anfrage-Body kein Modellfeld. Setzen Sie die URL in doppelte Anführungszeichen und schreiben Sie die Variable als ${SPICY_MODEL}, damit die Shell den nachfolgenden Doppelpunkt nicht als Modifikator der Variable liest.

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

Brauchen Sie kein Streaming, ersetzen Sie :streamGenerateContent?alt=sse durch :generateContent und lesen die Antwort aus candidates[0].content.parts[].text; der Verbrauch steht in usageMetadata. Ohne alt=sse liefert eine Streaming-Anfrage ein einziges JSON-Array, das nach und nach geschrieben wird; die offiziellen SDKs fügen alt=sse für Sie hinzu.

Nativer Stream mit bestätigtem Preis

POST /api/v1/jobs/stream ist der einzige Textendpunkt, der eine Kostenschätzung annimmt, und kann sich deshalb weigern zu laufen, wenn sich der Preis geändert hat. Der Anfrage-Body hat dieselbe Form wie bei createTask: model plus ein input, dessen Felder aus dem inputSchema des Modells stammen (bei Textmodellen messages und die übrigen kanonischen Parameter aus der Tabelle oben).

# 1. Die Anfrage einmal in eine Datei schreiben.
jq -n --arg model "$SPICY_MODEL" '{
  model: $model,
  input: {messages: [{role: "user", content: "Explain a rainbow in one sentence."}]}
}' > native-request.json

# 2. Kostenschätzung einholen. Das legt nichts an und berechnet nichts.
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. Innerhalb von fünf Minuten quoteId und expectedCost zur unveränderten Anfrage hinzufügen und streamen.
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

Schritt 2 gibt code: 200 und den Preis in US-Dollar aus; jeder andere code bedeutet, dass die Kostenschätzung gescheitert ist, und msg sagt, warum. Scheitert der Stream-Aufruf, bevor Text gesendet wurde, etwa mit 40901, weil sich Preis oder Anfrage geändert haben, oder mit 40201 wegen zu wenig Guthaben, erhalten Sie das normale JSON {code,msg,data,request_id}, und es wird nichts reserviert. Hat das Streaming bereits begonnen, beendet ein Fehler den Stream mit event: error und data: {"code":"stream_interrupted"}, und dafür wird nichts berechnet. Bild-, Video- und Audiomodelle liefern hier 503; verwenden Sie für sie createTask.

Die Antwort kommt als SSE-Zeilen mit data: an. Anders als /v1/chat/completions verspricht dieser Endpunkt nicht für jedes Modell dasselbe Ereignisformat; testen Sie Ihren Parser also mit dem Modell, das Sie gewählt haben. Brauchen Sie ein einziges, stabiles Format, verwenden Sie einen /v1-Endpunkt und begrenzen Sie die Ausgaben mit den Limits des Schlüssels.

Bilder, Video und Audio an ein Textmodell senden

Manche Textmodelle lesen neben Text auch Bilder, einige zusätzlich Video und Audio. Sie senden die Medien zusammen mit einer Frage, die Antwort bleibt Text. Was ein Modell akzeptiert, steht in seinem Schema und ergibt sich nicht aus dem Protokoll. Prüfen Sie das gewählte Modell:

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]'

Die Liste nennt die Inhaltsteile, die das Modell annimmt. ["text","image_url"] bedeutet Text und Bilder; ein Modell, das auch Video und Audio liest, führt zusätzlich video_url, audio_url und input_audio auf. Eine leere Liste bedeutet, dass das Modell nur Text liest. Sendet man einen Teil, den das Modell nicht aufführt, antwortet es mit 400 und berechnet nichts.

Um Medien zu senden, machen Sie den content der Nachricht zu einer Liste von Teilen statt zu einem String – ein text-Teil für die Frage und ein Teil pro Datei:

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

Jede Medienart hat ihren eigenen Teil:

TeilFormWoher die Datei kommt
Bild{"type": "image_url", "image_url": {"url": "…"}}Eine öffentliche HTTPS-URL, eine hochgeladene Datei (spicy://f/…) oder eine kleine data:-URI
Video{"type": "video_url", "video_url": {"url": "…"}}Eine hochgeladene Datei (spicy://f/…) oder eine öffentliche HTTPS-URL
Audiodatei{"type": "audio_url", "audio_url": {"url": "spicy://f/…"}}Nur eine hochgeladene Datei
Inline-Audio{"type": "input_audio", "input_audio": {"data": "<base64>", "format": "wav"}}Base64 in der Anfrage, wav oder mp3

Wie Sie eine Datei hochladen und ihre spicy://f/…-URI erhalten, steht unter Medien; Uploads gelten einen Tag. Einige Regeln ergeben sich daraus, wie jede Art das Modell erreicht:

  • Bilder können eine öffentliche HTTPS-URL sein, die die Bilddatei selbst liefert (nicht eine Seite, die sie anzeigt), eine hochgeladene Datei oder eine data:-URI innerhalb der Inline-Grenzen. JPEG und PNG funktionieren bei jedem Modell, das Bilder akzeptiert.
  • Video und Audio funktionieren am besten als Uploads. Wir lesen die hochgeladene Datei und übergeben sie dem Modell innerhalb der Anfrage; deshalb dürfen hochgeladenes Video und Audio in einer Anfrage zusammen höchstens 14 MB groß sein – Video als MP4 oder WebM, Audio als MP3 oder WAV. Eine größere Summe liefert 400 und berechnet nichts. Eine öffentliche Video-URL wird stattdessen als Link weitergereicht und funktioniert nur, wenn der Anbieter des Modells sie herunterladen kann.
  • Inline-Audio (input_audio) folgt dem OpenAI-Format und muss in den 2 MiB großen Anfragekörper passen; es eignet sich daher für kurze Clips. Laden Sie alles Längere hoch und senden Sie es als audio_url.

In einer Nachricht können mehrere Dateien stehen, jede als eigener Teil, und auch frühere Nachrichten im Verlauf dürfen Medien enthalten.

Die anderen Protokolle transportieren Bilder in ihrer eigenen Form, und alle erreichen das Modell auf dieselbe Weise. Video- und Audioteile werden über Chat Completions und den nativen Stream gesendet.

ProtokollBildteil
Chat Completions{"type": "image_url", "image_url": {"url": "…"}}
Responses{"type": "input_image", "image_url": "…"}
Anthropic Messages{"type": "image", "source": {"type": "url", "url": "…"}}, oder eine base64-Quelle
GeminiinlineData (base64) oder fileData.fileUri (eine HTTPS-URL)

Was es kostet. Medien werden als Eingabe-Token zum normalen Eingabepreis des Modells gezählt – es gibt keinen separaten Preis pro Datei. Ein Bild kostet je nach Modell und Bildgröße typischerweise einige hundert bis einige tausend Token; Video und Audio wachsen mit ihrer Länge. Der usage-Block der Antwort nennt die genaue Zahl.

Was vorab reserviert wird. Bevor eine Anfrage läuft, reservieren wir eine Obergrenze und geben die Differenz bei der Abrechnung wieder frei. Bei einer Anfrage, deren einzige Medien Bilder sind, die Sie hochgeladen oder inline gesendet haben, addiert die Obergrenze pro Bild einen festen Zuschlag. Bei einer Anfrage mit Video oder Audio oder mit einem Bild, das als öffentliche URL angegeben ist, richtet sich die Obergrenze nach dem gesamten Eingabelimit des Modells, weil sich die Größe vorab nicht kennen lässt – der reservierte Betrag kann daher weit über der endgültigen Abrechnung liegen. Laden Sie Bilder hoch, damit die Reservierung klein bleibt, halten Sie genug Guthaben für die Reservierung vor, oder lassen Sie sich die Anfrage vorab mit POST /api/v1/jobs/quote berechnen, um sie zu sehen.

Im Playground. Bei einem Modell, das Medien akzeptiert, zeigt das Nachrichtenfeld eine Schaltfläche zum Anhängen. Sie können auch einen Screenshot einfügen oder Dateien auf das Feld ziehen; jede Nachricht nimmt bis zu vier Anhänge auf, und Video und Audio darunter dürfen zusammen 14 MB groß sein. Modelle, die nur Text lesen, zeigen keine Schaltfläche.

Unterhaltungen und Tool-Aufrufe

Chat Completions

  • Ohne Streaming: Lesen Sie choices[0].message.
  • Mit Streaming: Verarbeiten Sie choices[].delta SSE-Ereignis für SSE-Ereignis, nicht pro Netzwerkpaket. Setzen Sie tool_calls anhand von index zusammen, und prüfen und führen Sie einen Aufruf erst aus, wenn seine arguments vollständig sind.
  • Behalten Sie die Assistentennachricht im Verlauf, und geben Sie jedes Ergebnis in einer tool-Nachricht mit der passenden tool_call_id zurück.

Responses

  • Senden Sie den vollständigen Verlauf in input; previous_response_id wird abgelehnt.
  • Geben Sie Funktionsergebnisse als Elemente vom Typ function_call_output mit der passenden call_id zurück.

Messages

  • Verwenden Sie die Inhaltsblöcke text, tool_use und tool_result statt der Nachrichtenform von Chat.

Gemini

  • contents wechseln zwischen zwei Rollen, user und model.
  • Möchte das Modell ein Tool aufrufen, liefert es einen Teil functionCall. Verpacken Sie das Ergebnis nach dem Ausführen des Tools in einen Teil functionResponse und senden Sie ihn im nächsten user-Inhalt zurück, mit demselben name wie beim Aufruf.

Abschluss, Fehler und Verbindungsabbrüche

ProtokollNormaler AbschlussVerbrauch
Chat Completionsdata: [DONE]usage im letzten Stück
ResponsesEreignis response.completedresponse.usage in diesem Ereignis
Messagesmessage_stop nach message_deltausage in message_delta
GeminiLetztes Ereignis mit finishReason; es gibt kein [DONE]usageMetadata

Prüfen Sie HTTP-Status und Content-Type, bevor Sie einen Stream lesen; Fehler kommen im jeweils eigenen JSON des Protokolls zurück:

ProtokollFehler-Body
Chat Completions, Responses{"error": {"message", "type", "param", "code"}}
Messages{"type": "error", "error": {"type", "message"}}
Gemini{"error": {"code", "message", "status"}}

Die HTTP-Statuscodes sind bei allen vier Protokollen gleich. status in Gemini verwendet die Statusnamen von Google: 400 ist INVALID_ARGUMENT, 401 ist UNAUTHENTICATED, 402 ist FAILED_PRECONDITION, 403 ist PERMISSION_DENIED, 404 ist NOT_FOUND, 409 ist ABORTED, 429 ist RESOURCE_EXHAUSTED, 500 ist INTERNAL, 503 ist UNAVAILABLE und 504 ist DEADLINE_EXCEEDED. Verzweigen Sie in Ihrem Code nach Statuscode und Fehlertyp, niemals nach dem Meldungstext.

Schlägt nach Beginn des Streamings etwas fehl, senden Chat und Gemini ein Ereignis mit einem Fehlerobjekt, Responses sendet ein Fehlerereignis und Messages sendet event: error. Ihr Client muss diese Fehlerereignisse behandeln, ebenso Timeouts und eine Verbindung, die sich ohne das normale Abschlusszeichen schließt. Leiten Sie den Stream über Ihren eigenen Proxy, schalten Sie dort die Pufferung der Antwort ab, sonst kommen die Stücke schubweise an.

Was Sie zahlen, wenn ein Stream vorzeitig endet

  • Sie trennen die Verbindung (Tab schließen, Anfrage abbrechen, Client-Timeout): Das Trennen stoppt die Abrechnung nicht. Berechnet wird der Verbrauch, den das Modell meldet; bei vielen Modellen umfasst er die gesamte Antwort, auch Text, den Sie nie erhalten haben, und nie mehr als den Betrag, der bei Annahme der Anfrage reserviert wurde. Nicht empfangenen Text können Sie später nicht abrufen. Um weniger auszugeben, senken Sie max_tokens, statt die Verbindung zu trennen.
  • Das Modell oder unser Dienst fällt während des Streams aus (Sie erhalten ein Fehlerereignis, oder der Stream endet ohne sein Abschlusszeichen): Die Anfrage wird als fehlgeschlagen markiert, und es wird nichts berechnet.
  • Das Modell hat Ihnen Text oder Reasoning gesendet, danach wurde seine Antwort als leer oder als Verweigerung eingestuft: Was Sie bereits erhalten haben, wird nach dem vom Modell gemeldeten Verbrauch berechnet, nie mehr als der reservierte Betrag, und der Stream endet normal. Nur wenn überhaupt nichts bei Ihnen angekommen ist, wird der volle Betrag erstattet.
  • Die Anfrage überschreitet ihre serverseitige Frist: Sie endet als Fehlschlag, und es wird nichts berechnet.

Timeouts und Streaming

Ohne Streaming kommt die Antwort in einem Stück, sobald sie fertig ist, und lange Antworten und langes Reasoning brauchen Zeit — Modelle wie Grok, GLM, DeepSeek Pro und Claude Opus können minutenlang nachdenken, bevor sie etwas schreiben, besonders bei hohem reasoning_effort. Der Netzwerk-Edge vor der API schließt eine Verbindung, auf der etwa 100 Sekunden lang nichts angekommen ist. Deshalb halten wir Anfragen beider Arten offen, solange das Modell arbeitet.

  • Eine Anfrage ohne Streaming, die nach etwa 30 Sekunden noch läuft, beginnt ihre Antwort früher. Sie erhalten Status 200, Content-Type: application/json und den Header X-Spicy-Keepalive: 1, danach alle 15 Sekunden einen Zeilenumbruch und zuletzt den JSON-Body. JSON erlaubt Leerraum vor einem Wert, deshalb lesen Standard-Parser (json.loads, JSON.parse, response.json()) und die offiziellen SDKs den Body unverändert. Eine Anfrage, die früher fertig ist, erhält eine normale Antwort mit ihrem tatsächlichen Statuscode und ohne diesen Header.
  • Ab diesem Punkt steht ein Fehler im Body, nicht im Status. Die 200 ist bereits gesendet, daher enthält der Body das übliche Fehlerobjekt des Protokolls aus der Tabelle oben, mit demselben Code und derselben Meldung; im Gemini-Format enthält error.code weiterhin den HTTP-Status, den der Fehler sonst gehabt hätte. Ist X-Spicy-Keepalive gesetzt, prüfen Sie den Body auf ein error-Objekt, bevor Sie die Antwort lesen — die offiziellen SDKs behandeln eine 200 nicht als Fehler. Eine fehlgeschlagene Anfrage wird nicht berechnet.
  • Auch Streaming hält die Verbindung offen. Solange das Modell noch arbeitet, enthält eine gestreamte Antwort Keep-alive-Zeilen — : keep-alive-Kommentare bei SSE, Leerzeilen im Gemini-Format —, sodass die Verbindung nie untätig ist, ganz gleich, wie lange das Modell nachdenkt. Ihr Parser muss sie überspringen, wie es das SSE-Format verlangt; die offiziellen SDKs tun das bereits.
  • Verwenden Sie für alles, was lange dauern kann, möglichst stream: true. Sie sehen die Antwort, während sie geschrieben wird, und ein Fehler kommt als Fehler-Event an, das die SDKs verstehen. Chat Completions, Responses und Messages akzeptieren stream: true; im Gemini-Format rufen Sie :streamGenerateContent?alt=sse statt :generateContent auf. Ein geänderter Body ist eine neue Anfrage, senden Sie sie also mit einem neuen Idempotency-Key: Der alte liefert 409.
  • Eine vom Netzwerk-Edge abgebrochene Anfrage wird nicht berechnet. Kommt etwa 100 Sekunden lang gar nichts bei Ihnen an und wird die Verbindung dann getrennt, erscheint die Anfrage unter Konsole → Logs und in GET /api/v1/jobs als fehlgeschlagen, mit errorCode timeout und einer Meldung, die erklärt, was passiert ist. Senden Sie sie mit demselben Idempotency-Key erneut, kommt genau diese Meldung als Fehler zurück.
  • Ihre eigenen Zeitlimits gelten weiterhin. Ein Gesamt-Timeout in Ihrem HTTP-Client oder in einem Proxy, den Sie betreiben, beendet die Anfrage ebenfalls, und sobald etwas bei Ihnen angekommen ist — eine Keep-alive-Zeile eingeschlossen —, gilt das als Verbindungsabbruch Ihrerseits (siehe Was Sie zahlen, wenn ein Stream vorzeitig endet). Die offiziellen OpenAI-SDKs warten standardmäßig bis zu 10 Minuten; die curl-Beispiele auf dieser Seite geben nach 2 Minuten auf, erhöhen Sie --max-time also für langes Reasoning.

So schalten Sie Streaming in den OpenAI-SDKs und in curl ein:

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

Das Node.js-Beispiel verwendet await auf oberster Ebene; führen Sie es daher als ES-Modul aus (speichern Sie es zum Beispiel als stream.mjs). Der curl-Befehl sendet chat-request.json aus Einen Chat-Stream senden, die bereits stream: true setzt; mit --max-time 900 darf die Antwort bis zu 15 Minuten dauern.

Kostenschätzungen und Abrechnung

Für eine ausdrückliche Kostenbestätigung verwenden Sie den nativen Stream mit einer Kostenschätzung. /v1 und /v1beta nehmen keine Angaben aus einer Kostenschätzung an; sie wenden den Preis an, der bei Annahme gilt.

Textaufrufe liefern keine Task-ID, und eine Endmarkierung im Text ist kein Beweis für die endgültige Belastung. Um herauszufinden, was ein Aufruf gekostet hat, listen Sie mit demselben API-Schlüssel die letzten Tasks auf (GET /api/v1/jobs?model=<model>&from=<YYYY-MM-DD>) und ordnen sie über createdAt zu, oder Sie sehen unter Konsole → Logs nach. Behandeln Sie cost erst dann als endgültig, wenn settled den Wert true hat. Siehe Asynchrone Tasks zu Task-Verlauf und Wiederherstellung sowie Abrechnung.

reasoning_tokens ist in completion_tokens bereits enthalten: Zählen Sie es nicht doppelt.

Weitere Anfrageformen

Diese Beispiele beschreiben nur die Hüllen der Protokolle. Ersetzen Sie MODEL_ID_FROM_CATALOG durch ein Live-Modell, das die gezeigten Felder unterstützt; senden Sie nie den Platzhalter selbst.

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
}

Weiterlesen

Kostenschätzungen und Protokollkompatibilität · Idempotenz · Abrechnung · Integration für den Produktivbetrieb · Fehlerbehebung und Wiederherstellung

Auf dieser Seite