JAiRouter:让推理引擎彻底消失的 OpenAI 协议网关
·
JAiRouter:让推理引擎私有协议彻底消失的 OpenAI 协议网关
标题党我承认,但内容值得你看完。
deepwiki 文档 快速访问
00 干嘛的?
- 做了什么:一个 25 MB 的 Spring-Boot JAR,把 Chat / Embedding / Rerank / TTS / STT / 文生图 / 图生图 全部统一到 OpenAI 协议。
- 解决了什么:前端一行代码不改,后端想换 vLLM、Ollama、GPUStack、Xinference、LocalAI 随时换;还能按权重、最少连接、一致性哈希等多策略负载。
- 怎么用:写好
application.yml,java -jar即可。
01 背景:为什么还需要一个 AI 网关?
| 后端 | 协议差异 | 典型场景 |
|---|---|---|
| Ollama | 近似 OpenAI,缺少 rerank / audio | 本地开发机 |
| vLLM | 100% OpenAI,模型独占进程 | 线上 8×A100 |
| Xinference | 自定义 JSON,社区支持有限 | Embedding / Rerank |
| GPUStack | 私有协议与 openai 协议并存 | 国产化交付 |
传统网关(Nginx、Kong、Spring Cloud Gateway)可以反向代理,但:
- 协议不统一:OpenAI、Anthropic、自研格式混用。
- 不懂长连接:LLM 30 s+ 的流式输出,传统“QPS”统计失效。
- 缺少 GPU 维度健康检查:TCP 探活无法识别 “GPU OOM 500”。
于是写了 JAiRouter —— 只做两件事:
- 协议对齐:任何后端 → 100% OpenAI 格式。
- 负载均衡:随机 / 轮询 / 最少连接 / IP 哈希,全部支持权重。
02 整体架构
- 绿色 = 应用层无感知
- 蓝色 = OpenAI 协议
- 灰色 = 多实例并行
03 十分钟上手
3.1 启动两个 vLLM
# 实例 1
python -m vllm.entrypoints.openai.api_server \
--model Qwen-7B --port 8000 --tensor-parallel-size 2
# 实例 2
python -m vllm.entrypoints.openai.api_server \
--model Qwen-7B --port 8001 --tensor-parallel-size 2
3.2 写一份最小配置 application.yml
server:
port: 8080
chat:
load-balance:
type: round-robin
instances:
- base-url: http://localhost:8000
weight: 1
- base-url: http://localhost:8001
weight: 1
3.3 启动网关
./mvnw clean package -DskipTests
java -jar target/model-router-*.jar
3.4 客户端零改动
import openai
openai.api_base = "http://localhost:8080/v1"
resp = openai.ChatCompletion.create(
model="qwen-7b",
messages=[{"role": "user", "content": "hello"}],
stream=True
)
for chunk in resp:
print(chunk.choices[0].delta.get("content", ""), end="")
04 全功能配置示例
server:
port: 8080
model:
adapter: gpustack # 全局默认适配器
load-balance:
type: random # 全局默认策略
hash-algorithm: md5
services:
chat:
load-balance:
type: least-connections # 长连接最优
adapter: gpustack
instances:
- name: qwen3:1.7B
base-url: http://172.16.30.6:9090
path: /v1-openai/chat/completions
weight: 1
embedding:
load-balance:
type: round-robin
instances:
- name: nomic-embed-text-v1.5
base-url: http://172.16.30.6:9090
path: /v1/embeddings
weight: 1
- name: bge-large-zh-v1.5
base-url: http://172.16.30.6:9090
path: /v1/embeddings
weight: 1
rerank:
load-balance:
type: ip-hash
hash-algorithm: sha256
instances:
- name: bge-reranker-v2-m3
base-url: http://172.16.30.6:9090
path: /v1/rerank
weight: 2 # 高配机器权重翻倍
tts:
load-balance:
type: random
instances:
- name: cosyvoice-300m
base-url: http://172.16.30.6:9090
path: /v1/audio/speech
weight: 1
stt:
load-balance:
type: round-robin
instances:
- name: faster-whisper-tiny
base-url: http://172.16.30.6:9090
path: /v1/audio/transcriptions
weight: 2
imgGen:
load-balance:
type: round-robin
instances:
- name: stable-diffusion-2-1
base-url: http://172.16.30.6:9090
path: /v1/images/generations
weight: 1
imgEdit:
load-balance:
type: round-robin
instances:
- name: stable-diffusion-2-1
base-url: http://172.16.30.6:9090
path: /v1/images/edits
weight: 1
05 负载均衡策略
| 类名 | 算法 | 支持权重 |
|---|---|---|
RandomLoadBalancer |
随机 | ✅ |
RoundRobinLoadBalancer |
轮询 | ✅ |
LeastConnectionsLoadBalancer |
最少连接 | ✅ |
IpHashLoadBalancer |
一致性哈希 | ✅ |
接口(LoadBalancer.java):
public interface LoadBalancer {
ModelInstance selectInstance(List<ModelInstance> instances, String clientIp);
default void recordCall(ModelInstance instance) {}
default void recordCallComplete(ModelInstance instance) {}
}
06 协议适配:Adapter 机制
6.1 统一入口
public interface ServiceCapability {
default Mono<?> chat(ChatDTO.Request req, String auth, ServerHttpRequest http) {
throw new UnsupportedOperationException("does not support chat");
}
// 其余服务同此模式
}
6.2 注册中心
@Configuration
public class AdapterRegistry {
private final Map<String, ServiceCapability> adapters = new HashMap<>();
public AdapterRegistry() {
adapters.put("normal", new NormalOpenAiAdapter(registry));
adapters.put("gpustack", new GpuStackAdapter(registry));
adapters.put("ollama", new OllamaAdapter(registry));
adapters.put("vllm", new VllmAdapter(registry));
adapters.put("xinference", new XinferenceAdapter(registry));
adapters.put("localai", new LocalAiAdapter(registry));
}
}
6.3 模板方法(BaseAdapter)
protected <T> Mono<?> processRequest(
T request,
String authorization,
ServerHttpRequest httpRequest,
ServiceType serviceType,
String modelName,
RequestProcessor<T> processor) {
ModelInstance instance = registry.selectInstance(serviceType, modelName,
IpUtils.getClientIp(httpRequest));
WebClient client = registry.getClient(serviceType, modelName, instance);
String path = registry.getModelPath(serviceType, modelName);
return processor.process(request, authorization, client, path, instance, serviceType);
}
子类仅需重写 transformRequest / transformResponse / adaptModelName,即可完成协议转换。
07 后续 Roadmap(欢迎 PR / Issue)
- 动态权重:按 GPU 显存实时调整。
- 模型级限流:多租户场景下,按模型 / Token 维度限流。
- 或者任何你觉得有意思的想法,直接开 Issue 🙌
08 如何参与
- GitHub:https://github.com/Lincoln-cn/JAiRouter
- GitHub Search:直接搜
JAiRouter - Issue:已标记
good first issue,Java / WebFlux 新手友好 - 讨论:在博客评论区留言,作者实时回复
09 授权
项目采用 Apache 2.0 协议,随便折腾。
Apache-2.0 license
更多推荐

所有评论(0)