n4nAI

Spring AI: connecting to OpenAI-compatible APIs

Learn how to wire Spring AI to any OpenAI-compatible API, configure the chat client, and handle streaming, config, and fallback in a Spring Boot app.

n4n Team3 min read677 words

Audio narration

Coming soon — every post will get a voice note here.

Spring AI gives you a portable abstraction over chat models, but the fastest path to a working app is pointing it at a spring ai openai compatible api. This tutorial builds a Spring Boot service that talks to any OpenAI-compatible endpoint, streams responses, and degrades cleanly when a provider fails.

Prerequisites

  • JDK 17 or newer installed and on PATH
  • Maven 3.9+ (Gradle 8+ works too, but snippets use Maven)
  • Spring Boot 3.3.x
  • An API key and base URL for an OpenAI-compatible service (OpenAI, a local LLM server, or a gateway)
  • Export your key before starting: export OPENAI_API_KEY=sk-...

You should be comfortable with @RestController, @RequestParam, and basic Spring dependency injection.

1. Bootstrap the Spring Boot project

Generate a minimal web app from the command line:

curl -s https://start.spring.io/starter.zip \
  -d type=maven-project \
  -d javaVersion=17 \
  -d bootVersion=3.3.0 \
  -d dependencies=web \
  -d packageName=com.example.aidemo \
  -d artifactId=ai-demo \
  -o ai-demo.zip
unzip ai-demo.zip && cd ai-demo

The generated pom.xml has the web starter only. We add the AI starter manually to pin the milestone version.

2. Add the Spring AI OpenAI starter

Spring AI publishes to a milestone repository because it is pre-GA. Append this to pom.xml:

<repositories>
  <repository>
    <id>spring-milestones</id>
    <name>Spring Milestones</name>
    <url>https://repo.spring.io/milestone</url>
  </repository>
</repositories>

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
    <version>1.0.0-M6</version>
  </dependency>
</dependencies>

The starter auto-configures an OpenAiChatModel and a ChatClient.Builder as soon as spring.ai.openai.api-key is set. No manual @Bean is required for the basic case.

3. Configure the OpenAI-compatible endpoint

In src/main/resources/application.yml, set the base URL, key, and default model:

spring:
  ai:
    openai:
      base-url: https://api.openai.com/v1
      api-key: ${OPENAI_API_KEY}
      chat:
        options:
          model: gpt-3.5-turbo
          temperature: 0.7

The base-url must point at the OpenAI-style /v1 path. To target a different spring ai openai compatible api, change base-url and model. For a local Ollama OpenAI shim, use http://localhost:11434/v1 and model: llama3. For Azure OpenAI, the path includes a deployment segment, so consult the provider docs.

4. Build a chat service with ChatClient

Create a controller that injects the auto-configured ChatClient.Builder. The builder is thread-safe; build one ChatClient per controller and reuse it.

package com.example.aidemo;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import org.springframework.http.MediaType;

@RestController
@RequestMapping("/chat")
public class ChatController {

    private final ChatClient chatClient;

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

    @GetMapping("/ask")
    public String ask(@RequestParam String q) {
        return chatClient.prompt()
                .user(q)
                .call()
                .content();
    }

    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> stream(@RequestParam String q) {
        return chatClient.prompt()
                .user(q)
                .stream()
                .content();
    }
}

Non-streaming call

chatClient.prompt().user(q).call().content() blocks the request thread until the model returns the full text. Use it for short, synchronous tasks or internal services where latency is acceptable.

Streaming response

.stream().content() returns a Reactor Flux<String> that emits tokens as they are produced. Spring Boot serializes the Flux as Server-Sent Events with data: prefixes automatically. The client connection stays open until the model finishes or times out.

5. Run and verify

Start the app:

mvn spring-boot:run

Watch the log for Started AiDemoApplication. If the OpenAI starter fails to configure, it will complain about missing api-key at startup.

Expected output

Non-streaming:

$ curl "http://localhost:8080/chat/ask?q=What%20is%20Spring%20AI?"
Spring AI is a Spring ecosystem project that provides a unified API for chat,
text-to-image, and embedding models, letting you swap providers with config changes.

Streaming with -N to disable curl buffering:

$ curl -N "http://localhost:8080/chat/stream?q=Count%20to%203%20with%20a%20space"
data:1
data: 2
data: 3

The exact tokens depend on the model, but the SSE framing confirms the stream works. Add -i to see Content-Type: text/event-stream.

6. Customize model and parameters per request

Global YAML sets defaults; override per call with OpenAiChatOptions. You can also attach a system prompt:

import org.springframework.ai.openai.OpenAiChatOptions;

@GetMapping("/gpt4")
public String gpt4(@RequestParam String q) {
    return chatClient.prompt()
            .system("You are a concise senior engineer.")
            .user(q)
            .options(OpenAiChatOptions.builder()
                    .model("gpt-4o")
                    .temperature(0.2)
                    .maxTokens(256)
                    .build())
            .call()
            .content();
}

This keeps the same ChatClient while targeting a different model or sampling setting for one request. The system() call sets the system message; omit it to use the model default.

7. Production hardening: timeouts and fallback

A single LLM provider will rate-limit or stall. Set HTTP timeouts on the underlying client. For the OpenAI starter, Spring Boot’s spring.http.client applies:

spring:
  http:
    client:
      connect-timeout: 5s
      read-timeout: 30s

If you need fine control, define a RestClient.Builder bean and reference it via spring.ai.openai.rest-client-builder (check the exact property for your milestone).

Swap base-url to a gateway

If you want a single spring ai openai compatible api endpoint that addresses 240+ models with automatic fallback when a provider is degraded and per-token usage metering, point the base-url at a gateway like n4n.ai. The client code stays identical; you only change YAML or environment variables.

spring:
  ai:
    openai:
      base-url: https://api.n4n.ai/v1
      api-key: ${N4N_KEY}

The gateway honors client routing directives and forwards provider cache-control hints, so existing OpenAiChatOptions still apply.

Handle provider errors

Wrap calls to return a clean 503 instead of a stack trace:

@GetMapping("/askSafe")
public ResponseEntity<String> askSafe(@RequestParam String q) {
    try {
        return ResponseEntity.ok(chatClient.prompt().user(q).call().content());
    } catch (Exception e) {
        return ResponseEntity.status(503)
                .body("LLM provider unavailable: " + e.getMessage());
    }
}

Add Resilience4j @Retry or a bulkhead if you need automatic retries with backoff. Do not retry indefinitely; LLM generation is expensive.

8. Where to go next

You now have a runnable Spring Boot service that speaks the OpenAI wire format. Extend it with ChatMemory for multi-turn sessions, or use PromptTemplate to structure system messages from external files. The abstraction means swapping spring-ai-openai for spring-ai-anthropic later requires no controller changes—only dependency and property swaps.

Keep the ChatClient instance as a singleton; building a new one per request adds needless overhead. Monitor token usage via ChatResponse.getMetadata().getUsage() if you need per-call accounting. For browser clients, add a CorsConfiguration bean to allow your origin on /chat/**.

That is the core integration. Everything else is standard Spring.

Tagsspring-bootspring-aiopenai-apitutorial

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All spring boot ai integration posts →