OpenAI 兼容 API 中转 / Key 池网关:多上游端点、优先级调度、失败自动换 Key、探测与模型映射、请求/调度日志,以及中英文 Web 管理台。
仓库:https://github.com/azoway/somanyapikeys
预置上游示例:https://integrate.api.nvidia.com/v1。也可添加任意 OpenAI 兼容 base_url。
- 功能
- 概念:三种密钥
- 快速开始
- Web 管理台
- 客户端用法
- Docker
- 反代与 TLS(Caddy 示例)
- 环境变量
- 数据文件与备份
- 管理 API 摘要
- 模型映射
- 安全说明
- 开发
- 发布与二进制
- License
| 能力 | 说明 |
|---|---|
| OpenAI 兼容代理 | /v1/chat/completions、/v1/responses、聚合 /v1/models 等,支持流式 SSE |
| 多 API 端点 | 系统预置 + 自定义 base URL;创建时 SSRF 校验 |
| Key 池调度 | 优先级 0–1000;成功率排序 + 可选 per-key 并发上限;额度用尽 / 鉴权失败 / 限流时自动换 Key |
| 探测 | 定时或手动;用最小 chat/completions 真鉴权(不用不可靠的 GET /models 判活) |
| 自动禁用 | 可选:失效 / 耗尽 Key 设为 enabled=false;可 Webhook 告警 |
| 模型探测 + 映射 | 缓存上游模型;客户端 model → 上游 id(全局或按端点);支持批量导入 |
| 日志与用量 | 请求 / 调度日志;按天用量 rollup 与仪表盘趋势 |
| 访问密钥 | 客户端调 /v1 的凭证;支持过期、Token 配额、模型白名单、日志归属 |
| 备份恢复 | 配置包导出/导入 + 在线数据库快照 |
| 安全默认 | 无 change-me-* 弱口令;首次随机生成 Admin / 访问密钥 / 落盘加密密钥 |
| 落盘加密 | AES-GCM;密钥自动生成或由环境变量指定 |
| 可观测 | GET /healthz、可选 GET /metrics(Prometheus)、/v1 可选 per-IP 限流 |
| Web 管理台 | 中 / 英;嵌入二进制,无需单独前端部署 |
请勿混用:
| 名称 | 用途 | 获取位置 |
|---|---|---|
ADMIN_TOKEN(sk-admin-…) |
登录管理台、调用 /admin/api/* |
首次生成 → data/credentials.txt |
访问密钥(sk-gw-…) |
客户端 api_key,请求本站 /v1 |
总览页,或 credentials 中的 GATEWAY_ACCESS_KEY |
上游 Key(如 nvapi-…) |
网关出站调用厂商 API | Key列表 / 导入 |
另外:
| 名称 | 用途 |
|---|---|
KEY_ENCRYPTION_SECRET(sk-enc-…) |
加密 SQLite 内的密钥材料;不是 API 调用凭证 |
可选遗留变量 GATEWAY_TOKEN:若设置且库中尚无访问密钥,会作为首条访问密钥;仅当库中没有同值行时,运行时才作为 /v1 鉴权回退。若该密钥在库中被禁用或过期,不会被 env 回退复活。推荐只使用总览页管理的访问密钥。
要求:Go 1.25+(见 go.mod)。
git clone https://github.com/azoway/somanyapikeys.git
cd somanyapikeys
# 推荐:不设任何 Token / 加密密钥,首次启动自动生成
go run ./cmd/server
# 或
make run首次启动:
- 日志只打印 prefix(不会把完整密钥打进 journald / Docker 日志)。
- 完整密钥写入
data/credentials.txt(权限0600),例如:
ADMIN_TOKEN=sk-admin-…
GATEWAY_ACCESS_KEY=sk-gw-…
KEY_ENCRYPTION_SECRET=sk-enc-…
- 落盘加密密钥保存在
data/key_encryption.secret(0600);同目录还会生成data/key_encryption.salt(0600,enc:v2:记录的 PBKDF2 盐)。两个文件重启时自动加载,缺一不可。 - 打开管理台:http://localhost:8080/ ,用
ADMIN_TOKEN登录。 - 在 Key列表 / 导入 粘贴上游厂商 Key,绑定端点与优先级。
- 客户端使用
GATEWAY_ACCESS_KEY(或总览里新建的访问密钥)请求/v1。
也可自行指定强随机密钥(禁止 change-me-admin / change-me-gateway,启动会拒绝):
export ADMIN_TOKEN="$(openssl rand -hex 24)"
# export KEY_ENCRYPTION_SECRET=... # 可选;不设则自动生成
go run ./cmd/server| 入口 | 地址 |
|---|---|
| 管理台 | http://localhost:8080/ |
| 代理 base | http://localhost:8080/v1 |
| 健康检查 | GET /healthz(公开、无密钥,适合 LB 探针) |
| 指标 | GET /metrics(默认开启;默认需 Admin 鉴权,METRICS_AUTH=true) |
侧边栏(中文):
- 总览 — 池统计、Tokens 使用(24h/7d/Top models)、访问密钥(创建 / 查看 / 复制 / RPM / 启停 / 删除)
- API 端点 — 上游 base URL
- Key列表 — 上游 Key 池、筛选、单 Key 对话探测
- 导入 — 批量粘贴上游 Key
- 模型 — 已探测模型 + 模型映射
- 探测 — 全量 / 策略
- 日志 — 请求日志 / 调度日志(页内切换)
- 设置 — 探测与日志清理等
- 说明 — 简要帮助
支持界面语言中 / 英切换。管理 Token 仅保存在本机浏览器 localStorage。
从 data/credentials.txt 取出 GATEWAY_ACCESS_KEY,或在总览页创建访问密钥:
export ACCESS_KEY=sk-gw-… # 不是上游 nvapi Key
curl http://localhost:8080/v1/chat/completions \
-H "Authorization: Bearer $ACCESS_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "meta/llama-3.1-8b-instruct",
"messages": [{"role":"user","content":"hello"}]
}'Python(OpenAI SDK):
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:8080/v1",
api_key="sk-gw-…", # 访问密钥
)
print(client.chat.completions.create(
model="meta/llama-3.1-8b-instruct",
messages=[{"role": "user", "content": "hello"}],
))docker compose up -d --build- Token / 加密密钥均可留空:首次启动写入卷内
data/credentials.txt、data/key_encryption.secret与data/key_encryption.salt。 - 查看:
docker compose logs -f(日志仅 prefix)或进入数据卷读 credentials。 - 需要固定密钥时再设置环境变量
ADMIN_TOKEN、KEY_ENCRYPTION_SECRET等(强随机,勿用模板字面量)。 - 盐没有对应的环境变量,只以文件形式存在于数据卷——数据卷必须持久化且可写,否则每次重建都会生成新盐,已有
enc:v2:记录随之报废。
docker compose exec gateway cat /app/data/credentials.txt进程默认本地 HTTP 监听(如 :8080)。TLS、域名、公网入口请用 Caddy / Nginx 等反代完成。
整站反代到本机进程即可。保护依赖:
- 强随机
ADMIN_TOKEN(管理台登录 //admin/api/ 默认/metrics) - 客户端只用 访问密钥 调
/v1,不要把 Admin Token 发给调用方 - HTTPS(Caddy 自动证书)
- 浏览器侧勿在不可信设备保存 Token;定期轮换访问密钥
# 管理台 + OpenAI 兼容 API 同一域名(HTTPS)
gateway.example.com {
# 可选:压缩、安全头
encode gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options nosniff
X-Frame-Options DENY
Referrer-Policy no-referrer
-Server
}
reverse_proxy 127.0.0.1:8080
}说明:
| 路径 | 公网 | 鉴权 |
|---|---|---|
/ 管理台 UI |
可开 | 浏览器输入 Admin Token |
/admin/api/* |
可开 | Authorization: Bearer <ADMIN_TOKEN> |
/v1/* |
可开 | 访问密钥 sk-gw-… |
/healthz |
可开 | 无(仅 {"ok":true},适合探活) |
/metrics |
可开 | 默认需 Admin(METRICS_AUTH=true) |
Prometheus 抓取示例(带 Admin Token):
# scrape_configs 片段
- job_name: somanyapikeys
scheme: https
metrics_path: /metrics
static_configs:
- targets: ["gateway.example.com"]
authorization:
type: Bearer
credentials: "sk-admin-…" # 与管理台同一 ADMIN_TOKEN;建议专用只读网络或内网抓取若 Prometheus 在内网、不想带 Token:本机进程设 METRICS_AUTH=false,并在 Caddy 不要 把 /metrics 暴露到公网,例如:
gateway.example.com {
# 公网拒绝 metrics(由内网直连 :8080 或另一条内网反代抓取)
@metrics path /metrics
respond @metrics 404
reverse_proxy 127.0.0.1:8080
}api.example.com {
reverse_proxy /v1/* 127.0.0.1:8080
reverse_proxy /healthz 127.0.0.1:8080
# 不转发 /、/admin、/metrics
}管理台:ssh -L 8080:127.0.0.1:8080 user@host 后访问 http://127.0.0.1:8080/。
| 端点 | 建议 | 原因 |
|---|---|---|
GET /healthz |
保持公开、体积极小 | 负载均衡 / Caddy / k8s 探活需要无鉴权;当前仅返回 {"ok":true},不含密钥与业务细节,无需再加密。进程日志默认不打印 healthz 访问,避免刷屏。 |
GET /metrics |
默认 METRICS_AUTH=true |
指标会暴露请求量、上游成败、换 Key 次数等运维面信息;管理台已公网时,无鉴权 metrics 等于白给侦察。抓取端带 Admin Bearer,或内网关闭鉴权且公网屏蔽该路径。 |
| 关闭指标 | METRICS_ENABLED=false |
完全不注册路由 |
本机开发可直接访问 http://127.0.0.1:8080/。
| 变量 | 默认 | 说明 |
|---|---|---|
LISTEN |
:8080 |
监听地址 |
UPSTREAM |
https://integrate.api.nvidia.com/v1 |
默认上游(系统端点) |
ADMIN_TOKEN |
(空 → 首次随机生成) | 管理台登录;生成 sk-admin-… → credentials |
GATEWAY_TOKEN |
(空) | 可选:首条访问密钥 / 遗留 /v1 回退;空则生成 sk-gw-… |
KEY_ENCRYPTION_SECRET |
(空 → 首次随机生成) | 落盘 AES-GCM;生成 sk-enc-… → key_encryption.secret + credentials。启用时还会在同目录建立 key_encryption.salt(enc:v2: 的 PBKDF2 盐,无对应环境变量,仅文件) |
DB_PATH |
./data/gateway.db |
SQLite 路径 |
RETRY_MAX_KEY_SWITCHES |
5 |
单请求最多换 Key 次数(账号/Key 级失败;容量类错误不按此扫同端点 Key) |
RATE_LIMIT_COOLDOWN |
60s |
上游 429 冷却 |
SERVER_ERROR_COOLDOWN |
10s |
上游 5xx 冷却 |
INVALID_COOLDOWN |
24h |
判定失效后的冷却 |
CAPACITY_COOLDOWN |
0 |
worker/容量饱和后 Key 冷却(默认 0:容量是端点共享,勿停车全部 Key) |
CAPACITY_SAME_KEY_RETRIES |
6 |
容量错误时同 Key 额外退避重试次数 |
CAPACITY_RETRY_BASE |
1s |
同 Key 退避基数(× 第几次:1s、2s…,单次上限 8s) |
ENDPOINT_MODEL_MAX_INFLIGHT |
32 |
每端点+模型网关侧并发上限,0=关闭(防打爆上游 worker) |
ENDPOINT_MODEL_INFLIGHT_WAIT |
45s |
槽位满时排队等待;超时返回 503 + Retry-After |
UPSTREAM_RESPONSE_HEADER_TIMEOUT |
120s |
等待上游响应头上限 |
UPSTREAM_NON_STREAM_TIMEOUT |
300s |
单次非流式请求总时长上限(含读完 body) |
UPSTREAM_STREAM_IDLE_TIMEOUT |
120s |
流式请求空闲超时:无字节到达即中止(防半开连接钉死并发槽位) |
PROBE_ENABLED |
false |
启动时开启定时探测 |
PROBE_INTERVAL |
30m |
探测间隔(支持 7d / 365d 等 day 单位) |
PROBE_SMART |
true |
定时探测跳过近期 valid Key,优先问题 Key |
STREAM_INCLUDE_USAGE |
true |
流式请求自动注入 stream_options.include_usage 以便统计 tokens |
PROBE_CONCURRENCY |
10 |
探测并发 |
PROBE_AUTO_DISABLE |
false |
失效自动禁用 |
PROBE_TIMEOUT |
15s |
单 Key 探测超时 |
PROBE_MODEL |
meta/llama-3.1-8b-instruct |
探测用模型 |
LOG_CLEANUP_ENABLED |
true |
日志自动清理 |
LOG_CLEANUP_INTERVAL |
1h |
清理间隔 |
LOG_KEEP_REQUEST |
5000 |
请求日志保留条数 |
LOG_KEEP_SCHEDULE |
15000 |
调度日志保留条数 |
CORS_ORIGINS |
(空) | 空=不发 CORS;*=全部;逗号白名单 |
ALLOW_PRIVATE_ENDPOINTS |
false |
允许私有/本地域名端点(SSRF 放宽) |
GATEWAY_RATE_LIMIT_PER_MIN |
0 |
/v1 每 IP 每分钟上限,0=关闭 |
TRUSTED_PROXIES |
127.0.0.0/8,::1/128 |
仅这些来源的 X-Forwarded-For / X-Real-IP 被采信;其余用直连对端地址(防 IP 伪造) |
ADMIN_MAX_FAIL_PER_MIN |
10 |
Admin Token 错误次数上限,超出返回 429,0=关闭 |
ADMIN_FAIL_BLOCK |
5m |
Admin 失败计数窗口 / 封禁时长 |
METRICS_ENABLED |
true |
暴露 GET /metrics |
METRICS_AUTH |
true |
/metrics 是否要求 Admin Token(公网管理台时请保持 true) |
ASYNC_LOGS |
true |
异步缓冲写入请求/调度日志 |
GLOBAL_UPSTREAM_MAX_CONNS |
200 |
上游 HTTP 连接池相关上限 |
完整示例见 .env.example。
DB_PATH 所在目录(默认 ./data/,已在 .gitignore):
| 文件 | 权限 | 说明 |
|---|---|---|
gateway.db(及 -wal / -shm) |
0600 |
业务库;密钥材料为 enc:v2:… 密文(当前写入与启动迁移目标格式)。若仍有未迁移的 enc:v1:… 行,仅依赖 secret 可读,启动成功后会重加密为 v2 |
credentials.txt |
0600 |
首次生成的 Admin / 访问密钥 / 加密密钥(勿提交 Git) |
key_encryption.secret |
0600 |
落盘加密密钥;与 DB 分开存放,重启必需 |
key_encryption.salt |
0600 |
enc:v2: 的 PBKDF2 盐(16 字节,hex 编码);同样与 DB 分开存放,重启必需,重要性与 secret 等同 |
启动时会将目录收紧为 0700、上述文件为 0600。
- 全新数据目录首次启动会自动创建:没有 DB、或 DB 里尚无任何
enc:密文时,启动会随机生成盐并写入DB_PATH所在目录——因此该目录必须可写,否则启动失败。 - 启动时会把可读的
enc:v1:重加密为enc:v2:(api_keys、access_keys、meta.admin_token)。成功迁移后,这些记录同时需要 secret 与当时生成的盐才能解密。请勿再假设「历史 v1 永久只靠 secret 恢复」。 - 库内已有密文时,缺少 salt/secret 会直接拒绝启动(fail-closed):不会静默生成新盐或新 secret 继续跑(那等于报废全部
enc:v2:)。请从备份恢复 sidecar 文件。空/损坏的 salt 或 secret 文件同样 fatal,不会覆盖重生。 - 仅恢复
gateway.db、不带 salt/secret 会启动失败——这是有意的,避免“服务起来了但密钥全读不出来”。请始终同时备份/恢复三个文件。 - 仍无法解密的
enc:v1:行会跳过并告警,不阻塞启动。
备份:请同时备份 SQLite、key_encryption.secret 与 key_encryption.salt(以及你保存的 credentials.txt)。管理台「设置 → 备份与恢复」可导出配置包 / 在线库快照;DB 快照不含 secret/salt。丢失加密密钥或盐中的任意一个,都将无法解密库内 enc:v2: 字段。
均需 Header:Authorization: Bearer <ADMIN_TOKEN> 或 X-Admin-Token: <ADMIN_TOKEN>。
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /admin/api/stats |
池统计 |
| GET | /admin/api/config |
公开运行配置摘要 |
| GET/POST | /admin/api/access-keys |
客户端访问密钥列表 / 创建 |
| GET | /admin/api/access-keys/{id} |
查看完整访问密钥(可复制) |
| PATCH/DELETE | /admin/api/access-keys/{id} |
启停 / 删除访问密钥 |
| GET/POST | /admin/api/endpoints |
端点列表 / 添加 |
| PATCH/DELETE | /admin/api/endpoints/{id} |
更新 / 删除端点 |
| GET | /admin/api/keys |
上游 Key 列表 |
| POST | /admin/api/keys/import |
导入上游 Key |
| PATCH | /admin/api/keys/{id} |
更新 |
| GET | /admin/api/keys/export |
导出明文上游 Key |
| POST | /admin/api/keys/batch |
批量启停 / 删除 |
| POST | /admin/api/probe |
批量状态探测 |
| POST | /admin/api/keys/{id}/test |
单 Key 对话探测 |
| GET/PUT | /admin/api/settings |
运行时设置 |
| GET | /admin/api/backup/export |
导出配置包 JSON(含明文密钥) |
| POST | /admin/api/backup/db |
在线 SQLite 快照(VACUUM INTO) |
| POST | /admin/api/backup/import |
合并导入配置包 |
| GET | /admin/api/usage/series |
按天用量趋势(仪表盘) |
| POST | /admin/api/logs/cleanup |
立即清理日志 |
| GET | /admin/api/models |
已缓存上游模型 |
| POST | /admin/api/models/discover |
探测上游 /models |
| GET/POST | /admin/api/model-maps |
映射列表 / 创建 |
| POST | /admin/api/model-maps/import |
批量导入映射(JSON items 或 CSV text) |
| PATCH/DELETE | /admin/api/model-maps/{id} |
更新 / 删除映射 |
| GET | /admin/api/logs/requests |
请求日志(含 mapped_model) |
| GET | /admin/api/logs/schedule |
调度日志 |
解析顺序(命中即停):
- 端点级精确映射(
client_model完全相等 + 当前 Key 所属端点) - 全局精确(
endpoint_id = 0) - 端点级通配符
client_model = *(该端点任意未精确命中的客户端 model) - 全局通配符
client_model = *+endpoint_id = 0 - 无映射则 原样转发 客户端
model
* 是整条兜底(不是 gpt-* 这类前缀通配)。同一范围下精确规则永远优先于 *。
GET /v1/models 聚合:启用中的映射客户端 model(不含 *)+ 已探测到的上游 model。
示例:把所有未单独配置的客户端 model 统一改写到某上游:
| client_model | target_model | endpoint_id |
|---|---|---|
gpt-4o |
meta/llama-3.1-70b-instruct |
0(精确) |
* |
meta/llama-3.1-8b-instruct |
0(兜底) |
- 无默认弱口令:
change-me-*字面量会启动失败;推荐 Token / 加密密钥留空,首次随机生成。 - 日志脱敏:FIRST RUN 只打 prefix;完整密钥仅在
credentials.txt(0600)。 - 文件权限:数据目录
0700,库与凭证0600(启动自动收紧)。 - 落盘加密默认开启:首次生成
KEY_ENCRYPTION_SECRET;新写入采用enc:v2:(PBKDF2-HMAC-SHA256 派生密钥 + 持久化盐),历史enc:v1:记录永久可读。备份务必同时包含key_encryption.secret与key_encryption.salt——丢失任一,enc:v2:密钥材料都无法恢复。 - 部署模型:默认本地 HTTP;TLS / 域名由 Caddy/Nginx 等处理。支持整站公网(管理台 +
/v1)或仅暴露 API(见上文 Caddy 场景 A / B)。 - Admin 公网可用但失陷代价高:可 export 全部上游 Key、配置自定义端点(出站带 Key)。务必使用强随机
ADMIN_TOKEN、HTTPS,并避免在公共电脑保存 Token。 /healthz公开无鉴权:仅存活探测,可安全给负载均衡使用。/metrics默认需 Admin 鉴权(METRICS_AUTH=true);内网抓取可关鉴权并在反代屏蔽公网路径。- CORS 默认关闭;浏览器跨域时再配置
CORS_ORIGINS。 - 自定义端点 默认禁止私有/本地域名;仅内网调试时打开
ALLOW_PRIVATE_ENDPOINTS。 - 列表与日志只展示 Key 前缀;
keys/export仍返回完整上游 Key。
go test ./...
go build -ldflags="-s -w" -o bin/somanyapikeys ./cmd/server
# 或
make test
make build主要目录:
cmd/server/ 入口
internal/admin/ 管理 API
internal/auth/ 鉴权
internal/config/ 环境变量
internal/keypool/ 调度
internal/proxy/ /v1 代理
internal/probe/ Key 探测
internal/store/ SQLite、bootstrap、加密、访问密钥
web/ 嵌入式管理台静态资源
推送符合 v* 的 tag 后,GitHub Actions 会自动交叉编译常用平台并创建 Release:
| 平台 | 产物 |
|---|---|
| Linux amd64 / arm64 | .tar.gz |
| macOS amd64 / arm64 | .tar.gz |
| Windows amd64 / arm64 | .zip |
同时附带 checksums.txt(SHA-256)。
# 打 tag 并推送(触发 .github/workflows/release.yml)
git tag v0.1.0
git push origin v0.1.0
# 本地同样产物(写入 dist/)
make release-local
# 或指定版本号
make release-local VERSION=v0.1.0二进制为 CGO_ENABLED=0 静态构建(modernc.org/sqlite),无需系统 SQLite。启动时日志会打印 version / commit / date。
见 LICENSE。