Connect Spring AI's OpenAI starter to Router One
Spring AI gives Spring Boot applications a ChatClient over a portable model API. Its OpenAI starter sends Chat Completions requests, so one Router One key reaches every chat model in the catalog served on /v1/chat/completions, with a cost trace per request. The one detail that changes by release line is base-url: Spring AI 2.0.x hands the value to the official openai-java SDK, which appends /chat/completions itself, while 1.x appends its own /v1/chat/completions path. This guide covers both lines, then verifies one plain call and one stream.
Add the Spring AI BOM and the OpenAI starter
Spring AI GA releases are on Maven Central, so no extra repository is needed. Import spring-ai-bom and add spring-ai-starter-model-openai; the starter brings the OpenAI chat model, its auto-configuration, and the ChatClient.Builder bean this guide injects. Match the BOM version to your Spring Boot line: Spring AI 2.0.x (2.0.1 at the time of writing) supports Spring Boot 4.0.x and 4.1.x, and Spring AI 1.1.x (1.1.8) supports Spring Boot 3.4.x and 3.5.x. The coordinates are identical on both lines; only the version differs.
<!-- pom.xml: Spring AI 2.0.x on Spring Boot 4.x; use 1.1.8 with Spring Boot 3.4/3.5 -->
<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>Configure Spring AI to use the Router One base URL
For Spring AI 2.0.x, set three properties in src/main/resources/application.yml. base-url is https://api.router.one/v1, with /v1: 2.0 passes the value unchanged to the openai-java SDK, which appends /chat/completions itself (the SDK's own default already ends in /v1, even though the 2.0.1 properties table still prints the older host-only default). api-key reads ROUTER_ONE_API_KEY through a Spring placeholder; the variable name belongs to this guide and is deliberately not OPENAI_API_KEY, because 2.0 falls back to OPENAI_API_KEY and OPENAI_BASE_URL from the environment whenever the properties are unset. chat.model takes the exact catalog ID, including any provider prefix that is part of the ID; 2.0 flattened the former chat.options.* names, which still bind but are deprecated for removal:
# src/main/resources/application.yml: Spring AI 2.0.x (Spring Boot 4.x)
spring:
ai:
openai:
base-url: https://api.router.one/v1
api-key: ${ROUTER_ONE_API_KEY}
chat:
model: <model-id-from-/models>
# Shell (macOS/Linux) before starting the app:
# export ROUTER_ONE_API_KEY=sk-your-router-one-keySpring AI 1.x: host root plus completions-path
Spring AI 1.0.x and 1.1.x build the URL themselves: base-url plus the chat.completions-path property, whose default is /v1/chat/completions. On 1.x, base-url is therefore the host root https://api.router.one without /v1, and the model sits under chat.options.model. Keeping /v1 in base-url there sends the request to /v1/v1/chat/completions. The completions-path line below is the default, shown so the mechanism is visible. 1.x also sets temperature to 0.7 through its properties, while 2.0 sends temperature only when you set it:
# application.yml: Spring AI 1.0.x / 1.1.x (Spring Boot 3.4 / 3.5)
spring:
ai:
openai:
base-url: https://api.router.one
api-key: ${ROUTER_ONE_API_KEY}
chat:
completions-path: /v1/chat/completions
options:
model: <model-id-from-/models>Which value goes where, by release line
Both lines are current GA releases on Maven Central (2.0.1 and 1.1.8 at the time of writing) and share the same artifact coordinates; the pre-1.0 milestone name spring-ai-openai-spring-boot-starter stopped at 1.0.0-M6. Match the row to the Spring Boot line you run:
| Spring AI | Spring Boot | spring.ai.openai.base-url | Model property | Path appended by |
|---|---|---|---|---|
| 2.0.x (2.0.1) | 4.0.x / 4.1.x | https://api.router.one/v1 | spring.ai.openai.chat.model | openai-java SDK: /chat/completions |
| 1.1.x (1.1.8), 1.0.x (1.0.9) | 3.4.x / 3.5.x | https://api.router.one | spring.ai.openai.chat.options.model | spring.ai.openai.chat.completions-path, default /v1/chat/completions |
Make one call and one stream through ChatClient
Inject the auto-configured ChatClient.Builder, build a ChatClient, and run a plain call() before anything else; the model and endpoint come from the properties above, so nothing is repeated in code. Then use stream().content(), which returns a Reactor Flux<String>; Spring AI's ChatClient documentation states that streaming runs on the reactive stack. Start the application and confirm both outputs before adding tool calling, chat memory, or structured output. Those stay in your application, while the gateway records each model request:
package com.example.demo;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
class RouterOneSmokeTest {
@Bean
CommandLineRunner routerOneSmokeTest(ChatClient.Builder builder) {
ChatClient chatClient = builder.build();
return args -> {
// One plain call first; the model comes from spring.ai.openai.chat.model
String reply = chatClient.prompt()
.user("Reply with one short greeting.")
.call()
.content();
System.out.println(reply);
// Then streaming: a Flux<String> from the same ChatClient
chatClient.prompt()
.user("Count from 1 to 5, one number per line.")
.stream()
.content()
.doOnNext(System.out::print)
.blockLast();
};
}
}Which model ID should Spring AI send?
Copy the exact model ID from /models, preserving case, hyphens, and version suffixes; do not substitute a display name. Open its detail page and match the supported API endpoints, context window, and capabilities such as tool calling to the provider and features selected in Spring AI. A catalog listing does not mean the client can use every feature of that model. Give each tool a dedicated API key with a maxSpend cap.
Which API protocol is Spring AI using?
OpenAI-compatible describes an interface format; it does not make Chat Completions (/v1/chat/completions), Responses (/v1/responses), and Anthropic Messages (/v1/messages) interchangeable. Check the installed client version, provider configuration, and actual request path against the model detail page and API compatibility fact sheet. A successful plain-text chat does not establish support for hosted tools, conversation state, or file-editing features.
Verify the Spring AI call in your request trace
Send a simple text request from Spring AI, then match its trace in Dashboard → Logs by time, model, and request_id: tokens, cost, latency, and status. Next, test streaming, tool calls, and multi-turn history separately. For failures, retain the actual request path, full error message, and request_id. If there is no matching log, check client configuration and connectivity before attributing the error to the gateway or upstream.
FAQ
Does Spring AI's OpenAI starter use the Responses API?
No. The starter sends Chat Completions to /v1/chat/completions, and Spring AI's 2.0.1 documentation describes the Responses API as a separate endpoint not currently supported by this client. Features that exist only on /v1/responses, such as hosted tools, previous_response_id, and conversation state, are therefore out of reach from this starter, and a model's Responses support does not matter here. What matters is that the model's detail page lists POST /v1/chat/completions.
I moved from Spring AI 1.x to 2.0 and requests now fail with 404. What changed?
Two things. base-url must now include /v1, because 2.0 forwards it to the openai-java SDK and no longer has a completions-path property; with the old host-root value the request goes to /chat/completions, and the gateway answers 404 with a not_found error whose message spells out that OpenAI-compatible clients need a base URL ending in /v1. And the chat.options.* properties were flattened to chat.*; the old names still bind but are marked deprecated for removal. Update both, then rerun the plain call.
Why does one ChatClient call show up as several requests in Logs?
Client-side retries. In 2.0 the openai-java SDK retries 408, 409, 429, and 5xx responses and connection errors, up to spring.ai.openai.max-retries (documented default 3). In 1.x the spring.ai.retry template retries transient failures up to spring.ai.retry.max-attempts (documented default 10) and skips 4xx unless on-client-errors is enabled. Every attempt that reaches the gateway is its own request with its own trace and charge, whereas a same-family fallback inside one gateway request is one trace. Lower the retry setting for batch jobs, and give the application a dedicated key with maxSpend.
Do I need the Anthropic starter for Claude-family models?
Not for this setup. The gateway's /v1/chat/completions serves every chat model in the catalog, so a Claude-family ID copied from /models goes into chat.model like any other. Spring AI's Anthropic starter targets the Anthropic Messages format on /v1/messages with its own spring.ai.anthropic.* properties; that path is outside this guide and needs its own verification.
On 1.x the first call fails with a 400 that mentions temperature. Is that the gateway?
Usually not. 1.x sends temperature 0.7 by default from its properties, and Spring AI's documentation notes that some GPT-5 family models reject a temperature value. Read the full error and request_id in Logs: a 400 that names the parameter is the model rejecting it. 2.0 sends temperature only when you set it. Change the temperature setting for that model rather than the base-url or the model ID.
Which models can Spring AI use through the gateway?
Choose a current catalog model that supports both the endpoint and the features Spring AI uses. Check /models and the model detail page for the exact ID, current rates, and capabilities; a family name such as GPT or Claude is not a compatibility guarantee. Seeing a model in the picker confirms discovery, so verify an actual request too.
Models are listed, but requests fail with 400 or 404. What should I check?
Record the actual request path and error message, then check the exact model ID. A 400 can indicate invalid parameters, unsupported tools, or a model/endpoint mismatch; a 404 can indicate an incorrect path or missing resource, so it does not by itself establish that a model was retired. If the error says must be called via, use the named endpoint or select a model supported on the current endpoint. Do not add or remove /v1 or /chat/completions across all clients indiscriminately.
Does this work from Mainland China?
Yes. The gateway is reachable from Mainland China without a VPN, and the configuration is identical to the global setup.
How do I debug a 401/402/403/429?
Match the request and error message in Dashboard → Logs. For 401, check whether the key was sent and is valid; for 402, check wallet balance and maxSpend; for 403, check key permissions and access restrictions. For 429, distinguish request/token limits from upstream throttling using the error details. Keep the request_id and follow the error-codes reference.