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 version | How the request URL is built | What to put in base-url |
|---|---|---|
| 1.0.x and 1.1.x | base-url + completions-path (default /v1/chat/completions) | https://api.zukko.pro |
| 2.0.x and later | the official openai-java SDK under the hood appends /chat/completions to the base; the default base is https://api.openai.com/v1 | https://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:
<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:
spring:
ai:
openai:
api-key: ${ZUKKO_API_KEY}
base-url: https://api.zukko.pro/v1
timeout: 120s
chat:
model: claude-sonnet-5
max-tokens: 1024For Spring AI 1.x the model properties live under chat.options, and the base has no /v1:
spring:
ai:
openai:
api-key: ${ZUKKO_API_KEY}
base-url: https://api.zukko.pro
chat:
options:
model: claude-sonnet-5
max-tokens: 1024The 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:
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);
}
@BeanChatModel and per-request options
If one endpoint needs a different model or limit, pass OpenAiChatOptions with the prompt; they override the configuration values:
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:
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:
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:
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.
Related articles
- Prompt Injection: What It Is and How to Protect a Bot and an AgentHow prompt injection works through user input and third-party documents, and which measures limit the damage: data isolation, tool permissions, key limits.
- Your Own Neural Network Proxy on Node.js: Keep the API Key Off the FrontendA small Express server between your frontend and the model API: the key stays on the server, and models, request size and rate are capped.
- DeepSeek API in Python: Requests, Streaming, JSON and Error HandlingReady code for DeepSeek V4.1 Flash and V4 Pro on the OpenAI SDK: a request, streaming, JSON parsing and careful error handling.
- Prompt Engineering Examples: 12 Ready-Made Templates for WorkTwelve prompt templates for everyday tasks: what each one says, why it is built that way, and what a single API call costs.