spicyapiDocumentation
SDK
Contenu principal

SDK Java

Le SDK Java officiel de SpicyAPI (ai.spicyapi spicyapi-java) — l’installer, obtenir un devis, lancer votre première génération, téléverser un fichier local, vérifier les callbacks et parcourir de façon exhaustive une taxonomie d’erreurs scellée.

spicyapi-java est le SDK Java officiel de SpicyAPI. Il place la génération d’images, de vidéos et de sons derrière un seul SpicyClient : il joint votre clé API, projette l’enveloppe de réponse sur des records, attend une tâche avec une temporisation bornée, téléverse les fichiers locaux en un seul appel, vérifie les signatures de callback et transforme chaque échec en un type d’exception scellé, si bien qu’un switch sur la taxonomie est exhaustif dès la compilation.

Il demande Java 17 ou plus récent et a une seule dépendance : Jackson (com.fasterxml.jackson.core:jackson-databind), en 2.12 ou plus récent — c’est la version à partir de laquelle les records sont projetables, et les types utilisés ici sont des records. Jackson est une simple dépendance de compilation, ni optional ni relocalisée : c’est donc la version que votre projet possède déjà qui l’emporte. Sur le chemin de modules, le client est ai.spicyapi, fixé dans le manifeste plutôt que déduit du nom du fichier JAR.

Est-ce le bon outil ?

Vous voulez…Utilisez
Appeler SpicyAPI depuis votre propre code Java ou JVMCe SDK
L’appeler depuis Node.js ou TypeScriptLe SDK TypeScript
L’appeler depuis Python, Go ou PHPLe SDK Python, Go ou PHP
Générer des contenus depuis un terminal sans écrire de codeLa CLI
Laisser un assistant IA appeler SpicyAPI pour vousLe serveur MCP et l’Agent Skill
Utiliser un modèle de chat ou de texteUn client Java établi pour les formats OpenAI, Anthropic ou Gemini ; voir Texte et streaming

L’installer

Ajoutez la dépendance depuis Maven Central :

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

Ou, avec Gradle (Kotlin DSL) :

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

Placez votre clé dans l’environnement, pour qu’elle n’apparaisse jamais dans votre code ni dans votre gestionnaire de versions :

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

Votre premier résultat

Demandez le prix, décidez, créez la tâche, attendez, lisez la sortie. Le devis est gratuit et ne réserve rien. run enchaîne createTask puis 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);              // gratuit ; ne réserve rien
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);     // certains points répondent en texte, pas en fichiers
for (TaskAsset asset : task.assets()) {
    asset.url().ifPresent(System.out::println);
}

MODEL_ID_FROM_CATALOG est un emplacement réservé. Prenez un vrai identifiant dans le catalogue authentifié — client.listModels(), c’est-à-dire GET /api/v1/models — et jamais de mémoire ni d’une page ancienne : des modèles sont ajoutés, suspendus et retirés. Construisez l’entrée à partir de l’inputSchema propre à ce modèle, que renvoie listModels(ModelFilter.all().withIncludeSchema(true)) ; chaque modèle accepte des champs différents.

Vérifiez outputText() avant de parcourir assets(). La transcription et les points de terminaison de ce genre placent tout le résultat dans le champ texte et ne renvoient aucun fichier : un client qui ne cherche que des fichiers déclare qu’une tâche parfaitement réussie n’a rien produit.

Les montants sont des chaînes décimales en dollars américains, jamais des flottants, parce qu’un flottant binaire ne peut pas contenir exactement la plupart des montants décimaux. Il n’y a ni crédits ni points nulle part dans cette API.

Partir d’un fichier local

L’image vers vidéo, le remplacement de personne et la retouche d’image demandent tous que votre matière première soit d’abord chez nous. uploadFile réalise les trois étapes en un seul appel et renvoie une URI spicy://. C’est cette URI qui part dans l’entrée. Une URL HTTPS publiquement joignable peut y aller directement, sans aucun téléversement.

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

Les trois étapes sont aussi publiques, pour des octets diffusés depuis ailleurs :

UploadTicket ticket = client.createUploadUrl(UploadContentType.IMAGE_PNG, data.length);
client.putUploadBytes(ticket, data);                  // part vers le stockage objet, pas vers l’API
UploadedFile file = client.commitUploadedFile(ticket.fileId());

Deux détails de l’étape du milieu échouent de façon opaque si vous la réimplémentez. Les en-têtes du ticket font partie de ce que l’URL présignée a signé : ils sont donc transmis tels quels — en supprimer un, en renommer un ou en ajouter un pour bien faire, et le stockage répond 403. Et aucune clé SpicyAPI n’y est jointe : l’URL est elle-même l’autorisation, et elle pointe vers le stockage, pas vers nous.

Les images sont acceptées jusqu’à 10 Mio, l’audio et la vidéo jusqu’à 90 Mio. Le client refuse localement un fichier trop volumineux, avant même de demander le ticket. Voir Médias.

Conserver le résultat moins longtemps

Les sorties et le texte de requête enregistré sont conservés aussi longtemps que le compte le prévoit. Une tâche peut demander moins :

TaskRecord task = client.run(request, idempotencyKey, Duration.ofHours(1));
task.retention().outputsExpireAt();                   // ce qui a réellement été appliqué

Cela ne fait jamais qu’écourter : le service retient la plus petite des trois valeurs — la vôtre, le réglage du compte et le maximum de la plateforme — si bien qu’une requête ne peut jamais racheter une fenêtre plus longue que ce que le compte autorise. Duration.ZERO est une valeur valable : les sorties disparaissent dès que la tâche atteint un état final. Une valeur trop grande est ramenée à la borne plutôt que rejetée, et c’est pourquoi ce qui a été appliqué se lit dans retention() sur le record, pas dans le nombre que vous avez envoyé. retryTask n’a pas de durée de conservation propre : une relance hérite de celle de sa tâche d’origine. Voir Conservation et destruction.

Solde, consommation et historique

Balance balance = client.getBalance();                          // pour tout le compte
UsageReport usage = client.getUsage(                            // cette clé uniquement
        LocalDate.of(2026, 9, 13), LocalDate.of(2026, 9, 20));  // [from, to)
TaskPage page = client.listTasks(TaskFilter.all().withState(TaskState.FAILED));

Un solde est un indicateur de budget, pas une décision d’admission. Une partie peut être une dotation réservée à certains modèles — balance.fundingSources() en donne le détail, et un modelSlugs vide sur une dotation signifie tous les modèles, pas aucun. La réponse exacte pour une requête exacte, c’est quote(), qui applique les mêmes règles d’admission et ne réserve rien.

La consommation sert au rapprochement comptable, pas au suivi d’avancement. Elle ne compte que les montants définitivement réglés : une période encore en cours affiche donc toujours un chiffre trop bas ; un règlement tardif peut modifier un jour passé ; et elle porte sa propre limite de débit valable pour tout le compte, dont le limiteur se ferme en cas de doute, si bien que l’interroger en boucle peut priver toutes les clés du compte de leurs propres rapports. Suivez plutôt une seule tâche avec waitForTerminal.

listTasks est limité à la clé appelante et ne renvoie que des métadonnées. Parcourez les pages en faisant bouger le curseur et rien d’autre, et fixez from et to avant de commencer : laissés vides, le service recalcule sa fenêtre par défaut (to vaut demain en UTC) à chaque requête, si bien qu’un parcours qui franchit minuit pose deux questions différentes et recolle les réponses. Rien ne le signale.

Callbacks

Réglez callBackUrl sur la requête et le record final vous est livré. Vérifiez-le avant d’y croire :

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

byte[] body = request.getInputStream().readAllBytes();   // les octets bruts, jamais sérialisés à nouveau
WebhookDelivery delivery = verifier.verify(body, request::getHeader);
delivery.task().ifPresent(record -> { /* le même record que renvoie getTask */ });

Trois façons de se tromper ici, que cette classe évite toutes : sérialiser à nouveau le corps avant de le vérifier (un readValue suivi d’un writeValueAsString modifie les octets, et l’empreinte ne correspondra plus jamais), comparer les signatures avec equals (il rend la main au premier octet différent, ce qui constitue un écart de temps mesurable) et sauter le contrôle de fraîcheur (une signature valide le reste pour toujours, donc une livraison capturée peut être rejouée).

Les livraisons sont réessayées. Dédoublonnez sur delivery.deliveryId(). Voir Webhooks.

Erreurs

Chaque échec est une SpicyException, qui est scellée : un switch sur la taxonomie est donc exhaustif.

TypeSignifie
SpicyApiExceptionLe service a répondu, et la réponse était non
SpicyProtocolExceptionQuelque chose a répondu, mais pas avec notre enveloppe : un proxy, un portail captif, une mauvaise URL de base
SpicyTransportExceptionAucune réponse exploitable : DNS, TLS, une connexion coupée
SpicyTimeoutExceptionUn budget de requête local est écoulé. Résultat inconnu, et non « cela n’a pas eu lieu »
SpicyWaitTimeoutExceptionL’interrogation a abandonné. La tâche continue et reste facturable ; l’exception porte son identifiant
SpicyUploadExceptionLe stockage objet a refusé le PUT présigné
SpicyWebhookExceptionUne livraison entrante n’était pas authentique, fraîche et bien formée

Branchez sur businessCode(), jamais sur le message : les messages sont de la prose, ils sont traduits et ils ne font pas partie du contrat. Le statut HTTP 503 est partagé par trois codes métier différents : le statut seul ne peut donc pas vous dire quoi faire.

CodeSignificationQue faire
40003Les octets téléversés ne correspondent pas à leur ticketRecommencez avec un ticket neuf ; valider de nouveau n’y changera rien
40004Aucun déploiement ne sert cette combinaison de paramètresChangez le paramètre nommé dans le message
40201Solde insuffisantRechargez
40901Le prix a bougé avant la création de la tâcheRedemandez un devis, en gardant la même clé d’idempotence
503Une dépendance est brièvement indisponiblePatientez selon Retry-After
50301Aucun déploiement ni prix utilisable pour l’instantPatientez, ou choisissez un autre modèle
50302Une génération synchrone a échoué en amont et a été rembourséeRéessayez sous une nouvelle clé d’idempotence ; l’originale rejoue l’échec

remediation() renvoie les mêmes conseils à l’exécution, et describe() place statut, code, identifiant de requête, message et conseil sur une seule ligne, prête pour un journal. Citez requestId() quand vous contactez le support. Tous les codes sont listés dans Erreurs.

Deux choses qui vous feront économiser

Gardez une clé d’idempotence par soumission. Réutilisez-la pour chaque renvoi, y compris après un dépassement de délai. Une réponse perdue ne prouve pas que la tâche n’a pas été créée : une clé neuve transforme un résultat inconnu en une seconde tâche payante. C’est aussi pourquoi createTask ne réessaie automatiquement que si vous avez passé une clé : sans elle, une relance après un dépassement de délai serait une deuxième génération et une deuxième facturation. Voir Idempotence.

Une tâche qui réussit est facturée, même si le résultat déçoit. quote() ne réserve rien et ne crée rien : demandez donc un devis d’abord quand le prix compte. createTask met de côté estimatedCost, et ce montant est le plafond : les fonds inutilisés reviennent, et rien au-delà n’est prélevé plus tard.

Ce que ce paquet ne fait pas

  • Les modèles de texte. Ils parlent les formats réseau d’OpenAI, d’Anthropic et de Gemini : les clients Java établis pour ces formats fonctionnent déjà avec https://api.spicyapi.ai/v1 et la même clé. Voir Texte et streaming.
  • Les applications côté client. N’embarquez jamais cette clé dans une application Android, un binaire de bureau ou quoi que ce soit qu’un utilisateur peut ouvrir. Appelez depuis votre serveur.
  • Choisir un modèle ou remplir ses champs. Il vous fournit le catalogue et le schéma de chaque modèle ; c’est vous qui décidez quoi envoyer.
  • Annuler une tâche. Une fois acceptée, une tâche va jusqu’au bout.

Le code source

L’artefact est sous licence MIT et développé au grand jour sur github.com/spicyapi-ai/spicy-java. mvn -B verify exécute la suite de tests unitaires contre un bouchon local, plus un contrôle de dérive du contrat qui compare le contrat embarqué à celui publié sur https://docs.spicyapi.ai/openapi.yaml, puis revérifie chaque énumération que le paquet code en dur — parce qu’un contrat qui gagne une valeur sans que le client la gagne aussi n’échoue nulle part : il se contente de rejeter la nouvelle valeur localement, en silence.

Étapes suivantes

Sur cette page