spicyapiDokumentation
Text und Chat
Hauptinhalt

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 above

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

chat.py
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:

TeilInhaltBeispiel
IdentitätWer die Figur ist, mit zwei oder drei verhaltensprägenden FaktenPensionierter Detektiv, der nun einen Leuchtturm betreut
StimmeSatzlänge, Wortwahl, Gewohnheiten, NutzeranspracheBedächtig, spricht kleine Beobachtungen aus, nennt den Nutzer „Freund“
WeltSchauplatz und ein bis zwei zentrale HandlungsfaktenFischerdorf der 1920er, in dem um Mitternacht die Glocke eines versunkenen Schiffs läutet
GrenzenVerhalten, Ausschlüsse und Ausstieg aus der GeschichteBleibt in der Welt; beantwortet (OOC: ...) kurz außerhalb der Rolle; allgemein geeignete Geschichte
ErzählungPerspektive, Zeitform und SprachmarkierungDritte Person, Vergangenheit; gesprochene Sätze in Anführungszeichen
FormatLänge und Ende jeder AntwortHö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.

roleplay.py
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:

Another card: a daily companion
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:

  1. Fordern Sie Verbrauch an, wenn Sie Tokenzahlen benötigen. Ohne stream_options.include_usage enthält kein Chunk usage. Mit "include_usage": true erscheint unmittelbar vor [DONE] ein Zusatzchunk mit endgültigem usage und leerem choices; andere Chunks haben usage: null.
  2. Prüfen Sie choices vor choices[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.
  3. Denken bleibt getrennt. Denkmodelle senden es als delta.reasoning_content, die Antwort als delta.content. Zeigen Sie ersteres gedimmt, eingeklappt oder gar nicht; nur letzteres ist die Antwort.
  4. finish_reason: "length" bedeutet abgeschnitten. Das Modell erreichte max_tokens. Erhöhen Sie den Wert oder senken reasoning_effort.
  5. 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.
  6. 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.

stream.py
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:

  1. Senden Sie Nachrichten zusammen mit tools, einer JSON-Schema-beschriebenen Funktionsliste.
  2. 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 eine tool-Nachricht mit passender tool_call_id.
  3. 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.

tools.py
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.

json_output.py
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.

Statuserror.codeUrsacheMaßnahme
400unsupported_parameterNicht 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 genanntEntfernen; Felder wie unter Vor dem Start prüfen
400invalid_requestFehlendes Feld oder nicht akzeptierter Wert, etwa reasoning_effort: must be one of: "low", "high", "max", oder kein Chatmodell mit param gleich modelGenannten Wert ändern oder Modell mit /chat wählen
400content_rejectedInhaltsrichtlinie des Modells lehnt Anfrage abGeeignete andere Formulierung wählen; gleiche Eingabe wird erneut abgelehnt
401invalid_api_keyKey fehlt, ist falsch geschrieben oder widerrufenSPICY_API_KEY prüfen; Präfix ist sk-spicy-
402insufficient_balance, spend_cap_exceededGuthaben oder Ausgabenlimit deckt Reservierung nichtAufladen, Limit erhöhen oder max_tokens senken, siehe unten
404model_not_foundID existiert nicht, param ist model. Meldung nennt Format und gegebenenfalls nächstähnliches aufrufbares ChatmodellExakte ID aus Livekatalog kopieren
404not_foundModell existiert, ist derzeit nicht aufrufbarAnderes Livemodell wählen
409conflictIdempotency-Key mit geändertem Anfrageinhalt wiederverwendetNeue Nachricht mit neuem Schlüssel senden; siehe Textidempotenz
429rate_limit_exceededZu viele AnfragenWarten, wiederholen; vorhandenes Retry-After nennt Sekunden
503upstream_failed, model_unavailableModell gescheitert oder vorübergehend nicht verfügbar. Keine BelastungSpä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.

task_id.py
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

Auf dieser Seite