给 LLM 调用加一层计量表。 一个轻量的 OpenAI 兼容反向代理:转发请求的同时实时统计 token 用量,并按使用者 / 模型 / 上游拆分展示。
部署一次,团队共用一个入口;每个工作台只拿网关发的 key,不需要上游 API key。
工作台 A ──┐
工作台 B ──┼──► 网关 (:8080) ──► DeepSeek / OpenAI / 任意 OpenAI 兼容 API
工作台 C ──┘ │ (真密钥只存在网关里)
└──► 实时面板 · /metrics · /api/stats
直接调上游 API 时,你不知道谁用了多少:
- 多人 / 多服务共用一个 API key,账单是一笔糊涂账
- 想知道某个模型的消耗占比,只能去供应商后台翻
- 想接 Prometheus / Grafana,上游不提供这个能力
这个网关照常转发请求(对你的代码零侵入),在旁边记一笔账。它不改变任何 API 语义 —— 把 SDK 的 base_url 指过来就行。
- 实时面板:SSE 推送,按使用者 / 模型 / 上游三个维度拆分,无外部依赖、可离线使用
- 密钥隔离:网关持有上游密钥,工作台用网关下发的 key 调用;网关 key 绝不转发给上游
- 多上游:一个网关接多个供应商,可把某个 key 钉死到某个上游
- 流式友好:SSE 逐块转发不做缓冲,延迟不受影响
- 多种 usage 格式:同时兼容 OpenAI 嵌套写法与 DeepSeek 扁平写法
- Prometheus 指标:
/metrics直接可被抓取 - 单二进制 / 容器部署:只依赖 gin 和 yaml,
CGO_ENABLED=0静态编译
cp gateway.example.yaml gateway.yaml # 编辑上游和客户端 key
export DEEPSEEK_API_KEY=sk-... # 上游密钥走环境变量
docker compose up -d打开 http://localhost:8080/ 看面板。
go build -o llmgateway .
# 最简:单上游、不鉴权(仅限本机 / 受信网络)
./llmgateway -upstream https://api.deepseek.com -listen :8080
# 正式:多上游、按 key 鉴权
./llmgateway -config gateway.yaml改一行 base_url,不需要上游密钥:
from openai import OpenAI
client = OpenAI(
base_url="http://gateway-host:8080/v1", # ← 指向网关
api_key="gw-your-key", # ← 网关发的 key
)
resp = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "user", "content": "你好"}],
)
print(resp.usage.total_tokens)流式照常工作:
stream = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "user", "content": "写首诗"}],
stream=True,
)
for chunk in stream:
print(chunk.choices[0].delta.content or "", end="")或用 curl:
curl http://gateway-host:8080/v1/chat/completions \
-H "Authorization: Bearer gw-your-key" \
-H "Content-Type: application/json" \
-d '{"model":"deepseek-chat","messages":[{"role":"user","content":"hi"}],"stream":true}'X-Api-Key: gw-your-key 也可以。
完整字段见 gateway.example.yaml。
listen: ":8080"
default_upstream: deepseek
upstreams:
- name: deepseek
base_url: https://api.deepseek.com
api_key_env: DEEPSEEK_API_KEY # 推荐:密钥走环境变量,不写进文件
client_keys: # 每个工作台一个
- name: alice-workbench
key: gw-alice-key # 或 key_env: ALICE_KEY
upstream: deepseek # 可选:钉死到某个上游
cors: # 仅浏览器工作台需要
enabled: true
allowed_origins: ["http://localhost:3000"]
# dashboard_key_env: LLM_GATEWAY_DASHBOARD_KEY # 可选:保护面板环境变量优先级高于配置文件,方便容器覆盖而不重建镜像:
| 变量 | 作用 |
|---|---|
LLM_GATEWAY_CONFIG |
配置文件路径 |
LLM_GATEWAY_LISTEN |
监听地址 |
LLM_GATEWAY_UPSTREAM |
单上游简写(替换文件里的上游) |
LLM_GATEWAY_UPSTREAM_API_KEY |
配合上面那个的密钥 |
LLM_GATEWAY_CORS_ORIGINS |
逗号分隔的来源列表 |
| 端点 | 说明 |
|---|---|
GET / · /dashboard |
实时面板(SSE) |
GET /api/stats |
当前快照 JSON |
GET /api/stream |
SSE 推送,每完成一次请求推一次 |
GET /metrics |
Prometheus 文本格式 |
usage_reported: false 表示上游没返回 usage —— 这是"未知",不是"免费"。
client_keys为空 = 网关完全开放。 只在本机或受信网络这么用;暴露前务必配 key(启动日志会警告)。- 别把上游密钥写进配置文件,用
api_key_env。仓库的.gitignore已排除gateway.yaml。 - 面板公开访问时设
dashboard_key,用http://host:8080/?key=YOUR_KEY打开。 - 浏览器工作台不要把网关 key 写进前端代码,那等于公开它。前端应调你自己的后端。
- 网关 key 经常数时间比较校验,且未授权请求从不转发到上游。
- 路径不可用于 SSRF:请求路径无法改变目标主机(有专门测试覆盖)。
- 目前没有限流和配额,防滥用请在前置代理层做。
实现遵循一个硬约束:统计绝不影响客户端看到的响应。
- 流式零缓冲:SSE 每读到一块立即转发并 flush,解析在旁路进行
- 内存恒定:流式响应以 tee 方式喂给解析器,不因输出很长而堆积
- 统计失败不影响请求:任何计数环节出错都被吞掉
- 自动补
stream_options.include_usage:OpenAI 兼容上游默认不在流式响应返回 usage,网关自动补上(客户端已显式设置则不覆盖) - 上游错误不泄露内部信息:传输错误详情只写日志,回给调用方的只有上游逻辑名
- 计数在内存,重启归零。 需要持久化请订阅
/api/stream落库。 - 面板不是"边生成边涨":token 用量只有上游算完才知道,所以每次请求完成后跳一次。这是原理性限制,不是实现取舍。
- 速率是进程生命周期均值,不是滑动窗口。
- 没有限流 / 配额 / 多租户计费 / 金额换算(只统计 token,各模型单价不同且会变)。
- 仅支持 OpenAI 兼容协议。Anthropic
/v1/messages的 usage 结构不同,尚未支持。
go test ./... # 单元测试
go vet ./... # 静态检查
gofmt -l . # 格式检查
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./... # 依赖漏洞扫描(当前 0 漏洞)测试重点覆盖易错处:
- SSE 分片重组:在每一个可能的字节位置切开同一个流,结果必须一致;以及逐字节投递
- 被截断的流:最后一个 usage 事件不能丢
- 密钥不泄露:断言网关 key 永远不出现在发给上游的请求里
- 错误不泄露内部信息:断言上游地址不出现在回给客户端的错误里
- SSRF 防护:
//host、绝对 URL、路径穿越都不能改变目标主机 - 鉴权失败不转发:坏 key 的请求绝不能到达上游,也不计入上游统计
- 按 key 路由:不同 key 打到各自被钉死的上游
- 流式是增量的:第一个 chunk 必须在流结束前到达客户端
- 配置校验:重复上游名、未知默认上游、
key_env未设置等都报错而非静默降级
tools/ 下有四个开发用工具(不属于网关本体):
go build -o mockupstream.exe ./tools/mockupstream # 假的 OpenAI 兼容上游,不花额度
go build -o verifye2e.exe ./tools/verifye2e # 校验鉴权 / 路由 / CORS / 统计
go build -o ssestream.exe ./tools/ssestream # 验证 SSE 实时推送
go build -o seeddemo.exe ./tools/seeddemo # 灌入演示流量,填充面板
./mockupstream.exe -listen 127.0.0.1:9099 -mode ok # 也支持 -mode nousage / slow
./llmgateway.exe -config _e2e.yaml -release
./verifye2e.exe_e2e.yaml 是两上游 + 两 key 的验证配置,两个上游都指向本地 mock。
想看看面板填满数据的样子(例如截图),用 demo.yaml + seeddemo:
./mockupstream.exe -listen 127.0.0.1:9099 -mode ok
./mockupstream.exe -listen 127.0.0.1:9098 -mode ok
./llmgateway.exe -config demo.yaml -release
./seeddemo.exe # 三个工作台、两个上游、五种模型的混合流量MIT
{ "requests": 128, "errors": 2, "in_flight": 1, "prompt_tokens": 40000, "completion_tokens": 12000, "total_tokens": 52000, "cached_tokens": 9000, "reasoning_tokens": 500, "models": [{ "model": "deepseek-chat", "requests": 90, "total_tokens": 40000 }], "clients": [{ "client": "alice-workbench", "requests": 60, "total_tokens": 30000 }], "upstreams": [{ "upstream": "deepseek", "requests": 128, "total_tokens": 52000 }], "recent": [{ "client": "alice-workbench", "model": "deepseek-chat", "total_tokens": 87, "stream": true, "usage_reported": true }] }