Spring AI: подключение Claude API к OpenAI провайдеру

Spring AI работает с Claude и GPT через OpenAI-модуль. Главное — правильно указать base-url: в Spring AI 1.x и 2.x он разный.

4 мин · · разработка

Spring AI позволяет вызывать Claude и GPT из Java-приложения через привычные ChatClient и ChatModel, а к Zukko он подключается через OpenAI-модуль: шлюз говорит на OpenAI-совместимом API. Почти все проблемы при настройке сводятся к одному свойству — spring.ai.openai.base-url. Ниже разберём, какое значение нужно в вашей версии Spring AI, и покажем рабочий код: конфиг, вызов модели, стриминг и обработку ошибок.

Какой base-url указывать: зависит от версии Spring AI

Поведение OpenAI-модуля поменялось между ветками 1.x и 2.x, и это видно по исходникам и документации Spring AI:

Версия Spring AIКак строится адрес запросаЧто указать в base-url
1.0.x и 1.1.xbase-url + completions-path (по умолчанию /v1/chat/completions)https://api.zukko.pro
2.0.x и новеепод капотом официальный SDK openai-java, он добавляет к базе /chat/completions; базовый адрес по умолчанию — https://api.openai.com/v1https://api.zukko.pro/v1

Итог должен быть один и тот же: https://api.zukko.pro/v1/chat/completions. Если в логах видите 404, посмотрите, куда ушёл запрос: путь /v1/v1/chat/completions значит, что в 1.x лишний /v1 в базе, а путь /chat/completions без /v1 — что в 2.x его не хватает.

Свою версию смотрите в pom.xml или build.gradle по артефакту spring-ai-bom. Spring AI 2.0.x работает со Spring Boot 4.0 и 4.1, ветка 1.x — со Spring Boot 3.

Зависимости

Maven, на примере Spring AI 2.0:

bash
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>2.0.1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-openai</artifactId>
    </dependency>
</dependencies>

Для Gradle — implementation platform("org.springframework.ai:spring-ai-bom:2.0.1") и implementation "org.springframework.ai:spring-ai-starter-model-openai".

Настройка application.yml

Для Spring AI 2.x:

bash
spring:
  ai:
    openai:
      api-key: ${ZUKKO_API_KEY}
      base-url: https://api.zukko.pro/v1
      timeout: 120s
      chat:
        model: claude-sonnet-5
        max-tokens: 1024

Для Spring AI 1.x свойства модели лежат под chat.options, а база — без /v1:

bash
spring:
  ai:
    openai:
      api-key: ${ZUKKO_API_KEY}
      base-url: https://api.zukko.pro
      chat:
        options:
          model: claude-sonnet-5
          max-tokens: 1024

Ключ берётся из переменной окружения ZUKKO_API_KEY — не храните его в репозитории. Модель можно поставить любую из доступных: claude-opus-5-5, gpt-5.6-sol, gpt-5.6-luna, glm-5.3, glm-5.3-flash. Шлюз сам конвертирует запрос в нужный формат, поэтому Claude через OpenAI-модуль работает так же, как GPT.

ChatClient: самый короткий путь

ChatClient.Builder Spring Boot создаёт автоматически по настройкам выше:

bash
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class ChatApplication {

    public static void main(String[] args) {
        SpringApplication.run(ChatApplication.class, args);
    }

    @Bean

ChatModel и параметры на конкретный запрос

Если для одного эндпоинта нужна другая модель или лимит, передайте OpenAiChatOptions вместе с промптом — они перекроют значения из конфига:

bash
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ChatController {

    private final ChatModel chatModel;

    public ChatController(ChatModel chatModel) {

Текст ответа достаётся через getOutput().getText(): метод getContent() из ранних milestone-версий в 1.0 и 2.x уже не используется.

Стриминг ответа

ChatClient умеет отдавать ответ по частям как Flux<String>, а Spring превращает его в Server-Sent Events:

bash
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

@RestController
public class StreamController {

    private final ChatClient chatClient;

    public StreamController(ChatClient.Builder builder) {
        this.chatClient = builder.build();

Обработка ошибок

Шлюз отвечает кодами 401 (неверный ключ), 402 (не хватает баланса), 403 (модель не разрешена для ключа), 413 (слишком большой запрос), 429 (слишком часто) и 503 (провайдер недоступен). В Spring AI 2.x ошибки HTTP приходят как исключения SDK openai-java — наследники com.openai.errors.OpenAIServiceException с методом statusCode():

bash
import com.openai.errors.OpenAIServiceException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class AiErrorHandler {

    @ExceptionHandler(OpenAIServiceException.class)
    public ResponseEntity<String> handle(OpenAIServiceException e) {
        String message = switch (e.statusCode()) {
            case 401 -> "Неверный API-ключ";
            case 402 -> "Недостаточно средств на балансе";
            case 403 -> "Модель не разрешена для этого ключа";
            case 429 -> "Слишком много запросов, повторите позже";
            case 503 -> "Провайдер модели временно недоступен";
            default -> "Ошибка API: " + e.getMessage();
        };
        return ResponseEntity.status(HttpStatus.BAD_GATEWAY).body(message);
    }
}

Клиенту лучше отдавать 502, а не исходный 401: для него это ошибка вашего сервера, а не его авторизации. Повторы при сбоях SDK делает сам, число попыток задаёт свойство spring.ai.openai.max-retries. В Spring AI 1.x вместо исключений SDK используются NonTransientAiException (ошибки 4xx, без повторов) и TransientAiException (временные сбои) из пакета org.springframework.ai.retry.

Без Spring AI: java.net.http

Если фреймворк не нужен, хватит встроенного HttpClient (Java 11+) и Gson для JSON:

bash
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;

public class ZukkoClient {

    private static final HttpClient HTTP = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(10))

Заголовок X-Zukko-Cost-Micros — стоимость запроса в микродолларах; подробнее о нём — в разделе о лимитах ключей и кэше. HttpClient потокобезопасен: создайте его один раз и переиспользуйте.

Частые вопросы

Почему 404, хотя ключ правильный?

Почти всегда дело в base-url. Spring AI 1.x сам дописывает /v1/chat/completions, поэтому база — https://api.zukko.pro. Spring AI 2.x дописывает только /chat/completions, поэтому база — https://api.zukko.pro/v1. Сверьтесь с таблицей в начале статьи и с описанием Chat Completions.

Где создать API-ключ?

В кабинете, на странице API-ключей. Полный ключ показывается один раз. Для сервиса на Spring удобно завести отдельный ключ с дневным лимитом и списком разрешённых моделей.

Как сменить модель?

Глобально — свойством spring.ai.openai.chat.model (2.x) или spring.ai.openai.chat.options.model (1.x), для отдельного запроса — через OpenAiChatOptions.builder().model("..."). Актуальный список моделей — GET https://api.zukko.pro/v1/models или страница «Модели» в кабинете. Какая модель подойдёт вашей задаче, проверьте на наборе типовых запросов из своего приложения.

Можно ли подключить Claude через Anthropic-модуль Spring AI?

Да, у Spring AI есть отдельный Anthropic-модуль, а Zukko принимает и Anthropic-формат (/v1/messages). Но настройки адреса у этого модуля свои — сверьтесь с документацией Spring AI для вашей версии. Через OpenAI-модуль все модели Zukko работают с одним и тем же конфигом, что проще.

zukko

Платите в 25×
меньше

за Claude Code & ChatGPT

Один ключ fetch_* и base URL api.zukko.pro — в Cursor, Claude Code, Codex и любом SDK.