Pular para o conteúdo principal

Geração em lote de imagens e vídeos sem colapso da fila

Desenho de produção para concorrência, backpressure, idempotência, agendamento justo, retentativas, controle de custos e observabilidade em lote.

Autor
SpicyAPI Engineering
Publicado
Leitura
23 min de leitura
Atualizado
Um operador diante de monitores observa esteiras separadas de imagens e vídeos em uma sala de servidores ilustrada

Ilustração: filas separadas de imagens e vídeos deixam cada carga avançar no próprio ritmo.

Um gerador em lote costuma falhar no momento menos interessante: não ao gerar uma imagem, mas quando um cliente importa 20.000 linhas e a aplicação transforma todas elas em requisições ativas de uma só vez.

A API pode estar saudável e cada payload ser válido. Mesmo assim o sistema colapsa: as conexões com o banco de dados se esgotam, os callbacks chegam mais rápido do que você consegue fazer o commit, um tenant grande bloqueia todos os outros, uma retentativa duplica um vídeo caro e um pico de tráfego vira uma fatura.

Mantenha o trabalho em espera em uma fila durável, limite as tarefas em andamento e só tente de novo quando souber se já existe uma tarefa remota. Um Promise.all maior não faz nada disso.

Um fato molda tudo o que vem a seguir: a SpicyAPI não tem endpoint em massa nem em lote. Cada linha é uma chamada createTask com a própria tarefa, o próprio hold e a própria liquidação, e uma tarefa aceita não pode ser cancelada. A fila, o limite de concorrência e o botão de parar ficam, portanto, na sua aplicação.

Experimente primeiro no Playground

Abra o Playground do z-image. Escreva um Prompt representativo, escolha a Aspect ratio, defina Prompt expansion como você pretende rodar o lote, clique em Run model e confira o Output. Só aumente a escala quando esse único resultado for o que você quer; um agendador multiplica a requisição, inclusive os erros dela.

A resposta curta

  • Mantenha o backlog na sua própria fila durável, não em uma requisição HTTP nem em uma promise em memória.
  • Limite você mesmo as tarefas em andamento. O limite de taxa da plataforma (1.000 requisições a cada 10 segundos, separadamente para leituras e escritas) não limita quantas tarefas aceitas rodam ao mesmo tempo; quem limita são o seu saldo e os seus tetos de gasto. Escolha um número, por classe de mídia, e mantenha-o até a tarefa ser terminal.
  • Admita pela idade da fila, não pelo tamanho, e decida o custo do lote inteiro antes do primeiro dispatch.
  • Uma chave de idempotência salva por saída lógica, sempre enviada com a mesma API key e nunca considerada confiável depois de 24 horas.
  • Tente de novo conforme o código de erro: erros de capacidade e de transporte, sim; entrada inválida, saldo vazio e tetos de gasto, não.
  • Round-robin entre tenants, e uma chave de emergência do dispatch que deixe o tratamento das conclusões funcionando.

Três limites e quem define cada um

Trate envio, dispatch e conclusão como etapas separadas:

lote do cliente

│

▼

admissão + checagem de custo ──► fila durável ──► workers limitados ──► createTask

│

conclusão por polling / webhook

│

copiar saída ──► armazenamento de resultados + evento ao cliente

LimiteO que ele restringeQuem define na SpicyAPI
AdmissãoQuanto backlog você mantém e promete concluirVocê
Taxa de envioChamadas createTask por segundoPlataforma: 1.000 escritas a cada 10 segundos por conta, um token bucket de cerca de 100 requisições por segundo, com X-RateLimit-Remaining em toda resposta e Retry-After em um 429
Tarefas em andamentoTarefas aceitas e não concluídasPrincipalmente você. A plataforma só as limita pelos fundos que cada tarefa retém, pelos tetos de gasto da chave, da equipe e da plataforma (40202) e pela capacidade do modelo (50301)

O limite de taxa governa o envio, não a concorrência: tarefas aceitas não continuam consumindo o bucket de requisições enquanto geram.

Com 5 conclusões por segundo, você cria cerca de 5 tarefas por segundo, uma pequena fração do bucket de 100 por segundo. Para a maioria dos lotes, o limite de envio é um fusível de segurança, não o seu acelerador; o dinheiro retido pelas tarefas em andamento e os seus próprios tetos param você primeiro. O polling usa o bucket de leitura separado, então um loop de polling descontrolado não consegue bloquear createTask, mas pode começar a retornar 429 em recordInfo e deixar você às cegas. Leia o cabeçalho em vez de fixar os valores padrão no código.

Outras APIs podem impor limites separados de concorrência e de fila pendente; o guia de concorrência da Leonardo os distingue. A SpicyAPI não tem um teto numérico documentado para tarefas em andamento, então configure o seu em vez de depender do limite de taxa de requisições.

Dimensione o teto em andamento com tempo e dinheiro

Para uma classe de carga estável, a Lei de Little relaciona o número médio de tarefas no sistema com a taxa de conclusão e o tempo médio que cada tarefa passa nele:

tarefas em andamento médias ≈ conclusões por segundo × segundos médios da aceitação ao estado terminal

Se as tarefas de imagem levam em média 8 segundos e você precisa de 5 por segundo:

5 jobs/s × 8 s = 40 tarefas de imagem simultâneas

Trate 40 como ocupação média, não como garantia. Um teto inicial de 50 acrescenta 25 % de folga; aumente-o apenas enquanto idade da fila, erros e gasto continuarem saudáveis. Use p95 e p99 para as caudas e para os alarmes de idade da fila. Meça o tempo da tarefa a partir de createdAt e completedAt em recordInfo, por modelo e configuração.

Vídeo merece o próprio pool. Se os clipes do Seedance 2.0 levam em média 70 segundos na sua carga e você quer 0,2 por segundo:

0,2 job/s × 70 s = 14 tarefas de vídeo simultâneas

As médias de 8 e 70 segundos são suposições de dimensionamento, não tempos de geração prometidos.

Compartilhando um único pool de 50, uma rajada de vídeos longos pode ocupar todos os slots enquanto imagens rápidas esperam. Com 50 slots de imagem e 14 de vídeo, as duas classes continuam andando. Z-Image e Seedream 5.0 Pro também podem ter pools separados se as durações ou prioridades forem diferentes.

Agora confira o teto contra o dinheiro. Cada tarefa aceita retém o seu estimatedCost até ser liquidada, e esse hold conta para o teto de cada chave e equipe. Com preços públicos do catálogo:

Em andamentoRetido ao mesmo tempo
50 imagens Seedream 5.0 Pro em 1K ($0,036 cada)$1,80
14 clipes de texto para vídeo Seedance 2.0, 5 s em 720p ($0,152 por segundo, $0,76 cada)$10,64

Um saldo que não cubra esses holds faz as novas tarefas falharem com 40201, por melhor que seja o seu agendador. O gasto total importa mais: 20.000 imagens Seedream 5.0 Pro em 1K somam $720 e 20.000 imagens Z-Image a $0,01 com Prompt expansion desativado, $200. Os preços mudam, então leia-os na página do modelo ao vivo ou em uma cotação. O teto que impede um lote ruim é o que você define antes de ele começar: defina um teto de gasto diário na chave de produção antes do dispatch, dimensionado pelo máximo que você pode perder em um dia ruim. Um teto em branco significa sem limite; os limites da equipe também podem barrar novas tarefas com 40202.

A estimativa do tempo de escoamento é a outra metade:

tempo de escoamento ≈ jobs na fila ÷ conclusões sustentáveis por segundo

18.000 imagens na fila a 5 por segundo são uma hora de trabalho. Se você promete começar em 15 minutos, a admissão precisa desacelerar bem antes de 18.000. Um backlog que cresce não é regime estável, então confie mais na idade da fila observada do que nas equações.

O backpressure começa antes do dispatch

Backpressure significa que o sistema a jusante consegue fazer o produtor desacelerar. Sem ele, a fila absorve trabalho para sempre e o cliente só descobre a sobrecarga horas depois.

Use dois limiares de admissão sobre a idade da fila, a idade do item pronto mais antigo. O tamanho engana quando os itens custam valores diferentes; a idade diz diretamente se a espera prometida está sendo cumprida.

  • Limiar brando: aceitar o lote, devolver uma estimativa atrasada, desacelerar o produtor.
  • Limiar rígido: adiar ou rejeitar o trabalho novo com uma resposta de produto repetível antes que exista qualquer tarefa remota.

Compare dinheiro em unidades decimais exatas, não em ponto flutuante binário. Faça o commit da reserva de forma atômica junto com a admissão do lote no seu adaptador de fila, para que envios concorrentes não consumam o mesmo orçamento restante.

Não transforme toda sobrecarga interna em um 429 de API para os seus próprios clientes. Se o trabalho está armazenado de forma durável, devolva um ID de lote aceito em estado adiado. Rejeite apenas o trabalho que você não consegue manter com responsabilidade e, ao fazê-lo, informe um horário para tentar de novo e torne idempotente o próprio envio do lote.

Dois detalhes desta API decidem por quanto tempo um item pode esperar com segurança na sua fila:

  • Arquivos de referência expiram em um dia. Um URI spicy:// de um upload é válido até o expiresAt da sua resposta de commit (um dia por padrão), e uma retentativa reutiliza a entrada original, então uma retentativa depois disso retorna 400. Guarde na fila uma URL HTTPS pública ou o seu próprio ID de origem e faça o upload no momento do dispatch.
  • Cotações duram cinco minutos. Se você quer travar o preço, cote no dispatch e envie quoteId com expectedCost para createTask; uma mudança de preço então falha com 40901 em vez de cobrar um valor novo. Cotações são gratuitas, mas contam contra o bucket de leitura, então cotar 20.000 linhas na admissão é o lugar errado.

A idempotência tem duas camadas

Dois duplicados diferentes precisam ser evitados: o cliente enviando o mesmo lote duas vezes e o seu dispatcher sem saber se uma criação teve sucesso. Dê ao lote uma identidade estável e depois gere e salve um UUID para cada novo item lógico. A recuperação reutiliza a chave salva desse item.

Mantenha uma restrição de unicidade nessa chave no seu banco de dados. Grave o item como «intenção» antes de chamar a API, envie a chave como Idempotency-Key e armazene o taskId retornado na mesma linha. Depois de uma queda, o próximo worker reutiliza a chave salva se a aceitação ainda for desconhecida. Gravar a intenção só depois de enviar deixa uma janela em que uma reinicialização pode comprar a mesma saída duas vezes.

O que a documentação de idempotência acrescenta e a fila precisa respeitar:

  • Uma chave vale por 24 horas a partir da primeira requisição aceita; replays não a prolongam. Depois disso, a mesma chave cria uma nova tarefa cobrada. Antes de reproduzir um item antigo da fila ou da dead-letter, consulte o taskId armazenado em vez de confiar na chave.
  • A chave pertence à mesma conta e à mesma API key, e ao modelo, input e callBackUrl exatos. Mudar qualquer coisa retorna 409. Uma chave irmã da sua conta também recebe 409 e não consegue ler a tarefa: só a chave que criou uma tarefa pode lê-la ou tentá-la de novo, então use uma chave só para criar, consultar e repetir em uma frota de workers.
  • Um replay retorna a tarefa original no estado atual, sem marcação de que foi um replay.
  • Uma rara corrida com a mesma chave retorna 409 «Idempotency key conflict, please retry shortly». Tente de novo uma vez após algumas centenas de milissegundos; as outras duas mensagens 409 nunca terão sucesso.

Um dispatcher que mantém o teto até a tarefa terminar

Salve um objeto por linha em batch.jsonl, por exemplo {"model": "alibaba/z-image-turbo/text-to-image", "input": {"prompt": "<one row's prompt>"}}, usando as entradas aceitas pelo modelo escolhido. Defina SPICY_API_KEY, BATCH_BUDGET_USD e, opcionalmente, BATCH_WORKERS (padrão 2). Execute este exemplo em Python e aprove o teto do lote. Ele mantém cada worker ocupado até a sua tarefa ser terminal, liquidada e não estar mais preparando a saída.

Guarde a pasta batch-run para a recuperação e não reordene as linhas de origem dela. A mesma pasta preserva o gasto liquidado e as reservas entre reinicializações. Execute apenas um coordenador por pasta.

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

A documentação de integração em produção cobre a implantação. Este exemplo isola linhas inválidas, pausa a nova admissão por saldo ou erros incertos e limita retentativas transitórias. Tarefas já aceitas continuam sendo coletadas depois que a admissão pausa. Ele deliberadamente não faz retentativa paga automática; uma linha com falha permanece registrada para uma nova tentativa explícita após a liquidação. Rode um processo desses por classe de mídia e coloque o round-robin por tenant na frente do arquivo.

Agendamento justo: proteja os tenants pequenos

Primeiro a entrar, primeiro a sair é justo com os jobs, não com os clientes. Se o tenant A enfileira 100.000 itens por minuto antes de o tenant B enfileirar dez, B espera atrás de todos os de A. Mantenha uma fila por tenant e pegue um item por tenant a cada rodada:

rodada 1: A1, B1, C1

rodada 2: A2, B2, C2

Os pesos podem refletir a capacidade contratada, mas dê a cada tenant ativo uma cota mínima. Limite também as tarefas em andamento de cada tenant: o teto global protege a sua conta, o do tenant contém os vizinhos barulhentos. Dentro de um tenant, faixas de prioridade (prévias interativas, lotes normais, backfills) só valem a pena quando a latência das prévias for uma reclamação real. Se você as adotar, faça a prioridade envelhecer com o tempo de fila; caso contrário, um fluxo constante de prévias deixa um backfill sem atendimento para sempre. Tudo em uma conta compartilha os mesmos dois buckets de requisições e o mesmo saldo, então a justiça entre tenants é trabalho seu, não da API.

Classifique as falhas antes de tentar de novo

«Tentar três vezes» não é uma política. A ação certa depende do que você sabe e de uma tarefa já poder existir. Esta tabela usa os códigos documentados; os valores de code estão no corpo da resposta, e as falhas de tarefa chegam como state: "failed" com um errorCode em uma chamada recordInfo bem-sucedida (code: 200).

OndeSinalRetentar?O que fazer
CriarTimeout, conexão interrompida, corpo ilegívelSimResultado desconhecido. Mesma chave, mesma API key, backoff com jitter
Criar429 com Retry-AfterSimEspere os segundos indicados, some jitter, reduza a taxa de dispatch. Não interprete como prova de que você estava rápido demais
Upload429 sem Retry-After (uploads pendentes)Sem retentativa imediataConclua os uploads pendentes ou espere os tickets deles expirarem antes de pedir mais
Criar500, 503, 50301 (modelo indisponível)Sim, com limiteBackoff com prazo; 50301 também pode significar ausência de preço ativo, então atualize o catálogo
Criar409 corrida com a mesma chaveUma vezAlgumas centenas de ms, mesma chave. Outros 409 significam payload alterado ou outra chave: pare
Criar400, 40003, 40004NãoCampo inválido, combinação não suportada ou tipo/tamanho de arquivo incompatível; corrija o item e mostre a mensagem
Criar40201 saldo, 40202 teto de gastoNãoPause o dispatch e alerte. Tentar de novo não muda um saldo nem um teto
Criar401, 403, 40301NãoPermissão de chave ou de modelo: pause a integração e alerte um operador
Tarefarate_limited, upstream_unavailable, upstream_failedSimSó depois de settled: true: POST /api/v1/jobs/retry com uma chave ligada à tarefa de origem; cria uma nova tarefa cobrada. Uma retentativa antes disso retorna 409
Tarefatimeout, generation_failedTalvezCostuma falhar do mesmo jeito. Tente uma duração menor ou uma resolução mais baixa, ou desista
Tarefainvalid_request, unsupported_combination, content_rejectedNãoA mesma entrada falha de novo. Mude a entrada ou escolha outro modelo
Tarefainvalid_assetNova revisãoO arquivo de referência sumiu: faça upload de novo, atualize a entrada e cote uma nova tarefa
LocalCallback recebido, a sua transação falhouSimRepita o seu handler, não a criação; deduplique por ID de entrega ou taskId

Use backoff aleatório para que os workers não repitam em sincronia.

Mova as falhas confirmadas e as entradas rejeitadas para uma dead-letter queue com a classificação e a ação de replay segura. Mantenha uma criação ambígua ou uma tarefa aceita não resolvida na fila de reconciliação com a chave salva e o ID da tarefa; um prazo de retentativa local não prova que ela falhou. Uma tarefa com falha normalmente libera o hold por completo, mas alguns resultados content_rejected cobráveis são liquidados na cotação, então leia settled e cost em vez de presumir reembolso.

A proteção de custos pertence ao agendador

Limites de concorrência protegem máquinas, não dinheiro. Um worker perfeitamente estável ainda pode fazer o dispatch de um lote impagável. Confira o custo estimado contra cada envelope antes do dispatch:

permitido = restante diário do tenant

∩ restante mensal do tenant

∩ restante do lote do projeto

∩ restante global por hora

Reconcilie com a liquidação real: createTask retorna o estimatedCost retido, uma tarefa bem-sucedida é liquidada nesse valor ou abaixo dele, e settled: true com cost é a cobrança final. Libere a sua própria reserva dos itens que a API rejeitou e mantenha a reserva dos aceitos até settled virar. Não adivinhe a partir de um timeout HTTP.

Proteções úteis:

  • um custo estimado máximo por lote, mostrado a uma pessoa antes de um grande começar;
  • um orçamento de dispatch por hora por tenant e um alarme de velocidade de gasto global;
  • um circuit breaker por modelo ou tenant;
  • tetos de API key abaixo do saldo da conta e um alerta de saldo baixo;
  • uma chave de emergência que pare o novo dispatch e deixe o processamento das conclusões funcionando.

Como as tarefas aceitas não podem ser canceladas, parar a criação é o único freio: o teto de tarefas em andamento, e não um botão de cancelar, limita o dano em um incidente de custo. Continue recebendo webhooks, consultando tarefas antigas, liquidando reservas e copiando saídas.

Conclusão em escala de lote

A entrega por callBackUrl de cada tarefa expira em 15 segundos; um endpoint de conta registrado também pode receber a própria entrega, então o receptor deve verificar a assinatura contra o corpo bruto, gravar o evento em uma tabela de inbox, retornar 2xx e deixar um worker fazer o resto. Deduplique pelo request_id de entrega v2 e decida por data.state, não pela ordem de chegada. Mantenha um polling lento como reserva para eventos perdidos (veja a documentação de webhooks).

Copie as saídas para o seu próprio armazenamento à medida que as tarefas terminam. Os links de resultado duram cerca de 20 minutos e a mídia gerada é mantida por cerca de 14 dias no máximo (retenção); a 5 conclusões por segundo, a etapa de cópia faz parte do seu throughput, e um uploader lento vira o gargalo que se esconde em tarefas «concluídas». Encurtar a retenção também pode remover resultados de tarefas que você já executou.

Observe o fluxo, não só os erros

Um bom painel responde a quatro perguntas: estamos aceitando trabalho, ele está começando, está terminando, o custo está sendo liquidado como esperado? Acompanhe por modelo, classe de mídia, nível de tenant e resultado:

SinalPor que importa
Idade do item pronto mais antigoMedida direta da espera do cliente e da falha de backpressure
Contagens na fila, em andamento e terminaisMostra onde o trabalho se acumula
Tarefas aceitas por segundo vs. criações tentadasSepara o throughput real das retentativas
Duração da tarefa p50/p95/p99 (completedAt - createdAt)Orienta o dimensionamento da concorrência e a detecção de tarefas travadas
Resultados terminais e mix de errorCodeDegradação específica de modelo ou de entrada
X-RateLimit-RemainingAvisa antes de um 429
Saldo retido vs. disponível (GET /api/v1/chat/credit)Prevê 40201 antes que ele pare o lote
Custo reservado vs. liquidado, gasto por hora por tenantReservas vazadas, lotes descontrolados
Atraso de webhooks e recuperações por pollingSaúde do caminho de conclusão

Carregue uma única cadeia de correlação (batchId, ID do item, chave de idempotência, taskId, request_id) por logs e eventos, e mantenha prompts, API keys e URLs de assets privados fora dos rótulos de métricas e dos logs de rotina. Alerte sobre sintomas: «50.000 itens na fila» pode ser normal; «o item interativo mais antigo tem 12 minutos» pede ação.

Comece com um lote limitado e depois aumente a capacidade

Aumente a capacidade apenas enquanto a idade do item mais antigo, as falhas de tarefa e o gasto ficarem dentro dos seus limiares; mais workers não consertam um caminho de conclusão sobrecarregado.

Antes do primeiro lote pago, use um mock para interromper um worker imediatamente antes e depois do envio e para retornar 429, tarefas com falha e erros de teto de gasto. A recuperação deve reutilizar a intenção salva, manter as tarefas desconhecidas sem resolução e parar o novo dispatch sem parar a conclusão. Depois rode um pequeno lote aprovado antes de abrir a admissão para importações grandes.

Para o tratamento de conclusões, veja «Polling vs webhooks for async generation jobs»; para o orçamento de um lote, veja Per-unit pricing vs subscriptions. O checklist de produção cobre a configuração de chaves e os testes de lançamento.

FAQ

A SpicyAPI tem endpoint em lote ou limite de concorrência?

Não há endpoint em lote: cada linha é um createTask, e não existe teto documentado para tarefas em andamento (os limites estão na tabela acima; a taxa atual está em X-RateLimit-Limit). Se o limite de taxa for baixo demais para o seu pico, peça um maior ao suporte do console, descrevendo o pico de requisições por segundo e o tamanho do lote.

Posso cancelar um lote iniciado por engano?

Não. Use a chave de emergência do dispatch para parar novas tarefas enquanto o tratamento das conclusões continua.

Um 429 deve consumir uma tentativa de retentativa?

Espere Retry-After mais jitter e mantenha a mesma chave de idempotência. Limitação repetida pede uma taxa de dispatch menor. Limite as retentativas por tentativas e por tempo; se a aceitação ainda for incerta quando esse orçamento acabar, reconcilie a intenção salva antes de criar um substituto.

Como evito cobranças duplicadas após um timeout?

Reenvie com a mesma Idempotency-Key e a mesma API key em até 24 horas, e armazene o taskId assim que tiver. Depois de 24 horas, consulte o taskId armazenado.

Uma fila basta para evitar sobrecarga?

Não. Uma fila armazena a sobrecarga; o backpressure a controla. Você ainda precisa de limiares de admissão sobre a idade da fila, de um teto de tarefas em andamento e de uma resposta de produto para quando a espera prometida não puder ser cumprida.

Qual é uma boa concorrência inicial?

Conclusões por segundo vezes o tempo médio da tarefa, por classe de mídia, mais cerca de 25 % de folga: 40 para 5 imagens por segundo a 8 segundos cada. Aumente apenas enquanto idade da fila, erros e gasto continuarem saudáveis.

Jobs de alta prioridade podem contornar os limites?

Não. A prioridade escolhe entre o trabalho admissível e nunca contorna o teto global nem os orçamentos. Dê ao trabalho interativo uma capacidade reservada e faça o de menor prioridade envelhecer para não ficar sem atendimento.

Deixe um Agent cuidar da chamada

Use um Agent quando quiser que ele prepare as entradas e devolva o resultado enquanto você aprova o preço e as escolhas criativas.

  • CLI: com o objeto input de uma linha salvo como input.json (para Z-Image: prompt, aspect_ratio, enable_prompt_expansion, output_format, seed), use npx @spicyapi/cli tasks create --model alibaba/z-image-turbo/text-to-image --input-file input.json --wait. A CLI confirma o preço; guarde o ID de tarefa retornado para a recuperação.
  • MCP: peça ao seu assistente para usar spicyapi_task_create com alibaba/z-image-turbo/text-to-image e depois spicyapi_task_wait. O servidor MCP pede confirmação do preço antes da criação.
  • Skill: instale com npx skills add spicyapi-ai/spicy-skill e depois peça ao $spicyapi que execute o mesmo workflow e pare para a sua aprovação. A Skill usa uma conexão CLI ou MCP configurada.

Também em English · 日本語 · 한국어 · Deutsch · Français · Español · Русский · Italiano · Polski

Soluções

API de vídeo generativo: testar e integrarCompare endpoints de vídeo por duração, resolução, áudio e preço e envie depois a mesma requisição a partir do seu servidor.

Experimente você mesmo

Você paga só pelo que executar — sem plano e sem assinatura.

Feito com MiniMax H3 · ByteDance Seedance 2.0 Fast