Vai al contenuto principale

Generazione batch di immagini e video senza far collassare la coda

Design di produzione per concorrenza, backpressure, idempotenza, scheduling equo, retry, controllo dei costi e osservabilità nei batch di media.

Autore
SpicyAPI Engineering
Pubblicato
Lettura
23 min di lettura
Aggiornato
Un operatore davanti ai monitor sorveglia nastri trasportatori separati per immagini e video in una sala server illustrata

Illustrazione: code separate per immagini e video lasciano avanzare ogni carico al proprio ritmo.

Un generatore batch di solito cede nel momento meno interessante: non mentre genera una singola immagine, ma quando un cliente importa 20.000 righe e l'applicazione le trasforma tutte insieme in richieste attive.

L'API può essere in salute e ogni payload valido. Il sistema collassa comunque: le connessioni al database si esauriscono, i callback arrivano più in fretta di quanto riesci a fare il commit, un grande tenant blocca tutti gli altri, un retry duplica un video costoso e un picco di traffico diventa un conto.

Tieni il lavoro in attesa in una coda durevole, limita i task in corso e riprova solo quando sai se esiste già un task remoto. Un Promise.all più grande non fa nulla di tutto questo.

Un fatto determina tutto quello che segue: SpicyAPI non ha un endpoint bulk o batch. Ogni riga è una chiamata createTask con il proprio task, il proprio hold e il proprio regolamento, e un task accettato non può essere annullato. La coda, il tetto di concorrenza e il pulsante di stop vivono quindi tutti nella tua applicazione.

Prova prima nel Playground

Apri il Playground di z-image. Scrivi un Prompt rappresentativo, scegli l'Aspect ratio, imposta Prompt expansion come intendi eseguire il batch, premi Run model e controlla l'Output. Scala solo quando quel singolo risultato è quello che vuoi; uno scheduler moltiplica la richiesta, errori compresi.

La risposta breve

  • Tieni l'arretrato nella tua coda durevole, non in una richiesta HTTP né in una promise in memoria.
  • Limita tu i task in corso. Il rate limit della piattaforma (1.000 richieste ogni 10 secondi, separatamente per letture e scritture) non limita quanti task accettati girano insieme; lo fanno il tuo saldo e i tuoi tetti di spesa. Scegli un numero, per classe di media, e mantienilo finché il task non è terminale.
  • Ammetti in base all'età della coda, non alla sua lunghezza, e decidi il costo dell'intero batch prima del primo dispatch.
  • Un'idempotency key salvata per ogni output logico, sempre inviata con la stessa API key e mai considerata affidabile oltre le 24 ore.
  • Riprova in base al codice di errore: errori di capacità e di trasporto sì; input non valido, saldo esaurito e tetti di spesa no.
  • Round-robin fra i tenant, e un kill switch del dispatch che lascia attiva la gestione dei completamenti.

Tre limiti e chi li imposta

Tratta invio, dispatch e completamento come fasi separate:

batch del cliente

│

▼

ammissione + controllo costi ──► coda durevole ──► worker limitati ──► createTask

│

completamento via polling / webhook

│

copia dell'output ──► archivio risultati + evento al cliente

LimiteCosa limitaChi lo imposta su SpicyAPI
AmmissioneQuanto arretrato tieni e prometti di completareTu
Frequenza di invioChiamate createTask al secondoPiattaforma: 1.000 scritture ogni 10 secondi per account, un token bucket di circa 100 richieste al secondo, con X-RateLimit-Remaining in ogni risposta e Retry-After su un 429
Task in corsoTask accettati e non finitiSoprattutto tu. La piattaforma li limita solo tramite i fondi che ogni task trattiene, i tetti di spesa di chiave, team e piattaforma (40202) e la capacità del modello (50301)

Il rate limit regola l'invio, non la concorrenza: i task accettati non attingono più al bucket delle richieste mentre generano.

A 5 completamenti al secondo crei circa 5 task al secondo, una piccola frazione del bucket da 100 al secondo. Per la maggior parte dei batch il limite di invio è un fusibile di sicurezza, non il tuo acceleratore; prima ti fermano il denaro trattenuto dai task in corso e i tuoi tetti. Il polling usa il bucket di lettura separato, quindi un ciclo di polling fuori controllo non può bloccare createTask, ma può iniziare a restituire 429 su recordInfo e lasciarti alla cieca. Leggi l'header invece di scrivere nel codice i valori predefiniti.

Altre API possono imporre limiti distinti di concorrenza e di coda in attesa; la guida alla concorrenza di Leonardo li distingue. SpicyAPI non documenta un tetto numerico ai task in corso, quindi configura il tuo invece di affidarti al suo rate limit sulle richieste.

Dimensiona il tetto in corso con tempo e denaro

Per una singola classe di carico stabile, la legge di Little mette in relazione il numero medio di task nel sistema con la frequenza di completamento e il tempo medio che ogni task vi trascorre:

task in corso medi ≈ completamenti al secondo × secondi medi dall'accettazione allo stato terminale

Se i task di immagine durano in media 8 secondi e te ne servono 5 al secondo:

5 job/s × 8 s = 40 task di immagine concorrenti

Considera 40 un'occupazione media, non una garanzia. Un tetto iniziale di 50 aggiunge il 25 % di margine; alzalo solo finché età della coda, errori e spesa restano sani. Usa p95 e p99 per le code lunghe e per gli allarmi sull'età della coda. Misura la durata di un task da createdAt e completedAt in recordInfo, per modello e impostazioni.

Il video merita un pool a sé. Se le clip di Seedance 2.0 durano in media 70 secondi nel tuo carico e ne vuoi 0,2 al secondo:

0,2 job/s × 70 s = 14 task video concorrenti

Le medie di 8 e 70 secondi sono ipotesi di dimensionamento, non tempi di generazione promessi.

Con un solo pool condiviso da 50, un'ondata di video lunghi può occupare ogni slot mentre le immagini rapide aspettano. Con 50 slot per le immagini e 14 per i video, entrambe le classi avanzano. Anche Z-Image e Seedream 5.0 Pro possono avere pool separati se durate o priorità differiscono.

Ora confronta il tetto con il denaro. Ogni task accettato trattiene il proprio estimatedCost finché non viene regolato, e quel hold conta per ogni tetto di chiave e di team. Con i prezzi pubblici del catalogo:

In corsoTrattenuto insieme
50 immagini Seedream 5.0 Pro in 1K ($0.036 ciascuna)$1.80
14 clip da testo a video Seedance 2.0, 5 s in 720p ($0.152 al secondo, $0.76 ciascuna)$10.64

Un saldo che non copre quegli hold fa fallire i nuovi task con 40201, per quanto sia bravo il tuo scheduler. Conta di più la spesa totale: 20.000 immagini Seedream 5.0 Pro in 1K fanno $720 e 20.000 immagini Z-Image a $0.01 con Prompt expansion disattivato fanno $200. I prezzi cambiano, quindi leggili dalla pagina del modello aggiornata o da un preventivo. Il tetto che ferma un batch sbagliato è quello che imposti prima di partire: fissa un tetto di spesa giornaliero sulla chiave di produzione prima del dispatch, dimensionato sul massimo che puoi permetterti di perdere in una brutta giornata. Un tetto vuoto significa nessun limite; anche i limiti del team possono fermare nuovi task con 40202.

La stima del tempo di smaltimento è l'altra metà:

tempo di smaltimento ≈ job in coda ÷ completamenti sostenibili al secondo

18.000 immagini in coda a 5 al secondo sono un'ora di lavoro. Se prometti un avvio entro 15 minuti, l'ammissione deve rallentare molto prima di 18.000. Un arretrato che cresce non è uno stato stazionario, quindi fidati dell'età della coda osservata più che delle equazioni.

Il backpressure comincia prima del dispatch

Backpressure significa che il sistema a valle può far rallentare il produttore. Senza, la coda assorbe lavoro all'infinito e il cliente scopre il sovraccarico ore dopo.

Usa due soglie di ammissione sull'età della coda, l'età dell'elemento pronto più vecchio. La lunghezza inganna quando gli elementi hanno costi diversi; l'età dice direttamente se l'attesa promessa viene rispettata.

  • Soglia morbida: accetta il batch, restituisci una stima ritardata, rallenta il produttore.
  • Soglia dura: rimanda o rifiuta il nuovo lavoro con una risposta di prodotto riprovabile prima che esista qualsiasi task remoto.

Confronta il denaro in unità decimali esatte invece che in virgola mobile binaria. Fai il commit della prenotazione in modo atomico insieme all'ammissione del batch nel tuo adattatore di coda, così invii concorrenti non consumano lo stesso budget residuo.

Non trasformare ogni sovraccarico interno in un 429 dell'API per i tuoi clienti. Se il lavoro è salvato in modo durevole, restituisci un ID di batch accettato in stato ritardato. Rifiuta solo il lavoro che non puoi conservare responsabilmente e, quando lo fai, indica quando riprovare e rendi idempotente l'invio stesso del batch.

Due dettagli di questa API decidono per quanto un elemento può attendere in sicurezza nella tua coda:

  • I file di riferimento scadono in un giorno. Un URI spicy:// ottenuto da un upload è valido fino all'expiresAt della sua risposta di commit (un giorno per impostazione predefinita), e un retry riusa l'input originale, quindi un retry successivo restituisce 400. Tieni in coda un URL HTTPS pubblico o un tuo ID sorgente e fai l'upload al momento del dispatch.
  • I preventivi durano cinque minuti. Se vuoi bloccare il prezzo, chiedi il preventivo al dispatch e invia quoteId con expectedCost a createTask; un prezzo cambiato fallisce allora con 40901 invece di addebitare un importo nuovo. I preventivi sono gratuiti ma contano contro il bucket di lettura, quindi chiederne 20.000 all'ammissione è il posto sbagliato.

L'idempotenza ha due livelli

Vanno evitati due duplicati diversi: il cliente che invia lo stesso batch due volte e il tuo dispatcher che non sa se una creazione è riuscita. Dai al batch un'identità stabile, poi genera e salva un UUID per ogni nuovo elemento logico. Il recupero riusa la chiave salvata di quell'elemento.

Mantieni un vincolo di unicità su quella chiave nel database. Scrivi l'elemento come «intento» prima di chiamare l'API, invia la chiave come Idempotency-Key e salva il taskId restituito sulla stessa riga. Dopo un crash, il worker successivo riusa la chiave salvata se l'accettazione è ancora sconosciuta. Scrivere l'intento solo dopo l'invio lascia una finestra in cui un riavvio può comprare lo stesso output due volte.

Ciò che la documentazione sull'idempotenza aggiunge, e che la coda deve rispettare:

  • Una chiave è valida 24 ore dalla prima richiesta accettata; i replay non la prolungano. Dopo, la stessa chiave crea un nuovo task fatturato. Prima di rieseguire un vecchio elemento in coda o in dead-letter, cerca il suo taskId salvato invece di fidarti della chiave.
  • La chiave appartiene allo stesso account e alla stessa API key, e al modello, input e callBackUrl esatti. Cambiare qualcosa restituisce 409. Anche una chiave sorella del tuo account riceve 409 e non può leggere il task: solo la chiave che ha creato un task può leggerlo o riprovarlo, quindi usa una sola chiave per creare, fare polling e riprovare in una flotta di worker.
  • Un replay restituisce il task originale nel suo stato attuale, senza alcun segno che fosse un replay.
  • Una rara race con la stessa chiave restituisce 409 «Idempotency key conflict, please retry shortly». Riprova una volta dopo qualche centinaio di millisecondi; gli altri due messaggi 409 non riusciranno mai.

Un dispatcher che mantiene il tetto finché il task non termina

Salva un oggetto per riga in batch.jsonl, per esempio {"model": "alibaba/z-image-turbo/text-to-image", "input": {"prompt": "<one row's prompt>"}}, usando gli input accettati dal modello scelto. Imposta SPICY_API_KEY, BATCH_BUDGET_USD e, facoltativamente, BATCH_WORKERS (2 per impostazione predefinita). Esegui questo esempio Python e approva il tetto del batch. Tiene occupato ogni worker finché il suo task non è terminale, regolato e non sta più preparando l'output.

Conserva la cartella batch-run per il recupero e non riordinare le sue righe sorgente. La stessa cartella mantiene la spesa regolata e le prenotazioni tra un riavvio e l'altro. Esegui un solo coordinatore per cartella.

python
import json
import os
import time
import uuid
import urllib.error
import urllib.parse
import urllib.request
from concurrent.futures import ThreadPoolExecutor
from decimal import Decimal
from pathlib import Path
from threading import Event, Lock

BASE = "https://api.spicyapi.ai/api/v1"
API_KEY = os.environ["SPICY_API_KEY"]
CAP = Decimal(os.environ["BATCH_BUDGET_USD"])
WORKERS = int(os.environ.get("BATCH_WORKERS", "2"))
ROOT = Path(os.environ.get("BATCH_DIR", "batch-run"))
ROOT.mkdir(exist_ok=True, mode=0o700)
lock, stop = Lock(), Event()
reserved = Decimal("0")


class APIError(Exception):
    def __init__(self, status, body):
        self.status, self.code = status, body.get("code")
        super().__init__(f"{status}/{self.code}: {body.get('msg')}")


def call(path, payload=None, key=None):
    headers = {"Authorization": "Bearer " + API_KEY, "Content-Type": "application/json"}
    if key:
        headers["Idempotency-Key"] = key
    for attempt in range(5):
        request = urllib.request.Request(BASE + path,
            data=None if payload is None else json.dumps(payload).encode(), headers=headers)
        try:
            with urllib.request.urlopen(request, timeout=30) as response:
                body = json.load(response)
                if body.get("code") != 200:
                    raise APIError(response.status, body)
                return body["data"]
        except urllib.error.HTTPError as error:
            try:
                body = json.load(error)
            except ValueError:
                body = {}
            if error.code != 429 and error.code < 500:
                raise APIError(error.code, body)
            delay = float(error.headers.get("Retry-After") or min(15, 2 ** attempt))
        except (urllib.error.URLError, TimeoutError, ValueError):
            delay = min(15, 2 ** attempt)
        time.sleep(delay)
    raise RuntimeError("Unresolved API call; preserve this row for recovery")


def save(path, row):
    temporary = path.with_suffix(".tmp")
    temporary.write_text(json.dumps(row))
    temporary.chmod(0o600)
    temporary.replace(path)


def worker(pair):
    global reserved
    index, line = pair
    if stop.is_set():
        return
    try:
        request = json.loads(line)
        if not isinstance(request.get("model"), str) or not isinstance(request.get("input"), dict):
            raise ValueError("model and input are required")
    except (ValueError, TypeError, AttributeError) as error:
        print(index, "invalid local row:", error)
        return
    path = ROOT / f"{index}.json"
    row = json.loads(path.read_text()) if path.exists() else {
        "request": request, "key": str(uuid.uuid4())}
    if row["request"] != request:
        stop.set()
        raise ValueError("Batch changed; preserve the old folder and use a new one")
    if row.get("settled") or row.get("rejected"):
        return
    accounted = "sentAt" in row
    try:
        if not row.get("taskId"):
            if time.time() - row.get("sentAt", time.time()) >= 23 * 3600:
                raise RuntimeError("Reconcile old acceptance before submitting again")
            if "submission" not in row:
                quote = call("/jobs/quote", request)
                row["hold"] = quote["maxCharge"]
                row["submission"] = {**request, "quoteId": quote["quoteId"],
                                     "expectedCost": quote["estimatedCost"]}
            with lock:
                if stop.is_set():
                    return
                hold = Decimal(row["hold"])
                if not accounted and reserved + hold > CAP:
                    stop.set()
                    print(index, "batch budget reached")
                    return
                if not accounted:
                    reserved += hold
                    accounted = True
            row.setdefault("sentAt", time.time())
            save(path, row)
            accepted = call("/jobs/createTask", row["submission"], row["key"])
            row["taskId"] = accepted["taskId"]
            save(path, row)
        task_id = row["taskId"]
        for _ in range(3000):
            task = call("/jobs/recordInfo?taskId=" + urllib.parse.quote(task_id))
            if task["state"] not in {"queued", "running", "succeeded", "failed", "expired", "canceled"}:
                raise RuntimeError("Unknown state: keep the reservation")
            if task["state"] in {"succeeded", "failed", "expired", "canceled"} and task.get("settled") is True:
                assets = task.get("output", {}).get("assets", [])
                if task["state"] == "succeeded" and any(a.get("pending") for a in assets):
                    time.sleep(5)
                    continue
                row["result"], row["settled"] = task, True
                save(path, row)
                with lock:
                    reserved += Decimal(task["cost"]) - Decimal(row["hold"])
                print(index, task_id, task["state"], task["cost"])
                return
            time.sleep(5)
        raise RuntimeError("Wait interrupted; resume the saved task, not a new generation")
    except APIError as error:
        if error.status == 400 and error.code in {400, 40003, 40004} and not row.get("taskId"):
            row["rejected"] = str(error)
            save(path, row)
            with lock:
                if accounted:
                    reserved -= Decimal(row.get("hold", "0"))
            print(index, "rejected input:", error)
            return
        stop.set()
        print(index, "paused:", error)
    except Exception as error:
        stop.set()
        print(index, "paused:", error)


if CAP <= 0 or not 1 <= WORKERS <= 20:
    raise ValueError("Use a positive budget and 1–20 workers")
# Count settled spend from this batch again on restart.
for path in ROOT.glob("*.json"):
    row = json.loads(path.read_text())
    if row.get("settled"):
        reserved += Decimal(row["result"]["cost"])
    elif row.get("sentAt") and not row.get("rejected"):
        reserved += Decimal(row["hold"])
if input(f"Authorize this batch up to ${CAP}? Type yes: ") != "yes":
    raise SystemExit("No tasks submitted")
rows = Path("batch.jsonl").read_text().splitlines()
with ThreadPoolExecutor(max_workers=WORKERS) as pool:
    list(pool.map(worker, enumerate(rows)))

La documentazione sull'integrazione in produzione copre il deployment. Questo esempio isola le righe non valide, mette in pausa la nuova ammissione per saldo o per errori incerti e limita i retry transitori. I task già accettati continuano a essere raccolti dopo la pausa dell'ammissione. Di proposito non fa alcun retry a pagamento automatico; una riga fallita resta registrata per un nuovo tentativo esplicito dopo il regolamento. Esegui un processo di questo tipo per classe di media e metti il round-robin per tenant davanti al file.

Scheduling equo: proteggi i tenant piccoli

Il primo arrivato, primo servito è equo verso i job, non verso i clienti. Se il tenant A accoda 100.000 elementi al minuto prima che il tenant B ne accodi dieci, B aspetta dietro tutti quelli di A. Tieni una coda per tenant e prendi un elemento per tenant a ogni giro:

giro 1: A1, B1, C1

giro 2: A2, B2, C2

I pesi possono riflettere la capacità acquistata, ma dai a ogni tenant attivo una quota minima. Limita anche i task in corso di ciascun tenant: il tetto globale protegge il tuo account, quello del tenant contiene i vicini rumorosi. All'interno di un tenant, le corsie di priorità (anteprime interattive, batch normali, backfill) valgono la pena solo quando la latenza delle anteprime è un reclamo reale. Se le aggiungi, fai invecchiare la priorità con il tempo in coda, altrimenti un flusso costante di anteprime fa morire di fame un backfill per sempre. Tutto ciò che sta su un account condivide gli stessi due bucket di richieste e lo stesso saldo, quindi l'equità fra tenant è compito tuo, non dell'API.

Classifica i fallimenti prima di riprovare

«Riprova tre volte» non è una policy. L'azione giusta dipende da ciò che sai e dal fatto che un task possa già esistere. Questa tabella usa i codici documentati; i valori di code sono nel corpo della risposta, e i fallimenti dei task arrivano come state: "failed" con un errorCode in una chiamata recordInfo riuscita (code: 200).

DoveSegnaleRetry?Cosa fare
CreateTimeout, connessione interrotta, corpo illeggibileSìEsito sconosciuto. Stessa chiave, stessa API key, backoff con jitter
Create429 con Retry-AfterSìAttendi i secondi indicati, aggiungi jitter, riduci la frequenza di dispatch. Non leggerlo come prova che andavi troppo veloce
Upload429 senza Retry-After (upload in sospeso)Nessun retry immediatoCompleta gli upload in sospeso o attendi la scadenza dei loro ticket prima di chiederne altri
Create500, 503, 50301 (modello non disponibile)Sì, con limitiBackoff con scadenza; 50301 può significare anche nessun prezzo attivo, quindi aggiorna il catalogo
Create409 race con la stessa chiaveUna voltaQualche centinaio di ms, stessa chiave. Gli altri 409 indicano payload modificato o altra chiave: fermati
Create400, 40003, 40004NoCampo non valido, combinazione non supportata o tipo/dimensione del file non corrispondente; correggi l'elemento, mostra il messaggio
Create40201 saldo, 40202 tetto di spesaNoMetti in pausa il dispatch e avvisa. Riprovare non cambierà un saldo né un tetto
Create401, 403, 40301NoPermesso di chiave o di modello: metti in pausa l'integrazione, avvisa un operatore
Taskrate_limited, upstream_unavailable, upstream_failedSìSolo dopo settled: true: POST /api/v1/jobs/retry con una chiave legata al task di origine; crea un nuovo task fatturato. Un retry anticipato restituisce 409
Tasktimeout, generation_failedForseSpesso fallisce allo stesso modo. Prova una durata più breve o una risoluzione più bassa, oppure rinuncia
Taskinvalid_request, unsupported_combination, content_rejectedNoLo stesso input fallisce di nuovo. Cambia l'input o scegli un altro modello
Taskinvalid_assetNuova revisioneIl file di riferimento non c'è più: ricaricalo, aggiorna l'input e richiedi un preventivo per un nuovo task
LocaleCallback ricevuto, la tua transazione è fallitaSìRiprova il tuo handler, non la creazione; deduplica per ID di consegna o taskId

Usa un backoff casuale perché i worker non riprovino all'unisono.

Sposta i fallimenti confermati e gli input rifiutati in una dead-letter queue con la loro classificazione e l'azione di replay sicura. Tieni una creazione ambigua o un task accettato non risolto nella coda di riconciliazione con la sua chiave salvata e il task ID; una scadenza di retry locale non prova che sia fallito. Un task fallito di norma rilascia il proprio hold per intero, ma alcuni esiti content_rejected addebitabili vengono regolati al preventivo, quindi leggi settled e cost invece di presumere un rimborso.

La protezione dei costi appartiene allo scheduler

I limiti di concorrenza proteggono le macchine, non il denaro. Un worker perfettamente stabile può comunque fare il dispatch di un batch insostenibile. Controlla il costo stimato rispetto a ogni involucro prima del dispatch:

consentito = residuo giornaliero del tenant

∩ residuo mensile del tenant

∩ residuo del batch di progetto

∩ residuo orario globale

Riconcilia con il regolamento reale: createTask restituisce l'estimatedCost trattenuto, un task riuscito viene regolato a quell'importo o meno, e settled: true con cost è l'addebito finale. Rilascia la tua prenotazione per gli elementi che l'API ha rifiutato e mantieni la prenotazione per quelli accettati finché settled non cambia. Non tirare a indovinare da un timeout HTTP.

Salvaguardie utili:

  • un costo stimato massimo per batch, mostrato a una persona prima che ne parta uno grande;
  • un budget di dispatch orario per tenant e un allarme sulla velocità di spesa globale;
  • un circuit breaker per modello o tenant;
  • tetti delle API key sotto il saldo dell'account e un avviso di saldo basso;
  • un kill switch che ferma il nuovo dispatch lasciando attiva l'elaborazione dei completamenti.

Poiché i task accettati non possono essere annullati, fermare la creazione è l'unico freno: il tetto di task in corso, non un pulsante di annullamento, limita i danni in un incidente di costi. Continua a ricevere i webhook, a fare polling dei vecchi task, a regolare le prenotazioni e a copiare gli output.

Il completamento su scala batch

La consegna callBackUrl di ogni task scade dopo 15 secondi; anche un endpoint di account registrato può ricevere la propria consegna, quindi il ricevente dovrebbe verificare la firma sul corpo grezzo, scrivere l'evento in una tabella inbox, restituire 2xx e lasciare il resto a un worker. Deduplica sul request_id di consegna v2 e decidi in base a data.state, non all'ordine di arrivo. Tieni un polling lento come ripiego per gli eventi persi (vedi la documentazione dei webhook).

Copia gli output nel tuo storage man mano che i task finiscono. I link dei risultati durano circa 20 minuti e i media generati vengono conservati al massimo circa 14 giorni (retention); a 5 completamenti al secondo il passo di copia fa parte del tuo throughput, e un uploader lento diventa il collo di bottiglia che si nasconde nei task «completati». Accorciare la retention può anche eliminare i risultati di task che hai già eseguito.

Osserva il flusso, non solo gli errori

Una dashboard utile risponde a quattro domande: stiamo accettando lavoro, sta partendo, sta finendo, il costo viene regolato come previsto? Traccia per modello, classe di media, livello di tenant ed esito:

SegnalePerché conta
Età dell'elemento pronto più vecchioMisura diretta dell'attesa del cliente e del fallimento del backpressure
Conteggi di elementi in coda, in corso e terminaliMostra dove si accumula il lavoro
Task accettati al secondo vs. create tentateSepara il throughput reale dai retry
Durata dei task p50/p95/p99 (completedAt - createdAt)Guida il dimensionamento della concorrenza e il rilevamento dei task bloccati
Esiti terminali e mix di errorCodeDegrado specifico di un modello o di un input
X-RateLimit-RemainingAvvisa prima di un 429
Saldo trattenuto vs. disponibile (GET /api/v1/chat/credit)Prevede 40201 prima che fermi il batch
Costo prenotato vs. regolato, spesa all'ora per tenantPrenotazioni perse, batch fuori controllo
Ritardo dei webhook e recuperi tramite pollingSalute del percorso di completamento

Porta una sola catena di correlazione (batchId, ID elemento, idempotency key, taskId, request_id) attraverso log ed eventi, e tieni prompt, API key e URL di asset privati fuori dalle etichette delle metriche e dai log di routine. Allerta sui sintomi: «50.000 elementi in coda» può essere normale; «l'elemento interattivo più vecchio ha 12 minuti» richiede un'azione.

Parti con un batch limitato, poi aumenta la capacità

Aumenta la capacità solo finché età dell'elemento più vecchio, fallimenti dei task e spesa restano entro le tue soglie; più worker non possono riparare un percorso di completamento sovraccarico.

Prima del primo batch a pagamento, usa un mock per interrompere un worker subito prima e subito dopo l'invio e per restituire 429, task falliti ed errori di tetto di spesa. Il recupero dovrebbe riusare l'intento salvato, lasciare irrisolti i task sconosciuti e fermare il nuovo dispatch senza fermare il completamento. Poi esegui un piccolo batch approvato prima di aprire l'ammissione a importazioni grandi.

Per la gestione dei completamenti, vedi «Polling vs webhooks for async generation jobs»; per il budget di un batch, vedi Per-unit pricing vs subscriptions. La checklist di produzione copre la configurazione delle chiavi e i test di lancio.

FAQ

SpicyAPI ha un endpoint batch o un limite di concorrenza?

Nessun endpoint batch: ogni riga è un createTask, e non esiste un tetto documentato ai task in corso (i limiti sono nella tabella sopra; la frequenza attuale è in X-RateLimit-Limit). Se il rate limit è troppo basso per il tuo picco, chiedi un limite più alto al supporto della console, descrivendo il picco di richieste al secondo e la dimensione del batch.

Posso annullare un batch avviato per errore?

No. Usa il kill switch del dispatch per fermare i nuovi task mentre la gestione dei completamenti continua.

Un 429 deve consumare un tentativo di retry?

Attendi Retry-After più jitter e mantieni la stessa idempotency key. Un throttling ripetuto richiede una frequenza di dispatch più bassa. Limita i retry sia per tentativi sia per tempo; se l'accettazione è ancora incerta quando quel budget finisce, riconcilia l'intento salvato prima di creare un sostituto.

Come evito addebiti doppi dopo un timeout?

Reinvia con la stessa Idempotency-Key e la stessa API key entro 24 ore, e salva il taskId appena ce l'hai. Dopo 24 ore, cerca invece il taskId salvato.

Una coda basta a prevenire il sovraccarico?

No. Una coda conserva il sovraccarico; il backpressure lo controlla. Servono comunque soglie di ammissione sull'età della coda, un tetto ai task in corso e una risposta di prodotto per quando l'attesa promessa non può essere rispettata.

Qual è una buona concorrenza iniziale?

Completamenti al secondo per il tempo medio di task, per classe di media, più circa il 25 % di margine: 40 per 5 immagini al secondo da 8 secondi ciascuna. Alzala solo finché età della coda, errori e spesa restano sani.

I job ad alta priorità possono aggirare i limiti?

No. La priorità sceglie fra il lavoro ammissibile e non aggira mai il tetto globale né i budget. Dai al lavoro interattivo una capacità riservata e fai invecchiare il lavoro a priorità più bassa perché non muoia di fame.

Lascia che un Agent gestisca la chiamata

Usa un Agent quando vuoi che prepari gli input e restituisca il risultato mentre tu approvi il prezzo e le scelte creative.

  • CLI: con l'oggetto input di una riga salvato come input.json (per Z-Image: prompt, aspect_ratio, enable_prompt_expansion, output_format, seed), usa npx @spicyapi/cli tasks create --model alibaba/z-image-turbo/text-to-image --input-file input.json --wait. La CLI conferma il prezzo; conserva il task ID restituito per il recupero.
  • MCP: chiedi al tuo assistente di usare spicyapi_task_create per alibaba/z-image-turbo/text-to-image, poi spicyapi_task_wait. Il server MCP richiede la conferma del prezzo prima della creazione.
  • Skill: installala con npx skills add spicyapi-ai/spicy-skill, poi chiedi a $spicyapi di eseguire lo stesso workflow e di fermarsi per la tua approvazione. La Skill usa una connessione CLI o MCP configurata.

Anche in English · 日本語 · 한국어 · Deutsch · Français · Español · Português (Brasil) · Русский · Polski

Soluzioni

API di generazione video: confronta e integraConfronta gli endpoint video per durata, risoluzione, audio e prezzo, poi invia la stessa richiesta dal tuo server e seguila con l’ID dell’attività.

Provalo tu stesso

Paghi solo quello che esegui: nessun piano, nessun abbonamento.

Creato con MiniMax H3 · ByteDance Seedance 2.0 Fast