Skip to content
JunyangLianPublic

About

面向 AI Agent 与大模型应用的 Java API 网关与可靠计费系统:统一模型接入、Token 配额、SSE 流式代理、Outbox 有界异步派发与幂等结算。

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ModelGate

面向 AI Agent 与大模型应用的 API 网关、Token 配额与可靠用量计费系统。

将模型调用统一为“鉴权 → 额度预占 → 普通 / 流式代理 → 用量归一化 → 异步结算”的完整链路,处理并发额度竞争、重复调用、流式中断和消息重投带来的账务问题。

Java 21 · Spring Boot 3.5.16 · WebFlux / WebClient · MyBatis-Plus · MySQL · Redis / Lua · Caffeine · Redisson · RabbitMQ

架构与计费 · 配置与缓存 · 验证报告 · 演示指南 · 阶梯压测与可靠性报告 · 故障矩阵

ModelGate 调用控制台

核心能力

模块 实现
模型接入 模型别名路由;硅基流动 GLM 真实接入与独立模拟服务;JSON / SSE 两种响应模式
准入控制 Redis Lua 原子校验 RPM、活跃并发与可用 Token;MySQL 账户行锁维护权威预占
请求幂等 账户级幂等键、请求指纹与数据库唯一约束,避免重复推理及重复占额
流式代理 增量转发、累计 usage 归一化;区分完整结束、超时、断流和客户端取消
可靠结算 用量与事务 Outbox 同事务落库;发布确认、失败重投、消费重试、唯一账单及死信保留
精确计费 不可覆盖的价格版本及请求快照;按输入、输出、缓存 Token 使用整数 nano-CNY 计费
配置缓存 Caffeine / Redis 两级缓存;Redisson 互斥重建;版本校验与 TTL 收敛
异常恢复 UNKNOWN 保留预占;按外部用量凭据核对并恢复结算;账户、请求与账本一致性核查
控制台 账户预算、普通与流式调用、请求防重、调用详情、模型费用统计及恢复演示

调用与结算链路

flowchart LR
  A[Agent / 应用] --> B[API Key + 幂等校验]
  B --> C[Redis Lua 准入 / MySQL 预占]
  C --> D[WebClient 模型代理]
  D --> E[JSON / SSE 增量转发]
  D --> F[用量归一化]
  F --> G[请求用量 + Outbox 同事务]
  G --> H[RabbitMQ]
  H --> I[幂等结算 / 唯一账单]
  I --> J[额度同步 / 成本统计]
  F --> K[UNKNOWN 保留预占]
  K --> L[外部凭据核对]
  L --> G
Loading

JDBC 与阻塞 Redis 操作通过 boundedElastic 隔离。网络代理不自动重试模型推理,避免上游已执行却因重试产生重复用量。MQ 允许重复投递,账务通过事务和唯一约束保证同一请求只结算一次。

Outbox 与额度释放优化(2026-10-25 后续验证)

将已有 Outbox 改为有界异步发布,使用最多 32 个在途投递、4 个发布线程及 100ms 扫描;保留原租约、事务、唯一账单和消费幂等。回调使用尝试号条件更新,超期记录改为逐主键处理,并对瞬时数据库错误有限重试。

对照项 保留的旧 JAR 优化 JAR
负载条件 每轮 120 秒 × 25 请求/秒,20 账户 相同
ABBA 两轮累计测量请求 6,000 6,000
HTTP 错误 / 成功响应未结算 0 / 0 0 / 0
两轮采样 PENDING 峰值均值 1405 3
流量结束后的排空观测等待均值 104.91s 2.05s
包含排空的端到端结算速度 13.36 笔/s 24.58 笔/s

固定输入流量限制了新版吞吐,上表不能解释为最大容量对比;PENDING 每 2 秒采样,排空等待含轮询误差。原始请求、账本、积压与资源记录均保留,见 优化与持续负载报告。

失败归因只将明确的连接池获取超时 / 等待队列超限判定为未发送,按零用量经原 Outbox 链路释放预占。优化版 200 并发长流中,64 次成功、136 次池拒绝,模拟服务实际收到 64 次调用;136 次失败各自只生成一条零费用账单并释放预占。断流、读超时、取消、缺失 usage 和 500 仍保留未知用量,不自动退款。

# 需要保留的旧构建;每轮创建新测试账户,不覆盖历史结果。
.\scripts\Run-OptimizationComparison.ps1 -BaselineJar <baseline-directory>/modelgate-0.1.0.jar -Seconds 120 -Rate 25
.\scripts\Run-Validation.ps1 -Mode Pool

性能与可靠性验证(2026-10-25,优化前基线)

复用现有 Java / Python 标准库测试链路,完成 32 / 100 / 200 并发阶梯,没有新增组件或改动服务架构。

验证项 最终结果
自动化回归 61 项 Java(29 单元、29 MySQL / Redis 集成、3 上游连接池探针)+ 12 项前端 + 8 项指标计算,共 81 项通过
阶梯工作负载 多账户 JSON 两轮、单热点账户 JSON 一轮、长 SSE 一轮,共 12 阶段、6,332 次本地模拟调用
普通请求 6,000 次中 5,999 次成功并各自结算,1 次 503;成功响应未结算 0 次,普通调用没有新增遗留预占
长 SSE 超载 32 并发全部成功;100 / 200 并发各成功 64 次、失败 36 / 136 次;未知用量保留预占、无猜测账单
故障注入 15 场景通过,覆盖真实 RabbitMQ 停服恢复、重复投递、MySQL 事务冲突、SSE 中断、取消、强杀和陈旧恢复
幂等与独立结算 12 个不同请求 + 60 次额外投递仅生成 12 条对应账单;30 次同键并发只有 1 次合法调用
资源与瓶颈 CPU / 内存、JVM 线程、MySQL 行锁、Redis 预算和结算排空记录;确认账户锁争用、数据库连接等待、64 个上游连接及批次派发限制

完整 p50 / p95 / p99、完成 / 成功 QPS、响应体吞吐、端到端结算速度、资源占用与失败归因见 性能与可靠性报告。原始记录索引 保留全部尝试,包括一轮 200 并发的 7 次 503、6 个成功响应用量落库失败,以及一次测试命令参数绑定失败。没有删除这些记录或回填为成功。

本轮仅增加瞬时用量落库失败的有限退避重试,最多尝试 3 次,不重试模型推理、不重放流。HTTP 短时吞吐远高于异步结算排空速度,不能将 HTTP QPS 当作可持续计费吞吐;所有性能数字均为本机模拟结果。

# 先按下文启动网关和中间件。每次运行使用新输出目录。
.\scripts\Run-Validation.ps1 -Mode Load -Rounds 2
.\scripts\Run-Validation.ps1 -Mode Faults
# 指定子场景时将逗号列表整体加引号
.\scripts\Run-Validation.ps1 -Mode Load -Suites 'balanced,streaming' -Rounds 2

故障测试会重启本网关并暂停指定本机 RabbitMQ 应用,必须在没有其他服务使用者的本地测试环境执行;finally 恢复 Broker 与网关,不清空队列或账本。完整构建与复现命令见报告。

基础链路验证(2026-10-24)

验证项 结果
自动化测试 26 项 Java 单元、25 项真实 MySQL / Redis 集成、12 项前端核心测试,共 63 项通过
HTTP 与故障检查 23 项网关检查、14 项 MQ 故障及账本检查通过
配置回源对照 两种路径各读取 200 次,无缓存回源 200 次,两级缓存回源 1 次
并发缓存重建 两个独立 L1、32 个并发未命中,仅触发 1 次数据库回源
结算幂等 同一请求 20 次并发结算任务、5 次 MQ 重复投递,均仅记账一次
模拟调用 本机 32 并发、200 次调用,全部结算,无非预期错误及新增遗留预占
真实模型 硅基流动 zai-org/GLM-5.3-Flash SSE 调用、用量持久化与计费完成

数据为本地验证结果:模拟请求没有真实推理延迟,缓存对照针对配置读取;两个独立 L1 位于同一 JVM。完整条件、历史构建 SHA-256、测试名称和调用记录见 验证结果 及 机器可读报告。报告保留对应构建的快照,后续文档和启动脚本整理不改写历史数据。

本地运行(Windows / PowerShell)

1. 准备环境

安装 JDK 21、Maven 3.9+,配置 JAVA_HOME 与 PATH。可选安装 Python 3.10+ 和 Node.js,以执行 HTTP / 前端验证。

启动本地中间件,应用默认连接:

组件 默认配置
MySQL 8+ 127.0.0.1:3306,独立数据库 modelgate
Redis 7+ 127.0.0.1:16379,DB 1,业务键使用 mg: 前缀
RabbitMQ AMQP 127.0.0.1:5672,本地 guest/guest;故障检查需启用管理插件,端口 15672

本仓库直接连接这些服务,不下载或安装中间件。Redis / RabbitMQ 连接可通过 Spring Boot 标准环境变量配置,例如 SPRING_DATA_REDIS_PORT、SPRING_RABBITMQ_PASSWORD;本地故障脚本按上表默认管理端配置执行。

2. 配置与初始化

在仓库根目录运行:

New-Item -ItemType Directory -Path .runtime -Force | Out-Null
Copy-Item config/mysql.example.json .runtime/modelgate-mysql.local.json
Copy-Item config/mysql-admin.example.json .runtime/mysql-root.local.json
# 编辑两个本地文件:分别填写应用账号和本机 MySQL root 密码。
# 应用账号固定为 modelgate_app,应用密码需使用自己设置的非占位值。

.\scripts\Build.ps1
.\scripts\Init-Database.ps1
.\scripts\Start-App.ps1

首次初始化创建独立数据库和 modelgate_app@localhost 用户,并生成 5 个本地演示 / 测试账户。重复执行不会清空数据库、重置额度或轮换已有密钥,也不会覆盖已存在 MySQL 用户的密码。

打开 http://127.0.0.1:8088/,使用 .runtime/modelgate/clients.local.json 中 name=demo 的网关 API Key 连接。该 Key 与供应商 Key 独立;页面仅在内存中保存,刷新或断开后清除。默认 mock-chat 产生本地演示账单,无外部模型调用。

运行配置、密钥、日志和 PID 均保存在被 Git 忽略的 .runtime/。支持 MG_RUNTIME_DIR 指定外部绝对目录;现有工作区也兼容相邻目录下的运行配置。PowerShell 中可用 . .\scripts\Common.ps1 后的 $Runtime、$GateRuntime 获取实际位置。

3. 可选:启用真实模型

Copy-Item config/provider.example.json .runtime/modelgate-provider.local.json
# 仅在本地文件 apiKey 中填写硅基流动 API Key,然后重启应用。
.\scripts\Stop-App.ps1
.\scripts\Start-App.ps1

控制台选择 glm-flash 才调用真实供应商并产生用量。模型目录已持久化后,修改模型名和价格使用 配置发布命令,环境变量只负责首次种子配置。网关不接受客户端传入任意上游 URL。

4. 发起请求

. .\scripts\Common.ps1
$client = Get-Content "$GateRuntime\clients.local.json" -Raw -Encoding utf8 |
    ConvertFrom-Json | Where-Object name -eq 'demo'
$headers = @{
    Authorization = 'Bearer ' + $client.apiKey
    'Idempotency-Key' = [Guid]::NewGuid().ToString('N')
}
$body = @{
    model = 'mock-chat'
    messages = @(@{role='user';content='hello'})
    max_tokens = 32
    stream = $false
} | ConvertTo-Json -Depth 5
Invoke-RestMethod -Method Post -Uri 'http://127.0.0.1:8088/v1/chat/completions' `
    -Headers $headers -ContentType 'application/json' -Body $body

每次新调用使用新的 Idempotency-Key。同账户重复键返回 409 和已有请求 ID,不再次推理、不重放已输出内容。响应头 X-ModelGate-Request-Id 用于查询结算状态。

API

接口 用途
GET /health 就绪状态
GET /v1/models 模型别名及价格版本
POST /v1/chat/completions 普通 / SSE 文本调用;需 Bearer 与 Idempotency-Key
GET /v1/account 当前账户预算、已用与预占
GET /v1/requests?limit=20 当前账户最近调用;limit 1–100
GET /v1/requests/{id} 当前账户请求状态、用量、费用及失败原因
GET /v1/usage 按模型和价格版本统计已结算用量及精确费用

测试与演示

.\scripts\Build.ps1                   # 26 项 Java 单元测试及打包
.\scripts\Test-Database.ps1           # 25 项 MySQL / Redis 集成测试;会停止本网关
node --test .\scripts\console-core.test.mjs
.\scripts\Start-App.ps1
python .\scripts\verify_gateway.py    # 23 项检查及 200 次本地模拟调用
.\scripts\Test-Faults.ps1             # 暂停恢复、路由故障、重复消费与账本核对

# 可选:一次真实供应商流式调用,会产生用量
python .\scripts\verify_real.py

Python 验证脚本仅依赖标准库。故障测试会重启本项目并短暂移除、恢复专用 MQ 路由;UNKNOWN 用例故意保留预占。RPM 测试账户每自然分钟仅允许一个请求,连续运行需等下一分钟。正常流式、请求幂等和结算恢复的操作步骤见 演示指南。

设计边界

  • 当前支持文本 Chat Completions,最多 64 条消息 / 32 KiB,max_tokens 范围 1–4096;未实现多模态、Agent 编排及模型训练。
  • Token 预占为估计值,不是精确 tokenizer 或严格用量上界;实际超预占如实记账,后续预算不足时拒绝新请求。
  • 超时或断流不代表零用量,UNKNOWN 请求保留预占,凭外部用量证据核对后恢复。
  • SSE 开始后错误使用 event: error,不补 [DONE];HTTP 200 不等于流已完整结束或账务已结算。
  • 缓存采用 TTL 一致性,账务仍由数据库事务保证;未验证多节点灾备或持续生产容量。
  • 当前费用为按价格快照计算的内部用量账本,不包含支付、购买流程或供应商发票对账。

设计背景及第三方文档见 技术参考。

About

面向 AI Agent 与大模型应用的 Java API 网关与可靠计费系统:统一模型接入、Token 配额、SSE 流式代理、Outbox 有界异步派发与幂等结算。

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages