spicyapiДокументация
SDK
Основное содержимое

Java SDK

Официальный SDK SpicyAPI для Java (ai.spicyapi spicyapi-java): установка, расчёт стоимости и первая генерация, загрузка локального файла, проверка обратных вызовов и исчерпывающий switch по запечатанной иерархии ошибок.

spicyapi-java — официальный SDK SpicyAPI для Java. Он прячет генерацию изображений, видео и звука за одним SpicyClient: подставляет ваш API-ключ, раскладывает конверт ответа по записям (record), дожидается задачи с ограниченными сверху паузами, загружает локальные файлы одним вызовом, проверяет подписи обратных вызовов и сводит любую неудачу к запечатанному типу исключения, так что switch по этой иерархии оказывается исчерпывающим уже на этапе компиляции.

Ему нужна Java 17 или новее и одна зависимость: Jackson (com.fasterxml.jackson.core:jackson-databind) версии 2.12 или новее — именно с этого выпуска записи стали отображаться на JSON, а здешние типы как раз записи. Jackson подключён обычной зависимостью времени компиляции — не optional и не затенённой (shaded), — поэтому побеждает та версия, которая уже есть в вашем проекте. На module path клиент называется ai.spicyapi, и это имя закреплено в манифесте, а не выведено из имени JAR-файла.

Тот ли это инструмент?

Вы хотите…Что использовать
Вызывать SpicyAPI из собственного кода на Java или другом языке JVMЭтот SDK
Вызывать его из Node.js или TypeScriptTypeScript SDK
Вызывать его из Python, Go или PHPSDK для Python, Go или PHP
Генерировать из терминала, не написав ни строчки кодаCLI
Чтобы ИИ-ассистент вызывал SpicyAPI за васMCP-сервер и Agent Skill
Пользоваться чат-моделью или текстовой модельюПроверенный клиент на Java для форматов OpenAI, Anthropic или Gemini — см. Текст и потоковые ответы

Установка

Подключите зависимость из 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 — это заглушка. Настоящий идентификатор берите из каталога, доступного по ключу, — это client.listModels(), то есть GET /api/v1/models, — и никогда по памяти или со старой страницы: модели добавляются, приостанавливаются и снимаются с публикации. Входные данные собирайте по inputSchema самой модели: его возвращает listModels(ModelFilter.all().withIncludeSchema(true)), и каждая модель принимает свой набор полей.

Проверяйте outputText() прежде, чем обходить assets(). Расшифровка речи и похожие эндпоинты кладут весь результат в текстовое поле и не возвращают ни одного файла; клиент, который ищет только файлы, сообщит, что совершенно успешная задача ничего не произвела.

Суммы передаются десятичными строками в долларах США, а не числами с плавающей точкой, потому что двоичная плавающая точка не хранит большинство десятичных сумм точно. Ни кредитов, ни баллов в этом API нет нигде.

Начать со своего файла

Для «изображения в видео», замены персонажа и редактирования изображений ваш материал сначала должен оказаться на нашей стороне. uploadFile проводит все три шага за один вызов и возвращает URI вида spicy://. Именно этот URI и подставляется во входные данные. Публично доступный HTTPS-адрес можно подставить напрямую, загружать его никуда не нужно.

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

Все три шага доступны и по отдельности — на случай, когда байты приходят откуда-то ещё:

UploadTicket ticket = client.createUploadUrl(UploadContentType.IMAGE_PNG, data.length);
client.putUploadBytes(ticket, data);                  // уходит в объектное хранилище, а не в API
UploadedFile file = client.commitUploadedFile(ticket.fileId());

Если переписывать средний шаг самостоятельно, две вещи ломаются незаметно. Заголовки из разрешения входят в то, что подписано в предподписанной ссылке, поэтому они передаются без изменений: уберите один, переименуйте один или услужливо добавьте свой — и хранилище ответит 403. И никакой ключ SpicyAPI к этому запросу не прикладывается: сама ссылка и есть разрешение, а ведёт она в хранилище, а не к нам.

Изображения принимаются до 10 MiB, аудио и видео — до 90 MiB. Файл сверх лимита клиент отклоняет локально, ещё до запроса разрешения. См. Медиа.

Хранить результат меньше обычного

Результаты и сохранённый текст запроса хранятся столько, сколько настроено для аккаунта. Отдельная задача может попросить меньше:

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 у подарка означает любую модель, а не ни одной. Точный ответ для одного конкретного запроса даёт quote() — он проходит те же проверки допуска и ничего не резервирует.

Расход нужен для сверки, а не для отслеживания хода работ. Он считает только окончательно списанное, поэтому ещё не закончившийся период всегда выглядит заниженным; запоздавшее списание может изменить уже прошедший день; и у него собственный лимит запросов на весь аккаунт, ограничитель которого при сбое закрывается, так что опрос способен лишить отчётности все ключи аккаунта. Следите за отдельной задачей через waitForTerminal.

listTasks ограничен вызывающим ключом и возвращает только метаданные. Обходите страницы, двигая курсор и не меняя больше ничего, и перед обходом явно задавайте from и to: если их не задать, сервис пересчитывает окно по умолчанию (to равно завтрашнему дню по UTC) на каждый запрос, и обход, пересекающий полночь, задаёт два разных вопроса и склеивает ответы. Никто об этом не сообщит.

Обратные вызовы

Укажите в запросе 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 */ });

Три способа здесь ошибиться, и этот класс не допускает ни одного: пересобрать тело запроса до проверки (readValue, а следом writeValueAsString меняют байты, и свёртка уже никогда не совпадёт), сравнивать подписи через equals (метод возвращается на первом же несовпавшем байте, а это измеримая разница во времени) и пропустить проверку свежести (верная подпись остаётся верной навсегда, так что перехваченную доставку можно воспроизвести заново).

Доставки повторяются. Отсеивайте дубликаты по delivery.deliveryId(). См. Вебхуки.

Ошибки

Любая неудача — это SpicyException, тип запечатанный, поэтому switch по иерархии получается исчерпывающим:

ТипЧто означает
SpicyApiExceptionсервис ответил, и ответ был отрицательным
SpicyProtocolExceptionчто-то ответило, но не нашим конвертом: прокси, страница перехвата в сети, неверный базовый адрес
SpicyTransportExceptionпригодного ответа нет вовсе: DNS, TLS, оборванное соединение
SpicyTimeoutExceptionистёк локальный лимит времени на запрос. Исход неизвестен, а не «ничего не произошло»
SpicyWaitTimeoutExceptionопрос прекращён. Задача всё ещё выполняется и всё ещё будет оплачена; её ID лежит в исключении
SpicyUploadExceptionобъектное хранилище отклонило предподписанный PUT
SpicyWebhookExceptionвходящая доставка оказалась неподлинной, несвежей или неправильно оформленной

Ветвитесь по businessCode() и никогда по тексту сообщения: сообщения — это проза, они переводятся и не входят в контракт. За HTTP 503 стоят три разных бизнес-кода, поэтому один статус не подскажет, что делать:

CodeЗначениеЧто делать
40003загруженные байты не совпадают с выданным разрешениемначните заново, с нового разрешения; повторный commit тут не поможет
40004ни одна конфигурация не обслуживает такое сочетание параметровизмените параметр, названный в сообщении
40201на балансе недостаточно средствпополните баланс
40901цена изменилась до того, как задача была созданаполучите расчёт заново, сохранив тот же ключ идемпотентности
503зависимость ненадолго недоступнавыдержите паузу по Retry-After
50301сейчас нет ни рабочей конфигурации, ни ценывыдержите паузу или возьмите другую модель
50302синхронная генерация завершилась ошибкой на стороне исполнителя, средства возвращеныповторите под новым ключом идемпотентности; старый воспроизведёт ту же неудачу

remediation() возвращает ту же подсказку во время выполнения, а describe() укладывает статус, код, идентификатор запроса, сообщение и совет в одну строку для журнала. Обращаясь в поддержку, указывайте requestId(). Все коды перечислены на странице Ошибки и повторы.

Две вещи, которые сэкономят вам деньги

На одну отправку — один ключ идемпотентности. Используйте его при каждой повторной отправке, в том числе после таймаута. Потерянный ответ не доказывает, что задача не создана: новый ключ превращает неизвестный исход во вторую платную задачу. По этой же причине createTask повторяет запрос автоматически только тогда, когда вы передали ключ: без него повтор после таймаута означал бы вторую генерацию и второе списание. См. Идемпотентность.

Успешная задача оплачивается, даже если результат разочаровал. quote() ничего не резервирует и ничего не создаёт, так что, когда цена важна, сначала получите расчёт. createTask удерживает estimatedCost, и этот резерв — потолок: неизрасходованное вернётся, а сверх него потом ничего не спишут.

Чего этот пакет не делает

  • Текстовые модели. Они говорят в форматах OpenAI, Anthropic и Gemini — проверенные клиенты на Java для этих форматов уже работают с https://api.spicyapi.ai/v1 по тому же ключу. См. Текст и потоковые ответы.
  • Клиентские приложения. Никогда не встраивайте этот ключ в приложение для Android, в настольную программу или во что угодно ещё, что пользователь может открыть. Вызывайте со своего сервера.
  • Выбор модели и заполнение её полей. Пакет отдаёт вам каталог и схему каждой модели; что именно отправлять, решаете вы.
  • Отмена задачи. Принятая задача выполняется до конца.

Исходный код

Артефакт распространяется под лицензией MIT и разрабатывается открыто по адресу github.com/spicyapi-ai/spicy-java. Команда mvn -B verify прогоняет модульные тесты против локальной заглушки, а вместе с ними — проверку расхождений контракта: вшитая в пакет копия сверяется с опубликованной по адресу https://docs.spicyapi.ai/openapi.yaml, после чего заново проверяется каждое перечисление, которое пакет держит в коде. Иначе контракт, в котором появилось новое значение, нигде не вызовет ошибку — клиент просто начнёт отклонять это значение у себя, молча.

Что дальше

На этой странице