spicyapiDocumentazione
SDK
Contenuto principale

SDK Java

L’SDK Java ufficiale di SpicyAPI (ai.spicyapi spicyapi-java) — installalo, chiedi un preventivo, esegui la tua prima generazione, carica un file locale, verifica le callback e gestisci ogni errore con uno switch esaustivo su una gerarchia sigillata.

spicyapi-java è l’SDK Java ufficiale di SpicyAPI. Mette la generazione di immagini, video e audio dietro un unico SpicyClient: allega la tua chiave API, mappa la struttura della risposta su dei record, attende un’attività con un backoff limitato, carica i file locali con una sola chiamata, verifica le firme delle callback e trasforma ogni fallimento in un tipo di eccezione sigillato, così uno switch sulla gerarchia è esaustivo già in fase di compilazione.

Richiede Java 17 o successivo e ha una sola dipendenza: Jackson (com.fasterxml.jackson.core:jackson-databind), 2.12 o successiva — è la versione in cui i record sono diventati mappabili, e i tipi usati qui sono record. Jackson è una normale dipendenza di compilazione, né optional né shaded, quindi vince la versione che il tuo progetto ha già. Sul module path il client è ai.spicyapi, fissato nel manifest invece che dedotto dal nome del file JAR.

È lo strumento giusto?

Vuoi…Usa
Chiamare SpicyAPI dal tuo codice Java o JVMQuesto SDK
Chiamarlo da Node.js o TypeScriptL’SDK TypeScript
Chiamarlo da Python, Go o PHPL’SDK Python, Go o PHP
Generare contenuti da un terminale, senza scrivere codiceLa CLI
Far chiamare SpicyAPI al posto tuo da un assistente IAIl server MCP e l’Agent Skill
Usare un modello di chat o di testoUn client Java affermato per i formati OpenAI, Anthropic o Gemini: vedi Testo e streaming

Installalo

Aggiungi la dipendenza da Maven Central:

<dependency>
  <groupId>ai.spicyapi</groupId>
  <artifactId>spicyapi-java</artifactId>
  <version>0.1.0</version>
</dependency>

Oppure, con Gradle (Kotlin DSL):

implementation("ai.spicyapi:spicyapi-java:0.1.0")

Metti la chiave nell’ambiente, così non comparirà mai nel tuo codice né nel controllo di versione:

export SPICY_API_KEY="sk-spicy-..."

Il tuo primo risultato

Chiedi il prezzo, decidi, crea l’attività, aspetta, leggi l’output. Il preventivo è gratuito e non accantona nulla. run è createTask seguito da waitForTerminal.

SpicyClient client = SpicyClient.builder().apiKeyFromEnvironment().build();  // SPICY_API_KEY

CreateTaskRequest request = CreateTaskRequest.of(
        "MODEL_ID_FROM_CATALOG",
        Map.of("prompt", "a lantern in fog"));

TaskQuote quote = client.quote(request);              // gratuito; non accantona nulla
System.out.println("this will cost at most " + quote.maxCharge());

TaskRecord task = client.run(                         // createTask + waitForTerminal
        request.withQuote(quote), UUID.randomUUID().toString());

task.outputText().ifPresent(System.out::println);     // alcuni endpoint rispondono con testo, non con file
for (TaskAsset asset : task.assets()) {
    asset.url().ifPresent(System.out::println);
}

MODEL_ID_FROM_CATALOG è un segnaposto. Prendi un ID reale dal catalogo autenticato — client.listModels(), cioè GET /api/v1/models — e mai a memoria o da una pagina vecchia: i modelli vengono aggiunti, sospesi e ritirati. Costruisci l’input a partire dall’inputSchema di quel modello, che listModels(ModelFilter.all().withIncludeSchema(true)) restituisce; ogni modello accetta campi diversi.

Controlla outputText() prima di scorrere assets(). La trascrizione e gli endpoint simili mettono l’intero risultato nel campo di testo e non restituiscono alcun file; un client che cerca soltanto i file dichiara che un’attività perfettamente riuscita non ha prodotto nulla.

Gli importi sono stringhe decimali in dollari USA, mai numeri a virgola mobile, perché i float binari non riescono a contenere esattamente la maggior parte degli importi decimali. In questa API non esistono crediti né punti.

Parti da un file locale

Da immagine a video, sostituzione del soggetto e modifica delle immagini hanno tutte bisogno che il tuo materiale arrivi prima dalla nostra parte. uploadFile esegue i tre passaggi con una sola chiamata e restituisce un URI spicy://. È quell’URI che va nell’input. Un URL HTTPS raggiungibile pubblicamente può entrare direttamente e non richiede alcun caricamento.

UploadedFile uploaded = client.uploadFile(Path.of("/path/to/reference.png"));
// uploaded.uri() -> "spicy://f/fil_..."

TaskRecord task = client.run(
        CreateTaskRequest.of("MODEL_ID_FROM_CATALOG",
                Map.of("image", uploaded.uri(), "prompt", "slow dolly in")),
        UUID.randomUUID().toString());

I tre passaggi sono anche pubblici, per i byte che arrivano in streaming da qualche altra parte:

UploadTicket ticket = client.createUploadUrl(UploadContentType.IMAGE_PNG, data.length);
client.putUploadBytes(ticket, data);                  // va allo storage a oggetti, non all'API
UploadedFile file = client.commitUploadedFile(ticket.fileId());

Due dettagli del passaggio centrale falliscono in modo opaco se lo reimplementi. Le intestazioni del ticket fanno parte di ciò che l’URL prefirmato ha firmato, quindi vengono inoltrate immutate: toglierne una, rinominarne una o aggiungerne una «di aiuto» fa rispondere 403 allo storage. E non viene allegata alcuna chiave SpicyAPI: l’URL è esso stesso l’autorizzazione, e punta allo storage anziché a noi.

Le immagini sono accettate fino a 10 MiB, audio e video fino a 90 MiB. Il client rifiuta un file troppo grande in locale, prima ancora di chiedere il ticket. Vedi Media.

Conserva il risultato per meno tempo

Gli output e il testo delle richieste conservato restano per il periodo configurato sull’account. Una singola attività può chiederne meno:

TaskRecord task = client.run(request, idempotencyKey, Duration.ofHours(1));
task.retention().outputsExpireAt();                   // quello che e' stato applicato davvero

Può soltanto accorciare: il servizio tiene il più piccolo fra il tuo valore, l’impostazione dell’account e il massimo della piattaforma, quindi una richiesta non può mai riguadagnare una finestra più lunga di quanto l’account consenta. Duration.ZERO è un valore reale: gli output spariscono non appena l’attività raggiunge uno stato terminale. Un valore troppo grande viene limitato invece che rifiutato, ed è per questo che ciò che è stato applicato è retention() sul record, non il numero che hai inviato. retryTask non ha una conservazione propria: un nuovo tentativo eredita quella dell’attività di origine. Vedi Conservazione e distruzione.

Saldo, consumi e storico

Balance balance = client.getBalance();                          // tutto l'account
UsageReport usage = client.getUsage(                            // solo questa chiave
        LocalDate.of(2026, 9, 13), LocalDate.of(2026, 9, 20));  // [from, to)
TaskPage page = client.listTasks(TaskFilter.all().withState(TaskState.FAILED));

Un saldo è un’indicazione di budget, non una decisione di ammissione. Una sua parte può essere un bonus promozionale vincolato a determinati modelli: balance.fundingSources() ne è il dettaglio, e un modelSlugs vuoto su un bonus significa tutti i modelli, non nessuno. La risposta esatta per una singola richiesta esatta è quote(), che esegue le stesse regole di ammissione e non accantona nulla.

I consumi servono alla riconciliazione, non a seguire l’avanzamento. Contano soltanto gli addebiti liquidati, quindi un periodo ancora in corso risulta sempre più basso del reale; una liquidazione tardiva può cambiare un giorno passato; e portano con sé un limite di frequenza valido per tutto l’account, il cui limitatore si chiude in caso di dubbio, quindi interrogarli di continuo può escludere ogni chiave dell’account dai propri dati. Segui invece una singola attività con waitForTerminal.

listTasks riguarda soltanto la chiave che chiama e restituisce solo i metadati. Scorri le pagine spostando il cursore e senza cambiare nient’altro, e fissa from e to prima di iniziare: se non li imposti, il servizio ricalcola la propria finestra predefinita (to è il domani in UTC) a ogni richiesta, quindi uno scorrimento che attraversa la mezzanotte pone due domande diverse e ne incolla insieme le risposte. Nessuno te lo segnala.

Callback

Imposta callBackUrl sulla richiesta e il record terminale ti verrà consegnato. Verificalo prima di crederci:

CallbackSignatureVerifier verifier =
        new CallbackSignatureVerifier(System.getenv("SPICY_WEBHOOK_SECRET"));

byte[] body = request.getInputStream().readAllBytes();   // i byte grezzi, mai riserializzati
WebhookDelivery delivery = verifier.verify(body, request::getHeader);
delivery.task().ifPresent(record -> { /* lo stesso record restituito da getTask */ });

Ci sono tre modi in cui questo passaggio va storto, e la classe li evita tutti: riserializzare il corpo prima di verificarlo (un readValue seguito da writeValueAsString modifica i byte, e il digest non corrisponderà mai più), confrontare le firme con equals (che ritorna al primo byte diverso, il che è una differenza di tempo misurabile) e saltare il controllo di validità temporale (una firma valida resta valida per sempre, quindi una consegna intercettata può essere riprodotta).

Le consegne vengono ritentate. Deduplica su delivery.deliveryId(). Vedi Webhook.

Errori

Ogni fallimento è una SpicyException, che è sigillata: uno switch sulla gerarchia è quindi esaustivo.

TipoChe cosa significa
SpicyApiExceptionil servizio ha risposto, e la risposta è stata no
SpicyProtocolExceptionqualcosa ha risposto, ma non con la nostra struttura: un proxy, un captive portal, un base URL sbagliato
SpicyTransportExceptionnessuna risposta utilizzabile: DNS, TLS, una connessione caduta
SpicyTimeoutExceptionè scaduto un budget di tempo locale della richiesta. Esito sconosciuto, non «non è successo»
SpicyWaitTimeoutExceptionil polling si è arreso. L’attività è ancora in corso e ancora addebitabile; l’eccezione ne porta l’ID
SpicyUploadExceptionlo storage a oggetti ha rifiutato la PUT prefirmata
SpicyWebhookExceptionuna consegna in entrata non era autentica, recente e ben formata

Ramifica su businessCode(), mai sul messaggio: i messaggi sono testo in prosa, vengono tradotti e non fanno parte del contratto. L’HTTP 503 è condiviso da tre codici applicativi diversi, quindi il solo stato non può dirti che cosa fare:

CodiceChe cosa significaChe cosa fare
40003i byte caricati non corrispondono al loro ticketricomincia da un ticket nuovo; rifare il commit non lo risolve
40004nessuna istanza disponibile serve quella combinazione di parametricambia il parametro indicato nel messaggio
40201saldo insufficienteaggiungi fondi
40901il prezzo è cambiato prima che l’attività venisse creatachiedi di nuovo il preventivo, mantenendo la stessa chiave di idempotenza
503una dipendenza è momentaneamente non disponibileaspetta il tempo indicato da Retry-After
50301al momento non c’è né un’istanza utilizzabile né un prezzoaspetta, oppure scegli un altro modello
50302una generazione sincrona è fallita a monte ed è stata rimborsatariprova con una nuova chiave di idempotenza; quella originale ripete il fallimento

remediation() restituisce lo stesso consiglio a runtime, e describe() mette stato, codice, ID richiesta, messaggio e consiglio su una sola riga, pronta per un record di log. Cita il requestId() quando contatti l’assistenza. Tutti i codici sono elencati in Errori.

Due cose che ti faranno risparmiare

Tieni una sola chiave di idempotenza per ogni invio. Riusala a ogni reinvio, anche dopo un timeout. Una risposta persa non dimostra che l’attività non sia stata creata: una chiave nuova trasforma un esito sconosciuto in una seconda attività a pagamento. È anche il motivo per cui createTask riprova da solo soltanto se hai passato una chiave: senza, un nuovo tentativo dopo un timeout sarebbe una seconda generazione e un secondo addebito. Vedi Idempotenza.

Un’attività che riesce viene addebitata, anche se il risultato delude. quote() non accantona nulla e non crea nulla, quindi chiedi prima un preventivo quando il prezzo conta. createTask accantona estimatedCost, e quell’importo accantonato è il tetto massimo: i fondi non usati tornano indietro, e nulla oltre quella cifra viene incassato in seguito.

Che cosa questo pacchetto non fa

  • I modelli di testo. Parlano i formati OpenAI, Anthropic e Gemini: i client Java affermati per quei formati funzionano già su https://api.spicyapi.ai/v1 con la stessa chiave. Vedi Testo e streaming.
  • Le applicazioni lato client. Non spedire mai questa chiave dentro un’app Android, un binario desktop o qualsiasi altra cosa che un utente possa aprire. Chiama dal tuo server.
  • Scegliere un modello o compilarne i campi. Ti consegna il catalogo e lo schema di ogni modello; che cosa inviare lo decidi tu.
  • Annullare un’attività. Una volta accettata, un’attività arriva fino in fondo.

Codice sorgente

L’artefatto ha licenza MIT ed è sviluppato in modo aperto su github.com/spicyapi-ai/spicy-java. mvn -B verify esegue la suite di test unitari contro uno stub locale, più un controllo di deriva del contratto che confronta il contratto incluso nel pacchetto con quello pubblicato su https://docs.spicyapi.ai/openapi.yaml e poi ricontrolla ogni enumerazione che il pacchetto tiene fissa nel codice — perché un contratto che acquista un valore senza che il client faccia altrettanto non fa fallire nulla: si limita a rifiutare il nuovo valore in locale, in silenzio.

Prossimi passi

In questa pagina