Setting Up Spring AI with OpenAI
A complete Spring Boot + OpenAI setup: dependencies, API key management, model options, timeouts, retries and the five errors every developer hits on the first run.
On this page
- Step 1 — Add the dependencies
- Step 2 — Handle the API key properly
- Step 3 — Configure model options
- Choosing a model
- Step 4 — Make the call
- Step 5 — Add timeouts
- Step 6 — Verify it works
- The five errors everyone hits
- 401 Unauthorized
- 429 Too Many Requests
- NoSuchBeanDefinitionException: ChatClient.Builder
- Context length exceeded
- Dimension mismatch on a vector store
- What you have now
This is the setup that everything else in the Spring AI phase builds on. It takes about ten minutes, and the last section covers the five errors that account for most of the time people lose on it.
Step 1 — Add the dependencies
Spring AI's BOM manages the versions of every module together. Declare it once.
<properties>
<java.version>21</java.version>
<!-- Pin an exact version. This ecosystem moves fast. -->
<spring-ai.version>1.0.0</spring-ai.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
</dependencies>dependencies {
implementation(platform("org.springframework.ai:spring-ai-bom:1.0.0"))
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.ai:spring-ai-starter-model-openai")
}Step 2 — Handle the API key properly
There is one correct way to do this and several ways that end in a rotated key.
export OPENAI_API_KEY="sk-proj-..."spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}For local development, an untracked .env or an IDE run configuration is fine. In production use
your platform's secret manager — AWS Secrets Manager, Azure Key Vault, Kubernetes secrets mounted as
environment variables, or Spring Cloud Config with encryption.
Step 3 — Configure model options
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
# Start with a small, fast model. Move up only when quality demands it.
model: gpt-4o-mini
# 0.0-0.2 for extraction, classification, tool calling.
# 0.7+ only when you genuinely want variety.
temperature: 0.2
# A hard ceiling on response length — and therefore on cost per call.
max-tokens: 1000
embedding:
options:
# Configured separately. Must match your vector store's dimensions.
model: text-embedding-3-small
retry:
max-attempts: 3
backoff:
initial-interval: 2s
multiplier: 2
max-interval: 30s
# 429 and 5xx are retried. 4xx client errors are not.
on-client-errors: false
logging:
level:
# Uncomment while debugging to see the outgoing request bodies.
# org.springframework.ai: DEBUGChoosing a model
Start smaller than you think you need. The cheap, fast models handle summarisation, classification, extraction and routine tool calling well. Reserve the expensive ones for genuinely hard reasoning, and measure the difference on your own inputs rather than assuming it.
A useful pattern is a property per use case:
app:
ai:
models:
fast: gpt-4o-mini # classification, routing, extraction
strong: gpt-4o # multi-step reasoning, code analysis// Override per call — the application default stays untouched.
String analysis = chatClient.prompt()
.user(complexQuestion)
.options(ChatOptions.builder()
.model(properties.models().strong())
.temperature(0.1)
.build())
.call()
.content();Step 4 — Make the call
package com.javaagentic.demo.chat;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.stereotype.Service;
@Service
public class OpenAiChatService {
private final ChatClient chatClient;
private final MeterRegistry meterRegistry;
public OpenAiChatService(ChatClient.Builder builder, MeterRegistry meterRegistry) {
this.chatClient = builder
.defaultSystem("You are a helpful assistant for Java developers.")
.build();
this.meterRegistry = meterRegistry;
}
public String ask(String question) {
// chatResponse() rather than content() so the usage metadata is available.
ChatResponse response = chatClient.prompt()
.user(question)
.call()
.chatResponse();
recordUsage(response);
return response.getResult().getOutput().getText();
}
/**
* Token usage is the only reliable early warning for a cost problem.
* Recording it from the first day costs nothing and has saved many teams
* an unpleasant conversation about the monthly bill.
*/
private void recordUsage(ChatResponse response) {
var usage = response.getMetadata().getUsage();
if (usage == null) {
return;
}
meterRegistry.counter("ai.tokens", "type", "prompt")
.increment(usage.getPromptTokens());
meterRegistry.counter("ai.tokens", "type", "completion")
.increment(usage.getCompletionTokens());
}
}Step 5 — Add timeouts
Spring AI's retry configuration handles transient failures, but it will not save you from a call that simply never returns. Set an explicit timeout on the underlying HTTP client.
package com.javaagentic.demo.config;
import java.time.Duration;
import org.springframework.boot.web.client.RestClientCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
@Configuration
public class AiHttpConfig {
/**
* Without this, a stalled model call holds a request thread indefinitely.
* Read timeout must exceed your slowest legitimate generation — long
* completions on a large model can genuinely take 60 seconds.
*/
@Bean
RestClientCustomizer aiRestClientCustomizer() {
return restClientBuilder -> {
var factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(Duration.ofSeconds(10));
factory.setReadTimeout(Duration.ofSeconds(90));
restClientBuilder.requestFactory(factory);
};
}
}Step 6 — Verify it works
package com.javaagentic.demo;
import static org.assertj.core.api.Assertions.assertThat;
import com.javaagentic.demo.chat.OpenAiChatService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
// Guarded so CI without a key does not fail. Real calls cost money — keep
// these few and mock the model in your ordinary unit tests.
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+")
class ChatSmokeTest {
@Autowired
OpenAiChatService chatService;
@Test
void answersASimpleQuestion() {
String answer = chatService.ask("Reply with exactly the word: OK");
assertThat(answer).containsIgnoringCase("OK");
}
}Note the assertion style. containsIgnoringCase rather than isEqualTo, because the output is
probabilistic. Writing exact-match assertions against a language model produces a test suite that
fails for no reason. There is a full treatment in
testing AI applications.
The five errors everyone hits
401 Unauthorized
The key is not reaching the application. Check, in order: the environment variable name, whether
your IDE run configuration exports it, whether application.yml uses ${OPENAI_API_KEY} or a
literal, and whether the key has stray whitespace or quotes from copy-paste.
429 Too Many Requests
Two different causes wear the same status code. If it says "quota", you have no billing set up or
have hit a spend cap — no amount of retrying fixes that. If it is genuine rate limiting, Spring AI's
retry backoff handles it; raise max-attempts and lengthen the interval before you ask for a limit
increase.
NoSuchBeanDefinitionException: ChatClient.Builder
Auto-configuration did not run. Usually the starter is missing, or the API key property is absent so
the conditional bean never activated. Run with --debug and check the auto-configuration report for
OpenAiChatAutoConfiguration.
Context length exceeded
Your prompt plus expected response exceeds the model's context window. Long conversation history and over-large retrieved chunks are the usual culprits. Bound both — see tokenization and context windows.
Dimension mismatch on a vector store
expected 1536 dimensions, got 3072 means the embedding model does not match the store schema. The
dimension is fixed when the table is created; changing embedding model requires re-creating the
store and re-ingesting everything.
What you have now
A Spring Boot application that calls OpenAI with managed keys, sane model defaults, retries, timeouts and token metrics. That is a genuinely production-shaped foundation, not a demo.
Next: the Spring AI ChatClient API, which covers prompt templates, streaming responses and advisors — the parts that turn a single call into a feature.
Frequently Asked Questions
Why do I get a 401 Unauthorized from OpenAI in Spring Boot?
How do I change the OpenAI model in Spring AI?
How do I use Azure OpenAI instead of OpenAI?
Can I point Spring AI at a local model?
What does spring.ai.retry actually retry?
Related tutorials
- Introduction to the Spring AI FrameworkWhat Spring AI is, how its abstractions map onto Spring concepts you already know, when to choose it over LangChain4j, and a working ChatClient example in under five minutes.
- The Spring AI ChatClient APIMaster the Spring AI ChatClient: system messages, prompt templates, streaming with SSE, chat memory, advisors and per-call options — with complete Spring Boot code.
- Prompt Engineering for Java DevelopersPrompt engineering explained for engineers, not marketers: system prompts, few-shot, delimiters, output contracts and grounding — each as testable Spring AI code, not vibes.
- Spring AI Embeddings & Vector StoresHow embeddings and vector stores work in Spring AI, with a complete pgvector Spring Boot setup — schema, indexes, metadata filtering, dimensions and the mistakes that force a re-ingest.