Spring AI: connecting Claude API via OpenAI provider

Spring AI talks to Claude and GPT through its OpenAI module. The key detail is the base-url: it differs between Spring AI 1.x and 2.x.

4 min · · development

Spring AI lets a Java application call Claude and GPT through the familiar ChatClient and ChatModel, and it connects to Zukko through its OpenAI module, since the gateway speaks an OpenAI-compatible API. Almost every setup problem comes down to one property: spring.ai.openai.base-url. Below we cover which value your Spring AI version needs and show working code: configuration, a model call, streaming, and error handling.

Which base-url to use depends on your Spring AI version

The OpenAI module's behavior changed between the 1.x and 2.x lines, as the Spring AI sources and docs show:

Spring AI versionHow the request URL is builtWhat to put in base-url
1.0.x and 1.1.xbase-url + completions-path (default /v1/chat/completions)https://api.zukko.pro
2.0.x and laterthe official openai-java SDK under the hood appends /chat/completions to the base; the default base is https://api.openai.com/v1https://api.zukko.pro/v1

The end result must be the same: https://api.zukko.pro/v1/chat/completions. If the logs show a 404, check where the request went: a /v1/v1/chat/completions path means 1.x has an extra /v1 in the base, and a /chat/completions path without /v1 means 2.x is missing it.

Find your version in pom.xml or build.gradle by the spring-ai-bom artifact. Spring AI 2.0.x works with Spring Boot 4.0 and 4.1; the 1.x line works with Spring Boot 3.

Dependencies

Maven, using Spring AI 2.0 as an example:

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>

For Gradle: implementation platform("org.springframework.ai:spring-ai-bom:2.0.1") and implementation "org.springframework.ai:spring-ai-starter-model-openai".

Configuring application.yml

For 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

For Spring AI 1.x the model properties live under chat.options, and the base has no /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

The key comes from the ZUKKO_API_KEY environment variable — don't keep it in the repository. You can set any available model: claude-opus-5-5, gpt-5.6-sol, gpt-5.6-luna, glm-5.3, glm-5.3-flash. The gateway converts the request to the right format, so Claude works through the OpenAI module just like GPT.

ChatClient: the shortest path

Spring Boot auto-configures ChatClient.Builder from the settings above:

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 and per-request options

If one endpoint needs a different model or limit, pass OpenAiChatOptions with the prompt; they override the configuration values:

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) {

The answer text comes from getOutput().getText(); the getContent() method from early milestone builds is no longer used in 1.0 and 2.x.

Streaming responses

ChatClient can return the answer in pieces as a Flux<String>, and Spring turns it into 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();

Error handling

The gateway returns 401 (invalid key), 402 (insufficient balance), 403 (model not allowed for the key), 413 (request too large), 429 (too many requests), and 503 (provider unavailable). In Spring AI 2.x, HTTP errors arrive as openai-java SDK exceptions — subclasses of com.openai.errors.OpenAIServiceException with a statusCode() method:

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 -> "Invalid API key";
            case 402 -> "Insufficient balance";
            case 403 -> "Model not allowed for this key";
            case 429 -> "Too many requests, try again later";
            case 503 -> "Model provider temporarily unavailable";
            default -> "API error: " + e.getMessage();
        };
        return ResponseEntity.status(HttpStatus.BAD_GATEWAY).body(message);
    }
}

Return 502 to your client rather than the original 401: for them it's your server's failure, not their authorization. The SDK retries failures itself; the number of attempts is set by spring.ai.openai.max-retries. In Spring AI 1.x, instead of SDK exceptions you get NonTransientAiException (4xx errors, no retries) and TransientAiException (temporary failures) from the org.springframework.ai.retry package.

Without Spring AI: java.net.http

If you don't need a framework, the built-in HttpClient (Java 11+) plus Gson for JSON is enough:

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

The X-Zukko-Cost-Micros header is the request cost in microdollars; more about it in the key limits and cache section. HttpClient is thread-safe: create it once and reuse it.

FAQ

Why do I get 404 when the key is correct?

Almost always it's the base-url. Spring AI 1.x appends /v1/chat/completions itself, so the base is https://api.zukko.pro. Spring AI 2.x appends only /chat/completions, so the base is https://api.zukko.pro/v1. Check the table at the top and the Chat Completions reference.

Where do I create an API key?

In the dashboard, on the API keys page. The full key is shown once. For a Spring service, a separate key with a daily limit and an allowed-models list is convenient.

How do I change the model?

Globally with spring.ai.openai.chat.model (2.x) or spring.ai.openai.chat.options.model (1.x); per request with OpenAiChatOptions.builder().model("..."). The current model list is at GET https://api.zukko.pro/v1/models or on the "Models" page in the dashboard. Test which model suits your task on a set of typical requests from your own application.

Can I connect Claude through Spring AI's Anthropic module?

Yes, Spring AI has a separate Anthropic module, and Zukko also accepts the Anthropic format (/v1/messages). But that module has its own address settings, so check the Spring AI docs for your version. Through the OpenAI module, every Zukko model works with the same configuration, which is simpler.

zukko

Pay 25×
less

for Claude Code & ChatGPT

One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.

Get API keyDocumentation

api.zukko.pro · fetch_*

Spring AI OpenAI base-url configuration for Zukko · Zukko