spicyapiDocumentazione
Per iniziare
Contenuto principale

Guida rapida

Ottieni una chiave, crea un'attività e recupera il risultato. Il percorso completo, dall'inizio alla fine.

SpicyAPI riunisce la generazione di immagini e video in un unico protocollo di autenticazione, attività e risultati, e offre modelli di testo nei formati OpenAI, Anthropic e Gemini che già utilizzi. Mantieni lo stesso flusso di chiamate quando cambi modello e costruisci input dal suo inputSchema, cioè l'elenco dei campi accettati dal modello, dei loro tipi e dei valori consentiti.

Questa guida usa l'API nativa delle attività su https://api.spicyapi.ai/api/v1. Gli endpoint di compatibilità per testo e video usano https://api.spicyapi.ai/v1. Entrambi accettano la stessa chiave API, ma i formati di richiesta e risposta sono diversi.

Segui questa pagina per scegliere un modello e recuperare un risultato. Per vedere il prezzo esatto prima di pagare, consulta Preventivi e compatibilità. Per la chat, consulta Testo e streaming. Prima di distribuire l'integrazione, segui Integrazione in produzione; usa Risoluzione dei problemi quando un passaggio fallisce. Le parole meno familiari sono spiegate nel glossario.

Vuoi vedere un risultato prima di scrivere codice?

Apri un modello su spicyapi.ai/explore e usa la scheda Playground; la scheda API mostra la stessa richiesta come comando da copiare. Per lavorare da terminale senza scrivere codice, usa la CLI, oppure lascia che un assistente AI lo faccia tramite il server MCP.

Strumenti ufficiali

Le chiamate dirette all'API HTTP sono pienamente supportate: è il metodo mostrato nel resto della pagina. Se preferisci non scrivere tu le chiamate HTTP, ci sono quattro pacchetti npm con licenza MIT, ciascuno installabile separatamente e con una propria guida:

Altri quattro SDK ufficiali coprono i linguaggi esterni a Node.js. Tutti implementano lo stesso flusso e hanno licenza MIT; attualmente soltanto il modulo Go si installa dal repository con il comando consueto, mentre le altre tre pagine mostrano l'installazione da usare fino alla pubblicazione dei pacchetti:

Lavori invece in ComfyUI? I nodi ComfyUI portano ogni modello di immagini, video e audio sul tuo canvas come nodo, mostrando il prezzo prima dell'esecuzione.

Prima di iniziare

Ti servono quattro cose. Saltarne una causa più avanti un errore che non specifica quale requisito manchi.

  1. Un account con email verificata. Registrati su spicyapi.ai e apri il link nell'email di verifica. Fino a quel momento, ogni chiamata effettuata con la chiave restituisce 403.
  2. Un saldo. Le generazioni vengono pagate da un saldo in dollari statunitensi, senza crediti, e un nuovo account parte da zero. Aggiungi fondi in Console → Fatturazione. Senza un saldo sufficiente, la creazione di un'attività fallisce con il codice applicativo 40201. Configurazione dell'account spiega registrazione, ricarica e creazione di una chiave.
  3. Un terminale. Su macOS, l'app Terminale; su Linux, qualsiasi shell; su Windows, PowerShell. Quando i comandi differiscono, ogni passaggio ha una scheda Windows PowerShell. Se preferisci, i comandi macOS / Linux funzionano senza modifiche anche in WSL o Git Bash su Windows.
  4. Qualche centesimo. Il passaggio 3 crea una vera attività di generazione di immagini, addebitata quando riesce. Il passaggio 2 mostra il prezzo di ogni modello e un comando facoltativo indica prima l'importo esatto, gratuitamente.

Valutare prima: modelli e prezzi senza account

Non ti servono né una chiave né un account per vedere cosa è disponibile e quanto costa. Il catalogo pubblico usato dal sito è leggibile senza autenticazione e questo comando non richiede alcuna sostituzione:

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

La struttura della risposta è quella descritta in fondo alla pagina. In data.items, ogni voce contiene:

  • model: il valore esatto che in seguito invierai come model; slug contiene lo stesso valore;
  • callable: indica se il modello può essere eseguito adesso. Scegli le voci con valore true: status indica lo stato operativo previsto, ma un modello live può comunque essere temporaneamente non eseguibile;
  • modality e tasks: il tipo di output che produce;
  • price: il livello più economico, con amount in USD per unit (per_image, per_second, per_request o per_1k_tokens) e tiers, il numero di livelli disponibili. Per i modelli di testo, leggi price.perMillionTokens, che fornisce separatamente i prezzi di input, input in cache e output per milione di token; amount da solo è soltanto la tariffa di output.

Aggiungi un ID di modello per ottenere il record completo di quel modello, inclusi inputSchema, paramNotes ed examples:

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 è un segnaposto usato nella documentazione, non un modello reale: copia sempre un ID effettivo dal catalogo corrente.

locale usa en per impostazione predefinita; passa ja, ko, de, fr, es, pt-BR, ru, it o pl per nomi, riepiloghi e note sui parametri tradotti. Qualsiasi altro valore restituisce 400. strict=1 esclude le voci il cui testo non è pubblicato in quella lingua, invece di usare l'inglese come ripiego; normalmente non serve. Questa interfaccia ha un limite di frequenza basato sull'indirizzo di origine, non sull'account, e dispone di un proprio contatore: consulta Limiti di frequenza. È anche memorizzata in cache, quindi una variazione di prezzo può richiedere tempo prima di comparire qui.

Quando tiers è maggiore di 1, il price indicato è il livello più economico, non il prezzo delle tue impostazioni. price.variant mostra quali impostazioni usa quel livello, per esempio 720p oppure generate_audio=false;resolution=480p quando si combinano più campi. Il campo inputSchema["x-pricing"].variantFields del record di un singolo modello, quando presente, elenca tutti i campi che selezionano un livello. pricingNote è un promemoria generale che fa riferimento alla stima corrente come fonte autorevole; non elenca campi. Per l'importo esatto di una richiesta, usa POST /api/v1/jobs/quote dopo aver ottenuto una chiave.

Catalogo pubblico per valutare, catalogo autenticato per chiamare

Il catalogo pubblico risponde a «che cosa è in vendita e qual è il prezzo di listino». Non può rispondere a «che cosa può chiamare la mia chiave» o «quanto verrà addebitato al mio account», perché entrambe le risposte dipendono dall'account. Quando hai una chiave, passa a GET /api/v1/models nel passaggio 2 e consideralo l'unica fonte degli ID eseguibili: un ID ricordato, una fotografia del catalogo al lancio o una pagina promozionale non sono un'alternativa.

Cinque passaggi

Ottieni una chiave

Una chiave API è la password che il codice invia con ogni richiesta. Identifica il tuo account e determina a chi viene addebitato l'utilizzo.

  1. Apri Console → Chiavi API e fai clic su Crea chiave.
  2. Assegna un nome, per esempio my-first-test. Lascia invariate tutte le impostazioni in Avanzate.
  3. Copia la chiave che inizia con sk-spicy-, oppure fai clic su Salva come file. Viene mostrata una sola volta; se la perdi devi crearne una nuova.
  4. Nel terminale che userai per i passaggi successivi, salvala in una variabile d'ambiente, cioè un valore con nome che il terminale passa a ogni comando eseguito, così non dovrai digitare la chiave in un comando:
export SPICY_API_KEY="sk-spicy-paste-your-key-here"

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

Che cosa dovresti vedere: sk-spicy-. Il valore dura fino alla chiusura di quella finestra del terminale, quindi esegui tutti i passaggi successivi nella stessa finestra.

Se non c'è il pulsante Crea chiave, il tuo ruolo nel team corrente non permette di creare chiavi: chiedi al proprietario del team oppure usa Crea il tuo team nella stessa pagina. Le nuove chiavi non hanno un limite di spesa, salvo che tu ne imposti uno; un limite giornaliero nella console mette un tetto ai danni di una configurazione errata o di una chiave compromessa. Consulta Autenticazione.

Scegli un modello eseguibile

Il catalogo autenticato è l'unica fonte di ID eseguibili, schemi correnti ed esempi. Leggilo con due chiamate: l'elenco è leggero e indica che cosa puoi chiamare; poi un singolo record fornisce lo schema del modello scelto. Leggere il catalogo è gratuito.

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"

La risposta è lunga. La parte che serve è simile a questa, abbreviata:

{
  "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"
}

Scegli una voce con enabled e available entrambi true, controlla il prezzo, copia il valore model e conservalo per il resto della pagina. Se hai installato jq, curl … | jq -r '.data.items[] | select(.enabled and .available) | .model' stampa soltanto gli ID che puoi chiamare.

# 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"

Le barre nell'ID passano nel percorso senza modifiche; codificale in percentuale soltanto se il client HTTP esige che un parametro di percorso sia un singolo segmento. inputSchema del record è la fonte autorevole per input: required elenca i campi obbligatori e properties descrive ciascuno. examples contiene input già convalidati dal server. Catalogo dei modelli e schemi spiega come leggere il resto del record, incluso pricing.

Richiedi gli schemi soltanto dove servono. Aggiungere includeSchema=1&includeExamples=1 alla chiamata dell'elenco restituisce in una sola risposta il JSON Schema completo di ogni modello abilitato, con un corpo molto più grande. Le letture del catalogo usano la disponibilità di lettura dell'account, indipendente dalla creazione delle attività. Non rileggere nemmeno il catalogo prima di ogni attività; consulta Memorizzare il catalogo in cache.

Se non c'è alcuna voce corrispondente, fermati prima di creare un'attività a pagamento. Una fotografia del catalogo al lancio, una pagina promozionale o un ID ricordato non sostituiscono un catalogo corrente vuoto. Per selezionare il modello dal codice invece che manualmente, consulta Selezionare il modello dal codice.

Facoltativo: verifica prima il prezzo esatto. Un preventivo è gratuito, non crea nulla e non riserva fondi. data.estimatedCost è il costo massimo di questa esatta richiesta, in USD:

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\"}}"

Crea un'attività

Un'attività è una generazione eseguita in background: ottieni subito il suo ID e il risultato più tardi. Questo passaggio è a pagamento.

Ogni nuova attività riceve una nuova chiave di idempotenza: un ID casuale che alleghi affinché l'invio della stessa richiesta due volte, per esempio dopo un errore di rete, non crei mai due attività. Generala con un comando separato e poi invia l'attività:

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\"}}"

Se 3b supera il tempo massimo o la connessione si interrompe, esegui di nuovo 3b senza eseguire 3a: la stessa chiave restituisce l'attività già avviata invece di farti pagare due volte. Per creare un'immagine diversa, esegui prima 3a. Una risposta 409 significa che hai riutilizzato una chiave con un prompt o modello diverso. Maggiori dettagli in Idempotenza.

Questa richiesta invia un solo campo. Se lo schema letto nel passaggio 2 indica altri campi obbligatori, aggiungili: i valori ammessi in input sono definiti dallo schema di ogni modello e un solo campo sconosciuto provoca un 400.

La connessione non resta mai aperta. Ricevi subito un ID di attività:

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"
}

Su macOS / Linux, copia taskId dalla risposta e conservalo; PowerShell lo ha già fatto per te:

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

202 significa accettata, non completata

Un'attività appena creata ha state: "queued"; un reinvio sicuro con lo stesso Idempotency-Key restituisce l'attività originale nel suo stato corrente. HTTP 202 significa che la richiesta è stata accettata, non che la generazione è completata. Alla prima accettazione, i fondi vengono riservati, non addebitati: estimatedCost viene accantonato dal saldo. La riserva è anche il tetto all'addebito dell'attività: i fondi inutilizzati vengono liberati e non viene riscosso successivamente alcun importo superiore. Le attività fallite normalmente liberano la riserva; determinati rifiuti di contenuto dichiarati nella pagina del modello vengono addebitati al preventivo accettato. Controlla settled e cost. deadlineAt è l'ultimo momento in cui l'attività può terminare prima di concludersi come expired. Il code applicativo di successo nella risposta resta 200. Un'attività accettata non può essere annullata.

Recupera il risultato

Se la richiesta in produzione include callBackUrl, ti inviamo una callback quando l'attività raggiunge uno stato terminale: una callback, o webhook, è un indirizzo HTTPS sul tuo server che chiamiamo al termine dell'attività, così non devi continuare a chiedere lo stato. La richiesta minima sopra lo omette per poter funzionare senza un endpoint pubblico. La firma delle callback deve essere verificata: consulta Webhook.

Senza un endpoint di callback, interroga l'attività:

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

Usa la stessa chiave API che ha creato l'attività: un'altra chiave riceve 404. Guarda data.state:

data.stateSignificatoAzione
queued o runningLa generazione è in corso. I fondi sono riservati, non spesiAspetta qualche secondo ed esegui di nuovo lo stesso comando. Le immagini normalmente richiedono secondi; i video possono richiedere minuti. deadlineAt è il limite superiore
succeededCompletataPassa al punto successivo
failed o expiredLa generazione non è riuscita. Normalmente la riserva viene liberata; alcuni rifiuti di contenuto dichiarati espressamente nella pagina del modello vengono addebitati. Controlla settled e costLeggi data.errorCode, stabile, e data.errorMessage, leggibile da una persona, correggi l'input e ricomincia da 3a

Il code della risposta è 200 anche se l'attività è fallita: code indica se la lettura dell'attività è riuscita. L'esito dell'attività si trova sempre in data.state. Ecco le risposte delle varie fasi, abbreviate:

{
  "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"
}

Finché settled è false, cost è l'importo riservato, non l'addebito finale.

Scarica il file generato

Prendi data.output.assets[0].url dal risultato riuscito e scaricalo direttamente. Non allegare la chiave API. Il link funziona per circa 20 minuti, indicati da expiresAt; se è scaduto, esegui di nuovo il passaggio 4, perché ogni lettura restituisce un nuovo link. I file generati vengono eliminati entro un massimo di 14 giorni, quindi conserva una tua copia di tutto ciò che ti serve.

# 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

Se hai installato jq, puoi evitare di copiare e incollare:

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')"

Che cosa dovresti vedere: un file result.png nella cartella corrente del terminale. Controlla l'addebito in Console → Log, oppure in cost quando settled è true.

Modelli di testo: una richiesta, risposta immediata

I modelli di testo rispondono nella stessa risposta HTTP: niente polling né download. L'header di risposta X-Spicy-Task-Id identifica l'attività che contiene l'addebito, se vuoi controllarlo in seguito. Nel passaggio 2, elenca invece i modelli con modality=text, esportane uno come SPICY_MODEL, poi esegui:

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.\"}]}"

La risposta si trova in choices[0].message.content. Questa chiamata è addebitata in base ai token utilizzati. L'endpoint usa il formato di risposta OpenAI, non la struttura {code, msg, data}. Consulta Testo e streaming.

Selezionare il modello dal codice

I cinque passaggi sopra prevedono l'intervento di una persona. Un'integrazione distribuita sceglie da sola il modello subito prima dell'invio: un modello eseguibile la scorsa settimana potrebbe essere sospeso oggi e i prezzi dipendono dall'account. Richiedi lo schema e un esempio convalidato dal server nella stessa chiamata, poi verifica enabled e available:

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"

Se la selezione non restituisce nulla, fermati invece di usare un ID ricordato come ripiego. I tre programmi seguenti eseguono la stessa selezione senza jq.

Il flusso completo

I tre programmi seguenti leggono il catalogo, creano un'attività, interrogano lo stato con attese crescenti e stampano un URL di download. In produzione usa invece i webhook: il polling è un'alternativa di ripiego, non il percorso principale.

Eseguire questi programmi crea una vera attività a pagamento

Ogni programma sceglie il primo modello text-to-image eseguibile nel tuo catalogo, non il più economico, e invia uno dei suoi esempi convalidati. Controlla prima il prezzo del modello oppure modifica la selezione per usare esattamente quello che vuoi.

LinguaggioSalva comePrima installaEsegui
Node.js 20 o successivogenerate.mjsNullanode generate.mjs
Python 3.9 o successivogenerate.pypip install requestspython3 generate.py (Windows: py generate.py)
Go 1.21 o successivomain.go in una cartella vuotago mod init examplego run .

Tutti e tre leggono la chiave da SPICY_API_KEY, impostata nel passaggio 1, e stampano l'URL del risultato quando l'attività termina. Attendono fino a dieci minuti, una durata adatta ai modelli di immagini; per i video attendi invece fino a deadlineAt dell'attività.

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)
}

Struttura della risposta

Le risposte JSON dell'API nativa delle attività usano questa struttura; la compatibilità /v1 e SSE usano le forme previste dai rispettivi protocolli:

{ "code": 200, "msg": "success", "data": { }, "request_id": "req_…" }
  • code è un codice applicativo, non uno stato HTTP. 200 significa successo. Anche lo stato HTTP è impostato correttamente, quindi puoi prendere decisioni in base all'uno o all'altro, ma scegline uno. I due differiscono nei codici specifici: un saldo insufficiente è code: 40201 con HTTP 402. Mescolarli significa prima o poi non gestire un caso.
  • In caso di errore, data è assente, non null.
  • request_id è presente sia in caso di successo sia di errore, ed è ripetuto nell'header di risposta X-Request-Id. Riportalo quando segnali un problema.

Passi successivi

In questa pagina