spicyapiDokumentation
Erste Schritte
Hauptinhalt

Schnellstart

Key erstellen, Task starten und Ergebnis abholen – der vollständige Ablauf von Anfang bis Ende.

SpicyAPI bündelt Bild- und Videogenerierung hinter einem Authentifizierungs-, Task- und Ergebnisprotokoll und bietet Textmodelle in bekannten OpenAI-, Anthropic- und Gemini-Formaten. Beim Modellwechsel behalten Sie denselben Ablauf und bauen input aus dessen inputSchema auf, der Liste akzeptierter Felder, Typen und Werte.

Dieser Leitfaden verwendet die native Task-API unter https://api.spicyapi.ai/api/v1. Text- und Videokompatibilität nutzt https://api.spicyapi.ai/v1. Beide akzeptieren denselben API-Key, aber unterschiedliche Anfrage- und Antwortformate.

Diese Seite führt Sie von der Modellwahl bis zum Abrufen des Ergebnisses. Wie Sie vor der Bezahlung den exakten Preis erfahren, erklärt Preisabfragen und Kompatibilität; Informationen zu Chat finden Sie unter Text und Streaming. Arbeiten Sie vor Bereitstellung Produktive Integration durch und verwenden Sie bei Fehlern Fehlerbehebung. Das Glossar erklärt unbekannte Begriffe.

Ergebnis sehen, bevor Sie Code schreiben?

Öffnen Sie ein Modell unter spicyapi.ai/explore und verwenden Sie den Playground-Tab. Der API-Tab zeigt dieselbe Anfrage als kopierbaren Befehl. Ohne eigenen Code arbeiten Sie im Terminal mit der CLI oder lassen einen KI-Assistenten über den MCP-Server handeln.

Offizielle Werkzeuge

Direkte HTTP-API-Aufrufe werden vollständig unterstützt und im Rest der Seite gezeigt. Möchten Sie diese nicht selbst schreiben, gibt es vier MIT-lizenzierte npm-Pakete, jeweils separat installierbar und dokumentiert:

Vier weitere offizielle MIT-lizenzierte SDKs unterstützen Sprachen außerhalb Node.js mit demselben Workflow. Aktuell ist nur das Go-Modul mit üblichem Befehl aus seinem Repository installierbar; die anderen drei Seiten zeigen die Installation bis zur Paketveröffentlichung:

Arbeiten Sie in ComfyUI? Die ComfyUI-Nodes bringen jedes Bild-, Video- und Audiomodell als Node auf Ihre Zeichenfläche, mit Preis vor Ausführung.

Vor dem Start

Sie benötigen vier Dinge. Ein ausgelassener Punkt scheitert später mit einem Fehler, der nicht unbedingt den fehlenden Punkt nennt.

  1. Konto mit bestätigter E-Mail. Registrieren Sie sich unter spicyapi.ai und klicken Sie auf den Bestätigungslink. Zuvor liefert jeder Key-Aufruf 403.
  2. Guthaben. Generierungen werden aus US-Dollar-Guthaben bezahlt, nicht mit Credits. Neue Konten beginnen bei null. Laden Sie unter Konsole → Abrechnung auf. Ohne ausreichendes Guthaben scheitert die Taskerstellung mit Geschäftscode 40201. Kontoeinrichtung erklärt Registrierung, Aufladen und Key-Erstellung.
  3. Terminal. macOS: Terminal-App. Linux: beliebige Shell. Windows: PowerShell. Abweichende Befehle haben einen Windows-PowerShell-Tab. macOS-/Linux-Befehle funktionieren auch unverändert in WSL oder Git Bash unter Windows.
  4. Einige Cent. Schritt 3 erstellt einen echten Bildtask, der bei Erfolg kostet. Schritt 2 zeigt Modellpreise und einen optionalen kostenlosen Befehl für den exakten Betrag.

Zuerst bewerten: Modelle und Preise ohne Konto

Für verfügbare Modelle und Preise benötigen Sie weder Key noch Konto. Der öffentliche Katalog hinter der Marketingwebsite ist anonym lesbar; dieser Befehl benötigt keinerlei Ersetzung:

Public catalogue — no key, no account
curl "https://api.spicyapi.ai/console/v1/catalog/models?locale=en"

Die Antwortstruktur wird unten erklärt. Unter data.items enthält jeder Eintrag:

  • model: exakter später als model gesendeter Wert; slug enthält denselben;
  • callable: ob das Modell jetzt laufen kann. Wählen Sie true; status ist unsere Betriebsabsicht und live kann dennoch vorübergehend nicht aufrufbar sein;
  • modality und tasks: Ausgabeart;
  • price: günstigste Stufe, amount in USD je unit, also per_image, per_second, per_request oder per_1k_tokens, plus tiers für Stufenzahl. Bei Text lesen Sie price.perMillionTokens mit getrennten Eingabe-, Cacheeingabe- und Ausgabepreisen je Million Tokens; amount allein ist nur Ausgabepreis.

Hängen Sie eine Modell-ID für den vollständigen Eintrag mit inputSchema, paramNotes und examples an:

One model, in full — replace MODEL_ID_FROM_CATALOG with a model value from the list above
curl "https://api.spicyapi.ai/console/v1/catalog/models/MODEL_ID_FROM_CATALOG?locale=en"

MODEL_ID_FROM_CATALOG ist überall ein Platzhalter, kein reales Modell. Kopieren Sie immer eine echte Livekatalog-ID.

locale ist standardmäßig en; mit ja, ko, de, fr, es, pt-BR, ru, it oder pl erhalten Sie übersetzte Namen, Zusammenfassungen und Parameternotizen. Andere Werte liefern 400. strict=1 entfernt Einträge ohne veröffentlichte Texte in der Sprache statt Englischrückfall; normalerweise unnötig. Dieser Bereich wird nach Quelladresse statt Konto begrenzt und hat ein eigenes Kontingent, siehe Ratenbegrenzungen. Er ist auch gecacht, weshalb Preisänderungen verzögert erscheinen können.

Bei tiers größer als 1 zeigt price die günstigste Stufe, nicht Ihre Einstellungen. price.variant nennt deren Auswahl, etwa 720p oder kombinierte Felder wie generate_audio=false;resolution=480p. Das Einzelmodell nennt, sofern vorhanden, alle stufenwählenden Felder unter inputSchema["x-pricing"].variantFields. pricingNote erinnert allgemein an die maßgebliche Liveschätzung, listet keine Felder. Für exakten Betrag verwenden Sie mit vorhandenem Key POST /api/v1/jobs/quote.

Öffentlicher Katalog zur Bewertung, authentifizierter zum Aufruf

Der öffentliche Katalog zeigt angebotene Modelle und Listenpreise. Er zeigt nicht, was Ihr Key aufrufen darf oder Ihr Konto bezahlt; beides ist kontospezifisch. Wechseln Sie mit Key zu GET /api/v1/models in Schritt 2 als einziger Quelle aufrufbarer IDs. Erinnerte IDs, Veröffentlichungsschnappschüsse und Marketingseiten sind kein Ersatz.

Fünf Schritte

Key erstellen

Ein API-Key ist das Passwort, das Ihr Code bei jeder Anfrage sendet. Er identifiziert das Konto und dessen Abrechnung.

  1. Öffnen Sie Konsole → API-Keys und klicken Sie auf Key erstellen.
  2. Vergeben Sie etwa my-first-test als Namen. Lassen Sie Erweitert unverändert.
  3. Kopieren Sie den mit sk-spicy- beginnenden Key oder klicken Sie auf Als Datei speichern. Er wird genau einmal gezeigt; bei Verlust erstellen Sie einen neuen.
  4. Speichern Sie ihn im für die weiteren Schritte verwendeten Terminal als Umgebungsvariable, einen benannten Wert für dessen Befehle. So muss er nicht in einen Befehl geschrieben werden:
export SPICY_API_KEY="sk-spicy-paste-your-key-here"

# Check: this should print sk-spicy-
echo "${SPICY_API_KEY:0:9}"

Erwartete Anzeige: sk-spicy-. Der Wert bleibt bis zum Schließen des Terminalfensters; führen Sie weitere Schritte dort aus.

Fehlt Key erstellen, erlaubt Ihre aktuelle Teamrolle die Erstellung nicht. Fragen Sie den Inhaber oder verwenden Sie Eigenes Team starten auf derselben Seite. Neue Keys haben ohne Ihre Einstellung kein Ausgabenlimit; ein Tageslimit begrenzt Fehlkonfiguration oder Offenlegung. Siehe Authentifizierung.

Aufrufbares Modell wählen

Der authentifizierte Katalog ist die einzige Quelle aufrufbarer IDs, aktueller Schemas und Beispiele. Verwenden Sie zwei Aufrufe: Die leichte Liste zeigt was aufrufbar ist, dann liefert ein Eintrag das Schema des gewählten Modells. Kataloglesen ist kostenlos.

1. What can this key call?
curl "https://api.spicyapi.ai/api/v1/models?modality=image&task=text-to-image" \
  -H "Authorization: Bearer $SPICY_API_KEY"

Die Antwort ist lang; der benötigte Teil sieht gekürzt so aus:

{
  "code": 200,
  "msg": "success",
  "data": {
    "total": 1,
    "items": [
      {
        "model": "MODEL_ID_FROM_CATALOG",
        "displayName": "…",
        "modality": "image",
        "tasks": ["text-to-image"],
        "enabled": true,
        "available": true,
        "startingPrice": { "variant": "", "unit": "per_image", "price": "0.008", "currency": "USD" }
      }
    ]
  },
  "request_id": "req_01k3m8x9q2z4v7n5p6r8s0t1w2"
}

Wählen Sie einen Eintrag mit enabled und available jeweils true, prüfen Sie den Preis und speichern Sie seinen model-Wert für den Rest der Seite. Mit jq zeigt curl … | jq -r '.data.items[] | select(.enabled and .available) | .model' nur aufrufbare IDs.

# Replace MODEL_ID_FROM_CATALOG with the model value you copied
export SPICY_MODEL="MODEL_ID_FROM_CATALOG"
2. What fields does that one model take?
curl "https://api.spicyapi.ai/api/v1/models/${SPICY_MODEL}" \
  -H "Authorization: Bearer $SPICY_API_KEY"

Schrägstriche der ID bleiben im Pfad unverändert; prozentkodieren Sie nur bei Clients, die einen einzelnen Segmentparameter verlangen. inputSchema ist maßgeblich für input: required nennt Pflichtfelder, properties beschreibt sie. examples enthält serverseitig validierte Eingaben. Weitere Felder einschließlich pricing erklärt Modellkatalog und Schemas.

Fordern Sie Schemas nur bei Bedarf an. includeSchema=1&includeExamples=1 am Listenaufruf liefert alle aktivierten Modelle samt vollständigem JSON Schema und deutlich größerer Antwort. Katalogabfragen nutzen das Lesekontingent des Kontos unabhängig von der Taskerstellung. Lesen Sie den Katalog auch nicht vor jedem Task erneut; siehe Katalog zwischenspeichern.

Ist kein Eintrag geeignet, halten Sie an, bevor Sie einen kostenpflichtigen Task erstellen. Schnappschuss, Marketingseite oder erinnerte ID ersetzen keinen leeren Livekatalog. Für programmgesteuerte Auswahl siehe Modell aus Code wählen.

Optional: Exakten Preis vorher sehen. Preisabfragen sind kostenlos, erstellen nichts und reservieren nichts. data.estimatedCost ist der maximale USD-Betrag dieser exakten Anfrage:

Optional quote (macOS / Linux)
curl -X POST https://api.spicyapi.ai/api/v1/jobs/quote \
  -H "Authorization: Bearer $SPICY_API_KEY" \
  -H "Content-Type: application/json" \
  --data "{\"model\":\"${SPICY_MODEL}\",\"input\":{\"prompt\":\"A cinematic night portrait\"}}"

Task erstellen

Ein Task ist ein Hintergrund-Generierungsauftrag. Sie erhalten sofort seine ID, später das Ergebnis. Dieser Schritt kostet Geld.

Jeder neue Task erhält einen neuen Idempotenzschlüssel, eine zufällige Kennung, die bei zweimaligem Senden derselben Anfrage, etwa nach Netzwerkfehler, doppelte Tasks verhindert. Erzeugen Sie ihn in einem eigenen Befehl und senden Sie danach den Task:

3a. A new key for each new image (run once per image)
SPICY_IDEMPOTENCY_KEY="$(uuidgen)"
# Linux without uuidgen: SPICY_IDEMPOTENCY_KEY="$(cat /proc/sys/kernel/random/uuid)"
3b. Send the task (safe to run again after a network error)
curl -X POST https://api.spicyapi.ai/api/v1/jobs/createTask \
  -H "Authorization: Bearer $SPICY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $SPICY_IDEMPOTENCY_KEY" \
  --data "{\"model\":\"${SPICY_MODEL}\",\"input\":{\"prompt\":\"A cinematic night portrait\"}}"

Bei Timeout oder Verbindungsabbruch in 3b wiederholen Sie 3b ohne 3a. Derselbe Schlüssel liefert den gestarteten Task statt doppelter Bezahlung. Für ein anderes Bild führen Sie zuerst 3a aus. 409 bedeutet Schlüsselwiederverwendung mit anderem Prompt oder Modell. Mehr unter Idempotenz.

Die Anfrage sendet ein Feld. Verlangt das Schema aus Schritt 2 weitere, ergänzen Sie diese. Das jeweilige Schema bestimmt input; ein unbekanntes Feld verursacht 400.

Die Verbindung wird nie offengehalten. Die Task-ID kommt sofort:

HTTP/1.1 202 Accepted
{
  "code": 200,
  "msg": "success",
  "data": {
    "taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
    "state": "queued",
    "estimatedCost": "0.008",
    "deadlineAt": "2026-08-28T09:22:04Z"
  },
  "request_id": "req_01k3m8x9q2z4v7n5p6r8s0t1w2"
}

Kopieren und speichern Sie auf macOS / Linux die taskId; PowerShell hat das bereits übernommen:

export SPICY_TASK_ID="paste-the-taskId-here"

202 bedeutet angenommen, nicht abgeschlossen

Ein neuer Task hat state: "queued"; sichere Wiederholung mit demselben Idempotency-Key liefert den ursprünglichen Task im aktuellen Zustand. HTTP 202 bestätigt Annahme, nicht abgeschlossene Generierung. Bei der ersten Annahme werden Gelder reserviert und noch nicht berechnet: estimatedCost wird vom Guthaben zurückgelegt. Diese Reservierung ist zugleich Kostenobergrenze. Ungenutzte Beträge werden freigegeben; darüber hinaus wird kein weiterer Betrag eingezogen. Fehlgeschlagene Tasks geben die Reservierung normalerweise frei; ausdrücklich auf der Modellseite genannte Inhaltsablehnungen werden zum angenommenen Angebotspreis berechnet. Prüfen Sie settled und cost. deadlineAt ist die späteste Abschlusszeit vor expired. Der Geschäftscode code einer erfolgreichen Antwort bleibt 200. Ein angenommener Task ist nicht abbrechbar.

Ergebnis abholen

Enthält Ihre produktive Anfrage callBackUrl, rufen wir Sie zurück, wenn der Task einen Endzustand erreicht. Ein Callback oder Webhook ist eine HTTPS-Adresse Ihres Servers, die wir nach Abschluss aufrufen; ständiges Nachfragen entfällt. Die Minimalanfrage lässt sie für Betrieb ohne öffentlichen Endpunkt weg. Callbacks benötigen Signaturprüfung, siehe Webhooks.

Ohne Callback fragen Sie den Task ab:

curl "https://api.spicyapi.ai/api/v1/jobs/recordInfo?taskId=${SPICY_TASK_ID}" \
  -H "Authorization: Bearer $SPICY_API_KEY"

Verwenden Sie den API-Key, mit dem Sie den Task erstellt haben; ein anderer Key erhält 404. Prüfen Sie data.state:

data.stateBedeutungMaßnahme
queued oder runningGenerierung läuft; Gelder reserviert, nicht ausgegebenEinige Sekunden warten, gleichen Befehl erneut ausführen. Bilder meist Sekunden, Videos Minuten. deadlineAt ist Obergrenze
succeededFertigZum nächsten Schritt
failed oder expiredNicht gelungen. Reservierung normalerweise freigegeben; ausdrücklich modellseitig genannte Inhaltsablehnungen berechnet. settled und cost prüfenStabiles data.errorCode und lesbares data.errorMessage lesen, Eingabe korrigieren, bei 3a neu starten

Der code der Antwort bleibt auch dann 200, wenn der Task fehlgeschlagen ist: code bestätigt, dass das Lesen des Tasks erfolgreich war. Das Ergebnis des Tasks steht immer in data.state. So sehen die Antworten in den einzelnen Phasen aus, gekürzt dargestellt:

{
  "code": 200,
  "msg": "success",
  "data": {
    "taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
    "model": "MODEL_ID_FROM_CATALOG",
    "state": "running",
    "cost": "0.008",
    "settled": false,
    "createdAt": "2026-08-28T09:12:04Z",
    "deadlineAt": "2026-08-28T09:22:04Z"
  },
  "request_id": "req_01k3m8x9q2z4v7n5p6r8s0t1w4"
}

Solange settled gleich false ist, zeigt cost die Reservierung, nicht endgültige Belastung.

Ergebnisdatei herunterladen

Verwenden Sie data.output.assets[0].url aus dem Erfolgsergebnis direkt ohne API-Key. Der Link gilt ungefähr 20 Minuten, siehe expiresAt. Bei Ablauf wiederholen Sie Schritt 4 für einen frischen Link. Erzeugte Dateien werden spätestens nach 14 Tagen gelöscht; bewahren Sie benötigte eigene Kopien.

# Paste data.output.assets[0].url. Keep the double quotes: the URL contains & characters.
RESULT_URL="paste-the-url-here"
curl -L "$RESULT_URL" -o result.png   # use result.mp4 when mime is video/mp4

Mit jq entfällt Kopieren und Einfügen:

RESULT_URL="$(curl -s "https://api.spicyapi.ai/api/v1/jobs/recordInfo?taskId=${SPICY_TASK_ID}" \
  -H "Authorization: Bearer $SPICY_API_KEY" | jq -r '.data.output.assets[0].url')"

Erwartetes Ergebnis: result.png im aktuellen Terminalordner. Prüfen Sie Kosten unter Konsole → Logs oder cost bei settled gleich true.

Textmodelle: Eine Anfrage, Antwort direkt zurück

Textmodelle antworten in derselben HTTP-Antwort, ohne Polling oder Download. X-Spicy-Task-Id bezeichnet den abrechnenden Task zur späteren Prüfung. Listen Sie in Schritt 2 stattdessen modality=text, exportieren Sie eines als SPICY_MODEL und führen Sie Folgendes aus:

curl https://api.spicyapi.ai/v1/chat/completions \
  -H "Authorization: Bearer $SPICY_API_KEY" \
  -H "Content-Type: application/json" \
  --data "{\"model\":\"${SPICY_MODEL}\",\"messages\":[{\"role\":\"user\",\"content\":\"Say hello in one sentence.\"}]}"

Die Antwort steht in choices[0].message.content; berechnet werden verwendete Tokens. Der Endpunkt nutzt OpenAI-Antwortformat statt {code, msg, data}. Siehe Text und Streaming.

Modell aus Code wählen

Die fünf Schritte oben halten einen Menschen beteiligt. Eine bereitgestellte Integration wählt selbst unmittelbar vor Einreichung: Letzte Woche aufrufbare Modelle können heute pausiert sein, Preise sind kontospezifisch. Fordern Sie Schema und servervalidiertes Beispiel zusammen an und prüfen Sie, dass enabled und available zutreffen:

Select a live text-to-image model (requires jq)
CATALOG_JSON="$(curl --fail-with-body \
  "https://api.spicyapi.ai/api/v1/models?modality=image&task=text-to-image&includeSchema=1&includeExamples=1" \
  -H "Authorization: Bearer $SPICY_API_KEY")"

MODEL_ID="$(printf '%s' "$CATALOG_JSON" | jq -er \
  '[.data.items[] | select(.enabled == true and .available == true and (.examples | length > 0))][0].model')"
MODEL_INPUT="$(printf '%s' "$CATALOG_JSON" | jq -cer \
  '[.data.items[] | select(.enabled == true and .available == true and (.examples | length > 0))][0].examples[0].input')"

printf 'Selected %s\n' "$MODEL_ID"
Create the task from those two values (this costs money)
SPICY_IDEMPOTENCY_KEY="$(uuidgen)"   # new task, new key; reuse it only to resend this same request
TASK_PAYLOAD="$(jq -cn \
  --arg model "$MODEL_ID" \
  --argjson input "$MODEL_INPUT" \
  '{model: $model, input: $input}')"

curl -X POST https://api.spicyapi.ai/api/v1/jobs/createTask \
  -H "Authorization: Bearer $SPICY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $SPICY_IDEMPOTENCY_KEY" \
  --data "$TASK_PAYLOAD"

Bei leerer Auswahl stoppen Sie, statt eine erinnerte ID zu verwenden. Die drei Programme unten treffen dieselbe Auswahl ohne jq.

Alles zusammenführen

Die drei Programme lesen den Katalog, erstellen einen Task, fragen mit wachsenden Abständen ab und geben eine Download-URL aus. Verwenden Sie produktiv stattdessen Webhooks. Polling ist Ersatz, nicht der geplante Hauptweg.

Diese Programme erstellen einen echten kostenpflichtigen Task

Jedes wählt das erste aufrufbare Text-to-Image-Modell Ihres Katalogs, nicht das günstigste, und reicht ein validiertes Beispiel ein. Prüfen Sie dessen Preis vorher oder wählen Sie ausdrücklich Ihr gewünschtes Modell.

SpracheDateinameVorher installierenAusführen
Node.js 20 oder neuergenerate.mjsNichtsnode generate.mjs
Python 3.9 oder neuergenerate.pypip install requestspython3 generate.py, Windows: py generate.py
Go 1.21 oder neuermain.go in leerem Ordnergo mod init examplego run .

Alle lesen SPICY_API_KEY aus Schritt 1 und zeigen nach Abschluss die Ergebnis-URL. Sie warten bis zehn Minuten, passend für Bildmodelle; bei Video warten Sie stattdessen bis deadlineAt.

Node.js 20+
const BASE = 'https://api.spicyapi.ai/api/v1';
const KEY = process.env.SPICY_API_KEY;

const headers = {
  Authorization: `Bearer ${KEY}`,
  'Content-Type': 'application/json',
};
async function call(path, init) {
  const res = await fetch(`${BASE}${path}`, {
    ...init,
    headers: { ...headers, ...init?.headers },
    signal: AbortSignal.timeout(30_000),
  });
  const body = await res.json();
  // Branch on `code`, not on res.ok — the business code says far more
  if (body.code !== 200) {
    throw new Error(`${body.code} ${body.msg} (request_id=${body.request_id})`);
  }
  return body.data;
}

async function generate() {
  const catalog = await call(
    '/models?modality=image&task=text-to-image&includeSchema=1&includeExamples=1',
  );
  const selected = catalog.items.find(
    (item) => item.enabled && item.available && item.examples?.length,
  );
  if (!selected) throw new Error('No callable text-to-image model with a validated example');

  const idempotencyKey = crypto.randomUUID();
  const { taskId } = await call('/jobs/createTask', {
    method: 'POST',
    headers: { 'Idempotency-Key': idempotencyKey },
    body: JSON.stringify({
      model: selected.model,
      input: selected.examples[0].input,
    }),
  });

  // Start at 2s, multiply by 1.5, cap at 15s. The first check is almost
  // certainly still `queued`, so a shorter start just wastes a round trip.
  const deadline = Date.now() + 10 * 60_000;
  let wait = 2000;
  while (Date.now() < deadline) {
    await new Promise((r) => setTimeout(r, wait));
    wait = Math.min(wait * 1.5, 15000);

    const task = await call(`/jobs/recordInfo?taskId=${taskId}`);
    if (task.state === 'succeeded') {
      const asset = task.output?.assets?.find((item) => item.url);
      if (asset) return asset.url;
      if (task.output?.assets?.some((item) => item.pending)) continue;
      throw new Error('Result is unavailable');
    }
    // Anything other than queued / running is terminal. This also handles
    // historical canceled records and future terminal states safely.
    if (task.state !== 'queued' && task.state !== 'running') {
      throw new Error(`${task.state}: ${task.errorMessage ?? 'no detail'}`);
    }
  }
  throw new Error(`task ${taskId} timed out`);
}

generate().then(console.log);
Python 3.9+
import os, time, uuid, requests

BASE = "https://api.spicyapi.ai/api/v1"
HEADERS = {
    "Authorization": f"Bearer {os.environ['SPICY_API_KEY']}",
    "Content-Type": "application/json",
}


def call(method: str, path: str, headers=None, **kwargs) -> dict:
    res = requests.request(
        method, f"{BASE}{path}", headers={**HEADERS, **(headers or {})}, timeout=30, **kwargs
    )
    body = res.json()
    # Branch on `code`, not on status_code
    if body["code"] != 200:
        raise RuntimeError(f'{body["code"]} {body["msg"]} (request_id={body.get("request_id")})')
    return body["data"]


def generate() -> str:
    catalog = call(
        "GET",
        "/models?modality=image&task=text-to-image&includeSchema=1&includeExamples=1",
    )
    selected = next(
        (
            item
            for item in catalog["items"]
            if item["enabled"] and item["available"] and item.get("examples")
        ),
        None,
    )
    if selected is None:
        raise RuntimeError("No callable text-to-image model with a validated example")

    idempotency_key = str(uuid.uuid4())
    task_id = call("POST", "/jobs/createTask", headers={"Idempotency-Key": idempotency_key}, json={
        "model": selected["model"],
        "input": selected["examples"][0]["input"],
    })["taskId"]

    # Start at 2s, multiply by 1.5, cap at 15s
    deadline = time.monotonic() + 600
    wait = 2.0
    while time.monotonic() < deadline:
        time.sleep(wait)
        wait = min(wait * 1.5, 15.0)

        task = call("GET", "/jobs/recordInfo", params={"taskId": task_id})
        if task["state"] == "succeeded":
            assets = task.get("output", {}).get("assets", [])
            ready = next((asset for asset in assets if asset.get("url")), None)
            if ready:
                return ready["url"]
            if any(asset.get("pending") for asset in assets):
                continue
            raise RuntimeError("Result is unavailable")
        # Anything other than queued / running is terminal.
        if task["state"] not in {"queued", "running"}:
            raise RuntimeError(f'{task["state"]}: {task.get("errorMessage", "no detail")}')
    raise TimeoutError(f"task {task_id} timed out")


print(generate())
Go 1.21+
package main

import (
	"bytes"
	"crypto/rand"
	"encoding/json"
	"encoding/hex"
	"fmt"
	"math"
	"net/http"
	"os"
	"time"
)

const base = "https://api.spicyapi.ai/api/v1"

// envelope is the shape every endpoint returns. Data stays raw so each
// call site can decode it into whatever it actually expects.
type envelope struct {
	Code      int             `json:"code"`
	Msg       string          `json:"msg"`
	Data      json.RawMessage `json:"data"`
	RequestID string          `json:"request_id"`
}

var client = &http.Client{Timeout: 30 * time.Second}

func newIdempotencyKey() string {
	b := make([]byte, 16)
	if _, err := rand.Read(b); err != nil {
		panic(err)
	}
	return "job-" + hex.EncodeToString(b)
}

func call(method, path string, body any, idempotencyKey string, out any) error {
	var rdr *bytes.Reader
	if body != nil {
		raw, err := json.Marshal(body)
		if err != nil {
			return err
		}
		rdr = bytes.NewReader(raw)
	} else {
		rdr = bytes.NewReader(nil)
	}

	req, err := http.NewRequest(method, base+path, rdr)
	if err != nil {
		return err
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("SPICY_API_KEY"))
	req.Header.Set("Content-Type", "application/json")
	if idempotencyKey != "" {
		req.Header.Set("Idempotency-Key", idempotencyKey)
	}

	res, err := client.Do(req)
	if err != nil {
		return err
	}
	defer res.Body.Close()

	var env envelope
	if err := json.NewDecoder(res.Body).Decode(&env); err != nil {
		return err
	}
	// Branch on Code, not on res.StatusCode
	if env.Code != 200 {
		return fmt.Errorf("%d %s (request_id=%s)", env.Code, env.Msg, env.RequestID)
	}
	return json.Unmarshal(env.Data, out)
}

func generate() (string, error) {
	var catalog struct {
		Items []struct {
			Model     string `json:"model"`
			Enabled   bool   `json:"enabled"`
			Available bool   `json:"available"`
			Examples  []struct {
				Input map[string]any `json:"input"`
			} `json:"examples"`
		} `json:"items"`
	}
	if err := call(http.MethodGet,
		"/models?modality=image&task=text-to-image&includeSchema=1&includeExamples=1",
		nil, "", &catalog); err != nil {
		return "", err
	}
	var modelID string
	var modelInput map[string]any
	for _, item := range catalog.Items {
		if item.Enabled && item.Available && len(item.Examples) > 0 {
			modelID, modelInput = item.Model, item.Examples[0].Input
			break
		}
	}
	if modelID == "" {
		return "", fmt.Errorf("no callable text-to-image model with a validated example")
	}

	var created struct {
		TaskID string `json:"taskId"`
	}
	err := call(http.MethodPost, "/jobs/createTask", map[string]any{
		"model": modelID,
		"input": modelInput,
	}, newIdempotencyKey(), &created)
	if err != nil {
		return "", err
	}

	// Start at 2s, multiply by 1.5, cap at 15s
	wait := 2 * time.Second
	deadline := time.Now().Add(10 * time.Minute)
	for time.Now().Before(deadline) {
		time.Sleep(wait)
		wait = time.Duration(math.Min(float64(wait)*1.5, float64(15*time.Second)))

		var task struct {
			State    string `json:"state"`
			ErrorMsg string `json:"errorMessage"`
			Output struct { Assets []struct { URL string `json:"url"`; Pending bool `json:"pending"` } `json:"assets"` } `json:"output"`
		}
		if err := call(http.MethodGet, "/jobs/recordInfo?taskId="+created.TaskID, nil, "", &task); err != nil {
			return "", err
		}

		if task.State == "succeeded" {
			pending := false
			for _, asset := range task.Output.Assets {
				if asset.URL != "" { return asset.URL, nil }
				pending = pending || asset.Pending
			}
			if pending { continue }
			return "", fmt.Errorf("result is unavailable")
		}
		// Terminal means "not one of the two in-flight states" — do not
		// enumerate the terminal states, that breaks when a new one appears
		if task.State != "queued" && task.State != "running" {
			return "", fmt.Errorf("%s: %s", task.State, task.ErrorMsg)
		}
	}
	return "", fmt.Errorf("task %s timed out", created.TaskID)
}

func main() {
	url, err := generate()
	if err != nil {
		panic(err)
	}
	fmt.Println(url)
}

Die Antwortstruktur

Native Task-API-JSON-Antworten nutzen diese Struktur; /v1-Kompatibilität und SSE eigene Protokollformate:

{ "code": 200, "msg": "success", "data": { }, "request_id": "req_…" }
  • code ist ein Geschäftscode, kein HTTP-Status. 200 bedeutet Erfolg. Auch der HTTP-Status wird korrekt gesetzt, sodass Sie Ihre Fehlerbehandlung an einem der beiden Werte ausrichten können; wählen Sie einen. Bei spezifischen Codes unterscheiden sich die Werte, etwa bei unzureichendem Guthaben mit code: 40201 und HTTP 402. Wenn Sie beide Ansätze vermischen, übersehen Sie irgendwann einen Fall.
  • Bei Fehler fehlt data, statt null zu sein.
  • request_id erscheint bei Erfolg und Fehler und im Antwortheader X-Request-Id. Nennen Sie es bei Problemmeldungen.

Nächste Schritte

Auf dieser Seite