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.
Jedes Beispiel ist ein vollständiges Programm: Zwei Umgebungsvariablen setzen, kopieren, ausführen. Alle verwenden POST https://api.spicyapi.ai/v1/chat/completions mit offiziellen OpenAI-Bibliotheken. Derselbe Code funktioniert daher mit jedem Textmodell im Katalog. Text und Streaming erklärt das Protokoll; diese Seite zeigt typische Anwendungen darauf.
Vor dem Start
1. Speichern Sie den API-Key als Umgebungsvariable, damit er nie in Code eingefügt werden muss:
export SPICY_API_KEY="sk-spicy-paste-your-key-here"2. Wählen Sie ein Textmodell aus dem Livekatalog und speichern seine ID in SPICY_MODEL. Jedes Beispiel liest diese. Der folgende Befehl listet aktuell für Ihren Key aufrufbare Textmodelle; er benötigt jq:
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'
export SPICY_MODEL="MODEL_ID_FROM_CATALOG" # replace with one line from the list aboveTextmodell-IDs haben das Format publisher/model/chat. MODEL_ID_FROM_CATALOG ist nur ein Platzhalter und darf nie wörtlich gesendet werden.
3. Prüfen Sie akzeptierte Eingaben. Modelle unterscheiden sich: Manche akzeptieren temperature, andere nicht; reasoning_effort hat unterschiedliche Werte, und nur manche Modelle lesen Bilder. Nicht unterstützte Sampling-Felder wie temperature oder frequency_penalty werden ignoriert und im Antwortheader X-Spicy-Ignored-Params genannt. Auch übliche Clientstandards werden berücksichtigt: Nicht unterstütztes stop wird ignoriert; ein nicht unterstütztes n: 1 bedeutet eine Antwort. Eine erkannte Reasoning-Stufe wird bei Bedarf auf die nächstgelegene unterstützte Stufe abgebildet. Der Header X-Spicy-Adjusted-Params nennt angepasste Einstellungen. Nicht unterstützte Funktionsfelder wie tools oder response_format führen weiterhin zu 400. Dieser Befehl zeigt die Felder, reasoning_effort-Werte und akzeptierten Nachrichteninhalte:
curl -s "https://api.spicyapi.ai/api/v1/models/$SPICY_MODEL" \
-H "Authorization: Bearer $SPICY_API_KEY" \
| jq '.data.inputSchema.properties | {
fields: keys,
reasoning_effort: .reasoning_effort.enum,
content_parts: [.messages.items.properties.content.items.oneOf[]?.properties.type.const]
}'Für den Überblick aller Textmodelle liefert GET /v1/models?modality=text jeweils supported_parameters, context_length und max_output_tokens. Es listet nur Feldnamen; für erlaubte Werte verwenden Sie den Befehl oben.
4. Installieren Sie die OpenAI-Bibliothek: pip install openai für Python oder npm install openai für Node.js 18 oder neuer. Speichern Sie Node.js-Programme als .mjs, da sie Top-Level-await verwenden, und starten mit node file.mjs.
Lassen Sie Raum fürs Nachdenken
Viele Textmodelle denken vor der Antwort; dies zählt zu max_tokens. Die Beispiele lassen max_tokens weg und verwenden den Modellstandard. Setzen Sie einen Wert, planen Sie großzügig mit einigen tausend Tokens: Ein zu kleiner Wert kann vollständig durch Denken aufgebraucht werden. Die Anfrage scheitert dann mit Hinweis auf höheres max_tokens oder niedrigeres reasoning_effort. Dieser Fehler wird nicht berechnet.
Mehrteiliges Gespräch
Die API bewahrt zwischen Aufrufen kein Gedächtnis. Ein Gespräch ist eine von Ihnen gespeicherte und bei jeder Runde vollständig erneut gesendete Nachrichtenliste: zuerst system, danach abwechselnd user und assistant. Hängen Sie jede Antwort als assistant-Nachricht an.
Jede erneut gesendete Nachricht wird wieder als Eingabe berechnet. Kürzen Sie bei langen Gesprächen die ältesten Runden. Behalten Sie immer system und entfernen Nutzer-/Assistentenpaare, damit der Verlauf weiter mit user beginnt.
import os
from openai import OpenAI
client = OpenAI(api_key=os.environ["SPICY_API_KEY"], base_url="https://api.spicyapi.ai/v1")
MODEL = os.environ["SPICY_MODEL"]
KEEP = 20 # messages kept after the system prompt; an even number keeps user/assistant pairs together
history = [{"role": "system", "content": "You are a patient cooking coach. Keep every answer under 80 words."}]
while True:
try:
user_text = input("you> ").strip()
except EOFError: # Ctrl-D
break
if user_text in ("", "quit", "exit"):
break
history.append({"role": "user", "content": user_text})
reply = client.chat.completions.create(model=MODEL, messages=history)
answer = reply.choices[0].message.content or ""
print(f"coach> {answer}\n")
history.append({"role": "assistant", "content": answer})
# The API remembers nothing between calls: resend the conversation every turn,
# and trim it so the prompt (and what you pay for it) stops growing.
history = history[:1] + history[1:][-KEEP:]Das Modell beantwortet die letzte Nachricht anhand des bisherigen Inhalts. Fragen Sie danach nach erwähnten Zutaten, kennt es diese. Denkmodelle liefern auch reasoning_content neben content; bei einem einfachen Gespräch müssen Sie diesen nicht zurücksenden.
Rollenspiel mit Figurenkarte
Eine Figur ist eine sorgfältig geschriebene system-Nachricht. Da die API kein Gedächtnis hält, reist die Karte mit jeder Anfrage und unterstützt dieselbe Figur auch in Runde fünfzig. Eine gute Karte beantwortet sechs Fragen:
| Teil | Inhalt | Beispiel |
|---|---|---|
| Identität | Wer die Figur ist, mit zwei oder drei verhaltensprägenden Fakten | Pensionierter Detektiv, der nun einen Leuchtturm betreut |
| Stimme | Satzlänge, Wortwahl, Gewohnheiten, Nutzeransprache | Bedächtig, spricht kleine Beobachtungen aus, nennt den Nutzer „Freund“ |
| Welt | Schauplatz und ein bis zwei zentrale Handlungsfakten | Fischerdorf der 1920er, in dem um Mitternacht die Glocke eines versunkenen Schiffs läutet |
| Grenzen | Verhalten, Ausschlüsse und Ausstieg aus der Geschichte | Bleibt in der Welt; beantwortet (OOC: ...) kurz außerhalb der Rolle; allgemein geeignete Geschichte |
| Erzählung | Perspektive, Zeitform und Sprachmarkierung | Dritte Person, Vergangenheit; gesprochene Sätze in Anführungszeichen |
| Format | Länge und Ende jeder Antwort | Höchstens 120 Wörter; endet mit zwei oder drei nummerierten Entscheidungen |
Zwei Gewohnheiten helfen: Legen Sie die Eröffnungszeile als assistant-Nachricht in den Verlauf, sodass schon der erste sichtbare Satz zur Figur passt. Beenden Sie jede Antwort mit Frage oder Entscheidung für eine klare Fortsetzung. Nummerierte Optionen erlauben bei interaktiven Geschichten eine einzelne Ziffer als Antwort.
Ein Navigator eines Fantasieluftschiffs. Die Antwort erscheint Wort für Wort.
import os
from openai import OpenAI
client = OpenAI(api_key=os.environ["SPICY_API_KEY"], base_url="https://api.spicyapi.ai/v1")
MODEL = os.environ["SPICY_MODEL"]
CARD = {
"name": "Captain Ilsa Varn",
"identity": "Navigator of the airship Kestrel. Forty years old, calm under pressure, dry sense of humour, fiercely loyal to her crew.",
"voice": "Short, vivid sentences full of wind and weather. Calls the user 'deckhand'. No modern slang.",
"world": "Islands drift on warm updrafts above an endless sea of cloud. Storms here are alive and hungry, and maps go out of date within a week.",
"boundaries": (
"Stay in character and inside this world. If the user asks about the real world, react as Ilsa would: puzzled but polite. "
"If the user writes (OOC: ...), step out of the story for one short, plain answer, then return. "
"Keep the story suitable for a general audience."
),
"narration": "Describe the scene in the second person ('You feel the deck tilt...'). Put Ilsa's spoken lines in quotation marks.",
"format": "At most 120 words. End every reply with a question or a choice, so the user always knows how to continue.",
}
system_prompt = "\n".join(
[f"You are {CARD['name']}, a character in an interactive story."]
+ [f"{key.capitalize()}: {value}" for key, value in CARD.items() if key != "name"]
)
history = [
{"role": "system", "content": system_prompt},
{"role": "assistant", "content": "\"Welcome aboard, deckhand. Storm wall to the north, a lost island to the east. Which way do we steer?\""},
]
print(history[-1]["content"])
while True:
try:
user_text = input("\nyou> ").strip()
except EOFError: # Ctrl-D
break
if user_text in ("", "quit", "exit"):
break
history.append({"role": "user", "content": user_text})
answer = ""
stream = client.chat.completions.create(model=MODEL, messages=history, stream=True)
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
piece = chunk.choices[0].delta.content
answer += piece
print(piece, end="", flush=True)
print()
history.append({"role": "assistant", "content": answer})Dieselben sechs Teile funktionieren für jedes Genre. Schiffscomputer, Begleiter für Alltagsroutinen oder Textabenteuererzähler benötigen nur eine andere Karte:
You are Juniper, a friendly running coach inside a fitness app.
Identity: a former trail runner, upbeat and practical, never judgemental.
Voice: warm and brief; uses the user's name when they give it; celebrates small wins.
World: the user's own week. Remember goals and aches they mention earlier in this conversation.
Boundaries: give general fitness encouragement only; for pain, injury or medical questions, suggest seeing a professional. Stay in character; answer (OOC: ...) plainly.
Narration: first person, present tense, talking directly to the user.
Format: at most 80 words; end with one concrete suggestion for today.Halten Sie bei langen Geschichten die Karte kurz und stabil. Ersetzen Sie älteste Runden bei wachsendem Verlauf durch eine kurze „Bisherige Handlung“-Notiz in zweiter system-Nachricht, statt den Prompt unbegrenzt wachsen zu lassen.
In Ihre eigene Oberfläche streamen
Streaming zeigt die Antwort während des Schreibens und macht Chatoberflächen reaktionsfähig. Sechs Regeln decken alle Katalogmodelle ab:
- Fordern Sie Verbrauch an, wenn Sie Tokenzahlen benötigen. Ohne
stream_options.include_usageenthält kein Chunkusage. Mit"include_usage": trueerscheint unmittelbar vor[DONE]ein Zusatzchunk mit endgültigemusageund leeremchoices; andere Chunks habenusage: null. - Prüfen Sie
choicesvorchoices[0]. Außer dem letzten Verbrauchschunk enthält jeder mindestens eine Auswahl. Die Prüfung kostet kaum etwas und schützt die Schleife auch ohne Verbrauchsanforderung. - Denken bleibt getrennt. Denkmodelle senden es als
delta.reasoning_content, die Antwort alsdelta.content. Zeigen Sie ersteres gedimmt, eingeklappt oder gar nicht; nur letzteres ist die Antwort. finish_reason: "length"bedeutet abgeschnitten. Das Modell erreichtemax_tokens. Erhöhen Sie den Wert oder senkenreasoning_effort.- Fehler können im laufenden Stream erscheinen. Nach Streamstart kommt ein Fehler als Ereignis mit
error-Objekt, das OpenAI-Bibliotheken als Exception auslösen. Die fehlgeschlagene Anfrage wird nicht berechnet. War die Antwort bei Verbindungsabbruch bereits fertig, kommt kein Fehler: Der Stream endet normal und wird wie jede andere Antwort berechnet. Siehe Abschluss, Fehler und Verbindungsabbrüche. - Frühes Stoppen stoppt die Rechnung nicht. Verbindungsschließen stoppt das Modell nicht; berechnet wird sein gemeldeter Verbrauch. Senken Sie stattdessen
max_tokens. Siehe Abrechnung bei frühem Streamende.
Ein Terminalrenderer mit allen sechs Regeln: Denken gedimmt, Antwort normal dargestellt und abschließend Tokenzahlen.
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": "Write a four-line poem about a cat who lives in a lighthouse."}],
stream=True,
stream_options={"include_usage": True},
)
finish_reason = None
usage = None
thinking_shown = False
for chunk in stream:
# With include_usage, only the last chunk before [DONE] carries usage; it has no choices.
if chunk.usage:
usage = chunk.usage
if not chunk.choices:
continue
choice = chunk.choices[0]
thinking = getattr(choice.delta, "reasoning_content", None) # reasoning models only
if thinking:
thinking_shown = True
print(f"\033[2m{thinking}\033[0m", end="", flush=True) # dimmed
if choice.delta.content:
if thinking_shown:
print("\n")
thinking_shown = False
print(choice.delta.content, end="", flush=True)
if choice.finish_reason:
finish_reason = choice.finish_reason
print()
if finish_reason == "length":
print("[stopped at max_tokens: raise it, or lower reasoning_effort]")
if usage:
print(f"[{usage.prompt_tokens} prompt tokens, {usage.completion_tokens} completion tokens]")Werkzeugaufrufe
Werkzeuge lassen das Modell Ihr Programm um eine Aktion bitten, etwa Würfeln, Bestellung suchen oder Kalender lesen, und mit dem Ergebnis antworten. Die Schleife ist stets gleich:
- Senden Sie Nachrichten zusammen mit
tools, einer JSON-Schema-beschriebenen Funktionsliste. - Enthält die Antwort
tool_calls, hängen Sie die Assistentennachricht exakt unverändert an, führen jeden Aufruf selbst aus und ergänzen je Aufruf einetool-Nachricht mit passendertool_call_id. - Senden Sie alles erneut. Wiederholen Sie bis zur Antwort ohne
tool_calls; das ist die endgültige Antwort.
Behandeln Sie Argumente als nicht vertrauenswürdige Eingabe: parsen, prüfen und Fehler als Werkzeugergebnis zurückgeben statt abstürzen. Begrenzen Sie die Rundenzahl gegen Endlosschleifen. Jedes Textmodell akzeptiert aktuell tools. Prüfen Sie vor erzwungenem Einzelwerkzeug tool_choice, da manche nur auto und none erlauben.
import json
import os
import random
from openai import OpenAI
client = OpenAI(api_key=os.environ["SPICY_API_KEY"], base_url="https://api.spicyapi.ai/v1")
MODEL = os.environ["SPICY_MODEL"]
def roll_dice(count, sides):
rolls = [random.randint(1, sides) for _ in range(count)]
return {"rolls": rolls, "total": sum(rolls)}
LOCAL_TOOLS = {"roll_dice": roll_dice}
TOOLS = [{
"type": "function",
"function": {
"name": "roll_dice",
"description": "Roll dice for a tabletop game and return every roll and the total.",
"parameters": {
"type": "object",
"properties": {
"count": {"type": "integer", "minimum": 1, "maximum": 10, "description": "How many dice to roll"},
"sides": {"type": "integer", "enum": [4, 6, 8, 10, 12, 20], "description": "Sides on each die"},
},
"required": ["count", "sides"],
},
},
}]
messages = [
{"role": "system", "content": "You are the game master of a fantasy adventure. Whenever the rules call for a roll, use roll_dice; never invent a result. A roll of 12 or more on a d20 succeeds."},
{"role": "user", "content": "I try to pick the lock on the old tower door. Roll a d20 for me."},
]
for _ in range(5): # a hard stop, so a confused model cannot loop forever
reply = client.chat.completions.create(model=MODEL, messages=messages, tools=TOOLS)
message = reply.choices[0].message
# Send the assistant turn back exactly as it came, tool calls included.
messages.append(message.model_dump(exclude_none=True))
if not message.tool_calls:
print(message.content)
break
for call in message.tool_calls:
tool = LOCAL_TOOLS.get(call.function.name)
try:
result = tool(**json.loads(call.function.arguments or "{}")) if tool else {"error": "unknown tool"}
except (TypeError, ValueError) as error: # the model sent arguments the tool cannot use
result = {"error": str(error)}
print(f"[tool] {call.function.name}({call.function.arguments}) -> {result}")
messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})Die erste Antwort endet mit finish_reason: "tool_calls" und content gleich null, die endgültige mit "stop". Denkmodelle können reasoning_content in der Assistentennachricht enthalten; bewahren Sie ihn beim Zurücksenden wie in den Beispielen durch unverändertes Anhängen. Gestreamte Werkzeugaufrufe kommen stückweise; setzen Sie diese anhand index zusammen, siehe Gespräche und Werkzeugaufrufe.
JSON-Ausgabe
Benötigt Ihr Programm Daten statt Prosa, fordern Sie JSON über response_format bei Modellen an, deren inputSchema dieses Feld nennt, und beschreiben die Felder im Prompt. Parsen Sie defensiv: Eine durch max_tokens abgeschnittene Antwort ist kein gültiges JSON.
import json
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": "system", "content": "Reply with a JSON object only, with the keys title (string), genre (string) and hook (one sentence)."},
{"role": "user", "content": "Invent a cosy mystery novel set in a mountain bakery."},
],
response_format={"type": "json_object"},
)
try:
book = json.loads(reply.choices[0].message.content)
print(book["title"], "-", book["hook"])
except (json.JSONDecodeError, KeyError, TypeError):
print("Not the JSON we asked for:", reply.choices[0].message.content)Fehlt response_format im Modell, lassen Sie das Feld weg und verwenden allein den Systemprompt. Der try-Block oben ist dann noch wichtiger.
Wenn etwas schiefgeht
Fehler kommen in OpenAI-Struktur {"error": {"message", "type", "param", "code"}}; Bibliotheken lösen ihre üblichen Exceptions aus. Verzweigen Sie nach HTTP-Status und error.code; zeigen Sie Menschen error.message, da es die nötige Änderung nennt. Bei feldbezogenem Fehler nennt param einen Punktpfad wie temperature oder messages.1.content.0.
| Status | error.code | Ursache | Maßnahme |
|---|---|---|---|
| 400 | unsupported_parameter | Nicht akzeptiertes Feld, etwa logprobs: is not a supported field, oder Nachrichtenteil, etwa image_url bei reinem Textmodell. Sampling wie temperature wird nicht abgelehnt, sondern ignoriert und in X-Spicy-Ignored-Params genannt | Entfernen; Felder wie unter Vor dem Start prüfen |
| 400 | invalid_request | Fehlendes Feld oder nicht akzeptierter Wert, etwa reasoning_effort: must be one of: "low", "high", "max", oder kein Chatmodell mit param gleich model | Genannten Wert ändern oder Modell mit /chat wählen |
| 400 | content_rejected | Inhaltsrichtlinie des Modells lehnt Anfrage ab | Geeignete andere Formulierung wählen; gleiche Eingabe wird erneut abgelehnt |
| 401 | invalid_api_key | Key fehlt, ist falsch geschrieben oder widerrufen | SPICY_API_KEY prüfen; Präfix ist sk-spicy- |
| 402 | insufficient_balance, spend_cap_exceeded | Guthaben oder Ausgabenlimit deckt Reservierung nicht | Aufladen, Limit erhöhen oder max_tokens senken, siehe unten |
| 404 | model_not_found | ID existiert nicht, param ist model. Meldung nennt Format und gegebenenfalls nächstähnliches aufrufbares Chatmodell | Exakte ID aus Livekatalog kopieren |
| 404 | not_found | Modell existiert, ist derzeit nicht aufrufbar | Anderes Livemodell wählen |
| 409 | conflict | Idempotency-Key mit geändertem Anfrageinhalt wiederverwendet | Neue Nachricht mit neuem Schlüssel senden; siehe Textidempotenz |
| 429 | rate_limit_exceeded | Zu viele Anfragen | Warten, wiederholen; vorhandenes Retry-After nennt Sekunden |
| 503 | upstream_failed, model_unavailable | Modell gescheitert oder vorübergehend nicht verfügbar. Keine Belastung | Später mit neuem Idempotency-Key wiederholen |
Warum trotz scheinbar ausreichendem Guthaben 402 auftreten kann: Vor Textausführung wird für Nachrichten und gesamte Ausgabe-max_tokens zum Modellpreis reserviert, bei Weglassen für Modellstandard-max_tokens. Bezahlt werden nur genutzte Tokens, der Rest wird nach Abschluss frei. Die vollständige Reservierung muss jedoch vor Start ins verfügbare Guthaben passen. Realistische max_tokens senken die Reservierung. Siehe Abrechnung.
Tatsächliche Aufrufkosten ermitteln
Jede angenommene Anfrage läuft als Task. Die Antwort nennt ihn mit oder ohne Stream in X-Spicy-Task-Id. Lesen Sie den Header und fragen den Task ab: cost ist endgültig, sobald settled gleich true ist. Vor Annahme abgelehnte Anfragen, etwa Validierungsfehler, erzeugen keinen Task und keinen Header.
import os
from openai import OpenAI
client = OpenAI(api_key=os.environ["SPICY_API_KEY"], base_url="https://api.spicyapi.ai/v1")
raw = client.chat.completions.with_raw_response.create(
model=os.environ["SPICY_MODEL"],
messages=[{"role": "user", "content": "Say hello in one sentence."}],
)
reply = raw.parse() # the usual ChatCompletion object
print(reply.choices[0].message.content)
print("task:", raw.headers.get("x-spicy-task-id"))Beim Stream ist der Header vor dem ersten Chunk verfügbar. Python-with_raw_response und Node.js-.withResponse() funktionieren ebenso mit stream: true. Einträge in GET /api/v1/jobs enthalten zusätzlich die requestId des erstellenden Aufrufs, entsprechend dem Antwortheader X-Request-Id.
Weiterlesen
Text und Streaming · Von OpenAI oder OpenRouter migrieren · Modellkatalog und Schemas · Abrechnung · Fehler
Text und Streaming
Textmodelle im offiziellen Format von OpenAI, Anthropic oder Google Gemini aufrufen und Unterhaltungen, Tools, Streams und Kosten richtig behandeln.
Von OpenAI oder OpenRouter migrieren
OpenAI-Bibliotheken mit SpicyAPI verbinden: Basis-URL, Key und Modell-ID ändern, Feldunterschiede beachten und OpenRouter-Zusätze entfernen.

