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 ersetzenMODEL_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.
| Protokoll | Endpunkt | Header für den API-Schlüssel |
|---|---|---|
| OpenAI Chat Completions | POST /v1/chat/completions | Authorization: Bearer |
| OpenAI Responses | POST /v1/responses | Authorization: Bearer |
| Anthropic Messages | POST /v1/messages | x-api-key oder Authorization: Bearer |
| Google Gemini | POST /v1beta/models/{model}:generateContent | x-goog-api-key oder Authorization: Bearer |
| Google Gemini (Streaming) | POST /v1beta/models/{model}:streamGenerateContent | x-goog-api-key oder Authorization: Bearer |
| Nativer Stream mit bestätigtem Preis | POST /api/v1/jobs/stream | Authorization: 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.
| SDK | Basis-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.
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)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/quoteeine Kostenschätzung ein und senden Sie dannPOST /api/v1/jobs/streammit den zurückgegebenen WertenquoteIdundexpectedCost. Hat sich der Preis dazwischen geändert, scheitert die Annahme mit40901, 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
40202statt 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:
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.
[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 Parameter | 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 (Typ function) | tools (eigene Tools) | tools[].functionDeclarations |
tool_choice | tool_choice | tool_choice | tool_choice | toolConfig.functionCallingConfig |
response_format | response_format | text.format | — | generationConfig.responseMimeType plus responseSchema oder responseJsonSchema |
reasoning_effort | reasoning_effort | reasoning.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,metadataundstorein Chat;user,metadata,storeundincludein Responses;metadatain Messages. - Responses unterstützt
previous_response_idnicht; senden Sie den vollständigen Verlauf ininput. safetySettingsin Gemini wird angenommen, hat aber keine Wirkung.cachedContentund serverseitige Tools wiegoogleSearchundcodeExecutionwerden nicht unterstützt und liefern400. Übergeben Sie Bilder alsinlineData(Base64) oderfileData.fileUri(eine https-URL); ob ein Modell Bilder annimmt, legt seininputSchemafest.
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.jsonBrauchen 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.jsonSchritt 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.jsonJede Medienart hat ihren eigenen Teil:
| Teil | Form | Woher 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
400und 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 alsaudio_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.
| Protokoll | Bildteil |
|---|---|
| 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 |
| Gemini | inlineData (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[].deltaSSE-Ereignis für SSE-Ereignis, nicht pro Netzwerkpaket. Setzen Sietool_callsanhand vonindexzusammen, und prüfen und führen Sie einen Aufruf erst aus, wenn seineargumentsvollständig sind. - Behalten Sie die Assistentennachricht im Verlauf, und geben Sie jedes Ergebnis in einer
tool-Nachricht mit der passendentool_call_idzurück.
Responses
- Senden Sie den vollständigen Verlauf in
input;previous_response_idwird abgelehnt. - Geben Sie Funktionsergebnisse als Elemente vom Typ
function_call_outputmit der passendencall_idzurück.
Messages
- Verwenden Sie die Inhaltsblöcke
text,tool_useundtool_resultstatt der Nachrichtenform von Chat.
Gemini
contentswechseln zwischen zwei Rollen,userundmodel.- Möchte das Modell ein Tool aufrufen, liefert es einen Teil
functionCall. Verpacken Sie das Ergebnis nach dem Ausführen des Tools in einen TeilfunctionResponseund senden Sie ihn im nächstenuser-Inhalt zurück, mit demselbennamewie beim Aufruf.
Abschluss, Fehler und Verbindungsabbrüche
| Protokoll | Normaler Abschluss | Verbrauch |
|---|---|---|
| Chat Completions | data: [DONE] | usage im letzten Stück |
| Responses | Ereignis response.completed | response.usage in diesem Ereignis |
| Messages | message_stop nach message_delta | usage in message_delta |
| Gemini | Letztes 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:
| Protokoll | Fehler-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/jsonund den HeaderX-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
200ist 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älterror.codeweiterhin den HTTP-Status, den der Fehler sonst gehabt hätte. IstX-Spicy-Keepalivegesetzt, prüfen Sie den Body auf einerror-Objekt, bevor Sie die Antwort lesen — die offiziellen SDKs behandeln eine200nicht 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 akzeptierenstream: true; im Gemini-Format rufen Sie:streamGenerateContent?alt=ssestatt:generateContentauf. Ein geänderter Body ist eine neue Anfrage, senden Sie sie also mit einem neuenIdempotency-Key: Der alte liefert409. - 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/jobsals fehlgeschlagen, miterrorCodetimeoutund einer Meldung, die erklärt, was passiert ist. Senden Sie sie mit demselbenIdempotency-Keyerneut, 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-timealso für langes Reasoning.
So schalten Sie Streaming in den OpenAI-SDKs und in curl ein:
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.jsonDas 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.
{
"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
}Weiterlesen
Kostenschätzungen und Protokollkompatibilität · Idempotenz · Abrechnung · Integration für den Produktivbetrieb · Fehlerbehebung und Wiederherstellung
Kostenschätzungen und Protokollkompatibilität
Den genauen Preis einer Anfrage erfahren, bevor Sie bezahlen, sie zu genau diesem Preis absenden, nach einer verlorenen Antwort sicher weitermachen und wissen, wie die Kompatibilitäts-APIs für Text und Video abrechnen.
Chatbeispiele
Vollständige ausführbare Programme für mehrteilige Gespräche, Figurenrollenspiel, Streaming in eigene Oberflächen und Werkzeugaufrufe mit Python, Node.js und curl.

