Go + SQLite 轻量级 API 反向代理网关,兼容三大主流AI API(OpenAI/Anthropic/Google Gemini),支持 API 格式转换、多 token 轮转、可用性检查、自动恢复与状态持久化。
src/
├── main.go 入口:flag 解析、加载配置、启动 HTTP 服务器、优雅关闭
├── globals.go 全局变量:配置状态、共享 transport/HTTP 客户端、writeJSON 工具
├── constants.go 常量定义:超时阈值、可用性类型共用名、并发与超时保护常量
├── types.go 数据结构:TokenMapConfig/UpstreamConfig/AvailabilityConfig/...
├── singleflight.go 可用性检查 singleflight(自实现,仅用标准库)
├── cron.go 轻量 Cron 解析调度 + LRU 缓存
├── providers.go 可用性 Provider:DeepSeek 余额、OpenCode-Go 用量等检查实现
├── state.go 状态管理:从 SQLite 加载/保存(锁外 I/O 快照写)、一致性协调
├── scheduler.go 定时调度:exhaust 恢复检查 + 状态持久化触发
├── transform.go API 格式转换:4 格式 × 请求/响应转换器(openai/anthropic/gemini 互转,anthropic pivot)+ 模型列表 6 向直转
├── transform_stream.go SSE 流式转换状态机(4 格式两两互转,经 anthropic pivot)
├── gemini_shadow.go Gemini thoughtSignature 影子存储(按 tool_call_id 存完整 parts,多轮回放)
├── reasoning_vendor.go reasoning vendor 兼容(thinking 历史重写、thinking 禁用时剥离 effort)
├── cache_injector.go Anthropic cache_control 自动注入(最多 4 个断点)
├── thinking_rectifier.go thinking signature 自动修复(400 重试时剥离 thinking 块)
├── gateway.go 核心转发:fakeToken → upstream 队列轮转(原子挑选)、请求注入、格式转换、流式响应
├── proxy.go 上游代理:per-upstream 统一代理 + per-model 覆盖,transport 缓存与 client 变体
└── auth.go IP 账号认证:-auth 启用后的登录/登出/中间件/会话管理
- Go ≥ 1.26
- 依赖
modernc.org/sqlite(纯 Go 实现,无需 CGo)与golang.org/x/crypto(bcrypt 密码哈希),保持单 exe 静态构建,跨平台编译方便 - 在
src/目录下执行:
go build -o api-gateway.exe .产物 api-gateway.exe 可独立部署,运行时仅需 gateway.db(首次启动自动创建空库)。
| flag | 说明 |
|---|---|
-p / -port |
运行端口,可重复指定以同时监听多个端口,支持纯端口(9090)或 host:port(127.0.0.1:9091、:9092),未传默认 :9090。所有端口共享同一份配置/DB/状态 |
-db |
SQLite 数据库文件路径,默认 gateway.db |
-auth |
启用 IP 账号认证。启用后所有 API 请求需先通过 /login 登录,否则返回 401。未启用时行为不变(接收所有 IP 来源的请求) |
-account |
交互式添加账户后退出(不启动服务器)。依次输入用户名、密码、确认密码,密码输入隐藏明文 |
-e <file> / -export <file> |
将 -db 库全量导出为 JSON 文件后退出(不启动服务器) |
-i <file> / -import <file> |
将 JSON 文件全量导入 -db 库后退出(不启动服务器,全量覆盖) |
-e 与 -i 互斥。
多端口示例:
# 同时监听 0.0.0.0:9090、127.0.0.1:9091、:9092,共享同一份配置
api-gateway -p 9090 -p 127.0.0.1:9091 -p :9092- 首次运行无
gateway.db时启动会创建空库,日志提示:
TokenMap loaded from DB (fakeTokens=0, upstreams=0)
State loaded from DB (0 upstreams)
请使用 -i example.json 导入配置,或直接用 sqlite3 CLI 编辑 gateway.db 后重启。
Gateway running on port 9090
- 参考
example.json编写配置(脱敏示例,含 fakeTokens 队列与 upstreams 配置),导入:
api-gateway -i my-config.json- 正常启动:
api-gateway -p 9090
# 或同时监听多个端口
api-gateway -p 9090 -p 127.0.0.1:9091- 客户端用 fakeToken 请求,token 注入方式按优先级递减:
Authorization: Bearer xxx/X-Goog-Api-Key/?key=/X-Api-Key。网关将 fakeToken 替换为 upstream 的 realToken 转发到 targetBase。
- 导出:
api-gateway -e dump.json— 备份当前库为可读 JSON - 导入:
api-gateway -i dump.json— 从 JSON 恢复/迁移配置(全量覆盖 DB) - 也可直接用
sqlite3CLI 编辑gateway.db后重启生效 - JSON 文件含明文
realToken,需自行保护文件权限
详见下文「状态持久化」与「配置导入导出」章节。
- 登录 https://opencode.ai
- 打开浏览器开发者工具 → Network
- 访问
/workspace/{workspaceId}/usage页面 - 找到
_server请求,从 Request Headers 复制完整的 cookie - workspaceId 从 URL 中获取(格式:
wrk_开头)
- fakeToken: 用户对外持有的假 token,请求中通过
Authorization: Bearer xxx、X-Goog-Api-Key、?key=或X-Api-Key传入。 - upstream: 一个真实 API Key 的抽象标识,包含
realToken(真实密钥)、targetBase(上游 base URL)以及可用性配置。 - 每个 fakeToken 对应一个有序的 upstream 队列(
FakeTokens),请求被转发到队列首部非 exhaust 的 upstream;请求后若触发 exhaust 则该 upstream 轮转到队尾,实现自动容灾。
count 类型的 limit 若为 0(或未配置),表示无上限,计数永不触发 exhaust。
未声明 type 的 upstream 默认使用 none 类型(不统计,永不耗尽)。
| 类型 | 含义 | exhaust 条件 | 恢复依据 |
|---|---|---|---|
none |
不统计(默认) | 永不 exhaust | 无(透传所有错误,不进入恢复调度) |
count |
请求次数限流 | count >= limit |
RecoveryCron(cron 周期匹配 = RefreshCron),按表达式定时重置 count |
balance |
余额型 | provider 返回 balance ≤ 0 | RecoveryAt(精确时间点),由 provider 按最长的 resetInSec 设定 |
usage |
用量型 | provider 返回任一层级 ≥ 100% | RecoveryAt(精确时间点),由 provider 按已耗尽层级最长的 resetInSec 设定 |
exhaust |
触发即耗尽 | 响应可用性错误时立即 exhaust | RecoveryAt(精确时间点),默认 30min 后自动恢复 |
所有配置结构(TokenMapConfig / UpstreamConfig / AvailabilityConfig / CacheInjectorConfig)在 loadFromDB() 启动加载与 importFromJSON() 导入时集中调用 Validate(),校验失败即 fail-fast:启动加载直接 log.Fatal 退出,导入在触碰 DB 前返回错误(DB 保持原状)。校验收集全部错误一次性返回,便于一次定位所有问题。
校验规则:
UpstreamConfig:targetBase非空、URL 合法、scheme 为http/https、host 非空。formatTransform空串合法(透传);非空必须命中{openai, openai_responses, anthropic, gemini}。realToken可空(本地 Ollama 等无需鉴权的 upstream 合法)。proxy非空时必须是合法代理 URL:scheme ∈{http, https, socks5}且 host 非空(空串=直连合法)。modelProxies的 value 为空串(显式直连豁免)或合法代理 URL,逐个校验。
AvailabilityConfig:type必须命中{count, usage, balance, exhaust, none}。count型要求limit > 0(否则Count>=0立即耗尽)。balance/usage型要求provider非空(否则静默走 fallback)。
CacheInjectorConfig:Enabled=false时跳过 TTL 校验;启用时ttl仅可为"5m"、"1h"或空串(=默认5m)。- 空库合法:
Upstreams为空不算错误(首次启动空库是正常场景)。
client → 网关 (带 fakeToken)
│
▼
┌─ 全局并发信号量(channel semaphore,容量 256)——超限请求阻塞排队
▼
提取 fakeToken(bearer / goog header / query key / api-key header 优先级递减)
│
▼
查找 FakeTokens[fakeToken] 队列
│
▼
pickFirstAvailableUpstream:写锁内原子扫描队列,跳过已 exhausted 的前缀(移到队尾),
返回首个可用 upstream 及其配置快照(省去三次单锁间的 TOCTOU 竞态)
│
├─ 整队列不可用 → 返回 503
│
└─ upstream 可用 → 用 realToken 替换 fakeToken,转发到 targetBase
│
├─ count 型 → incrementCount,达 limit 则标记 exhaust
│
└─ 检查响应状态码
├─ 401/402/403/429 → singleflight 去重(同 upstream
│ 并发仅首次调用 provider),exhaust 则 rotate
├─ 流式 (SSE) → streamIdleTimeout 空闲超时 + maxStreamLife 硬上限
└─ 其他 → 直接转发响应
请求中的 token 注入方式取决于原始请求如何传入 fakeToken:
X-Goog-Api-Keyheader → 替换同 header 为 realToken?key=query → 替换 query 中 key 值为 realToken- 两者同时存在 → 两者同时替换
X-Api-Keyheader → 替换该 headerAuthorization: Bearer/ 其他 → 替换 Authorization header
流式分支有两个保护层:
- 空闲读超时(
streamIdleTimeout = 5min):启动 goroutine 监控,超时内未读到上游数据则调用cancelReq()中断上游连接。 - 最大生命周期(
maxStreamLife = 30min):流式上下文叠加context.WithTimeout硬上限。即使空闲监控 goroutine 在边界情况未触发,到期也会强制取消上游连接,防止流式 goroutine 无限堆积。
- 所有配置与运行时状态统一存储在
gateway.db(SQLite,WAL 模式,明文不加密,依赖文件权限保护)。 - 表结构:
upstreams(配置列 + 运行时状态列同行,含 per-upstreamaliasesJSON 列)、upstream_tiers(usage 型层级配置+状态)、upstream_extra(Extra map)、fake_tokens(fakeToken→有序 upstream 队列,priority=队列下标)、accounts(IP 认证账号,bcrypt 哈希)、ip_sessions(IP 登录会话,FK 关联 accounts)。 - 恢复调度依据按类型二选一:
- count 型:
RecoveryCron(cron 表达式,对应配置的RefreshCron) - usage/balance/exhaust 型:
RecoveryAt(精确时间点time.Time,由 provider 在 exhaust 时设定)
- count 型:
- 内存中维护
stateDirty标志与stateGen代际计数器(atomic.Uint64),每 5min 检查并写入。 saveState()优化为锁内快照 → 锁外 I/O:在写锁内深拷贝 state 快照并记录代际后立即释放锁,SQLite 事务在锁外执行;提交后再次取锁,仅当代际未变(无新写入)才清 dirty,避免写库期间(可能数百 ms)阻塞所有请求。- 调度器退出前 final save 确保持久化一致性;main 通过
schedDonechannel 等待 final save 完成后再db.Close(),避免事务被截断。 - 启动时执行
cleanFakeTokenQueues(清理 FakeTokens 队列中引用不存在的 upstream)与对称的reconcileStateWithConfig(为 tokenMap 中存在的 upstream 补齐初始 AvailabilityState、删除孤立 state 条目),并重置stateDirty=false。 - 运行时 upstream 队列轮转顺序不持久化(重启恢复
fake_tokens.priority定义的配置顺序)。 - 旧文件迁移:usage/balance/exhaust 型旧
RecoveryCron字段在重启后被忽略,RecoveryAt为零值触发立即复查,首次写入后清空旧字段。
两个一次性管理 flag,执行后立即退出,不启动 HTTP 服务器:
-e <file>/--export <file>:将-db库全量导出为单个 JSON 文件-i <file>/--import <file>:将 JSON 文件全量导入-db库(全量覆盖,DB 中原有数据被清空替换)
两者互斥。JSON 文件格式为 DBDump:{ "tokenMap": {fakeTokens, upstreams}, "state": {upstream: AvailabilityState}, "accounts": [...], "ipSessions": [...] },结构与运行时内存模型一致,time.Time 走 RFC3339Nano 字符串,人类可读可编辑。accounts 与 ipSessions 为可选字段(omitempty),旧版导出文件不含这两个字段,导入时兼容。
accounts 与 ipSessions 格式示例:
{
"accounts": [
{ "username": "admin", "passwordHash": "$2a$10$N9qo8uLOickgx2ZMRZoMye..." }
],
"ipSessions": [
{ "ip": "192.168.1.100", "username": "admin", "loginAt": "2026-07-21T10:30:00+08:00" }
]
}导入流程(按顺序):
- 预校验:解析 JSON 后立即对其调用
Validate()(见上文「配置校验」),失败则拒绝,不触碰 DB,打印全部错误。 - 自动备份:若
gateway.db已存在,先整体读出并写入同目录gateway.db.bak(0600 权限),再开始写入——失败回滚也仍有原库备份可恢复。 - 全量覆盖:单事务内
DELETE6 张表(ip_sessions→accounts→子表upstream_tiers/upstream_extra/fake_tokens→父upstreams)后依次INSERT。 - 导入防御:fakeToken 队列中重复 upstream 在 Go 层用
seenmap 跳过保留首次(INSERT OR IGNORE仅作数据库层兜底);队列引用不存在的 upstream 跳过避免 FK 违约;state 中有但 upstreams 中无的孤儿条目警告忽略。 - 账号与会话导入:
accounts先于ip_sessions插入(FK 约束);ip_sessions中引用不存在账号的条目跳过并警告。 - 任一步失败整体
Rollback,DB 保持原状(前面已备份的.bak不受影响)。
典型用途:备份/恢复 gateway.db、手工编辑配置后导入、跨环境迁移。导出文件含明文 realToken,需自行保护文件权限。
每个 upstream 可选配置 aliases 字段,提供客户端请求模型名 → 上游真实模型名的映射,用于在转发前重写请求中的模型名。该字段缺省时该 upstream 不启用别名(请求原样转发);不同 upstream 可独立配置不同 alias 映射,互不干扰。别名替换不依赖 formatTransform——未配置格式转换时同样生效(body 的 model 字段或 Gemini path 会被改写,非字节级零改动)。
aliases 结构为 map[string]string,key 与 value 均为字符串:
"deepseek": {
"realToken": "***",
"targetBase": "https://api.deepseek.com",
"aliases": {
"gpt-4-turbo": "deepseek-chat",
"claude-3-opus": "deepseek-chat"
}
}替换规则:
- 仅当请求中提取到模型名(body 的
model字段,或 Gemini 风格 URL path/models/{name})且该模型名命中当前 upstream 的aliases的 key 时,替换为对应 value。 - 同时覆盖三种 API 风格:
- OpenAI/Anthropic 风格:重写请求体 JSON 的
model字段(重新序列化 body,Content-Length由 transport 自动重算)。 - Gemini 风格:重写 URL path 中的模型名段(如
/v1beta/models/gemini-pro:generateContent→/v1beta/models/gemini-1.5-pro:generateContent)。
- OpenAI/Anthropic 风格:重写请求体 JSON 的
- body 非 JSON 或
model字段非字符串时静默跳过 body 重写(不影响 URL path 重写)。 - 作用于 attempt 循环内:alias 重写仅作用于本次 attempt 的局部
sendBody/sendModel/basePath,不污染跨 attempt 复用的原始bodyBytes/r.URL.Path/modelStr。重试到另一个 different-format upstream 时按其各自的aliases重新计算,避免跨 upstream 串污染。 - DB 持久化:
upstreams表aliases列存 JSON 编码的 map 字符串;导入导出(-e/-i)跟着 upstream 配置一起序列化。旧库无此列时启动自动ALTER TABLE ADD COLUMN兼容。 - 模型列表响应反向展开:上游返回模型列表时,按 value→key 反向展开——见下文「模型列表请求转换」。
每个 upstream 可选配置两个代理字段,控制网关向该 upstream 发送请求时是否经代理:
"gemini": {
"realToken": "***",
"targetBase": "https://generativelanguage.googleapis.com",
"proxy": "socks5://127.0.0.1:1080",
"modelProxies": {
"gemini-2.5-pro": "http://user:pass@127.0.0.1:8080",
"gemini-2.5-flash": ""
}
}生效规则(优先级递减):
modelProxies[sendModel]命中 → 覆盖proxy。判断 key 是 alias 替换、格式转换后实际发往上游的模型名(sendModel),而不是客户端请求名——与 alias 机制类似,但以“实际发送的模型名”为准。例如客户端请求gpt-4经 alias 映射到gemini-2.5-pro,则以gemini-2.5-pro查modelProxies。modelProxiesvalue 为空串""→ 该模型显式直连(豁免 upstream 级proxy)。modelProxies未命中 → 回退 upstream 级proxy。proxy也未配置 → 直连,行为与旧版完全一致。
要点:
- 支持协议:
http://、https://、socks5://三种代理 URL(URL 内嵌user:pass认证原生支持)。标准库对socks5://的 hostname 解析在代理端完成(与socks5h等价),若需本地解析需自定义 dialer,当前不支持。 - 可用性检查也走代理:
proxy配置后,provider 可用性检查(DeepSeek 余额、OpenCode-Go 用量)同样经代理发出,避免 targetBase 需代理可达时检查误判 exhaust。modelProxies与模型无关,不适用于检查路径。 - 列表请求(
GET /v1/models等,无模型名)自然走 upstream 级proxy。 - 校验 fail-fast:
proxy或modelProxies任何 value 非法(无法解析 / scheme 不在上述三种 / host 为空)→ 启动log.Fatal退出 /-i导入拒绝且不触碰 DB,与targetBase同策略。 - 连接池复用:同一代理 URL 全局只建一个 Transport(连接池参数与
sharedTransport一致),流式/非流式 client 超时语义与直连一致。 - 日志脱敏:命中代理时记
[PROXY] upstream=xxx model=yyy route=modelProxies|upstream -> socks5://***@127.0.0.1:1080,代理凭据不落日志。 /status/check不回显代理配置(可能含凭据);查看完整配置用-e导出。- DB 持久化:
upstreams表proxy列存 URL 字符串、model_proxies列存 JSON 编码的 map;旧库启动时ALTER TABLE ADD COLUMN自动迁移;-e/-i跟随 upstream 序列化。
每个 upstream 可选配置 formatTransform 字段,使网关在转发前将客户端请求体转换为目标上游的 API 格式,并在响应返回时反向转换回客户端格式。不配置或留空时,formatTransform 相关逻辑完全跳过,请求体/path 多数情况下字节级透传(视 aliases / extra / cacheInjection 等独立配置而定,见下文「高级转换特性」)。
| 值 | 目标格式 | 说明 |
|---|---|---|
openai |
OpenAI Chat Completions | 上游走 /v1/chat/completions |
openai_responses |
OpenAI Responses API | 上游走 /v1/responses |
anthropic |
Anthropic Messages | 上游走 /v1/messages |
gemini |
Google Gemini | 上游走 /v1beta/models/{model}:generateContent(流式 :streamGenerateContent?alt=sse) |
| 不指定 | — | formatTransform 透传(body/path 不变);但 aliases/extra 增强项/cacheInjection 仍独立生效 |
- 目标格式 = upstream 配置的
formatTransform。 - 客户端格式 按请求 URL path 自动检测:
/v1/chat/completions→openai、/v1/responses→openai_responses、/v1/messages→anthropic、/v1beta/models/{model}:...→gemini,其他→unknown(透传)。 - 链式转换:任何两种不同格式间都经 anthropic 作为 pivot 中间格式两步完成(含
openai_chat↔openai_responses),对调用方透明。仅当客户端格式 == 目标格式时才透传。 - 目标 path 重写:转换路径下,目标 URL path 按目标格式规范端点替换;透传路径保留原始
r.URL.Path。 - 认证头重写:转换路径下,按目标格式注入认证(gemini→
X-Goog-Api-Key/?key=;anthropic→x-api-key+anthropic-version;openai→Authorization: Bearer);透传路径沿用原 token 注入优先级(X-Goog-Api-Key/?key=/X-Api-Key/Authorization)。
模型列表端点(GET /v1/models / GET /v1beta/models)也走格式转换路径,但与业务端点(chat/messages/responses/gemini generate)不同——不经 anthropic pivot,4 种格式 6 个方向直接两两转换,保留 inputTokenLimit/outputTokenLimit 等元数据,避免经 anthropic 中转丢失。
- 客户端格式判定(按 path + auth header):
/v1beta/models→ gemini(path 自带上游风格信号)。/v1/models+X-Api-Key或anthropic-version头任一 → anthropic。/v1/models+ 其余(Bearer / 兜底) → openai_chat(openai_responses 复用同一列表端点)。
- 目标 path 重写:gemini 上游走
/v1beta/models,其余走/v1/models。 - 认证头:与业务端点共用
swapAuthForTarget按目标格式注入。 - 6 向直转:openai↔anthropic、openai↔gemini、anthropic↔gemini(每对两向)共 6 个方向各自实现经中性
modelsList结构;openai_chat ↔ openai_responses 列表结构完全相同走 fast path 透传。 - 字段映射约定:
- OpenAI → Anthropic:
id→id,display_name用id兜底,owned_by丢弃。 - Anthropic → OpenAI:
id→id,owned_by留空。 - Gemini → OpenAI/Anthropic:从
name="models/x"提取尾段作为id,displayName直接保留为display_name/displayName。 - 反向 OpenAI/Anthropic → Gemini:
id拼回name="models/"+id,displayName兜底用id,固定填supportedGenerationMethods: ["generateContent","streamGenerateContent"],inputTokenLimit/outputTokenLimit在 openai/anthropic 源无对应字段时填 0。
- OpenAI → Anthropic:
- 别名反向展开(两阶段):upstream 配置
aliases时,模型列表响应按下列两阶段处理,使列表显示与请求路由的 alias 行为完全一致:- 删除被覆盖条目:若某 entry 的 ID 等于某个 alias key(且该 key 的
value不同——即该客户端名已被重定向到另一真实上游模型),则删除其原条目,避免显示与路由行为不符的字段。 - 追加 alias 克隆:为每个指向真实名的 alias key 追加一条克隆 entry,与原真实名条目字段完全相同——仅
id/name(ID 字段)改为 alias key,display_name/displayName等其余字段保留原真实名条目的值,不随 alias key 变。 一对多(多个 alias key → 同一真实名)会展开为多条 alias entry;k==v自指跳过,空串无效果。
- 删除被覆盖条目:若某 entry 的 ID 等于某个 alias key(且该 key 的
- 错误响应:列表请求的 4xx/5xx 错误响应同样走
TransformErrorResponse转为客户端列表格式后返回(inFormat 此时被覆写为客户端列表格式)。 - 未配置
formatTransform时完全透传(outFormat==""→doListTransform=false),行为同现状。上游格式 == 客户端列表判定格式时也透传。
所有 4 种格式两两之间的 SSE 流式转换均支持(openai_chat↔anthropic、openai_responses↔anthropic、gemini↔anthropic 三个直接方向 + 经 anthropic pivot 的链式方向如 openai_chat↔openai_responses、openai↔gemini)。流式转换器实现为状态机,按 SSE 事件块增量转换,保持上游→客户端的低延迟。Gemini 流式输出采用累积快照 diff 语义(每个 chunk 携带截至当前的完整内容)。
流式转换要点:
- reasoning 透传:OpenAI Chat 的
delta.reasoning/reasoning_content、Responses 的response.reasoning.delta/.done与response.reasoning_summary_text.*事件均转成 Anthropicthinkingblock(content_block_start+thinking_delta+content_block_stop)。 - 懒发 message_start:
message_start推迟到首个实际内容/usage 事件时才发送,避免空响应留下"悬挂消息"。 - UTF-8 安全累积:跨 TCP chunk 边界拆分的多字节 UTF-8 字符会正确累积,不损坏工具调用 JSON 参数。
- 流式 error 事件:上游流式传输中途出错时按客户端格式分流渲染:Anthropic 客户端发
event: error+data: {...};OpenAI Chat/Responses 客户端发data: {"error":{...}}+ 终止data: [DONE];Gemini 客户端发data: {"error":{"code":500,"status":"INTERNAL",...}}。并抑制合成的message_stop,避免把失败伪装成正常完成。 - 无限空白中止:工具调用
arguments中超过 500 连续空白字符视为上游异常,中止该 tool block 以防客户端挂起。 - 重复 finish_reason 去重:异常上游多次发送终止事件时,
stop_reason仅设置一次。
example.json 片段(让 OpenAI/Anthropic 客户端复用 Gemini 上游):
"gemini": {
"realToken": "***",
"targetBase": "https://generativelanguage.googleapis.com",
"formatTransform": "gemini",
"availability": { "type": "count", "limit": 250, "refreshCron": "0 0 16 * * *" }
}DB 直接编辑:upstreams 表 format_transform 列存配置值字符串(openai/openai_responses/anthropic/gemini/空)。注意新版 /status/check 响应不回显 formatTransform(仅返回健康/计数/恢复信息),需查看完整配置请直接读 DB 或用 -e 导出。
- 错误响应转换:4xx/5xx 错误响应体在转换路径下会经
TransformErrorResponse转为客户端格式后返回(包括可用性错误 401/402/403/429)。各厂商错误 JSON 结构差异较大,转换尽量保留error.message/type等通用字段,无法映射的字段按目标格式兜底。仅对resp.StatusCode < 300的成功响应走TransformResponse。 - 请求转换失败 → 继续尝试下一个 upstream:客户端请求体无法解析为目标格式时,不直接 400 中断,而是记录
[TRANSFORM] request convert failed (will try next upstream)日志后继续尝试队列中的下一个 upstream(透传 upstream 不进入转换路径,可正常处理)。全部 upstream 均失败后由循环外兜底返回503。 - 响应转换失败 → 回退原 body:响应转换出错时记日志并原样返回上游响应(非流式);流式转换 Feed 出错则中断流并记日志。
- Gemini 工具调用 ID:Gemini
functionCall无独立 ID 字段,转换到 anthropic/openai 时用crypto/rand合成随机十六进制 ID(前缀gemini_synth_,无状态),可能与上游真实 ID 不一致;反向(anthropic/openai→gemini)时真实 ID 保留写入functionCall.id,仅丢弃gemini_synth_合成前缀的 ID。 - 链式转换语义损耗:
openai_chat↔openai_responses等跨子格式转换经 anthropic pivot 两步完成,工具调用、reasoning 等字段经历两次映射,可能出现语义损耗(如 Responses 的 namespace tool 经 anthropic 中转后降级为普通 function tool)。 - 非法值兜底:
formatTransform配置为非上述 4 个合法值时,记[TRANSFORM] invalid formatTransform ... -> passthrough警告日志后按透传处理,不报错。
除 formatTransform 外,网关还内置以下增强能力(多数通过 upstreams[].extra map 的字符串参数按 upstream 单独启用)。extra map 同时承载可用性检查参数(如 OpenCode-Go 的 cookie/workspaceId,见上文)。以下 extra 参数不依赖 formatTransform 开启——即使透传(formatTransform 为空),只要满足各自的生效条件,仍会生效:
| key | 取值 | 作用 | 生效条件 |
|---|---|---|---|
pathPrefix |
自定义 API 版本前缀,如 "/api/v3" |
替换目标 URL path 开头的 /v1 或 /v1beta 为自定义前缀。用于上游 base URL 使用非标准 API 版本前缀的场景,如火山引擎的 /api/v3/chat/completions 而非 /v1/chat/completions。同时覆盖透传路径、格式转换路径和列表转换路径。 |
始终生效(无论是否开启 formatTransform,只要构造出的 targetPath 以 /v1 或 /v1beta 开头即触发替换) |
codexBackend |
"true" / "fast" |
对发往 ChatGPT Codex 后端的 Responses 请求做字段整形(注入 store:false/include/stream:true/兜底字段,剥离 max_output_tokens/temperature/top_p;fast 额外注入 service_tier:"priority") |
上游实际发送格式为 openai_responses(转换后或 Responses→Responses 透传) |
preserveReasoningContent |
"true" |
Anthropic→OpenAI Chat 转换时把 thinking 块提取为 reasoning_content 字段(Kimi/DeepSeek/MiMo 等 reasoning vendor 兼容)。仅当该 assistant 消息含 tool_use 块时才写入;纯 reasoning + text 的消息 thinking 会被静默丢弃 |
仅 formatTransform 开启时生效(需跨格式转换) |
reasoningVendor |
"auto" 或 "kimi"/"deepseek"/"mimo" 等非空值 |
重写 thinking 历史为占位符,兼容拒绝原始 thinking 块的供应商(auto 按 upstream 名/targetBase 自动检测) |
客户端格式为 anthropic(透传或转换前均可) |
stripEffortWhenThinkingDisabled |
"true" |
thinking.type != enabled 时剥离 reasoning_effort/output_config.effort 参数(DeepSeek Anthropic 兼容端点要求) |
客户端格式为 anthropic(透传或转换前均可) |
4 个参数在 attempt 内的作用时点分两类:reasoningVendor 与 stripEffortWhenThinkingDisabled 属于 pre-transform pass(作用于转换前的客户端请求体副本,再将结果写回 sendBody;!doTransform 时同样生效);codexBackend 属于 post-transform pass(作用于转换后的 sendBody 或透传路径下直接改写);preserveReasoningContent 仅通过 TransformOptions 透传到跨格式转换函数内部,透传路径无效果。
其他内置增强(无需配置,默认启用):
- cache_control 自动注入:upstream 配置
cacheInjection: { enabled: true, ttl: "5m" }时,自动在 tools/system/最后 assistant/最后 user 消息末尾注入最多 4 个cache_control: {"type":"ephemeral"}断点,享受 Anthropic Prompt Caching 折扣。不依赖formatTransform,透传 Anthropic→Anthropic 时同样生效。 - Gemini thoughtSignature 影子存储:按
tool_call_id维度存储 Gemini 的thoughtSignature与完整 assistant turnparts数组(含thought:true块),多轮工具调用时原样回放,避免 Gemini 签名校验失败 400。 - thinking signature 自动修复:上游返回 thinking signature 相关 400 错误时,自动剥离 thinking/redacted_thinking 块与残留
signature字段后重试同一 upstream(最多 1 次)。 - reasoning effort 4 档映射:
thinking.budget_tokens与output_config.effort映射到low/medium/high/xhigh四档(output_config.effort == "max"或thinkingadaptive→xhigh;thinking.type == "enabled"但无budget_tokens时默认high),xhigh在转 OpenAI 时降级为high。 - redacted_thinking 占位符:Anthropic 的
redacted_thinking块转 OpenAI/Gemini 时替换为[redacted thinking]占位文本,保留语义。 - 标准参数透传:Anthropic↔OpenAI 转换时透传
frequency_penalty/logit_bias/logprobs/metadata/n/parallel_tool_calls/presence_penalty/response_format/seed/service_tier/top_logprobs/user等 12 个标准参数,不再静默丢弃。 - document/input_file 支持:Anthropic
document块(PDF)转 Gemini 时变inlineData,转 Responses 时变input_file+file_data;转 OpenAI Chat 时被静默丢弃(OpenAI Chat 无文档能力)。 - incomplete_reason 细分:Responses
incomplete状态按incomplete_reason细分,仅max_output_tokens/max_tokens映射为max_tokens,其他映射为end_turn。
为避免泄露所有 upstream 配置与真实 token,状态查询拆为两个端点,且均返回 fakeToken 关联的 upstream:
GET /status:返回一个极简 HTML 查询页(含 token 输入框),前端固定POST到/status/check渲染结果。无需鉴权即可访问页面本身,但只有提交有效 token 才能看到对应队列状态。POST /status/check:接收{"token":"<fakeToken>"},校验该 fakeToken 存在且队列非空,仅返回队列内 upstream 的运行时状态(不含realToken、不含其他 fakeToken 的队列),token 无效返回 401。
curl -X POST http://localhost:9090/status/check \
-H 'Content-Type: application/json' \
-d '{"token":"sk-xxxxxx"}'响应体(upstreams 数组,仅包含该 fakeToken 队列中的 upstream):
{
"upstreams": [
{
"name": "gemini",
"targetBase": "https://generativelanguage.googleapis.com",
"exhausted": false,
"availType": "count",
"count": 10,
"limit": 250,
"recoveryCron": "0 0 16 * * *"
},
{
"name": "opencode-go",
"targetBase": "https://opencode.ai/zen/go",
"exhausted": true,
"availType": "usage",
"count": 0,
"balance": 0,
"tiers": [
{"name": "rolling", "usedPct": 100, "resetInSec": 2592000}
],
"recoveryAt": "2026-07-11T09:09:29Z"
}
]
}| 字段 | 说明 |
|---|---|
upstreams |
该 fakeToken 队列内 upstream 的运行时快照(按队列顺序) |
upstreams[].availType |
可用性类型(count/usage/balance/exhaust/none,缺省为 none) |
upstreams[].limit |
count 型的 limit(其余类型缺省) |
upstreams[].count |
count 型的当前计数(其余类型始终为 0) |
upstreams[].balance |
balance 型的余额(其余类型始终为 0) |
upstreams[].tiers |
usage 型的各配额层级用量(count/balance 型无此字段) |
upstreams[].recoveryCron |
count 型的恢复 cron 表达式(其余类型缺省) |
upstreams[].recoveryAt |
usage/balance/exhaust 型的精确恢复时间点(其余类型缺省) |
响应不包含 realToken、queueFor、lastChecked、顶层 fakeTokens 映射等敏感或全局字段——只对持 token 的调用方暴露其自身队列。HTML 页面在浏览器本地用 fetch 调用 /status/check 完成查询,无外部 JS 依赖。
通过 -auth 启动参数启用基于 IP 的账号认证机制。未启用时网关行为不变,接收所有 IP 来源的请求。
- 启用后,所有 API 请求(
/路由)需客户端 IP 已通过/login登录,否则统一返回401 Unauthorized。 /login和/status端点不受认证限制,始终可访问。- 一个账户可同时给多个 IP 地址登录。
- 账号信息与 IP 登录状态持久化在 SQLite 数据库中(
accounts表 +ip_sessions表),重启后会话不丢失。 - 启动时若
-auth已启用但数据库中没有任何账号,打印警告并退出。
使用 -account 交互式添加账户(密码隐藏明文,bcrypt 哈希存储):
api-gateway -account
# 用户名: admin
# 密码: ********
# 确认密码: ********也可通过 -e 导出 JSON 后编辑 accounts 数组,再用 -i 导入(见下文「配置导入导出」)。
- 客户端浏览器访问
/login,显示登录表单(用户名 + 密码) - 提交后验证凭据,成功则将当前 IP 记入会话,重定向回
/login - 已登录状态下
/login显示会话管理面板:- 当前 IP 地址与账户名
- 该账户下所有已登录 IP 列表
- 每个 IP 可单独退出
- 可一键退出该账户所有 IP
- 当前 IP 退出后立即刷新页面,回到登录表单
时区说明:两种恢复机制均按服务器本地时区处理。cron 表达式按本地时区匹配,
RecoveryAt以带本地 offset 的 RFC3339 格式存储。可通过设置进程的TZ环境变量调整时区(如TZ=Asia/Shanghai)。
使用 6 字段 cron 表达式(秒 分 时 日 月 周),支持 * / */N / N / a-b / a,b,c / a-b/S。RecoveryCron 直接取配置的 RefreshCron。调度器每 1s 匹配当前时间,命中且距上次恢复 ≥ recoveryMinGap(60s)时触发重置计数。
解析结果缓存在容量 256 的 LRU 中,避免重启后反复解析相同表达式。
由 provider 在 exhaust 检查时计算下一次复查时间点:
- OpenCode-Go 用量型:在已耗尽(usagePercent ≥ 100)的 rolling/weekly/monthly 层级中,选取最长的
resetInSec作为间隔,设为RecoveryAt = now + maxReset。因为只要任一层级仍耗尽,整体就不可用,必须等最慢的那个层级恢复。 - DeepSeek 余额型:余额 ≤ 0 时设为
now + 30min。 - 兜底 exhaust:设为
now + 30min。 - 当 provider 返回的
resetInSec异常(≤ 0)时,使用minRecoverGap = 60s地板保护,防止死循环。
调度器每 1s 遍历所有 exhausted upstream,检查 now >= RecoveryAt(零值视为旧文件迁移,立即触发)。触发后按类型分流恢复:
exhaust型 / 无 availability 配置:到达RecoveryAt直接自动恢复(清零Exhausted与RecoveryAt),不调用 provider——因为fallbackResult恒返回Exhausted=true,若复查会形成 60s 死循环。usage/balance型:通过availSF.Do()singleflight 调用 provider 复查,返回新的 exhausted 状态与RecoveryAt;若已恢复则清除 exhausted,upstream 重新参与请求轮转。
单请求体上限 32MB。使用 http.MaxBytesReader 包裹请求体,超限时内部自动写入 413 状态码并返回 *http.MaxBytesError。代码通过 errors.As(rerr, &maxBytesErr) 判断超限后直接 return,避免二次 WriteHeader 触发 "superfluous" 警告。
- 解析
-p/-port/-db/-auth/-e(-export) /-i(-import) flag - 若指定
-e或-i:执行导出/导入后os.Exit(0),不启动服务器(管理操作,互斥) - 调用
loadFromDB()从 SQLite 加载配置与状态(统一数据源),并运行Validate()校验——失败即log.Fatal退出 - 若
-auth启用:调用loadAuthFromDB()加载 IP 会话到内存,检查账号数量——为 0 则log.Fatal退出 - 启动
runSchedulergoroutine - 初始化
reqSem并发信号量(channel semaphore,容量 256),handler 入口 acquire、defer release - 启动 HTTP server,监听
:port,注册GET /status(HTML 查询页)、POST /status/check(按 fakeToken 查询关联 upstream 状态)、/login(IP 认证登录/会话管理)、/login/logout(登出)、/(核心代理 handler,经authMiddleware包裹);配置ReadTimeout=10s/IdleTimeout=120s/MaxHeaderBytes=1MB(防御慢速连接攻击;WriteTimeout=0保护流式 SSE) - 等待 SIGINT/SIGTERM,触发优雅关闭(等待 scheduler final save 完成后关闭 DB)
- 包级全局变量(
tokenMap、stateMap、mu sync.RWMutex、stateGen atomic.Uint64、db、dbPath等) - 共享 HTTP transport/客户端:
sharedTransport(MaxIdleConns=100、MaxIdleConnsPerHost=20、IdleConnTimeout=90s)— 所有代理/provided client 共享连接池,避免每请求新建 TransportproxyClient(120s 超时,复用 sharedTransport)— 普通代理请求streamClient(无整体超时,复用 sharedTransport)— 流式代理请求- provider 可用性检查不使用上述任一 client,而在
providers.go内走httpGetRaw的三阶段重试 client(2s+4s+8s 独立超时,每阶段新建 client 但共用 sharedTransport)
reqSem(channel semaphore,容量 256):全局并发上限,handler 入口 acquire,超限请求阻塞排队availSF(availSingleFlight):可用性检查 singleflight,同 upstream 并发触发仅首次执行 provider 调用writeJSON统一 JSON 响应写入,捕获并记录编码错误
openDB:打开 SQLitedb并设PRAGMA foreign_keys=ON+WAL+busy_timeout、conn.SetMaxOpenConns(1)(避免 WAL 下并发写竞争),CREATE TABLE IF NOT EXISTS6 张表(upstreams/upstream_tiers/upstream_extra/fake_tokens/accounts/ip_sessions)+idx_fake_tokens_order索引;随后用PRAGMA table_info检查upstreams列,对旧库执行ALTER TABLE ADD COLUMN (format_transform / aliases)运行时迁移。供loadFromDB与importFromJSON复用。loadFromDB:查 4 张表填充tokenMap+stateMap;cleanFakeTokenQueues(清队列中引用不存在的 upstream)+reconcileStateWithConfig(为 tokenMap 中存在的 upstream 补齐默认AvailabilityState、删除孤立 state 条目);末尾重置stateDirty=false;调用tokenMap.Validate(),失败log.Fatal。saveState:单事务写回——先持写锁深拷贝 state 快照(含 Tiers 副本)并记录stateGen,释放锁后遍历快照执行 SQLite I/O(UPDATE upstreams状态列 + 每个 upstream 的upstream_tiersDELETE/INSERT);提交后再取锁,仅当代际未变才清 dirty。锁外 I/O 避免写库阻塞所有请求。exportToJSON:复用loadFromDB载入内存 → 查询accounts/ip_sessions表 → marshalDBDump{tokenMap, stateMap, accounts, ipSessions}→ 原子写(tmp+rename,0600);导出 reconcile 后的规范视图importFromJSON:① 解析 JSON 后立即Validate()(不触碰 DB)→ ② 若gateway.db存在则先备份为gateway.db.bak(0600)→ ③ 单事务 DELETE 6 表(先子后父)+ INSERT 全量覆盖(含 accounts/ip_sessions);④ 防御:队列重复 upstream 用 Goseenmap 跳过(INSERT OR IGNORE仅作 DB 兜底从不触发)、引用不存在 upstream 跳过避免 FK 违约、orphan state 警告忽略、ip_sessions 引用不存在账号跳过并警告;任一步失败Rollback(前面已有.bak备份)。
parseCron/parseCronField:6 字段 cron 解析,生成CronSchedule([6]map[int]bool)parseCronCached:LRU 缓存封装cronLRU:双向链表 + map,容量 256,最近访问移头部,超容淘汰尾部
checkAvailability:入口分发,按availCount/availBalance/availUsage/availExhaust/availPassthrough调用对应逻辑(无配置或未知类型默认走 none 型)checkDeepSeekBalance:GET/user/balance,解析total_balance;余额耗尽时返回RecoveryAt = now + 30mincheckOpenCodeGoUsage:GET/_server,解析 rolling/weekly/monthly 三级用量;在所有已耗尽层级(usagePercent ≥ 100)中取最长resetInSec,返回RecoveryAt = now + maxReset- rolling 若不匹配直接 fallback(opencode API 必返回 rolling)
- weekly/monthly 可能缺失,不影响逻辑
- 其他 provider 仅框架,按类型返回兜底:
- balance 型(无具体实现):
Kimi、OpenRouter - usage 型(无具体实现):
Claude、Codex、Gemini、ZAI、MiniMax - 均返回
fallbackResult(Exhausted=true,RecoveryAt = now + 30min)
- balance 型(无具体实现):
httpGetJSON/httpGetText不用共享defaultClient,经httpGetRaw的三阶段重试:每阶段独立超时 client(2s + 4s + 8s),仅对网络错误/超时、5xx、429 触发重试;2xx 立即返回,其他 4xx(如 401/403)视为确定失败不重试。每阶段 client 复用sharedTransport保持连接池
runScheduler:主循环select三个 ticker:ticker(1s 恢复检查)、saveTicker(5min 状态保存)、shadowCleanupTicker(10min Gemini shadow 过期条目惰性清理,见gemini_shadow.go);通过donechannel 通知 main 已完成 final savecheckRecovery:遍历 all upstream,按类型分流:- count 型:
RecoveryCroncron 周期匹配 - usage/balance/exhaust 型:
now >= RecoveryAt时间点触发(零值为旧文件迁移,立即触发) - 统一受
recoveryMinGap(60s)约束,避免短时间重复触发
- count 型:
recoverUpstream:按类型分流恢复——- count 型:直接重置
Count=0、清除exhausted,不调用 provider(count 由网关自行计数 + cron 周期刷新驱动;Count==0 && !Exhausted时为 no-op,不触发 DB 写)。 exhaust型 / 无availability配置:到达RecoveryAt直接自动恢复(清零Exhausted与RecoveryAt),不调用 provider——fallbackResult恒返回Exhausted=true,复查会形成死循环。usage/balance型:通过availSF.Do()singleflight 去重调用 provider 复查(避免与 handler 路径并发重复检查),applyAvailabilityResult写入新Exhausted与RecoveryAt;若已恢复则清除exhausted,upstream 重新参与轮转。- 统一在恢复后更新
LastRecovery并markDirty(count 型LastRecovery仍更新以维持防抖但不持久化)。
- count 型:直接重置
- handler:核心请求处理函数,以
reqSem <- struct{}{}获取并发令牌(defer 释放),完成后返回 - 队列操作:
pickFirstAvailableUpstream(一次写锁内原子扫描队列,跳过所有 exhausted 前置 upstream 至队尾,返回首个可用 upstream 与配置快照)/rotateUpstreamToEnd/getUpstreamQueueLen/hasUpstreamQueue - 状态查询:
incrementCount(count 型自增)、applyAvailabilityResult(写入检查结果;按类型分流:count 写RecoveryCron、其余写RecoveryAt,并清理另一调度依据的残留) - 可用性错误(401/402/403/429)路径通过
availSF.Do()singleflight 去重 checkAvailability,避免并发请求重复调用 provider - 流式响应:idle 监控 goroutine +
maxStreamLifecontext 超时硬上限,双保险 - 格式转换集成:在 attempt 循环内按
inFormat/outFormat决定是否转换(needsTransform)。每次 attempt 先按选中 upstream 的aliases(per-upstream)重写请求模型名,作用于本次 attempt 的局部sendBody/sendModel/basePath(不污染跨 attempt 复用的bodyBytes/r.URL.Path/modelStr,避免重试到不同名称上游时串污染)。pre-transform pass 对客户端请求体副本应用reasoningVendor重写与stripEffortWhenThinkingDisabled剥离(透传路径下同样执行);post-transform pass 对转换后(或透传)的sendBody应用codexBackend整形。preserveReasoningContent通过TransformOptions透传到跨格式转换函数。 - 模型列表旁路:
detectListFormat识别列表请求并按客户端列表格式转换(6 向直转,不经 anthropic pivot),列表响应里反向展开 alias 条目;列表错误响应同样按客户端列表格式重建 - thinking signature 重试:上游返回 400 且
shouldRectifyThinkingSignature命中时,调用rectifyAnthropicRequest清理 thinking 块后重试同一 upstream(最多 1 次) writeJSON统一 JSON 响应写入(globals.go中定义)- 辅助函数:
maskURL/maskHeadersStr/forwardStreamHeaders/removeHopHeaders/maskFakeToken用于日志脱敏和头处理
- 全局
geminiShadowStore:map[string]geminiShadowEntry,按tool_call_id维度存储 Gemini 的thoughtSignature(签名字符串)与完整 assistant turnparts数组(含thought:true块) storeGeminiThoughtSignature/lookupGeminiThoughtSignature:签名存取(多轮工具调用回放签名,避免 Gemini 400)storeGeminiAssistantTurn/lookupGeminiAssistantParts:完整 parts 存取(回放原始 Gemini 形态的 thinking parts)- 条目 1h TTL(
geminiShadowTTL),惰性清理:读取时发现过期条目即删除 - 在
geminiToAnthropicResponse(非流式)与geminiToAnthropicStream(流式首个 tool call)写入;在convertAnthropicMessagesToGeminiContents(assistant turn 含tool_use块时)回放 - 局限:无 session 概念,用
tool_call_id全局唯一作键;多副本部署不跨实例共享
isReasoningVendorIdentifier:大小写不敏感匹配moonshot/kimi/deepseek/mimo/xiaomimimo关键词normalizeThinkingHistoryForVendor:对含tool_use块的 assistant 消息,剥离 thinkingsignature、空 thinking 文本替换为占位符"tool call"、redacted_thinking改写为 thinking 块、无 thinking 时插入占位 thinking 块stripEffortIfThinkingDisabled:thinking.type != enabled时移除reasoning_effort与output_config.effort(output_config仅含 effort 时整体移除)- byte 级封装
normalizeThinkingHistoryForVendorInBytes/stripEffortIfThinkingDisabledInBytes供gateway.go在 pre-transform pass 调用
injectCacheControl:在 Anthropic 请求体注入最多 4 个cache_control: {"type":"ephemeral"}断点(tools 末尾、system 末尾、最后 assistant 消息末尾非 thinking block、最后 user 消息末尾)- 已有断点不覆盖;
ttl为空或"5m"时不带 ttl 字段,其他值带"ttl"字段 - byte 级封装
injectCacheControlIntoBytes供gateway.go在发送 body 为 Anthropic 格式时调用(含转换到 anthropic 与透传 anthropic→anthropic 两条路径)。formatTransform为空时,若客户端格式为 anthropic 且 cacheInjection 启用,仍会注入断点。
thinkingSignatureErrorPatterns:7 个正则匹配 Anthropic thinking signature 错误消息(signature.*not.*valid、must start with a thinking block等)shouldRectifyThinkingSignature:检测错误响应体是否为 thinking signature 问题rectifyAnthropicRequest:移除所有 thinking/redacted_thinking 块、剥离非 thinking 块的signature字段、若最后一条 assistant 消息移除 thinking 后则移除顶层thinking配置
detectListFormat+targetListEndpointPath:列表端点 path + auth header 判定客户端格式与对应上游端点路径(见上文「模型列表请求转换」)。- 中性结构
modelsList/modelsListEntry:承载 4 种格式共有字段(ID/Created/CreatedAt/OwnedBy/DisplayName/Version/InputTokenLimit/OutputTokenLimit),作为 6 向直转的中间表示。 - 4 个解析器
parseOpenAIModelsList/parseAnthropicModelsList/parseGeminiModelsList(openai_chat 与 openai_responses 共用同一解析器)+ 3 个 builderbuildOpenAIModelsList/buildAnthropicModelsList/buildGeminiModelsList,dispatch 由parseModelsListByFormat/buildModelsListByFormat完成。 reverseAliasesMap+applyAliasesReverseToList:按 per-upstreamaliases的 value→key 反向展开模型列表 entry。两阶段实现使列表与请求路由一致:- 先删除"被覆盖"条目——若某 entry 的 ID 等于某个 alias key(该客户端名已被重定向到另一真实模型),删除其原条目,避免显示与路由行为不符的字段。
- 再为每个指向真实名的 alias key 追加一条克隆 entry(字段全相同,仅 ID 改为 alias key)。
alias 与真实名相同时(
k==v)跳过;空字符串自指无效果。
applyAliasesReverseToListInPlace+ApplyAliasesReverseToListInPlaceBytes:直连场景(无formatTransform)的就地 JSON 实现——不经中性结构中转,直接改写data[]/models[]数组,保留供应商特有字段(中性结构未承载的字段不丢失)。gemini的name字段以"models/x"尾段作为 ID 进行比较与还原。TransformModelsListResponse:formatTransform 场景的列表响应转换主入口,调用 parse →applyAliasesReverseToList→ build → marshal。fast-path 规则:无 alias 且同格式(needsTransform=false)或openai_chat↔openai_responses间原样透传;有 alias 或需跨格式时统一走 parse→build(含 alias 反向展开)。- 网关列表分支选择(
gateway.go):列表请求按outFormat区分两条路径——outFormat != ""走TransformModelsListResponse(中性结构 + alias);outFormat == ""但配置了 alias 时走ApplyAliasesReverseToListInPlaceBytes(就地 JSON + alias),无 auth 头重置、错误响应原样透传。两条路径均执行"删除覆盖条目 + 追加 alias 克隆",使模型列表显示与请求路由的 alias 行为完全一致。
- 全局状态:
authEnabled(-authflag)、ipSessions map[string]string(ip→username 内存缓存)、authMu sync.RWMutex clientIP:从r.RemoteAddr提取客户端 IP(net.SplitHostPort去端口)loadAuthFromDB:启动时从 DB 加载ip_sessions到内存,返回账号数量供 main 判断authMiddleware:包裹 catch-all handler,authEnabled时检查ipSessions[ip]是否存在,不存在返回 401loginHandler:GET 按当前 IP 是否已登录分流——未登录返回登录表单 HTML,已登录返回会话管理面板(当前 IP、账户、所有已登录 IP 列表、退出按钮);POST 处理表单提交(bcrypt 验证 → 写入 DB + 内存 → 302 重定向)logoutHandler:POST 接收{"ip":"..."}或{"all":true},校验当前 IP 已登录且只能操作同账户的 IP,从 DB + 内存删除会话dumpAccounts/dumpIPSessions:导出辅助,供exportToJSON查询 auth 相关表- HTML 模板:
loginFormHTML(登录表单)、renderDashboard(会话管理面板),硬编码内联,风格与/status页面一致
本项目使用MIT许可证。