spicyapiドキュメント
SDK
本文

Java SDK

SpicyAPI 公式の Java SDK(ai.spicyapi spicyapi-java)のガイドです。インストール、見積もりと最初の生成、ローカルファイルのアップロード、コールバックの検証、sealed なエラー分類による網羅的な分岐までをまとめています。

spicyapi-java は、SpicyAPI 公式の Java SDK です。画像・動画・音声の生成を、1 つの SpicyClient の後ろにまとめます。API キーを付ける、応答のエンベロープをレコードに写し取る、上限付きのバックオフでタスクを待つ、ローカルファイルを 1 回の呼び出しでアップロードする、コールバックの署名を検証する、そしてあらゆる失敗を sealed な例外型に変える。sealed なので、分類全体に対する switch はコンパイル時に網羅性が保証されます。

必要なのは Java 17 以降で、依存は 1 つだけです。Jackson(com.fasterxml.jackson.core:jackson-databind)の 2.12 以降。レコードをマッピングできるようになったのがこのリリースで、ここで使う型はレコードだからです。Jackson はふつうのコンパイル依存で、optional でもシェーディングでもありません。したがって、プロジェクトがすでに持っているバージョンがそのまま使われます。モジュールパス上では、クライアントは ai.spicyapi です。これは JAR のファイル名から導かれるのではなく、マニフェストに固定されています。

このツールで合っている?

やりたいこと使うもの
自分の Java や JVM のコードから SpicyAPI を呼び出すこの SDK
Node.js や TypeScript から呼び出すTypeScript SDK
Python、Go、PHP から呼び出すPython、Go、PHP の各 SDK
コードを書かずに、ターミナルから生成するCLI
AI アシスタントに、SpicyAPI を代わりに呼び出してもらうMCP サーバーと Agent Skill
チャットモデルやテキストモデルを使うOpenAI、Anthropic、Gemini 形式に対応した、定評のある Java 用クライアント。テキストとストリーミングを参照

インストール

Maven Central から依存を追加します。

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

Gradle(Kotlin DSL)なら次のとおりです。

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

キーは環境変数に入れてください。そうすれば、コードにもバージョン管理にも残りません。

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

最初の結果

価格を尋ねて、判断して、タスクを作成し、待って、出力を読み取ります。見積もりは無料で、何も確保しません。run は、createTask のあとに 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);              // 無料。何も確保しません
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);     // ファイルではなくテキストで応答するエンドポイントもあります
for (TaskAsset asset : task.assets()) {
    asset.url().ifPresent(System.out::println);
}

MODEL_ID_FROM_CATALOG は仮の値です。実際の ID は、認証付きのカタログ(client.listModels()、つまり GET /api/v1/models)から取得してください。記憶や古い Web ページから写してはいけません。モデルは追加・一時停止・終了されるからです。入力は、そのモデル自身の inputSchema(listModels(ModelFilter.all().withIncludeSchema(true)) が返します)に従って組み立ててください。受け付けるフィールドは、モデルごとに違います。

assets() をたどる前に、outputText() を確認してください。 文字起こしのようなエンドポイントは、結果をすべてテキストのフィールドに入れ、ファイルはまったく返しません。ファイルしか見ないクライアントは、完全に成功したタスクを「何も生成されなかった」と報告してしまいます。

金額は米ドルの十進文字列で、浮動小数点数ではありません。2 進数の浮動小数点数では、ほとんどの十進の金額を正確に保持できないからです。この API には、クレジットやポイントの仕組みはどこにもありません。

ローカルのファイルから始める

画像から動画を作る、被写体を差し替える、画像を編集する。どれも、まず素材を当社側に置く必要があります。uploadFile は 3 つのステップを 1 回の呼び出しで行い、spicy:// の URI を返します。入力に入れるのは、この URI です。公開されている HTTPS の URL なら、アップロードは一切必要なく、そのまま入れられます。

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

3 つのステップは、どこか別の場所から流れてくるバイト列のために、個別にも公開されています。

UploadTicket ticket = client.createUploadUrl(UploadContentType.IMAGE_PNG, data.length);
client.putUploadBytes(ticket, data);                  // 送り先は API ではなく、オブジェクトストレージです
UploadedFile file = client.commitUploadedFile(ticket.fileId());

真ん中のステップには、自分で実装し直すと分かりにくく失敗する点が 2 つあります。1 つは、チケットのヘッダーが署名済み URL の署名対象に含まれていることです。だからヘッダーはそのまま転送されます。1 つ落とす、名前を変える、親切のつもりで 1 つ足す。どれをやってもストレージは 403 を返します。もう 1 つは、ここに SpicyAPI のキーは付かないことです。URL 自体が許可の証であり、その宛先は当社ではなくストレージだからです。

画像は 10 MiB まで、音声と動画は 90 MiB までです。大きすぎるファイルは、チケットを要求する前に、クライアントが手元で拒否します。メディアを参照してください。

結果の保存期間を短くする

出力と、保存されたリクエストのテキストは、アカウントの設定どおりの期間だけ保持されます。タスク 1 件ごとに、それより短くすることもできます。

TaskRecord task = client.run(request, idempotencyKey, Duration.ofHours(1));
task.retention().outputsExpireAt();                   // 実際に適用された値

短くする方向にしか働きません。あなたが指定した値、アカウントの設定、プラットフォームの上限のうち、いちばん小さいものが採用されます。リクエストの側から、アカウントが許す期間より長い保存期間を買い戻すことはできません。Duration.ZERO も有効な値です。タスクが終端状態になった時点で、出力はすぐに消えます。大きすぎる値は拒否ではなく上限で切り詰められます。実際に適用された値が、送った数字ではなくレコードの retention() に現れるのは、そのためです。retryTask は独自の保存期間を取りません。再試行は、元のタスクの設定を引き継ぎます。データ保存と破棄を参照してください。

残高・利用状況・履歴

Balance balance = client.getBalance();                          // アカウント全体
UsageReport usage = client.getUsage(                            // このキーの分だけ
        LocalDate.of(2026, 9, 13), LocalDate.of(2026, 9, 20));  // [from, to)
TaskPage page = client.listTasks(TaskFilter.all().withState(TaskState.FAILED));

残高は予算の目安であって、受理されるかどうかの判定ではありません。 一部は、特定のモデルに用途を限った贈呈分かもしれません。その内訳は balance.fundingSources() にあり、贈呈分の modelSlugs が空なら「どれにも使えない」ではなく、すべてのモデルに使えるという意味です。ある 1 つのリクエストについての正確な答えは quote() です。受理の判定と同じルールを通したうえで、何も確保しません。

利用状況は突き合わせのためのもので、進捗を見るためのものではありません。 数えるのは精算済みの課金だけなので、まだ進行中の期間は必ず少なめに出ます。精算が遅れて確定し、過去の 1 日分が変わることもあります。さらに、この呼び出しにはアカウント単位のレート制限が付いていて、その制限はフェイルクローズで働きます。ポーリングすると、アカウント上のすべてのキーが自分の利用状況を読めなくなることがあります。1 件のタスクを追うには、代わりに waitForTerminal を使ってください。

listTasks は、呼び出しに使ったキーの範囲に限られ、返すのはメタデータだけです。ページをたどるときは、カーソルだけを動かしてほかは何も変えないでください。そして、たどり始める前に from と to を固定してください。指定しないままだと、サービスはリクエストごとに既定の期間を計算し直します(to は UTC の翌日です)。0 時をまたぐ走査は、2 つの異なる問いを投げ、その答えをつなぎ合わせてしまいます。それを知らせるものは、どこにもありません。

コールバック

リクエストに callBackUrl を設定すると、終端状態のレコードがあなたのところへ届きます。信じる前に、検証してください。

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

byte[] body = request.getInputStream().readAllBytes();   // 生のバイト列。決して組み直さないこと
WebhookDelivery delivery = verifier.verify(body, request::getHeader);
delivery.task().ifPresent(record -> { /* getTask が返すのと同じレコードです */ });

ここで間違えやすい点が 3 つあり、このクラスはそのいずれも避けています。検証の前にボディを組み直すこと(readValue してから writeValueAsString するとバイト列が変わり、ダイジェストは二度と一致しません)、署名を equals で比較すること(最初に食い違ったバイトで戻るので、処理時間の差として測れてしまいます)、そして有効期間の確認を省くこと(正しい署名はいつまでも正しいままなので、盗み取られた配信をあとから再生できてしまいます)。

配信は再試行されます。delivery.deliveryId() で重複を取り除いてください。Webhook を参照してください。

エラー

失敗はすべて SpicyException です。これは sealed なので、分類全体に対する switch は網羅的になります。

型意味
SpicyApiExceptionサービスが応答し、その答えが「ノー」だった
SpicyProtocolException何かが応答したが、当社のエンベロープではなかった。プロキシ、キャプティブポータル、ベース URL の誤り
SpicyTransportException使える応答がなかった。DNS、TLS、接続切れ
SpicyTimeoutException手元のリクエストの時間切れ。結果は不明であって、「起きなかった」ではない
SpicyWaitTimeoutExceptionポーリングを諦めた。タスクはまだ動いていて、課金対象のまま。例外がその ID を持っている
SpicyUploadExceptionオブジェクトストレージが、署名済み URL への PUT を拒否した
SpicyWebhookException受信した配信が、本物・新しい・形式が正しい、のいずれかを満たさなかった

分岐は必ず businessCode() で行い、メッセージでは判断しないでください。メッセージは文章であり、翻訳されるものであり、契約の一部ではありません。HTTP 503 は、3 つの異なるビジネスコードで共有されています。 そのため、ステータスだけでは、次に何をすべきかは分かりません。

コード意味対処
40003アップロードされたバイト列が、チケットと一致しないチケットを取り直して、最初からやり直します。確定をやり直しても直りません
40004そのパラメーターの組み合わせに対応できる構成がないメッセージに書かれているパラメーターを変更します
40201残高が足りないチャージします
40901タスクが作成される前に価格が変わった見積もりを取り直します。冪等キーは同じものを使ってください
503依存している何かが、短時間だけ利用できないRetry-After の秒数だけ待ってから再試行します
50301いま使える構成も価格もない時間をおくか、別のモデルを選びます
50302同期的な生成が上流で失敗し、返金された新しい冪等キーで再試行します。元のキーでは、同じ失敗が再生されます

remediation() は、実行時にこれと同じ助言を返します。describe() は、ステータス、コード、リクエスト ID、メッセージ、助言を 1 行にまとめて、ログに残せる形にします。サポートに問い合わせるときは requestId() を伝えてください。すべてのコードはエラーに一覧があります。

お金を守る 2 つのこと

1 回の送信につき、冪等キーは 1 つに保ってください。 タイムアウトのあとも含め、送り直すときは毎回同じキーを使います。応答が失われたことは、タスクが作成されなかったことの証明にはなりません。新しいキーを使うと、「結果が分からないだけ」だったものが、課金される 2 つ目のタスクに変わります。createTask がキーを渡したときにだけ自動で再試行するのも、同じ理由です。キーがなければ、タイムアウト後の再試行は 2 回目の生成であり、2 回目の課金になってしまいます。冪等性を参照してください。

成功したタスクは、結果が期待外れでも課金されます。 quote() は何も確保せず、何も作成しないので、価格が気になるときは先に見積もりを取ってください。createTask は estimatedCost を仮押さえし、その額が上限になります。使われなかった分は戻り、それを超える額があとから取られることはありません。

このパッケージが扱わないこと

  • テキストモデル。 OpenAI、Anthropic、Gemini のリクエスト形式に対応しています。それらに対応した定評のある Java 用クライアントが、同じキーのまま https://api.spicyapi.ai/v1 に対してそのまま動きます。テキストとストリーミングを参照してください。
  • クライアント側のアプリケーション。 このキーを、Android アプリ、デスクトップのバイナリ、そのほか利用者が開けるものの中に入れないでください。呼び出しは、必ず自分のサーバーから行ってください。
  • モデルを選んだり、そのフィールドを埋めたりすること。 SDK が渡すのは、カタログと各モデルのスキーマです。何を送るかは、あなたが決めます。
  • タスクのキャンセル。 いったん受理されたタスクは、最後まで実行されます。

ソースコード

このアーティファクトは MIT ライセンスで、github.com/spicyapi-ai/spicy-java で公開のまま開発しています。mvn -B verify は、ローカルのスタブに対してユニットテスト一式を実行し、さらに契約のずれを確認します。同梱している契約を、https://docs.spicyapi.ai/openapi.yaml で公開されているものと比較し、そのうえでパッケージがハードコードしているすべての列挙値を再確認する、という内容です。契約の側に値が増えてもクライアント側が増えない、という状態は、どこにもエラーとして現れません。新しい値を、手元で、静かに拒否し続けるだけだからです。

次のステップ

このページの内容