SDK dla TypeScriptu
Przewodnik krok po kroku po SDK dla TypeScriptu @spicyapi/sdk – instalacja, pierwszy wynik i opis każdej metody, opcji i błędu prostym językiem. SDK dla Pythona, Go, PHP i Javy mają własne strony.
@spicyapi/sdk to oficjalna biblioteka SpicyAPI dla TypeScriptu i JavaScriptu. Nie musisz jej wcześniej znać. Ta strona najpierw prowadzi cię od pustego folderu do zapisanego obrazu, a potem opisuje każdą funkcję pakietu: do czego służy, co jej przekazujesz, co zwraca, czy kosztuje pieniądze i co może pójść nie tak.
Czym jest SDK
SDK to gotowa biblioteka kodu, którą dodajesz do własnego programu. Bez niej twój program musi każde żądanie HTTP budować ręcznie: dołączać klucz API, zamieniać odpowiedź z formatu JSON na dane, raz za razem pytać, czy zadanie już się zakończyło, ponawiać próbę po zerwaniu połączenia, przesyłać pliki w trzech osobnych krokach i sprawdzać podpis każdego webhooka. SDK robi to wszystko za ciebie, więc cała generacja mieści się w jednym wywołaniu:
const task = await client.run({ model: modelId, input: { prompt: "A paper lantern at night" } });Przyda ci się, jeśli piszesz albo zamierzasz napisać program w Node.js (w JavaScripcie lub TypeScripcie), który działa na serwerze, jako skrypt na twoim komputerze albo jako proces w tle. Użytkownicy TypeScriptu dostają dodatkowo podpowiedzi i sprawdzanie typów dla każdego pola.
Czy to właściwe narzędzie?
| Chcesz… | Użyj |
|---|---|
| Wywoływać SpicyAPI z własnego kodu w Node.js albo TypeScripcie | Tego SDK |
| Generować bez pisania kodu, z terminala | CLI |
| Pozwolić, żeby asystent AI, taki jak Claude Code czy Cursor, wywoływał SpicyAPI za ciebie | Serwera MCP i Agent Skill |
| Korzystać z modelu czatu albo modelu tekstowego | Oficjalnego klienta OpenAI skierowanego na SpicyAPI – zobacz Tekst i streaming |
| Pisać w Pythonie, Go, PHP albo Javie | Oficjalnego SDK dla Pythona, SDK dla Go, SDK dla PHP albo SDK dla Javy |
| Pisać w dowolnym innym języku | Zwykłych wywołań HTTP albo klienta wygenerowanego na podstawie spisu endpointów API |
Czego SDK nie robi
- Nie działa w przeglądarce ani w aplikacji mobilnej. Wymaga Node.js, a twój klucz API nigdy nie może opuścić twojego serwera: każdy może wyczytać klucz ze strony internetowej albo z aplikacji. Niech twoja strona wywołuje twój własny backend, a dopiero backend – SpicyAPI.
- Nie obsługuje modeli czatu. Generowanie tekstu korzysta z endpointów zgodnych z OpenAI, a oficjalny pakiet
openaidobrze sobie z tym radzi. Zobacz Tekst i streaming. - Nie wybiera modelu ani nie wypełnia pól za ciebie. Każdy model przyjmuje własne pola. SDK daje ci listę modeli i schemat każdego z nich (listę pól, które przyjmuje); o tym, co wysłać, decydujesz ty.
- Nie potrafi anulować zadania. Gdy SpicyAPI przyjmie zadanie, wykonuje je do końca. Zatrzymanie programu kończy tylko twoje czekanie.
- Nie zarządza twoim kontem. Doładowania, tworzenie kluczy, limity wydatków i klucze podpisujące webhooki ustawiasz w konsoli.
Klucz API zostaje na twoim serwerze
Nigdy nie umieszczaj SPICY_API_KEY w JavaScripcie działającym w przeglądarce, w aplikacji mobilnej, w publicznym repozytorium, na zrzucie ekranu ani w wiadomości na czacie. Klucz, który wyciekł, może wydawać twoje saldo, dopóki go nie wyłączysz. SDK odczytuje klucz ze zmiennej środowiskowej, więc nie musi on nigdy pojawiać się w twoim kodzie.
Pojęcia używane na tej stronie
| Pojęcie | Co znaczy tutaj |
|---|---|
| Identyfikator modelu (model ID) | Dokładna nazwa modelu w postaci publisher/model/task. Zawsze kopiuj go z katalogu na żywo (live catalog, listModels()), nigdy z pamięci |
| Dane wejściowe (input) | Ustawienia wysyłane razem z zadaniem, takie jak prompt czy image_url. Każdy model ma własną listę dozwolonych pól, nazywaną jego schematem |
| Zadanie | Jedna praca generowania. Tworzysz je, SpicyAPI wykonuje je w tle, a ty odbierasz wynik. Każde zadanie ma identyfikator zaczynający się od job_ |
| Blokada | Gdy zadanie zostaje przyjęte, jego maksymalna cena jest odkładana z twojego salda. Nieudane lub wygasłe zadania zwykle zwalniają całą blokadę. Niektóre odmowy dotyczące treści są płatne według zaakceptowanej wyceny, wyłącznie gdy strona modelu wyraźnie wskazuje ten wyjątek. Sprawdź settled i cost, aby poznać ostateczną kwotę. |
| Klucz idempotentności | Etykieta, którą wymyślasz dla jednej porcji pracy, na przykład order-8814-image-1. Jeśli wyślesz to samo żądanie dwa razy z tą samą etykietą, SpicyAPI zwróci pierwsze zadanie, zamiast tworzyć drugie i pobierać za nie opłatę |
| Webhook | Twój adres internetowy, który SpicyAPI wywołuje, gdy zadanie się zakończy, dzięki czemu nie trzeba ciągle o nie pytać |
Co możesz zrobić
Każda z poniższych funkcji jest metodą klienta (client.listModels()), chyba że zaznaczono inaczej. „Płatne” oznacza, że wywołanie może skończyć się obciążeniem twojego salda.
| Co chcesz zrobić | Metoda | Płatne? |
|---|---|---|
| Sprawdzić, czy SpicyAPI jest osiągalne (bez klucza) | getStatus | Nie |
| Zobaczyć aktualną listę modeli, twoje ceny i pola każdego modelu | listModels, getModel | Nie |
| Poznać dokładną cenę żądania, zanim cokolwiek wydasz | quoteTask | Nie |
| Uruchomić generację | createTask | Tak |
| Uruchomić generację i poczekać na wynik w jednym wywołaniu | run | Tak |
| Jednorazowo sprawdzić zadanie | getTask | Nie |
| Poczekać, aż zadanie się zakończy | waitForTask | Nie |
| Ponowić nieudane zadanie | retryTask | Tak (nowe zadanie) |
| Wyświetlić zadania utworzone twoim kluczem | listTasks | Nie |
| Sprawdzić saldo | getBalance | Nie |
| Zobaczyć wydatki w podziale na dni i modele | getUsage | Nie |
| Przesłać lokalny obraz, wideo albo plik dźwiękowy | uploadFile, uploadBytes, uploadBase64 | Nie |
| Dostać świeży link do pobrania wyniku | createDownloadUrl | Nie |
| Usunąć zapisane pliki i prompt zakończonego zadania | purgeTask | Nie (i bez zwrotu pieniędzy) |
| Sprawdzić, czy webhook naprawdę przyszedł od SpicyAPI | verifyWebhook (zwykła funkcja) | Nie |
| Przeszukać dokumentację albo przeczytać kontrakt API bez połączenia z siecią | searchDocumentation, readOpenApiContract (zwykłe funkcje) | Nie |
Zanim zaczniesz
Potrzebujesz około dziesięciu minut. Przy każdym kroku jest wyjaśnione, co robi polecenie, więc niczego nie uruchamiasz w ciemno.
Zainstaluj Node.js w wersji 22.13 lub nowszej
Node.js to program, który uruchamia JavaScript poza przeglądarką. Zainstaluj wersję LTS z nodejs.org; zadziała każda wersja od 22.13 wzwyż. Potem otwórz terminal (Terminal w macOS, PowerShell w Windows) i sprawdź:
node -vPolecenie wypisuje zainstalowaną wersję, na przykład v22.13.0 albo v24.8.0. Jeśli numer jest niższy niż 22.13 albo polecenie nie zostało znalezione, zainstaluj Node.js ponownie i otwórz nowe okno terminala.
Utwórz folder projektu i zainstaluj SDK
Uruchamiaj te polecenia po jednym. Działają tak samo w macOS, Linuksie i Windows PowerShell.
mkdir spicy-demo
cd spicy-demo
npm init -y
npm pkg set type=module
npm install @spicyapi/sdk| Polecenie | Co robi |
|---|---|
mkdir spicy-demo | Tworzy pusty folder na twój projekt |
cd spicy-demo | Przenosi terminal do tego folderu; kolejne polecenia działają w nim |
npm init -y | Tworzy package.json, plik z ustawieniami i bibliotekami twojego projektu |
npm pkg set type=module | Informuje Node.js, że twoje pliki używają nowoczesnej składni import, której wymaga to SDK |
npm install @spicyapi/sdk | Pobiera SDK do folderu node_modules i zapisuje je w package.json |
SDK jest publikowane wyłącznie jako nowoczesny moduł ES: import { SpicyClient } from "@spicyapi/sdk" działa, a starsze require("@spicyapi/sdk") kończy się błędem ERR_PACKAGE_PATH_NOT_EXPORTED. Jeśli pominiesz npm pkg set type=module, nadawaj swoim plikom rozszerzenie .mjs.
Utwórz klucz API
- Zaloguj się na spicyapi.ai. Nie masz jeszcze konta? Zarejestruj się na spicyapi.ai/register; jeśli zakładanie nowych kont jest wstrzymane, ta strona zaproponuje zapisanie się na listę oczekujących.
- Otwórz w konsoli stronę Klucze API i kliknij Utwórz klucz. Wymagana jest tylko nazwa.
- Od razu skopiuj klucz. Zaczyna się od
sk-spicy-i jest pokazywany tylko raz. Jeśli go zgubisz, utwórz nowy.
Na tej samej stronie możesz nadać kluczowi dzienny limit wydatków; nowe klucze nie mają żadnego, dopóki go nie dodasz. Żeby uruchamiać płatne zadania, potrzebujesz też środków na saldzie – dodasz je w sekcji Rozliczenia.
Zapisz klucz w zmiennej środowiskowej
Zmienna środowiskowa to nazwana wartość, którą terminal przekazuje każdemu uruchamianemu przez siebie programowi. SDK szuka zmiennej o nazwie SPICY_API_KEY, więc klucza nigdy nie trzeba wpisywać do kodu.
# Ustaw ją dla tego okna terminala (wklej własny klucz)
export SPICY_API_KEY="sk-spicy-paste-your-key-here"
# Sprawdź: powinno się wypisać sk-spicy-
echo "${SPICY_API_KEY:0:9}"Wartość znika po zamknięciu okna. Żeby zachować ją na swoim komputerze, dopisz wiersz z export do ~/.zshrc (macOS) albo ~/.bashrc (Linux) i otwórz nowe okno.
Wolisz plik .env? Utwórz w folderze projektu plik o nazwie .env z jednym wierszem, SPICY_API_KEY=sk-spicy-paste-your-key-here, i uruchamiaj program poleceniem node --env-file=.env yourfile.mjs. SDK samo nie czyta plików .env; robi to ta flaga. Dopisz .env do pliku .gitignore, zanim pierwszy raz uruchomisz git add, żeby klucz nigdy nie trafił do repozytorium.
Na prawdziwym serwerze ustaw SPICY_API_KEY w ustawieniach sekretów albo zmiennych środowiskowych u swojego dostawcy hostingu albo przekaż klucz z menedżera sekretów przez new SpicyClient({ apiKey }).
Uruchom pierwszy plik
W folderze projektu utwórz w dowolnym edytorze tekstu plik o nazwie check.mjs, wklej do niego ten kod i zapisz:
import { SpicyClient } from "@spicyapi/sdk";
const client = new SpicyClient();
const status = await client.getStatus();
console.log(status.operational ? "SpicyAPI is up" : "SpicyAPI is not reachable right now");
console.log("Checked at", status.checkedAt);Uruchom go:
node check.mjsSpicyAPI is up
Checked at 2026-09-17T08:00:00.000ZTo sprawdzenie nie wymaga klucza i nic nie kosztuje, więc potwierdza, że Node.js i SDK są poprawnie zainstalowane.
Używasz TypeScriptu? Zapisz ten sam kod jako check.ts. Nowsze wydania Node.js 22 i 24 uruchomią go bezpośrednio poleceniem node check.ts. Jeśli twoja wersja zgłasza problem z rozszerzeniem .ts, uruchom node --experimental-strip-types check.ts albo npx tsx check.ts.
Pierwszy wynik
Ten program przechodzi całą drogę: sprawdza usługę, wybiera model obrazu dostępny dla twojego klucza, pobiera dokładną cenę i się zatrzymuje. Uruchomiony drugi raz z --yes tworzy obraz, czeka na niego i zapisuje go obok skryptu.
Uruchomienie z --yes kosztuje
Bez --yes program tylko odczytuje dane: nic nie powstaje i nic nie zostaje pobrane. Z --yes tworzy jedno zadanie obrazu, które blokuje kwotę z wyceny i pobiera ją, jeśli obraz się uda.
import { randomUUID } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { SpicyApiError, SpicyClient } from "@spicyapi/sdk";
// Odczytuje twój klucz ze zmiennej środowiskowej SPICY_API_KEY.
const client = new SpicyClient();
async function main() {
// Krok 1 — Czy SpicyAPI jest osiągalne? Bezpłatne i bez klucza.
const status = await client.getStatus();
if (!status.operational) {
throw new Error("SpicyAPI is not reachable right now. Try again in a few minutes.");
}
// Krok 2 — Które modele obrazu może wywołać ten klucz? Bezpłatne.
const catalog = await client.listModels({ modality: "image", includeExamples: true });
const model = catalog.items.find(
(item) => item.enabled && item.available && item.examples?.length,
);
if (!model) throw new Error("No image model is available to this key right now.");
console.log(`Model: ${model.displayName} (${model.model})`);
// Krok 3 — Zbuduj dane wejściowe. Zacznij od przykładu, który serwer już
// sprawdził, i wstaw własny prompt, jeśli model go przyjmuje.
const input = { ...model.examples?.[0]?.input };
if ("prompt" in input) input.prompt = "A paper lantern glowing on a rainy street at night";
// Krok 4 — Pobierz dokładną cenę. Bezpłatne: nic nie powstaje i nic nie zostaje zablokowane.
const request = { model: model.model, input };
const quote = await client.quoteTask(request);
console.log(`Price: $${quote.estimatedCost} USD`);
if (!process.argv.includes("--yes")) {
console.log("Nothing was charged. Run again with --yes to generate the image.");
return;
}
// Krok 5 — Utwórz zadanie i poczekaj na nie. To jest krok, który kosztuje.
const task = await client.run(
{ ...request, quoteId: quote.quoteId, expectedCost: quote.estimatedCost },
{
idempotencyKey: randomUUID(),
onAccepted: (accepted) => console.log(`Task accepted: ${accepted.taskId}`),
onUpdate: (update) => console.log(`State: ${update.state}`),
},
);
// Krok 6 — Nieudane zadanie to zwykła odpowiedź. Sprawdź rozliczenie.
if (task.state !== "succeeded") {
console.log(`The task ended as ${task.state}: ${task.errorCode} ${task.errorMessage ?? ""}`);
console.log("Check settled and cost for the final charge.");
return;
}
// Krok 7 — Pobierz wynik od razu. Link wygasa po około 20 minutach.
const asset = task.output?.assets?.find((item) => item.url);
if (!asset?.url) throw new Error("The task succeeded but its file is unavailable.");
const download = await fetch(asset.url); // nigdy nie wysyłaj tu klucza API
if (!download.ok) throw new Error(`Download failed with HTTP ${download.status}`);
const extension = asset.mime?.split("/")[1] ?? "png";
await writeFile(`result.${extension}`, Buffer.from(await download.arrayBuffer()));
console.log(`Saved result.${extension}. Charged: $${task.cost}${task.settled ? "" : " (not final yet)"}`);
}
main().catch((error) => {
if (error instanceof SpicyApiError) {
console.error(`SpicyAPI error ${error.code}: ${error.message} (request_id ${error.requestId})`);
} else {
console.error(error);
}
process.exitCode = 1;
});Uruchom go dwa razy:
node first-result.mjs
node first-result.mjs --yesModel: <display name> (<model ID>)
Price: $<amount> USD
Nothing was charged. Run again with --yes to generate the image.
Model: <display name> (<model ID>)
Price: $<amount> USD
Task accepted: job_…
State: running
State: succeeded
Saved result.png. Charged: $<amount>Co robi każdy krok
getStatus()pyta endpoint kondycji SpicyAPI, czy usługa działa. Przy problemie z siecią nigdy nie rzuca wyjątku; zamiast tego zwracaoperational: false.listModels()zwraca katalog modeli na żywo z cenami twojego konta. Używaj tylko pozycji, w których zarównoenabled, jak iavailablemają wartośćtrue.includeExamples: truedodaje przykładowe dane wejściowe, które serwer już zweryfikował – to najbezpieczniejszy punkt wyjścia. Wartość, którą wysyłasz później, to polemodeldanej pozycji, czyli identyfikator modelu.- Dane wejściowe. Każdy model przyjmuje inne pola; pole, którego model nie zna, zostaje odrzucone z błędem
400. Skopiowanie zweryfikowanego przykładu i zmiana samego promptu pozwala tego uniknąć. Żeby zobaczyć wszystkie pola, które model przyjmuje, wywołajgetModel()i przeczytaj jegoinputSchema. quoteTask()wycenia dokładnie to żądanie. Niczego nie tworzy i niczego nie blokuje. Kwoty to dolary amerykańskie zapisane jako tekst, na przykład"0.04", więc nie wkradają się błędy zaokrągleń.run()tworzy zadanie, a potem sprawdza je, dopóki się nie zakończy. PrzekazaniequoteIdiexpectedCostznaczy „przyjmij to tylko po cenie, którą mi pokazano”: jeśli cena się zmieniła, wywołanie kończy się błędem40901, zamiast pobrać inną kwotę.idempotencyKeysprawia, że ponowienie po błędzie sieci jest bezpieczne; w prawdziwej aplikacji zapisz go przed wywołaniem, żeby uruchomiony ponownie program mógł go użyć jeszcze raz.onAcceptedprzekazuje ci identyfikator zadania w chwili, gdy zadanie powstaje – zapisz go, żeby móc później sprawdzić zadanie, nawet jeśli twój program się zatrzyma.- Stan zadania.
run()zwraca wynik normalnie także dla nieudanych zadań. Nieudane lub wygasłe zadania zwykle zwalniają całą blokadę. Niektóre odmowy dotyczące treści są płatne według zaakceptowanej wyceny, wyłącznie gdy strona modelu wyraźnie wskazuje ten wyjątek. Sprawdźsettledicost, aby poznać ostateczną kwotę. - Pobranie. Każdy wynik ma tymczasowy
url. Pobierz plik bez klucza API i zachowaj własną kopię: link działa około 20 minut (po nowy wywołaj ponowniegetTask()), a SpicyAPI usuwa wygenerowane pliki najpóźniej po około 14 dniach. Zobacz Przechowywanie i usuwanie danych.
Wideo działa tak samo, ale może trwać kilka minut; przekaż do run() dłuższy timeoutMs (domyślnie czekanie trwa 10 minut). Identyfikator modelu w poniższych fragmentach kodu ma postać MODEL_ID_FROM_CATALOG: to symbol zastępczy, a nie prawdziwy model. Zastąp go wartością model z tej pozycji katalogu na żywo – listModels() w SDK albo uwierzytelniony endpoint GET /api/v1/models – w której zarówno enabled, jak i available mają wartość true.
Opis API
Każdy opis poniżej mówi, do czego służy metoda, jak ją wywołać, co zwraca, czy może kosztować i jakich błędów się spodziewać. Klasy błędów, takie jak SpicyApiError, wyjaśnia sekcja Obsługa błędów. Wszystkie kwoty to dolary amerykańskie zapisane jako tekst ("0.04"), a wszystkie czasy to ciągi ISO 8601 w UTC.
Tworzenie klienta
new SpicyClient(options?: SpicyClientOptions)Większość programów niczego tu nie przekazuje: new SpicyClient() odczytuje klucz z SPICY_API_KEY i łączy się z usługą produkcyjną. Nieprawidłowa opcja od razu rzuca TypeError, zanim zostanie wysłane jakiekolwiek żądanie.
Właściwość
Typ
Automatyczne ponowienia
SDK samo ponawia żądanie tylko wtedy, gdy nie może cię to kosztować podwójnie:
| Wywołanie | Ponawiane automatycznie? |
|---|---|
Odczyty: getStatus, listModels, getModel, getTask, listTasks, getBalance, getUsage; a także quoteTask, purgeTask, commitUploadedFile | Tak |
createTask, run (jego krok tworzenia zadania), retryTask | Tylko jeśli przekażesz idempotencyKey. Bez niego błąd sieci trafia prosto do ciebie, bo ponowne wysłanie mogłoby utworzyć drugie zadanie |
createUploadUrl, samo wysyłanie bajtów, createDownloadUrl | Nie |
Ponowienie następuje, gdy w ogóle nie ma odpowiedzi (błąd sieci albo lokalne przekroczenie czasu) albo gdy odpowiedź to HTTP 408, 429, 500, 502, 503 lub 504. Przerwa trwa tyle, ile każe nagłówek Retry-After wysłany przez serwer; bez niego 250 ms, potem 500 ms, potem 1 s i tak dalej, każda z odchyleniem do 25% i nie dłuższa niż maxRetryDelayMs. Gdy wszystkie próby się nie powiodą, dostajesz ostatni błąd.
Zatrzymywanie wywołania przez signal
Każda metoda przyjmuje opcję signal, czyli standardowy AbortSignal. Użyj AbortSignal.timeout(5_000), żeby ustawić termin, albo AbortController, żeby zatrzymać wywołanie na żądanie. Wywołanie rzuca wtedy przyczynę przerwania. Zatrzymanie kończy tylko twoje żądanie albo czekanie: zadanie, które SpicyAPI już przyjęło, działa dalej i nadal może zostać rozliczone.
getStatus()
Sprawdza, czy SpicyAPI jest osiągalne. Przydaje się jako pierwsza linijka skryptu albo jako test kondycji w twoim własnym monitoringu.
getStatus(options?: { signal?: AbortSignal }): Promise<ServiceStatus>| Zwracane pole | Znaczenie |
|---|---|
operational | true, gdy usługa odpowiada. Tego pola używaj |
health | Surowy wynik sprawdzenia kondycji: path, ok, status HTTP, pierwsze 512 znaków body oraz error, gdy nie nadeszła żadna odpowiedź |
readiness | Ta sama struktura dla drugiego sprawdzenia. W usłudze publicznej odpowiada 404 – to normalne i nie oznacza, że usługa nie działa |
checkedAt | Kiedy wykonano sprawdzenie |
Płatne: nie. Wymaga klucza: nie. Błędy: przy problemach z siecią nie rzuca wyjątku – dostajesz operational: false, a przyczynę w health.error. Rzuca tylko wtedy, gdy przerwiesz wywołanie przez signal. Przykład znajdziesz w kroku Uruchom pierwszy plik.
listModels(options?)
Wyświetla katalog modeli na żywo z cenami twojego konta. Stąd pochodzi każdy identyfikator modelu.
listModels(options?: ListModelsOptions): Promise<ModelList>Właściwość
Typ
Zwraca { total, items }. Najprzydatniejsze pola każdej pozycji:
| Pole | Znaczenie |
|---|---|
model | Identyfikator modelu. Przy tworzeniu zadania wysyłaj dokładnie tę wartość |
displayName | Czytelna nazwa |
modality, tasks | Co model wytwarza i jaką pracę wykonuje |
provider, family | Kto stworzył model i do jakiej linii produktów należy |
enabled, available | Oba muszą mieć wartość true, zanim użyjesz modelu |
pricing | Jedna pozycja na każdy poziom cenowy: price, unit (per_image, per_second, per_request albo per_1k_tokens) i variant (który to poziom, często rozdzielczość). W czasie promocji regularPrice pokazuje cenę bez rabatu |
startingPrice | Najtańszy poziom. Dokładną kwotę poda quoteTask |
quantityField | Pole wejściowe, które ustala rozliczaną ilość, na przykład czas trwania. Puste oznacza jedną jednostkę na wywołanie |
taskTimeoutSeconds | Jak długo zadanie może trwać, zanim zakończy się jako expired |
maxOutputDurationSeconds | Najdłuższy wynik, na jaki pozwala model, jeśli wynik ma długość |
inputSchema, examples | Obecne tylko wtedy, gdy o nie poprosisz |
updatedAt, version | Zmieniają się przy każdej zmianie danych modelu; po nich poznasz, kiedy odświeżyć zapisany schemat |
Płatne: nie. Wymaga klucza: tak. Błędy: SpicyApiError (na przykład 401 przy problemie z kluczem, 429 przy zbyt wielu żądaniach), SpicyTransportError.
const { items } = await client.listModels({ modality: "video", task: "image-to-video" });
for (const item of items) {
if (!item.enabled || !item.available) continue;
const from = item.startingPrice ? `from $${item.startingPrice.price} ${item.startingPrice.unit}` : "";
console.log(item.model, "|", item.displayName, "|", from);
}Odczyty katalogu korzystają z osobnego limitu odczytów konta i nie zużywają limitu tworzenia zadań. Zapisz listę i odświeżaj ją co jakiś czas albo po błędzie 400 lub 50301, zamiast czytać ją przed każdym zadaniem.
getModel(modelId)
Zwraca jeden model z pełnym schematem: listą pól wejściowych, które przyjmuje, informacją, które z nich są wymagane, i dozwolonymi wartościami.
getModel(modelId: string, options?: { signal?: AbortSignal }): Promise<ApiModel>Wynik ma wszystkie pola opisane przy listModels, zawsze razem z inputSchema i examples. Schemat to standardowy JSON Schema: properties wymienia każde pole z jego type, a często także z enum (dozwolone wartości), minimum / maximum i default; required wymienia pola, które musisz wysłać.
Płatne: nie. Wymaga klucza: tak. Błędy: SpicyApiError z 404, gdy żaden model nie ma takiego identyfikatora; TypeError, jeśli identyfikator jest pusty.
const model = await client.getModel("MODEL_ID_FROM_CATALOG");
console.log("Required fields:", model.inputSchema?.required);
console.log(JSON.stringify(model.inputSchema?.properties, null, 2));
console.log("A request that is known to work:", model.examples?.[0]?.input);quoteTask(request)
Podaje dokładną cenę żądania, niczego nie tworząc. Użyj tej metody, żeby pokazać cenę człowiekowi albo odmówić wykonania pracy powyżej budżetu.
quoteTask(request: { model: string; input: object; callBackUrl?: string }, options?: { signal?: AbortSignal }): Promise<TaskQuote>Przekaż te same model, input i opcjonalny callBackUrl, z którymi zamierzasz utworzyć zadanie.
| Zwracane pole | Znaczenie |
|---|---|
quoteId | Przekaż go do createTask albo run, żeby zapewnić sobie tę cenę |
estimatedCost | Kwota, która zostanie zablokowana, jeśli utworzysz zadanie teraz |
maxCharge | Najwyższa kwota, jaką to żądanie może kosztować |
currency | Zawsze USD |
quantity, unit | Rozliczana ilość, na przykład "5" i per_second |
expiresAt | Wycena jest ważna pięć minut |
Płatne: nie. Wymaga klucza: tak. Błędy: SpicyApiError, na przykład 400, gdy dane wejściowe nie pasują do schematu modelu, 50301, gdy model jest niedostępny, oraz 429.
const request = { model: "MODEL_ID_FROM_CATALOG", input: { prompt: "A paper lantern at night" } };
const quote = await client.quoteTask(request);
console.log(`This will cost $${quote.estimatedCost}. Quote valid until ${quote.expiresAt}.`);createTask(request, options?)
Uruchamia generację i od razu zwraca identyfikator zadania. Nie czeka na wynik: następnie użyj waitForTask albo webhooka albo skorzystaj z run, które robi jedno i drugie w jednym wywołaniu.
createTask(request: CreateTaskInput, options?: CreateTaskOptions): Promise<CreateTaskResult>Żądanie:
Właściwość
Typ
Opcje:
Właściwość
Typ
| Zwracane pole | Znaczenie |
|---|---|
taskId | Identyfikator nowego zadania. Zapisz go od razu |
state | queued dla nowego zadania. Jeśli powtórzono klucz idempotentności – bieżący stan pierwotnego zadania |
estimatedCost | Kwota, która jest teraz zablokowana; to także najwięcej, ile to zadanie może kosztować |
deadlineAt | Kiedy SpicyAPI przestanie pracować nad zadaniem, jeśli nie będzie ono zakończone |
Płatne: tak. Kwota estimatedCost zostaje zablokowana na twoim saldzie. Jeśli zadanie się powiedzie, płacisz za faktyczne zużycie, nigdy więcej niż wynosi blokada, a reszta zostaje zwolniona. Nieudane lub wygasłe zadania zwykle zwalniają całą blokadę. Niektóre odmowy dotyczące treści są płatne według zaakceptowanej wyceny, wyłącznie gdy strona modelu wyraźnie wskazuje ten wyjątek. Sprawdź settled i cost, aby poznać ostateczną kwotę.
Błędy:
error.code | Co oznacza | Co zrobić |
|---|---|---|
400 | Dane wejściowe nie pasują do schematu modelu albo callBackUrl jest niedozwolony | Porównaj żądanie z getModel().inputSchema i je popraw |
401 | Brak klucza, klucz ma zły format albo został unieważniony | Sprawdź SPICY_API_KEY |
40201 | Saldo nie pokrywa blokady | Doładuj konto w sekcji Rozliczenia w konsoli |
40202 | Osiągnięto dzienny, miesięczny albo łączny limit wydatków | Podnieś limit na stronie Klucze API albo poczekaj, aż się odnowi |
40301 | Ten klucz nie może używać tego modelu | Zmień listę modeli klucza w konsoli |
409 | Ten klucz idempotentności został już użyty z innym żądaniem | Do nowej pracy użyj nowego klucza |
40901 | Wycena wygasła albo zmieniła się cena lub żądanie | Pobierz nową wycenę i potwierdź nową cenę |
413 | Treść żądania jest za duża | Prześlij pliki mediów, zamiast wstawiać je bezpośrednio do żądania |
429 | Zbyt wiele żądań | Odczekaj error.retryAfterSeconds i spróbuj ponownie |
50301 | Model nie może teraz przyjmować zadań | Spróbuj później albo wybierz inny model |
SpicyTransportError albo SpicyTimeoutError oznacza, że nie nadeszła żadna odpowiedź, więc nie wiesz, czy zadanie powstało. Wyślij ponownie to samo żądanie z tym samym kluczem idempotentności: albo je utworzysz, albo dostaniesz z powrotem istniejące zadanie. TypeError oznacza, że któraś opcja była nieprawidłowa (na przykład ujemne retentionSeconds), i nic nie zostało wysłane.
import { randomUUID } from "node:crypto";
const idempotencyKey = randomUUID(); // w prawdziwej aplikacji najpierw zapisz go razem z zamówieniem
const accepted = await client.createTask(
{ model: "MODEL_ID_FROM_CATALOG", input: { prompt: "A paper lantern at night" } },
{ idempotencyKey },
);
console.log(accepted.taskId, accepted.state, "on hold: $" + accepted.estimatedCost);run(request, options?)
Najprostszy sposób na wygenerowanie czegoś: wywołuje createTask, potem waitForTask i zwraca zakończone zadanie.
run(request: CreateTaskInput, options?: RunTaskOptions): Promise<TaskRecord>request wygląda tak samo jak w createTask. Opcje to wszystko, co przyjmuje createTask, a do tego:
Właściwość
Typ
Zwraca zakończone zadanie z polami opisanymi przy getTask. Przy udanym zadaniu czeka też, aż pliki wynikowe zostaną w całości skopiowane, więc output.assets[].url jest od razu gotowy do pobrania.
Płatne: tak, dokładnie tak jak createTask. Nieudane zadanie jest zwracane, a nie rzucane jako wyjątek – sprawdź task.state.
Błędy: wszystko, co może rzucić createTask, a do tego:
SpicyWaitTimeoutError, gdytimeoutMsupłynie po przyjęciu zadania. Zadanie nadal działa w SpicyAPI. Wywołaj późniejwaitForTask(error.taskId), żeby je odebrać.SpicyTimeoutError, gdy czas upłynie, zanim SpicyAPI potwierdziło zadanie. Wyślij to samo żądanie z tym samym kluczem idempotentności, żeby się przekonać.- Każdy błąd rzucony w twoich własnych
onAcceptedlubonUpdateprzerywa czekanie; samo zadanie działa dalej. TypeErrorprzy nieprawidłowymtimeoutMsalbointervalMs. Te wartości są sprawdzane, zanim cokolwiek zostanie utworzone.
Kompletny przykład znajdziesz w sekcji Pierwszy wynik.
getTask(taskId)
Jednorazowo odczytuje zadanie: jego stan, linki do wyników i koszt.
getTask(taskId: string, options?: { signal?: AbortSignal }): Promise<TaskRecord>| Pole | Znaczenie |
|---|---|
taskId, model | Które zadanie i jakim modelem je wykonano |
state | queued albo running, dopóki trwa praca; succeeded, failed albo expired po zakończeniu (canceled pojawia się tylko w starych zapisach) |
output.assets | Pliki wynikowe, opisane w następnej tabeli. Tylko przy succeeded |
output.text | Wynik tekstowy, w przypadku modeli, które zwracają tekst |
errorCode, errorMessage | Dlaczego zadanie się nie powiodło. Decyzje podejmuj na podstawie errorCode; brzmienie komunikatu może się zmieniać |
cost | Dopóki settled ma wartość false – kwota zablokowana; gdy true – ostateczna opłata |
settled | Czy cost jest ostateczne |
createdAt, completedAt, deadlineAt | Kiedy zadanie utworzono, kiedy się zakończyło i kiedy by wygasło |
input | Dane wejściowe w zapisanej postaci. Usuwane po upływie okresu przechowywania promptów |
sourceTaskId | Ustawione w zadaniu utworzonym przez retryTask; wskazuje zadanie pierwotne |
contentState | present; expired (usunięte zgodnie z zasadami przechowywania) albo purged (usunięte przez ciebie za pomocą purgeTask) |
retention | Kiedy zostaną usunięte pliki (outputsExpireAt) i prompt (promptsExpireAt) |
Każdy element output.assets:
| Pole | Znaczenie |
|---|---|
url | Tymczasowy link do pobrania. Pobieraj plik bez klucza API |
expiresAt | Kiedy link przestanie działać, zwykle po około 20 minutach. Po świeży link wywołaj ponownie getTask |
mime | Typ pliku, na przykład image/png albo video/mp4 |
width, height, durationSeconds, bytes | Wymiary, długość i rozmiar, jeśli dotyczą |
pending | true, dopóki plik jest jeszcze kopiowany do magazynu. Sprawdź ponownie za kilka sekund |
unavailable | true, gdy plik zniknął na dobre. Czekanie nie pomoże; wygeneruj wynik ponownie |
key | Wewnętrzna nazwa pliku, potrzebna tylko w createDownloadUrl |
Pole, dla którego SpicyAPI nie ma wartości, jest pomijane, a nie ustawiane na null, więc każde z powyższych pól traktuj jako opcjonalne.
Płatne: nie. Błędy: SpicyApiError z 404, gdy twój klucz nie widzi zadania o tym identyfikatorze.
const task = await client.getTask("job_01k3m8x9q2z4v7n5p6r8s0t1w3"); // zapisany wcześniej identyfikator
if (task.state === "succeeded") {
for (const asset of task.output?.assets ?? []) console.log(asset.mime, asset.url);
} else if (task.state === "queued" || task.state === "running") {
console.log("Still working. On hold: $" + task.cost);
} else {
console.log(`Finished as ${task.state}: ${task.errorCode}`);
}waitForTask(taskId, options?)
Sprawdza zadanie raz za razem, aż się zakończy, a potem je zwraca. Użyj tej metody dla zadania utworzonego wcześniej, na przykład po ponownym uruchomieniu programu.
waitForTask(taskId: string, options?: WaitForTaskOptions): Promise<TaskRecord>Opcje: timeoutMs (domyślnie 600000, najwyżej 86400000), intervalMs, onUpdate i signal – wszystkie działają tak, jak opisano przy run.
Zwraca zadanie, gdy state ma wartość succeeded, failed, expired albo canceled, a w przypadku udanego zadania – gdy żaden plik wynikowy nie jest już pending. Każde sprawdzenie to jedno żądanie getTask, wliczane do limitu żądań twojego konta; dlatego domyślny odstęp z czasem się wydłuża.
Płatne: nie. Błędy: SpicyWaitTimeoutError, gdy upłynie timeoutMs. Zawiera taskId, timeoutMs i lastTask (ostatni zaobserwowany stan). Zadanie nie zostaje anulowane; możesz po prostu poczekać ponownie. Poza tym SpicyApiError (na przykład 404) i SpicyTransportError.
const task = await client.waitForTask("job_01k3m8x9q2z4v7n5p6r8s0t1w3", {
timeoutMs: 30 * 60_000,
onUpdate: (update) => console.log(new Date().toISOString(), update.state),
});
console.log(task.state, task.output?.assets?.[0]?.url);retryTask(taskId, options?)
Uruchamia nieudane zadanie ponownie jako zupełnie nowe zadanie z tym samym modelem i tymi samymi danymi wejściowymi. Pierwotne zadanie pozostaje dokładnie takie, jakie było, a nowe wskazuje na nie przez sourceTaskId.
retryTask(taskId: string, options?: { idempotencyKey?: string; signal?: AbortSignal }): Promise<RetryTaskResult>Działa dla zadań failed i expired (oraz starych canceled). Zadanie, które się powiodło albo nadal trwa, zostaje odrzucone z kodem 409. Cena, twoje saldo i uprawnienia klucza są sprawdzane ponownie, a nowe zadanie zachowuje ustawienie przechowywania z zadania pierwotnego.
Ponieważ dane wejściowe są używane ponownie bez zmian, ponowienie pomaga tylko wtedy, gdy niepowodzenie było przejściowe. Najpierw przeczytaj errorCode zadania źródłowego: przy rate_limited, upstream_unavailable, timeout i upstream_failed warto po przerwie ponowić raz; generation_failed i invalid_asset wymagają zmienionego żądania wysłanego przez createTask; content_rejected, invalid_request i unsupported_combination nie powinny być ponawiane bez zmian. Zobacz Najczęstsze kody błędów i Błędy.
Zwraca taskId (nowe zadanie), sourceTaskId (pierwotne zadanie), state, estimatedCost i deadlineAt.
Płatne: tak. Nowe zadanie ma własną blokadę i jest rozliczane osobno, jeśli się powiedzie. Przekaż idempotencyKey, żeby dwukrotne uruchomienie twojego kodu ponawiającego utworzyło tylko jedno ponowienie.
Błędy: 409 (zadania nie można ponowić), a także błędy salda, limitów i dostępności wymienione przy createTask. Jeśli pierwotne dane wejściowe korzystały z przesłanego pliku spicy://, taki plik można używać tylko przez jeden dzień; prześlij go ponownie i użyj zamiast tego createTask.
retryTask nie służy do obsługi błędów sieci
Jeśli samo createTask zakończyło się błędem sieci, nie masz zadania, które można by ponowić. Wyślij createTask jeszcze raz z tym samym kluczem idempotentności. retryTask używaj tylko dla zadania, którego state odczytano jako failed albo expired.
Kompletny przykład jest w przepisie Ponów nieudane zadanie.
listTasks(options?)
Wyświetla zadania utworzone tym kluczem API, od najnowszych, po jednej stronie naraz. Przydaje się, żeby odnaleźć pracę po ponownym uruchomieniu programu albo po przegapionym webhooku.
listTasks(options?: ListTasksOptions): Promise<TaskList>Właściwość
Typ
Zwraca items, hasMore i – jeśli są kolejne strony – nextCursor. Każda pozycja ma taskId, model, state, cost, settled, createdAt, deadlineAt i completedAt. Dane wejściowe, wyniki i linki nie są dołączane; dla potrzebnego zadania wywołaj getTask.
Płatne: nie. Błędy: SpicyApiError (na przykład 400 przy nieprawidłowym zakresie dat), SpicyTransportError.
let cursor;
do {
const page = await client.listTasks({
from: "2026-09-01", // na każdej stronie podawaj te same daty
to: "2026-09-08",
state: "failed",
limit: 100,
cursor,
});
for (const item of page.items) console.log(item.taskId, item.model, item.createdAt);
cursor = page.hasMore ? page.nextCursor : undefined;
} while (cursor);getBalance()
Pokazuje, ile pieniędzy jest na twoim koncie.
getBalance(options?: { signal?: AbortSignal }): Promise<Balance>| Pole | Znaczenie |
|---|---|
available | Pieniądze, z których można opłacić nowe zadania. Kwota może być ujemna, gdy twoje konto korzysta z zatwierdzonego kredytu |
held | Pieniądze odłożone na zadania, które jeszcze trwają |
total | available plus held |
funding | Opcjonalne rozbicie: środki przedpłacone, przyznane środki promocyjne (z modelami, do których się odnoszą, i terminem ważności) oraz ewentualny kredyt. Starsze odpowiedzi mogą go nie zawierać |
Płatne: nie. Nie musisz sprawdzać salda przed każdym zadaniem: SpicyAPI sprawdza je, gdy tworzysz zadanie, i odpowiada 40201, jeśli środków nie wystarcza. Wszystkie pola dotyczące środków opisuje strona Rozliczenia.
const balance = await client.getBalance();
console.log(`Available: $${balance.available} (on hold: $${balance.held})`);getUsage(options?)
Pokazuje, ile wydał ten klucz API, w podziale na dni i modele.
getUsage(options?: { from?: string; to?: string; signal?: AbortSignal }): Promise<UsageReport>from i to to daty UTC (YYYY-MM-DD); dzień from jest uwzględniany, a dzień to nie. Pomiń oba, żeby dostać ostatnie siedem dni łącznie z dzisiejszym. Zakres może obejmować najwyżej 92 dni.
| Pole | Znaczenie |
|---|---|
totalCalls | Liczba zadań w zakresie |
totalSpend | Tylko ostateczne opłaty; pieniądze wciąż zablokowane nie są liczone |
days | Jedna pozycja na każdy dzień z wywołaniami: day, calls, succeeded, failed, spend |
models | Jedna pozycja na każdy model: model, calls, succeeded, failed, spend |
Płatne: nie. Ten raport jest cięższy od innych odczytów, więc ma dodatkowy limit 30 żądań na minutę; używaj go do raportów, a nie w pętli. Błędy: SpicyApiError (429 powyżej tego limitu), SpicyTransportError.
const usage = await client.getUsage({ from: "2026-09-01", to: "2026-09-08" });
console.log(`${usage.totalCalls} tasks, $${usage.totalSpend} spent`);
for (const row of usage.models) console.log(row.model, row.calls, "$" + row.spend);Przesyłanie plików
Gdy model potrzebuje na wejściu obrazu, wideo albo dźwięku, możesz wpisać w pole wejściowe publiczny link HTTPS do pliku. Plik z twojego komputera najpierw prześlij. Dostaniesz adres spicy:// – wpisz go w pole wejściowe zamiast linku. Przesyłanie jest bezpłatne.
- Obrazy (JPEG, PNG, WebP, GIF): do 10 MiB.
- Wideo (MP4, WebM) i dźwięk (MP3, WAV): do 90 MiB i do 600 sekund długości.
- Model może ustawić w swoim schemacie niższy limit.
- Przesłanego pliku można używać przez jeden dzień i tylko z twojego konta.
uploadFile(path, options?)
Przesyła plik z dysku.
uploadFile(path: string, options?: { contentType?: string; signal?: AbortSignal }): Promise<UploadedFile>Typ pliku jest rozpoznawany po rozszerzeniu: .jpg, .jpeg, .png, .webp, .gif, .mp4, .webm, .mp3 albo .wav. Dla każdej innej nazwy przekaż contentType: image/jpeg, image/png, image/webp, image/gif, video/mp4, video/webm, audio/mpeg albo audio/wav.
| Zwracane pole | Znaczenie |
|---|---|
uri | Adres spicy://, który wpisujesz w dane wejściowe zadania |
expiresAt | Kiedy adres przestanie działać (po jednym dniu) |
fileId, status, bytes, contentType, sha256 | Szczegóły zapisanego pliku; status ma wartość ready |
width, height, durationSeconds | Zmierzone wymiary albo długość, jeśli dotyczą |
Płatne: nie. Błędy: TypeError, gdy typu nie da się rozpoznać – jego komunikat wymienia wszystkie rozpoznawane rozszerzenia: contentType is required unless the file extension is one of: gif, jpeg, jpg, png, webp, mp4, webm, mp3, wav; błąd Node.js ENOENT, gdy ścieżka jest błędna; SpicyUploadError (z polem status), gdy magazyn odrzucił bajty albo plik jest większy, niż wolno (413); SpicyApiError, gdy SpicyAPI odrzuciło przesłanie; SpicyTimeoutError, gdy wysyłanie trwało dłużej niż uploadTimeoutMs.
const file = await client.uploadFile("./photo.jpg");
console.log(file.uri, "can be used until", file.expiresAt);uploadBytes(data, options)
Przesyła bajty, które masz już w pamięci, na przykład plik, który użytkownik wysłał na twój serwer. Można przekazać Buffer z Node.js.
uploadBytes(data: Uint8Array, options: { contentType: string; signal?: AbortSignal }): Promise<UploadedFile>contentType jest wymagany. Wynik i błędy są takie same jak w uploadFile.
const response = await fetch("https://example.com/reference.png");
const bytes = new Uint8Array(await response.arrayBuffer());
const file = await client.uploadBytes(bytes, { contentType: "image/png" });uploadBase64(value, options?)
Przesyła plik, który masz w postaci tekstu Base64: albo jako data URI, na przykład data:image/png;base64,iVBOR…, albo jako zwykły Base64 razem z contentType.
uploadBase64(value: string, options?: { contentType?: string; signal?: AbortSignal }): Promise<UploadedFile>Kodowanie i rozmiar są najpierw sprawdzane na twoim komputerze; nieprawidłowe albo za duże dane rzucają TypeError i nic nie zostaje przesłane. Wynik jest taki sam jak w uploadFile.
createUploadUrl i commitUploadedFile
Trzy powyższe funkcje pomocnicze są zbudowane z tych dwóch wywołań niższego poziomu. Potrzebujesz ich tylko wtedy, gdy bajty wysyłasz z innego miejsca niż twój proces Node.js.
createUploadUrl(contentType: string, bytes: number, options?: { signal?: AbortSignal }): Promise<UploadTicket>
commitUploadedFile(fileId: string, options?: { signal?: AbortSignal }): Promise<UploadedFile>createUploadUrlzwracafileId,uploadUrl,method(PUT),headers,maxBytesiexpiresAt.- Wyślij plik żądaniem
PUTnauploadUrl, dokładnie z wymienionymiheadersi bez nagłówkaAuthorization. Zadeklarowany rozmiar musi zgadzać się z plikiem. commitUploadedFile(fileId)sprawdza zapisany plik i zwracauriz adresemspicy://. Pliku przesłanego, ale nigdy niezatwierdzonego, nie da się użyć.
Cały proces, łącznie z przesyłaniem z przeglądarki, opisuje strona Media.
createDownloadUrl(taskId, key?)
Tworzy świeży tymczasowy link do pobrania jednego pliku wynikowego. Rzadko jest potrzebna: getTask, waitForTask, run i webhooki już zawierają gotowy url dla każdego wyniku.
createDownloadUrl(taskId: string, key?: string, options?: { signal?: AbortSignal }): Promise<DownloadTicket>Przekaż key pliku, żeby go wybrać, albo pomiń ten argument, żeby dostać pierwszy plik. Zwraca key, url i expiresAt (około 20 minut).
Płatne: nie. Błędy: SpicyApiError z 409, dopóki plik jest jeszcze kopiowany (odczekaj kilka sekund), albo z 404, gdy zadanie nie zakończyło się powodzeniem albo podany key do niego nie należy.
purgeTask(taskId)
Trwale usuwa wygenerowane media, dane wyniku (zapisane dane rezultatu), prompt i inny tekst wejściowy zakończonego zadania – wcześniej, niż zrobiłby to okres przechowywania. Użyj tej metody, gdy wyniku nie wolno przechowywać.
purgeTask(taskId: string, options?: { signal?: AbortSignal }): Promise<PurgeTaskResult>| Zwracane pole | Znaczenie |
|---|---|
contentState | purged, gdy treść została usunięta. present oznacza, że tym razem nic nie usunięto; spróbuj ponownie później |
purgedAt | Kiedy treść została usunięta po raz pierwszy |
contentRemovedBy | user, gdy treść usunięto na twoje polecenie |
billingRetained | Zawsze true: zapis kosztu zadania zostaje zachowany |
mediaDeletionPending | true, dopóki zapisane pliki są jeszcze usuwane w tle, zwykle krócej niż minutę |
Płatne: nie – i to nie jest zwrot pieniędzy. Wpis w księdze rozliczeń, pobrana kwota, model, stan, znaczniki czasu i identyfikator żądania pozostają w zapisach. Usunięcia nie można cofnąć. Ponowne wywołanie dla tego samego zadania jest bezpieczne. Błędy: SpicyApiError z 400, gdy zadanie ma jeszcze stan queued albo running, i z 404, gdy zadanie nie istnieje. Przyjętego zadania nie da się anulować i nie ma API do anulowania, więc poczekaj na nie przez waitForTask, a potem usuń jego treść. Zobacz Przechowywanie i usuwanie danych.
const result = await client.purgeTask("job_01k3m8x9q2z4v7n5p6r8s0t1w3");
console.log(result.contentState, "billing kept:", result.billingRetained);Funkcje webhooków
verifyWebhook i computeWebhookSignature to zwykłe funkcje, a nie metody klienta. Razem z działającym przykładem opisuje je sekcja Webhooki.
Funkcje offline i typy
Działają bez połączenia z siecią i bez klucza.
| Eksport | Co daje |
|---|---|
searchDocumentation(query, limit) | Przeszukuje mały, wbudowany indeks tej dokumentacji. Zwraca najwyżej limit pozycji (od 1 do 25, domyślnie 10), każdą z polami slug, title, summary, keywords i url, gdzie url ma postać https://docs.spicyapi.ai/docs/<slug>. Brak dopasowań daje pustą tablicę; pusty query zwraca pierwsze pozycje |
DOCUMENTATION | Cały ten wbudowany indeks jako tablica, po jednej pozycji na stronę dokumentacji |
readOpenApiContract() | Kontrakt OpenAPI 3.1 dołączony do tej wersji SDK, jako tekst YAML |
DEFAULT_API_BASE_URL, DEFAULT_SERVICE_BASE_URL, DEFAULT_TIMEOUT_MS, DEFAULT_UPLOAD_TIMEOUT_MS, DEFAULT_WAIT_TIMEOUT_MS, DEFAULT_WAIT_INTERVAL_MS, MAX_API_RESPONSE_BYTES | Wartości domyślne opisane na tej stronie. Odpowiedzi API większe niż MAX_API_RESPONSE_BYTES (4 MiB) są odrzucane |
Eksportowane są też typy TypeScriptu dla każdego żądania i każdej odpowiedzi, na przykład import type { TaskRecord, ApiModel, Balance } from "@spicyapi/sdk".
| Ścieżka importu | Zawiera |
|---|---|
@spicyapi/sdk | Wszystko, co opisuje ta strona |
@spicyapi/sdk/webhooks | Tylko verifyWebhook, computeWebhookSignature i SpicyWebhookVerificationError – lekki import dla odbiornika webhooków |
@spicyapi/sdk/openapi | Tylko surowe typy OpenAPI: paths, components i operations |
Webhooki
Webhook pozwala SpicyAPI samemu dać ci znać, że zadanie się zakończyło, zamiast żeby twój program pytał o to co kilka sekund. Każdemu zadaniu podajesz callBackUrl, czyli adres na twoim własnym serwerze, a SpicyAPI wysyła tam zapis zakończonego zadania.
Dlaczego trzeba weryfikować każdy webhook
Adres twojego webhooka to publiczny URL: każdy, kto go odgadnie, może wysłać na niego fałszywą wiadomość „zadanie się powiodło, oto plik”. Jeśli twój kod w nią uwierzy, może dostarczyć klientowi niewłaściwy plik albo oznaczyć zamówienie jako zrealizowane.
Każde prawdziwe dostarczenie jest podpisane tajnym kluczem podpisującym, który znacie tylko ty i SpicyAPI. verifyWebhook oblicza ten podpis na nowo z dokładnie tych bajtów, które odebrano, i porównuje oba podpisy. Sprawdza też czas dostarczenia, żeby atakujący nie mógł przechwycić prawdziwej wiadomości i wysłać ci jej ponownie kilka godzin później. Reaguj na wiadomość dopiero wtedy, gdy przejdzie weryfikację.
Konfiguracja
Zdobądź klucz podpisujący
Klucz podpisujący powstaje automatycznie przy rejestracji, ale jego wartość nigdy nie jest pokazywana, więc musisz utworzyć taki, który da się skopiować. W konsoli otwórz Webhooki i kliknij Wygeneruj (albo Wymień, jeśli karta pokazuje, że klucz jest skonfigurowany). Nowy klucz jest pokazywany tylko raz: skopiuj go i zapisz na serwerze jako SPICY_WEBHOOK_SECRET. Jeśli go zgubisz, wymień klucz jeszcze raz.
Wymiana nie ma okresu przejściowego: stary klucz natychmiast przestaje podpisywać, a nowym są podpisywane nawet dostarczenia, które są jeszcze ponawiane. Zaraz po wymianie zaktualizuj każdy działający odbiornik. Szczegóły dotyczące klucza podpisującego opisuje strona Webhooki.
Uruchom odbiornik pod publicznym adresem HTTPS
SpicyAPI może wywołać tylko adres osiągalny z internetu, zaczynający się od https://. Zwykłe http://, localhost i adresy sieci prywatnych są odrzucane. W trakcie pracy nad kodem udostępnij lokalny serwer przez usługę tunelującą, która nada mu publiczny adres HTTPS, albo do czasu wdrożenia korzystaj z odpytywania. Pełne zasady dotyczące adresu zwrotnego opisuje strona Webhooki.
Wysyłaj callBackUrl razem z zadaniami
Dodaj callBackUrl: "https://your-app.example.com/hooks/spicy" do żądania, które przekazujesz do createTask. Zobacz przepis Wyniki przez webhook zamiast odpytywania.
Minimalny odbiornik
Ten odbiornik korzysta tylko z wbudowanego w Node.js modułu http, więc nie trzeba niczego doinstalowywać. Zapisz go jako receiver.mjs i uruchom poleceniem node receiver.mjs.
import { createServer } from "node:http";
import { SpicyWebhookVerificationError, verifyWebhook } from "@spicyapi/sdk/webhooks";
const secret = process.env.SPICY_WEBHOOK_SECRET;
if (!secret) throw new Error("Set SPICY_WEBHOOK_SECRET first");
// Identyfikatory już obsłużonych dostarczeń. Na produkcji użyj tabeli w bazie danych,
// żeby lista przetrwała ponowne uruchomienie.
const handled = new Set();
const server = createServer(async (request, response) => {
if (request.method !== "POST" || request.url !== "/hooks/spicy") {
response.writeHead(404).end();
return;
}
if (request.headers["x-webhook-payload-version"] !== "2") {
response.writeHead(400).end();
return;
}
// 1. Odczytaj treść jako surowe bajty. Nie parsuj jej przed weryfikacją.
const chunks = [];
for await (const chunk of request) chunks.push(chunk);
const rawBody = Buffer.concat(chunks);
// 2. Sprawdź podpis i znacznik czasu.
let event;
try {
event = verifyWebhook({
rawBody,
secret,
timestamp: String(request.headers["x-webhook-timestamp"] ?? ""),
signature: String(request.headers["x-webhook-signature"] ?? ""),
payloadVersion: 2,
});
} catch (error) {
const reason = error instanceof SpicyWebhookVerificationError ? error.reason : String(error);
console.warn("Rejected a webhook:", reason);
response.writeHead(401).end();
return;
}
// 3. Odpowiedz szybko. SpicyAPI czeka najwyżej 15 sekund, a potem ponawia.
response.writeHead(204).end();
// 4. To samo dostarczenie może przyjść więcej niż raz. Obsłuż każde tylko raz.
if (!event.deliveryId || handled.has(event.deliveryId)) return;
handled.add(event.deliveryId);
if (!("data" in event.payload)) return; // w wersji 2 zadanie jest w polu data
const task = event.payload.data; // te same pola, które zwraca getTask()
console.log("Task", task.taskId, "is", task.state);
if (task.state === "succeeded") {
console.log("Result:", task.output?.assets?.[0]?.url);
}
});
server.listen(8080, () => console.log("Listening on http://localhost:8080/hooks/spicy"));Co jest w nim ważne:
- Surowe bajty. Podpis obejmuje dokładnie te bajty, które wysłało SpicyAPI. Jeśli framework najpierw sparsuje JSON, a ty zamienisz go z powrotem na tekst, bajty się zmienią i każda weryfikacja się nie powiedzie. W Express użyj na tej trasie
express.raw({ type: "application/json" }). - Szybka odpowiedź. Dowolna odpowiedź
2xxoznacza „odebrano”. Brak odpowiedzi w ciągu 15 sekund albo jakikolwiek inny status sprawia, że SpicyAPI ponawia dostarczenie kilka razy w ciągu kolejnych godzin. Powolną pracę wykonuj dopiero po odpowiedzi. - Duplikaty się zdarzają. Ponowienia i chwilowe problemy z siecią mogą dostarczyć tę samą wiadomość dwa razy.
event.deliveryIdpozostaje taki sam przy ponowieniach, więc zapamiętuj obsłużone identyfikatory. - O powodzeniu mówi
state. Dostarczenie dotyczące nieudanego zadania nadal jest prawidłowym dostarczeniem. Czytajtask.state, nigdycodeze struktury odpowiedzi. - Na produkcji najpierw zapisz zweryfikowane zdarzenie w bazie danych i odpowiadaj
2xxdopiero po udanym zapisie, a identyfikatory zadań, których nie utworzyła twoja aplikacja, ignoruj. Przewodnik Webhooki zawiera wersję opartą na bazie danych.
Przetestuj odbiornik przed uruchomieniem na produkcji
computeWebhookSignature tworzy prawdziwy podpis, więc możesz wysłać sobie podpisane dostarczenie testowe. Gdy receiver.mjs działa w jednym terminalu, a w drugim ustawiono ten sam SPICY_WEBHOOK_SECRET, uruchom w tym drugim:
import { computeWebhookSignature } from "@spicyapi/sdk";
const secret = process.env.SPICY_WEBHOOK_SECRET;
if (!secret) throw new Error("Set SPICY_WEBHOOK_SECRET first");
const body = JSON.stringify({
code: 200,
msg: "success",
request_id: "whd_local_test_1",
data: {
taskId: "job_local_test",
state: "succeeded",
cost: "0.01",
settled: true,
output: { assets: [{ url: "https://example.com/result.png" }] },
},
});
const timestamp = String(Math.floor(Date.now() / 1000));
const signature = computeWebhookSignature("job_local_test", timestamp, body, secret);
const response = await fetch("http://localhost:8080/hooks/spicy", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Webhook-Timestamp": timestamp,
"X-Webhook-Signature": signature,
"X-Webhook-Payload-Version": "2",
},
body,
});
console.log("Your receiver answered", response.status); // oczekiwany wynik: 204Uruchom go dwa razy: odbiornik wypisze zadanie tylko raz, bo drugie dostarczenie ma to samo request_id. Zmień po podpisaniu jeden znak w body, a odbiornik odpowie 401.
verifyWebhook(options)
verifyWebhook(options: VerifyWebhookOptions): VerifiedWebhookWłaściwość
Typ
Zwraca payload (sparsowaną treść; w wersji 2 zadanie jest w payload.data), payloadVersion, taskId, deliveryId (request_id z wersji 2, taki sam przy każdym ponowieniu) i timestamp. Funkcja jest synchroniczna: await nie jest potrzebne.
Gdy weryfikacja się nie powiedzie, rzuca SpicyWebhookVerificationError, którego reason ma jedną z wartości: invalid_signature, stale_timestamp, invalid_timestamp, invalid_json, invalid_payload, body_too_large albo invalid_secret. Odpowiedz wtedy 401 i nie reaguj na wiadomość.
computeWebhookSignature(taskId, timestamp, rawBody, secret) zwraca podpis Base64 dla danej treści. Potrzebujesz tej funkcji tylko do testów takich jak powyższy. Pełny algorytm podpisywania, przydatny w innych językach, opisuje strona Webhooki.
Wskazówka dla TypeScriptu: payload ma typ obejmujący oba formaty. Zawęź go przez "data" in event.payload, tak jak robi to odbiornik, albo przekaż oczekiwany typ: verifyWebhook<components["schemas"]["TaskRecordEnvelope"]>(…) z import type { components } from "@spicyapi/sdk".
Obsługa błędów
Coś może pójść nie tak na dwa różne sposoby i każdy z nich wymaga innej obsługi:
- Nie udaje się wywołanie. SDK rzuca błąd: klucz był nieprawidłowy, saldo za niskie, połączenie zostało zerwane. Przechwyć go przez
try/catch. - Nie udaje się zadanie. Wywołanie zadziałało, ale generacja nie. Nic nie jest rzucane: zwrócone zadanie ma
state: "failed"ierrorCode. Nieudane lub wygasłe zadania zwykle zwalniają całą blokadę. Niektóre odmowy dotyczące treści są płatne według zaakceptowanej wyceny, wyłącznie gdy strona modelu wyraźnie wskazuje ten wyjątek. Sprawdźsettledicost, aby poznać ostateczną kwotę.
Klasy błędów
| Klasa | Kiedy się pojawia | Przydatne pola |
|---|---|---|
SpicyApiError | SpicyAPI odpowiedziało błędem, na przykład odrzuciło klucz albo saldo jest za niskie | code (kod biznesowy, na podstawie którego działasz, na przykład 40201), status (status HTTP), message, requestId, retryAfterSeconds, rateLimitLimit, rateLimitRemaining |
SpicyTransportError | Nie nadeszła żadna użyteczna odpowiedź: awaria sieci, problem z DNS albo odpowiedź większa niż 4 MiB | message, cause (pierwotny błąd) |
SpicyTimeoutError | Pojedyncze żądanie trwało dłużej niż timeoutMs (albo uploadTimeoutMs). To odmiana SpicyTransportError | timeoutMs |
SpicyWaitTimeoutError | Czas run albo waitForTask się skończył. Zadanie nadal działa | taskId, timeoutMs, lastTask |
SpicyUploadError | Magazyn odrzucił przesłany plik albo plik był za duży | status |
SpicyWebhookVerificationError | Webhook nie przeszedł weryfikacji | reason |
TypeError | Samo wywołanie było nieprawidłowe – brak ustawionego klucza, opcja poza zakresem, pusty identyfikator. Nic nie zostało wysłane | message |
O dalszym działaniu decyduj na podstawie code, nigdy message: komunikaty są dla ludzi, ich brzmienie może się zmieniać i zależą od ustawienia języka błędów na twoim koncie. Podawaj requestId przy każdym kontakcie ze wsparciem – pozwala nam znaleźć dokładnie to żądanie.
import { randomUUID } from "node:crypto";
import {
SpicyApiError,
SpicyClient,
SpicyTransportError,
SpicyWaitTimeoutError,
} from "@spicyapi/sdk";
const client = new SpicyClient();
const MODEL_ID = "MODEL_ID_FROM_CATALOG"; // prawdziwy identyfikator z listModels()
if (MODEL_ID === "MODEL_ID_FROM_CATALOG") throw new Error("Replace MODEL_ID_FROM_CATALOG first");
const idempotencyKey = randomUUID(); // zachowaj go: do ponownej próby potrzebny jest ten sam klucz
try {
const task = await client.run(
{ model: MODEL_ID, input: { prompt: "A paper lantern on a rainy street" } },
{ idempotencyKey, onAccepted: ({ taskId }) => console.log("Task ID:", taskId) },
);
if (task.state === "succeeded") console.log("Done:", task.output?.assets?.[0]?.url);
else console.log(`Finished as ${task.state} (${task.errorCode}). Cost: ${task.cost}; settled: ${task.settled}.`);
} catch (error) {
if (error instanceof SpicyWaitTimeoutError) {
// Skończyło się twoje czekanie, a nie zadanie. Sprawdź je później po tym samym identyfikatorze.
console.log(`Still running: ${error.taskId}. Call waitForTask() again later.`);
} else if (error instanceof SpicyApiError) {
console.log(`SpicyAPI said no: ${error.code} (HTTP ${error.status}) ${error.message}`);
console.log(`Quote this if you contact support: ${error.requestId}`);
if (error.code === 40201) console.log("Your balance is too low. Top up in the console.");
if (error.code === 40202) console.log("A spend cap was reached. Raise it on the API keys page.");
if (error.code === 429) console.log(`Too many requests. Wait ${error.retryAfterSeconds ?? 1} s.`);
} else if (error instanceof SpicyTransportError) {
// Awaria sieci albo lokalne przekroczenie czasu: zadanie mogło powstać albo nie.
// Wyślij to samo żądanie z TYM SAMYM idempotencyKey, żeby bezpiecznie to sprawdzić.
console.log(`Network problem (${error.message}). Retry with key ${idempotencyKey}.`);
} else {
throw error; // TypeError oznacza tu błąd w samym wywołaniu, na przykład brak klucza
}
}Najczęstsze kody błędów
error.code | Znaczenie | Czy warto ponawiać? |
|---|---|---|
400 | Nieprawidłowe żądanie, zwykle pole wejściowe, którego model nie przyjmuje | Nie. Popraw żądanie |
401 | Brak klucza albo klucz ma zły format, został unieważniony lub wygasł | Nie. Sprawdź klucz |
40201 | Saldo za niskie na blokadę | Nie. Najpierw doładuj konto |
40202 | Osiągnięto limit wydatków | Nie. Podnieś limit albo poczekaj na odnowienie dzienne lub miesięczne |
40301 | Ten klucz nie może używać tego modelu | Nie. Zmień listę modeli klucza |
404 | Brak takiego modelu albo zadania dla tego klucza | Nie |
409 | Konflikt z bieżącym stanem, na przykład klucz idempotentności użyty ponownie do innej pracy | To zależy; zobacz Błędy |
40901 | Wycena wygasła albo zmieniła się cena | Pobierz nową wycenę, a potem potwierdź |
429 | Zbyt wiele żądań | Tak, po retryAfterSeconds. Odczyty SDK ponawia już za ciebie |
500 | Coś zepsuło się po stronie SpicyAPI | Tak, po przerwie |
50301 | Model jest teraz niedostępny | Tak, później – albo wybierz inny model |
Gdy nie powiedzie się zadanie, task.errorCode ma jedną z wartości: invalid_request, unsupported_combination, content_rejected, rate_limited, upstream_unavailable, generation_failed, timeout, invalid_asset albo upstream_failed. retryTask wysyła pierwotne dane wejściowe ponownie bez zmian, więc następny krok wybieraj według kodu:
task.errorCode | Co zrobić |
|---|---|
rate_limited, upstream_unavailable, timeout, upstream_failed | Odczekaj, a potem raz użyj retryTask. Jeśli timeout się powtarza, spróbuj przez createTask z krótszym czasem trwania albo niższą rozdzielczością |
generation_failed | Zmień prompt albo obraz i wyślij nowe zadanie przez createTask; te same dane wejściowe zwykle zawodzą w ten sam sposób |
invalid_asset | Prześlij plik ponownie i wyślij nowe zadanie przez createTask z nowym uri; ponowienie użyłoby starego pliku |
invalid_request, unsupported_combination | Nie ponawiaj bez zmian. Zmień ustawienia, a potem użyj createTask |
content_rejected | Nie ponawiaj bez zmian. Wybrany model odrzucił żądanie zgodnie z własnymi zasadami; zmień prompt albo wybierz inny model |
Pełna tabela jest na stronie Błędy.
Zachowaj swoje wyniki
urlwyniku działa około 20 minut. Po świeży wywołaj ponowniegetTask.- Wygenerowane pliki są usuwane najpóźniej po około 14 dniach, a wcześniej, jeśli tak wynika z ustawień twojego konta albo z
retentionSeconds. Przesłane pliki wejściowe są przechowywane jeden dzień, a zapisane prompty – 30 dni. - To, co chcesz zachować, pobierz do własnego magazynu, gdy tylko zadanie się powiedzie. Nie zapisuj w bazie danych samego tymczasowego
url. Szczegóły: Przechowywanie i usuwanie danych.
Gotowe przepisy
W przepisach MODEL_ID_FROM_CATALOG jest symbolem zastępczym. Zanim uruchomisz przepis, zastąp go wartością model pasującej pozycji z katalogu na żywo (listModels() albo GET /api/v1/models), w której zarówno enabled, jak i available mają wartość true. Jeśli o tym zapomnisz, każdy przepis zatrzyma się z czytelnym komunikatem.
Z obrazu do wideo: najpierw prześlij lokalne zdjęcie
import { randomUUID } from "node:crypto";
import { SpicyClient } from "@spicyapi/sdk";
const client = new SpicyClient();
// Zastąp identyfikatorem modelu image-to-video z katalogu na żywo:
// await client.listModels({ modality: "video", task: "image-to-video" })
const MODEL_ID = "MODEL_ID_FROM_CATALOG";
if (MODEL_ID === "MODEL_ID_FROM_CATALOG") throw new Error("Replace MODEL_ID_FROM_CATALOG first");
// 1. Prześlij lokalne zdjęcie. Bezpłatne. Adres spicy:// działa przez jeden dzień.
const file = await client.uploadFile("./photo.jpg");
console.log("Uploaded:", file.uri);
// 2. Wypisz schemat modelu: które pole przyjmuje obraz i które pola są wymagane.
const { inputSchema } = await client.getModel(MODEL_ID);
console.log(JSON.stringify(inputSchema, null, 2));
// 3. Utwórz wideo i poczekaj na nie. Wideo często powstaje kilka minut.
const task = await client.run(
{
model: MODEL_ID,
input: {
prompt: "Slow push-in, light rain, neon reflections on wet stone",
image_url: file.uri, // użyj nazwy pola, którą pokazuje wypisany wyżej schemat
},
},
{
idempotencyKey: randomUUID(),
timeoutMs: 30 * 60_000,
onAccepted: ({ taskId }) => console.log("Task ID (save it):", taskId),
},
);
console.log(task.state, task.output?.assets?.[0]?.url ?? task.errorCode);Wypisany schemat rozstrzyga o nazwach pól i o tym, które pola są wymagane; model wideo może też wymagać czasu trwania albo rozdzielczości, a cena często od nich zależy. Jeśli twój obraz jest już dostępny w internecie pod publicznym adresem HTTPS, pomiń przesyłanie i wpisz ten adres bezpośrednio w pole obrazu. Jeśli chcesz zobaczyć cenę, najpierw uruchom quoteTask z tym samym żądaniem.
Generowanie wsadowe z ograniczoną współbieżnością
Uruchomienie setek zadań naraz może zderzyć się z twoimi limitami wydatków i limitem żądań. Ten skrypt przetwarza listę promptów tak, żeby jednocześnie trwały najwyżej dwa zadania.
import { SpicyApiError, SpicyClient } from "@spicyapi/sdk";
const client = new SpicyClient();
const MODEL_ID = "MODEL_ID_FROM_CATALOG"; // prawdziwy identyfikator z listModels()
if (MODEL_ID === "MODEL_ID_FROM_CATALOG") throw new Error("Replace MODEL_ID_FROM_CATALOG first");
// Zmieniaj nazwę partii przy każdej zmianie promptów.
const BATCH_NAME = "lanterns-2026-09-17";
const prompts = [
"A paper lantern on a rainy street",
"A paper lantern floating on a lake",
"A paper lantern in a snowy forest",
"A paper lantern above a night market",
];
const CONCURRENCY = 2; // ile zadań może trwać jednocześnie
const results = prompts.map((prompt) => ({ prompt, outcome: "not started", detail: "" }));
let nextIndex = 0;
async function worker() {
while (nextIndex < prompts.length) {
const index = nextIndex++;
try {
const task = await client.run(
{ model: MODEL_ID, input: { prompt: prompts[index] } },
{
// Ta sama nazwa partii + ta sama pozycja = ten sam klucz. Jeśli skrypt się wyłoży,
// a ty uruchomisz go ponownie w ciągu 24 godzin, dostaniesz z powrotem istniejące
// zadania, zamiast płacić za nie drugi raz.
idempotencyKey: `${BATCH_NAME}-${index}`,
timeoutMs: 20 * 60_000,
},
);
results[index].outcome = task.state;
results[index].detail = task.output?.assets?.[0]?.url ?? task.errorCode ?? "";
} catch (error) {
results[index].outcome = "error";
results[index].detail = error instanceof Error ? error.message : String(error);
// Brak środków (40201) albo przekroczony limit wydatków (40202): nie uruchamiaj nowych zadań.
if (error instanceof SpicyApiError && (error.code === 40201 || error.code === 40202)) {
nextIndex = prompts.length;
}
}
}
}
await Promise.all(Array.from({ length: CONCURRENCY }, () => worker()));
console.table(results);- Dlaczego klucze powstają z nazwy partii. Klucz idempotentności powiązany z „tą partią, pozycją 3” sprawia, że ponowne uruchomienie po awarii wykorzysta już utworzone zadania. Losowy klucz przy każdym uruchomieniu oznaczałby ponowną zapłatę za ten sam obraz.
- Dlaczego nazwa partii musi się zmieniać razem z promptami. Ponowne użycie klucza z innym żądaniem jest odrzucane z kodem
409. - Jak dobrać
CONCURRENCY. Każde trwające zadanie blokuje część twojego salda, a każde sprawdzenie stanu wlicza się do limitu żądań twojego konta. Zacznij od małej wartości i zwiększ ją, gdy partia będzie przechodzić bez problemów.
Wyniki przez webhook zamiast odpytywania
W aplikacji internetowej albo w procesie działającym w tle czekanie wewnątrz żądania to marnotrawstwo. Utwórz zadanie z callBackUrl, zapisz identyfikator zadania i pozwól, żeby wynikiem zajął się odbiornik, gdy wynik nadejdzie.
import { randomUUID } from "node:crypto";
import { SpicyClient } from "@spicyapi/sdk";
const client = new SpicyClient();
const MODEL_ID = "MODEL_ID_FROM_CATALOG"; // prawdziwy identyfikator z listModels()
if (MODEL_ID === "MODEL_ID_FROM_CATALOG") throw new Error("Replace MODEL_ID_FROM_CATALOG first");
// W prawdziwej aplikacji zapisz ten klucz razem z zamówieniem klienta przed wywołaniem.
const idempotencyKey = randomUUID();
const accepted = await client.createTask(
{
model: MODEL_ID,
input: { prompt: "A paper lantern on a rainy street" },
callBackUrl: "https://your-app.example.com/hooks/spicy",
},
{ idempotencyKey },
);
console.log(`Accepted ${accepted.taskId}; $${accepted.estimatedCost} is on hold.`);
// Zapisz teraz accepted.taskId. Gdy zadanie się zakończy, SpicyAPI wywoła twój odbiornik.Zabezpiecz się na wypadek dostarczeń, które nigdy nie dotrą, na przykład dlatego, że twój serwer nie działał przez wszystkie ponowienia: co jakiś czas wywołuj getTask dla zapisanych zadań, które są już dawno po swoim deadlineAt, a webhook dla nich nie przyszedł, albo przeglądaj listTasks strona po stronie. Historia dostarczeń webhooków, z przyciskiem ręcznego ponownego wysłania, jest na stronie Webhooki w konsoli.
Ponów nieudane zadanie
import { SpicyClient } from "@spicyapi/sdk";
const client = new SpicyClient();
const taskId = process.argv[2]; // uruchom tak: node retry.mjs job_...
if (!taskId) throw new Error("Usage: node retry.mjs <taskId>");
// Ponowienie wysyła pierwotne dane wejściowe bez zmian, więc opłaca się tylko przy przejściowych niepowodzeniach.
// Przy każdym innym kodzie zmień żądanie i użyj zamiast tego createTask.
const WORTH_ONE_RETRY = new Set(["rate_limited", "upstream_unavailable", "timeout", "upstream_failed"]);
const source = await client.getTask(taskId);
if (
(source.state !== "failed" && source.state !== "expired") ||
!WORTH_ONE_RETRY.has(source.errorCode ?? "")
) {
console.log(`Not retrying: the task is ${source.state} (${source.errorCode ?? "no error code"}).`);
} else {
// Stały klucz dla każdej próby ponowienia: dwukrotne uruchomienie skryptu tworzy jedno ponowienie, a nie dwa.
const retry = await client.retryTask(source.taskId, {
idempotencyKey: `retry-${source.taskId}-attempt-1`,
});
console.log(`New task ${retry.taskId}; $${retry.estimatedCost} is on hold.`);
const finished = await client.waitForTask(retry.taskId, { timeoutMs: 20 * 60_000 });
console.log("The retry finished as", finished.state);
}Skrypt ponawia tylko te niepowodzenia, które następnym razem mogą skończyć się inaczej – zajętą albo chwilowo niedostępną usługę, przekroczenie czasu albo nieznany błąd – i tylko raz, ze stałym kluczem idempotentności. Przy invalid_asset prześlij plik ponownie i utwórz nowe zadanie, bo ponowienie użyłoby wygasłego pliku. Przy generation_failed zmień prompt albo obraz i utwórz nowe zadanie. Przy content_rejected, invalid_request i unsupported_combination zmień żądanie, zamiast je ponawiać. Jeśli także to jedno ponowienie się nie powiedzie, przerwij i przyjrzyj się błędowi; przy powtarzającym się timeout spróbuj przez createTask z krótszym czasem trwania albo niższą rozdzielczością. Zobacz Błędy.
Rozwiązywanie problemów i najczęstsze pytania
Kod źródłowy
Pakiet jest na licencji MIT i powstaje otwarcie pod adresem github.com/spicyapi-ai/spicy-sdk. Tam przeczytasz kod każdej metody z tej strony, zgłosisz zachowanie inne niż opisane i zaproponujesz zmianę.
Co dalej
Katalog modeli i schematy
Jak czytać inputSchema modelu i dobierać prawidłowe dane wejściowe
Zadania asynchroniczne
Stany zadań, odpytywanie i znaczenie każdego pola
Webhooki
Format dostarczeń, harmonogram ponowień i weryfikacja w innych językach
Media
Przesyłanie plików, limity Base64 i pobieranie wyników
Błędy
Wszystkie kody błędów i to, które z nich warto ponawiać
Rozliczenia
Blokady, rozliczanie, limity wydatków i twoje saldo
Idempotentność
Bezpieczne ponawianie bez płacenia dwa razy
Narzędzie wiersza poleceń
Te same funkcje z terminala, bez pisania kodu
Serwer MCP
Niech asystent programistyczny AI korzysta ze SpicyAPI za ciebie
SDK dla Pythona
Ten sam proces w Pythonie, bez żadnych zależności w czasie działania
SDK dla Go
Ten sam proces w Go, wyłącznie na bibliotece standardowej
SDK dla PHP
Ten sam proces w PHP, bez żadnych zależności bibliotecznych
SDK dla Javy
Ten sam proces w Javie, z zapieczętowaną taksonomią błędów
Integracja produkcyjna
Lista kontrolna przed startem integracji, która przetrwa awarie – właściwy model, chronione klucze i saldo, potwierdzone koszty, trwałe śledzenie zadań, niezawodne callbacki i testy do wykonania przed uruchomieniem.
SDK dla Pythona
Oficjalne SDK SpicyAPI dla Pythona (spicyapi): instalacja, wycena i pierwsza generacja, wysyłanie plików z dysku, weryfikacja webhooków oraz wszystkie kody błędów.

