spicyapiDokumentation
SDKs
Hauptinhalt

Java-SDK

Das offizielle Java-SDK für SpicyAPI (ai.spicyapi spicyapi-java) – installieren, die erste Generierung schätzen und ausführen, eine lokale Datei hochladen, Callbacks prüfen und vollständig über eine versiegelte Fehlertaxonomie verzweigen.

spicyapi-java ist das offizielle Java-SDK für SpicyAPI. Es legt die Erzeugung von Bildern, Videos und Audio hinter einen einzigen SpicyClient: Er hängt Ihren API-Schlüssel an, bildet den Antwort-Envelope auf Records ab, wartet mit begrenztem Back-off auf einen Task, lädt lokale Dateien in einem Aufruf hoch, prüft Callback-Signaturen und macht aus jedem Fehlschlag einen versiegelten Ausnahmetyp, sodass ein switch über die Taxonomie schon zur Übersetzungszeit vollständig ist.

Es setzt Java 17 oder neuer voraus und hat genau eine Abhängigkeit: Jackson (com.fasterxml.jackson.core:jackson-databind), 2.12 oder neuer – das ist die Version, ab der sich Records abbilden lassen, und die Typen hier sind Records. Jackson ist eine gewöhnliche Compile-Abhängigkeit, weder optional noch shaded; es gewinnt also die Version, die Ihr Projekt ohnehin schon hat. Auf dem Modulpfad heißt der Client ai.spicyapi, festgelegt im Manifest statt aus dem Dateinamen des JARs abgeleitet.

Ist das das richtige Werkzeug?

Sie möchten …Verwenden Sie
SpicyAPI aus Ihrem eigenen Java- oder JVM-Code aufrufenDieses SDK
Es aus Node.js oder TypeScript aufrufenDas TypeScript-SDK
Es aus Python, Go oder PHP aufrufenDas Python-, Go- oder PHP-SDK
Ohne Code aus einem Terminal heraus generierenDie CLI
Einen KI-Assistenten SpicyAPI für Sie aufrufen lassenDen MCP-Server und den Agent Skill
Ein Chat- oder Textmodell nutzenEinen etablierten Java-Client für die Formate von OpenAI, Anthropic oder Gemini – siehe Text und Streaming

Installieren

Binden Sie die Abhängigkeit aus Maven Central ein:

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

Oder mit Gradle (Kotlin DSL):

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

Legen Sie Ihren Schlüssel in die Umgebung, damit er weder in Ihrem Code noch in der Versionsverwaltung auftaucht:

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

Ihr erstes Ergebnis

Preis erfragen, entscheiden, Task anlegen, warten, Ausgabe lesen. Eine Kostenschätzung ist kostenlos und reserviert nichts. run ist createTask gefolgt von 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);              // kostenlos; reserviert nichts
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);     // manche Endpunkte antworten mit Text
for (TaskAsset asset : task.assets()) {
    asset.url().ifPresent(System.out::println);
}

MODEL_ID_FROM_CATALOG ist ein Platzhalter. Holen Sie sich eine echte ID aus dem authentifizierten Katalog – client.listModels(), also GET /api/v1/models – und nie aus dem Gedächtnis oder von einer alten Seite: Modelle kommen hinzu, werden pausiert und eingestellt. Bauen Sie die Eingabe aus dem inputSchema genau dieses Modells, das listModels(ModelFilter.all().withIncludeSchema(true)) zurückgibt; jedes Modell nimmt andere Felder an.

Prüfen Sie outputText(), bevor Sie assets() durchgehen. Transkription und ähnliche Endpunkte legen das ganze Ergebnis in das Textfeld und geben überhaupt keine Dateien zurück; ein Client, der nur nach Dateien sucht, meldet bei einem vollkommen erfolgreichen Task, es sei nichts entstanden.

Beträge sind Dezimalzeichenketten in US-Dollar, nie Floats, denn binäre Gleitkommazahlen können die meisten Dezimalbeträge nicht exakt speichern. Credits oder Punkte gibt es in dieser API nirgends.

Mit einer lokalen Datei beginnen

Bild-zu-Video, Motivtausch und Bildbearbeitung brauchen Ihr Material zuerst auf unserer Seite. uploadFile erledigt die drei Schritte in einem Aufruf und gibt eine spicy://-URI zurück. Diese URI kommt in die Eingabe. Eine öffentlich erreichbare HTTPS-Adresse können Sie direkt eintragen; dafür ist gar kein Upload nötig.

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

Die drei Schritte sind auch einzeln zugänglich, für Bytes, die von woanders herkommen:

UploadTicket ticket = client.createUploadUrl(UploadContentType.IMAGE_PNG, data.length);
client.putUploadBytes(ticket, data);                  // geht in den Objektspeicher, nicht an die API
UploadedFile file = client.commitUploadedFile(ticket.fileId());

Zwei Dinge am mittleren Schritt scheitern undurchsichtig, wenn Sie ihn nachbauen. Die Header des Tickets gehören zu dem, was die vorsignierte Adresse signiert hat, und werden deshalb unverändert weitergereicht – lassen Sie einen weg, benennen Sie einen um oder ergänzen Sie hilfsbereit einen, antwortet der Speicher mit 403. Und es wird kein SpicyAPI-Schlüssel angehängt: Die Adresse selbst ist die Berechtigung, und sie zeigt auf den Speicher, nicht auf uns.

Bilder werden bis 10 MiB angenommen, Audio und Video bis 90 MiB. Eine zu große Datei lehnt der Client lokal ab, noch bevor das Ticket angefordert wird. Siehe Medien.

Das Ergebnis kürzer aufbewahren

Ergebnisse und gespeicherte Anfragetexte werden so lange aufbewahrt, wie es für das Konto eingestellt ist. Ein einzelner Task kann weniger verlangen:

TaskRecord task = client.run(request, idempotencyKey, Duration.ofHours(1));
task.retention().outputsExpireAt();                   // was tatsächlich angewendet wurde

Es verkürzt immer nur: Der Dienst behält den kleinsten Wert aus Ihrer Angabe, der Kontoeinstellung und dem Plattformmaximum; eine Anfrage kann also nie ein längeres Fenster zurückkaufen, als das Konto erlaubt. Duration.ZERO ist ein echter Wert – die Ergebnisse verschwinden, sobald der Task einen Endzustand erreicht. Ein zu großer Wert wird gekappt statt abgelehnt; deshalb steht das, was tatsächlich gilt, in retention() des Records und nicht in der Zahl, die Sie gesendet haben. retryTask nimmt keine eigene Frist entgegen: Eine Wiederholung erbt die ihres Ausgangstasks. Siehe Datenaufbewahrung und Löschung.

Guthaben, Verbrauch und Verlauf

Balance balance = client.getBalance();                          // kontoweit
UsageReport usage = client.getUsage(                            // nur dieser Schlüssel
        LocalDate.of(2026, 9, 13), LocalDate.of(2026, 9, 20));  // [from, to)
TaskPage page = client.listTasks(TaskFilter.all().withState(TaskState.FAILED));

Ein Guthaben ist ein Hinweis auf Ihr Budget, keine Zulassungsentscheidung. Ein Teil davon kann Gutschriftsgeld sein, das auf bestimmte Modelle beschränkt ist – die Aufschlüsselung liefert balance.fundingSources(), und ein leeres modelSlugs auf einer Gutschrift bedeutet jedes Modell, nicht keines. Die exakte Antwort für genau eine Anfrage gibt quote(): Es wendet dieselben Zulassungsregeln an und reserviert nichts.

Der Verbrauch dient dem Abgleich, nicht dem Fortschritt. Gezählt werden nur abgerechnete Belastungen, ein noch laufender Zeitraum fällt also immer zu niedrig aus; eine verspätete Abrechnung kann einen vergangenen Tag nachträglich verändern; und der Aufruf hat ein eigenes kontoweites Anfragelimit, dessen Begrenzer im Zweifelsfall sperrt – wer ihn ständig abfragt, kann jeden Schlüssel des Kontos von seinen eigenen Auswertungen aussperren. Verfolgen Sie einen einzelnen Task stattdessen mit waitForTerminal.

listTasks bezieht sich nur auf den aufrufenden Schlüssel und liefert ausschließlich Metadaten. Blättern Sie die Seiten durch, indem Sie nur den Cursor bewegen und sonst nichts ändern, und legen Sie from und to vorher fest – ohne Angabe berechnet der Dienst sein Standardfenster (to ist der morgige Tag in UTC) für jede Anfrage neu; ein Durchlauf über Mitternacht stellt also zwei verschiedene Fragen und klebt die Antworten aneinander. Gemeldet wird das nirgends.

Callbacks

Setzen Sie callBackUrl in der Anfrage, dann wird Ihnen der Datensatz im Endzustand zugestellt. Prüfen Sie ihn, bevor Sie ihm glauben:

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

byte[] body = request.getInputStream().readAllBytes();   // die rohen Bytes, nie neu serialisiert
WebhookDelivery delivery = verifier.verify(body, request::getHeader);
delivery.task().ifPresent(record -> { /* derselbe Datensatz, den getTask liefert */ });

Drei Wege, auf denen das schiefgeht und die diese Klasse allesamt vermeidet: den Body vor der Prüfung neu serialisieren (readValue und danach writeValueAsString verändert die Bytes, und der Digest passt nie wieder), Signaturen mit equals vergleichen (die Methode kehrt beim ersten abweichenden Byte zurück, und das ist ein messbarer Zeitunterschied) und die Frischeprüfung auslassen (eine gültige Signatur bleibt für immer gültig, sodass sich eine mitgeschnittene Zustellung erneut einspielen lässt).

Zustellungen werden wiederholt. Entdoppeln Sie über delivery.deliveryId(). Siehe Webhooks.

Fehler

Jeder Fehlschlag ist eine SpicyException, und die ist versiegelt; ein switch über die Taxonomie ist damit vollständig:

TypBedeutet
SpicyApiExceptionDer Dienst hat geantwortet, und die Antwort war Nein
SpicyProtocolExceptionIrgendetwas hat geantwortet, aber nicht mit unserem Envelope – ein Proxy, ein Captive Portal, eine falsche Basisadresse
SpicyTransportExceptionKeine brauchbare Antwort: DNS, TLS, abgebrochene Verbindung
SpicyTimeoutExceptionEin lokales Zeitbudget ist abgelaufen. Ausgang unbekannt, nicht „ist nicht passiert“
SpicyWaitTimeoutExceptionDas Abfragen hat aufgegeben. Der Task läuft weiter und wird weiterhin berechnet; die Ausnahme trägt seine ID
SpicyUploadExceptionDer Objektspeicher hat den vorsignierten PUT abgelehnt
SpicyWebhookExceptionEine eingehende Zustellung war nicht echt, frisch und wohlgeformt

Verzweigen Sie über businessCode(), nie über die Meldung – Meldungen sind Fließtext, sie werden übersetzt, und sie sind nicht Teil des Vertrags. HTTP 503 teilen sich drei verschiedene Business-Codes, der Status allein sagt Ihnen also nicht, was zu tun ist:

CodeBedeutungWas zu tun ist
40003Hochgeladene Bytes passen nicht zum TicketMit einem frischen Ticket neu beginnen; ein erneutes Commit hilft nicht
40004Keine Bereitstellung bedient diese ParameterkombinationDen in der Meldung genannten Parameter ändern
40201Guthaben reicht nichtGuthaben aufladen
40901Der Preis hat sich vor dem Anlegen geändertNeu schätzen, denselben Idempotenzschlüssel behalten
503Eine Abhängigkeit ist kurz nicht erreichbarGemäß Retry-After warten
50301Gerade keine nutzbare Bereitstellung und kein PreisSpäter erneut versuchen oder ein anderes Modell wählen
50302Synchrone Generierung bei der Ausführung fehlgeschlagen, erstattetUnter einem neuen Idempotenzschlüssel wiederholen; der alte spielt den Fehlschlag erneut ab

remediation() liefert dieselbe Empfehlung zur Laufzeit, und describe() setzt Status, Code, Request-ID, Meldung und Empfehlung für einen Logeintrag in eine Zeile. Nennen Sie requestId(), wenn Sie den Support kontaktieren. Jeder Code steht unter Fehler.

Zwei Dinge, die Ihnen Geld sparen

Bleiben Sie pro Absendung bei einem Idempotenzschlüssel. Verwenden Sie ihn für jedes erneute Senden wieder, auch nach einem Timeout. Eine verlorene Antwort ist kein Beweis dafür, dass der Task nicht angelegt wurde – ein frischer Schlüssel macht aus einem unbekannten Ausgang einen zweiten kostenpflichtigen Task. Genau deshalb wiederholt createTask nur dann automatisch, wenn Sie einen Schlüssel übergeben haben: Ohne ihn wäre eine Wiederholung nach einem Timeout eine zweite Generierung und eine zweite Belastung. Siehe Idempotenz.

Ein erfolgreicher Task wird berechnet, auch wenn das Ergebnis enttäuscht. quote() reserviert nichts und legt nichts an, holen Sie also zuerst eine Kostenschätzung ein, wenn es auf den Preis ankommt. createTask reserviert estimatedCost, und diese Reservierung ist die Obergrenze: Nicht verbrauchtes Geld kommt zurück, und darüber hinaus wird später nichts eingezogen.

Was dieses Paket nicht tut

  • Textmodelle. Sie sprechen die Formate von OpenAI, Anthropic und Gemini – die etablierten Java-Clients dafür funktionieren mit demselben Schlüssel bereits gegen https://api.spicyapi.ai/v1. Siehe Text und Streaming.
  • Anwendungen auf der Clientseite. Liefern Sie diesen Schlüssel niemals in einer Android-App, einer Desktop-Binärdatei oder irgendetwas anderem aus, das ein Nutzer öffnen kann. Rufen Sie von Ihrem Server aus auf.
  • Ein Modell auswählen oder seine Felder ausfüllen. Es reicht Ihnen den Katalog und das Schema jedes Modells; was Sie senden, entscheiden Sie.
  • Einen Task abbrechen. Ist ein Task erst angenommen, läuft er bis zum Ende.

Quellcode

Das Artefakt steht unter MIT-Lizenz und wird offen unter github.com/spicyapi-ai/spicy-java entwickelt. mvn -B verify führt die Unit-Suite gegen einen lokalen Stub aus, dazu eine Prüfung auf Vertragsdrift, die den mitgelieferten Vertrag mit dem veröffentlichten unter https://docs.spicyapi.ai/openapi.yaml abgleicht und danach jedes Enum erneut prüft, das im Paket fest verdrahtet ist – denn ein Vertrag, der einen Wert dazubekommt, ohne dass der Client ihn dazubekommt, scheitert nirgends: Er lehnt den neuen Wert einfach lokal ab, und zwar stillschweigend.

Nächste Schritte

Auf dieser Seite