JAiRouter:让推理引擎私有协议彻底消失的 OpenAI 协议网关

标题党我承认,但内容值得你看完。

deepwiki 文档 快速访问

00 干嘛的?

  • 做了什么:一个 25 MB 的 Spring-Boot JAR,把 Chat / Embedding / Rerank / TTS / STT / 文生图 / 图生图 全部统一到 OpenAI 协议。
  • 解决了什么:前端一行代码不改,后端想换 vLLM、Ollama、GPUStack、Xinference、LocalAI 随时换;还能按权重、最少连接、一致性哈希等多策略负载。
  • 怎么用:写好 application.ymljava -jar 即可。

01 背景:为什么还需要一个 AI 网关?

后端 协议差异 典型场景
Ollama 近似 OpenAI,缺少 rerank / audio 本地开发机
vLLM 100% OpenAI,模型独占进程 线上 8×A100
Xinference 自定义 JSON,社区支持有限 Embedding / Rerank
GPUStack 私有协议与 openai 协议并存 国产化交付

传统网关(Nginx、Kong、Spring Cloud Gateway)可以反向代理,但:

  1. 协议不统一:OpenAI、Anthropic、自研格式混用。
  2. 不懂长连接:LLM 30 s+ 的流式输出,传统“QPS”统计失效。
  3. 缺少 GPU 维度健康检查:TCP 探活无法识别 “GPU OOM 500”。

于是写了 JAiRouter —— 只做两件事:

  • 协议对齐:任何后端 → 100% OpenAI 格式。
  • 负载均衡:随机 / 轮询 / 最少连接 / IP 哈希,全部支持权重。

02 整体架构

OpenAI API
Web / APP / SDK
JAiRouter
统一协议 + 负载均衡
vLLM-1
Qwen-7B
vLLM-2
Qwen-7B
Ollama-1
qwen3:1.7B
  • 绿色 = 应用层无感知
  • 蓝色 = 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)

  1. 动态权重:按 GPU 显存实时调整。
  2. 模型级限流:多租户场景下,按模型 / Token 维度限流。
  3. 或者任何你觉得有意思的想法,直接开 Issue 🙌

08 如何参与

  • GitHubhttps://github.com/Lincoln-cn/JAiRouter
  • GitHub Search:直接搜 JAiRouter
  • Issue:已标记 good first issue,Java / WebFlux 新手友好
  • 讨论:在博客评论区留言,作者实时回复

09 授权

项目采用 Apache 2.0 协议,随便折腾。
Apache-2.0 license

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐