一、核心思路
OpenRouter 兼容 OpenAI API 格式,所以用 spring-ai-starter-model-openai OpenAiChatModel把 base-url 指到 OpenRouter。模型切换放在每次调用的 options 里覆盖,无需为每个模型建 Bean。
二、依赖(pom.xml)
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version>
</parent>
<properties>
<java.version>17</java.version>
<spring-ai.version>1.0.0-M5</spring-ai.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring AI OpenAI 起步依赖(OpenRouter 走 OpenAI 协议) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
</dependencies>
<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>
<repositories>
<repository>
<id>spring-milestones</id>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
三、application.yml
spring:
ai:
openai:
api-key: ${OPENROUTER_API_KEY:sk-or-xxxxxxxx}
base-url: https://openrouter.ai/api/v1
chat:
options:
# 默认模型,调用时可被覆盖
model: openai/gpt-4o-mini
temperature: 0.7
OpenRouter 的模型名格式是 供应商/模型名,例如 openai/gpt-4o、anthropic/claude-3.5-sonnet、google/gemini-pro-1.5、meta-llama/llama-3.1-70b-instruct。
四、模型枚举(统一管理可用模型)
package com.example.openrouter.model;
import lombok.Getter;
@Getter
public enum ChatModelEnum {
GPT4O_MINI("openai/gpt-4o-mini", "GPT-4o Mini"),
GPT4O("openai/gpt-4o", "GPT-4o"),
CLAUDE_35_SONNET("anthropic/claude-3.5-sonnet", "Claude 3.5 Sonnet"),
GEMINI_PRO_1_5("google/gemini-pro-1.5", "Gemini Pro 1.5"),
LLAMA_31_70B("meta-llama/llama-3.1-70b-instruct", "Llama 3.1 70B");
private final String code;
private final String displayName;
ChatModelEnum(String code, String displayName) {
this.code = code;
this.displayName = displayName;
}
public static ChatModelEnum fromCode(String code) {
for (ChatModelEnum m : values()) {
if (m.code.equalsIgnoreCase(code)) return m;
}
throw new IllegalArgumentException("未知模型: " + code);
}
}
五、ChatClient 配置
package com.example.openrouter.config;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ChatClientConfig {
/**
* 全局共享一个 ChatClient,模型在调用时通过 options 动态覆盖。
*/
@Bean
public ChatClient chatClient(OpenAiChatModel chatModel) {
return ChatClient.builder(chatModel).build();
}
}
六、核心服务:按用户动态切换模型
package com.example.openrouter.service;
import com.example.openrouter.model.ChatModelEnum;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class ChatService {
private final ChatClient chatClient;
/**
* 记录每个用户当前选择的模型(生产环境可换成 Redis / 数据库)。
* key: userId,value: 模型 code
*/
private final Map<String, String> userModelMap = new ConcurrentHashMap<>();
public ChatService(ChatClient chatClient) {
this.chatClient = chatClient;
}
/** 用户切换模型 */
public void switchModel(String userId, String modelCode) {
// 校验模型是否合法
ChatModelEnum.fromCode(modelCode);
userModelMap.put(userId, modelCode);
}
/** 获取用户当前模型,未设置则用默认 */
public String currentModel(String userId) {
return userModelMap.getOrDefault(userId, ChatModelEnum.GPT4O_MINI.getCode());
}
/** 同步聊天 */
public String chat(String userId, String userInput) {
String model = currentModel(userId);
return chatClient.prompt()
.user(userInput)
// 关键:在调用级别覆盖模型,实现动态切换
.options(OpenAiChatOptions.builder()
.model(model)
.temperature(0.7)
.build())
.call()
.content();
}
/** 带完整响应(可拿 usage / 元数据) */
public ChatResponse chatWithMeta(String userId, String userInput) {
return chatClient.prompt()
.messages(new UserMessage(userInput))
.options(OpenAiChatOptions.builder()
.model(currentModel(userId))
.build())
.call()
.chatResponse();
}
}
七、Controller 接口
package com.example.openrouter.controller;
import com.example.openrouter.model.ChatModelEnum;
import com.example.openrouter.service.ChatService;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/chat")
public class ChatController {
private final ChatService chatService;
public ChatController(ChatService chatService) {
this.chatService = chatService;
}
/** 列出所有可用模型 */
@GetMapping("/models")
public List<Map<String, String>> models() {
return Arrays.stream(ChatModelEnum.values())
.map(m -> Map.of("code", m.getCode(), "name", m.getDisplayName()))
.collect(Collectors.toList());
}
/** 用户切换模型 */
@PostMapping("/switch")
public Map<String, String> switchModel(@RequestHeader("X-User-Id") String userId,
@RequestParam String model) {
chatService.switchModel(userId, model);
return Map.of("userId", userId, "currentModel", chatService.currentModel(userId));
}
/** 聊天 */
@PostMapping
public Map<String, String> chat(@RequestHeader("X-User-Id") String userId,
@RequestBody Map<String, String> body) {
String reply = chatService.chat(userId, body.get("message"));
return Map.of(
"userId", userId,
"model", chatService.currentModel(userId),
"reply", reply
);
}
}
八、流式输出(SSE)可选
public Flux<String> chatStream(String userId, String userInput) {
return chatClient.prompt()
.user(userInput)
.options(OpenAiChatOptions.builder()
.model(currentModel(userId))
.build())
.stream()
.content();
}
Controller 用 text/event-stream 返回即可。