1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
|
# AIGW
AIGW 是一个轻量、无状态的 AI API 中转后端。当前阶段聚焦上游接入与 API 分发:提供 OpenAI Chat Completions、Responses、Embeddings 和 Anthropic Messages 兼容入口,支持公开模型名映射、多上游路由、加权分流、故障转移、SSE 直通、客户密钥鉴权以及持久化用量结算。
它借鉴了 ZenMux 的双协议、`provider/model` 模型命名、统一错误、请求 ID、路由和可观测性边界,但没有复制其业务实现。
## 当前能力
- OpenAI:`POST /v1/chat/completions`、`POST /v1/responses`、`POST /v1/embeddings`、`GET /v1/models`
- Anthropic:`POST /anthropic/v1/messages`、`GET /anthropic/v1/models`
- ZenMux 风格别名:所有入口同时提供 `/api/...` 路径
- 一个公开模型可配置多个同协议上游,低 `priority` 优先,同级按 `weight` 分流
- 请求可用 `model-id:provider-slug` 固定到模型目录公开的某个供应商;固定后不会回退到其他供应商
- 在 429、502、503、504 或连接失败时,于响应开始前自动尝试下一条路由
- 每条模型/供应商 route 记录真实请求的近期可用率和响应头延迟;连续 3 次可重试失败后熔断 30 秒,冷却期间路由自动绕行
- 可选的认证 `/models` 主动探测在无客户流量时更新同一熔断器;默认关闭,不发推理请求、不产生 token 费用
- SSE 增量直通、主动断连传播、共享 HTTP/2 连接池
- `Authorization: Bearer` 和 `x-api-key` 客户鉴权
- 统一 JSON 错误、`X-AIGW-Request-ID`、Prometheus 文本指标
- 非阻塞用量事件,包含租户、项目、模型、上游、尝试次数、耗时和 token 用量
- PostgreSQL 预付余额、请求额度冻结、实际 token 结算和不可变账本
- Stripe 托管 Checkout 充值、签名 Webhook 与事件/订单双重幂等
- Stripe 托管支付方式保存与低余额自动充值,off-session PaymentIntent 失败会暂停自动充值并提示客户处理
- PostgreSQL Usage Ledger、稳定游标分页、TTFT 与 P50/P95 聚合、按 API Key 成本追溯
- 邮箱验证、邀请注册、密码重置、登录限流、可撤销设备会话和管理 API 审计日志
- TOTP(含一次性恢复码)与 WebAuthn Passkey 注册、二次验证和无密码登录
- 六种 RBAC 角色、租户数据隔离和 CSRF 防护
- 项目级 RPM、估算 TPM、并发限制和月度消费配额
- API Key 级模型白名单、日/月消费上限、RPM/TPM、过期时间、标签、停用和原子轮换
- 无需登录的 `/admin/models` 模型与价格目录,支持搜索、协议/输入/开发者筛选;每个模型有服务端渲染的 canonical 详情 URL、价格和协议代码示例
## 快速运行
需要 Go 1.26。先准备配置和环境变量:
```bash
cp config.example.json config.json
export AIGW_API_KEYS='[{"key":"sk-local-change-me","key_id":"local-key","tenant_id":"tenant-demo","project_id":"project-default","scopes":["inference"]}]'
export AIGW_SERVER_ADDRESS=':8080'
export OPENAI_BASE_URL='https://api.openai.com/v1'
export OPENAI_API_KEY='your-upstream-key'
export ANTHROPIC_BASE_URL='https://api.anthropic.com/v1'
export ANTHROPIC_API_KEY='your-upstream-key'
go run ./cmd/aigw -config config.json
```
如果当前只有一种上游,从 `config.json` 删除未使用的 provider 和对应 model,避免启动时要求该密钥。
控制面模式下,公开目录位于 `AIGW_PUBLIC_URL` 同源的 `/models`(本地默认
`http://localhost:8081/admin/models`),匿名数据接口为
`GET /admin/api/public/models`。公开响应只包含全局可用模型的公开 ID、能力、
协议、价格与聚合可用性;租户/Key 限定模型和内部路由字段不会返回。
不使用真实密钥的本地体验方式:
```bash
# terminal 1
go run ./cmd/mockupstream
# terminal 2
export MOCK_UPSTREAM_KEY='local-only'
export MOCK_UPSTREAM_BASE_URL='http://127.0.0.1:18080/v1'
export AIGW_SERVER_ADDRESS='127.0.0.1:18081'
export AIGW_API_KEYS='[{"key":"sk-local-change-me","key_id":"local-key","tenant_id":"tenant-demo","project_id":"project-default","scopes":["inference"]}]'
go run ./cmd/aigw -config config.local.json
```
本地网关会监听 `http://127.0.0.1:18081`,公开模型为 `demo/openai` 和 `demo/anthropic`。`cmd/mockupstream` 只用于开发验证,不应部署到生产环境。
OpenAI 调用示例:
```bash
curl http://127.0.0.1:8080/v1/chat/completions \
-H 'Authorization: Bearer sk-local-change-me' \
-H 'Content-Type: application/json' \
-d '{"model":"openai/gpt-4.1-mini","messages":[{"role":"user","content":"hello"}],"stream":true}'
```
OpenAI Responses 供应商使用独立的 wire API 配置,不会与 Chat Completions 隐式互转:
```bash
export OPENAI_BASE_URL='https://your-provider.example/v1'
export OPENAI_API_KEY='your-upstream-key'
export AIGW_SERVER_ADDRESS='127.0.0.1:18081'
export AIGW_API_KEYS='[{"key":"sk-local-change-me","key_id":"local-key","tenant_id":"tenant-demo","project_id":"project-default","scopes":["inference"]}]'
go run ./cmd/aigw -config config.responses.example.json
curl http://127.0.0.1:18081/v1/responses \
-H 'Authorization: Bearer sk-local-change-me' \
-H 'Content-Type: application/json' \
-d '{"model":"openai/gpt-5.5","input":"Reply with OK.","max_output_tokens":32}'
```
OpenAI Embeddings 同样使用独立 `wire_api: "embeddings"` 路由,并按上游返回的输入 token 从预付余额扣费:
```bash
curl http://127.0.0.1:8080/v1/embeddings \
-H 'Authorization: Bearer sk-local-change-me' \
-H 'Content-Type: application/json' \
-d '{"model":"openai/text-embedding-3-small","input":["text to embed"]}'
```
Anthropic 调用示例:
```bash
curl http://127.0.0.1:8080/anthropic/v1/messages \
-H 'x-api-key: sk-local-change-me' \
-H 'anthropic-version: 2023-06-01' \
-H 'Content-Type: application/json' \
-d '{"model":"anthropic/claude-sonnet","max_tokens":256,"messages":[{"role":"user","content":"hello"}]}'
```
## 配置路由
每条 route 把一个对外模型映射到一个上游模型。供应商的 `protocol` 表示 OpenAI/Anthropic 协议族,`wire_api` 表示实际调用 `chat_completions`、`responses`、`embeddings` 或 `messages`。静态配置可为供应商设置唯一的公开 `slug`;省略时使用符合相同格式的 `id`:
```json
{
"id": "vendor/model-public",
"owned_by": "vendor",
"routes": [
{"provider": "provider-a", "upstream_model": "model-v3", "priority": 0, "weight": 80},
{"provider": "provider-b", "upstream_model": "model-v3", "priority": 0, "weight": 20},
{"provider": "provider-c", "upstream_model": "model-v2", "priority": 10, "weight": 100}
]
}
```
这里 A/B 承担约 80/20 的首选流量,C 只作为更低优先级的后备。所有上游密钥仅通过 `api_key_env` 指向的环境变量读取。客户密钥从 `AIGW_API_KEYS` JSON 数组读取,进程内只保存 SHA-256 摘要。
默认请求只传基础模型 ID,由网关自动进行权重分流、健康择优、熔断绕行和故障转移。同优先级 route 在积累至少 5 个可用性样本或 3 个 TTFT 样本后,会优先选择近期成功率和 TTFT 更好的供应商;每 20 次保留一次原权重探索,未测量的新 route 也不会被饿死。需要复现特定供应商行为时,可以先从 `GET /v1/models` 的 `providers` 字段读取公开 slug,再把它附加到模型名:
```bash
curl http://127.0.0.1:8080/v1/chat/completions \
-H 'Authorization: Bearer sk-local-change-me' \
-H 'Content-Type: application/json' \
-d '{"model":"vendor/model-public:provider-b","messages":[{"role":"user","content":"hello"}]}'
```
指定供应商时,路由器只会尝试该 slug 下与当前 API 协议兼容的 route,不会静默切换到其他供应商。该供应商不存在时返回 `404 provider_not_found`,正在熔断冷却时返回 `503 provider_unavailable`。API Key 模型白名单仍按基础模型 ID 校验,用量与扣费也归集到基础模型,避免供应商后缀拆分账单。
主动探测通过 `AIGW_PROVIDER_ACTIVE_PROBES_ENABLED=true` 显式开启;间隔和超时由 `provider_health.probe_interval_seconds` 与 `probe_timeout_seconds` 配置。探测只调用每个供应商一次认证 `GET {base_url}/models`,要求 2xx、`application/json` 和标准 `data` 数组,再把结果投影到该供应商的全部 route。没有兼容 Models API 的供应商应保持关闭,避免被误熔断。指标为 `aigw_provider_probes_total`、`aigw_provider_probe_failures_total`、`aigw_upstream_ttft_ms_count` 和 `aigw_upstream_ttft_ms_sum`。
## 生产边界
当前版本可以作为带预付计费的数据面,并已把控制面、账务和运营入口拆开:
- 控制面计费模式会把 UsageEvent、冻结记录和扣费流水同步、幂等写入 PostgreSQL;响应结束只负责把结算事件投递到持久化队列,worker 负责重试、过期冻结恢复和本地 JSONL spool 补偿。
- 客户密钥和模型目录在控制面模式下从 PostgreSQL 载入到原子内存快照;Redis 只是可选的变更广播加速层,故障时通过 PostgreSQL generation 轮询收敛。
- 当前只把 Chat Completions、Responses、Embeddings、Anthropic Messages 入口发给相同 wire API 的兼容上游,不做隐式跨协议转换。
- 自动故障转移可能在极少数网络错误下造成上游重复执行。正式计费时需要上游幂等能力、请求去重策略和重复成本对账。
- 供应商健康状态使用 100 次滑动窗口,默认只使用本机真实流量;显式开启后还可包含认证 Models API 主动探测。控制面部署默认通过有界、带 TTL 的 Redis Stream 共享近期 route 结果和 TTFT,新实例会回放仍在 TTL 内的样本,再继续实时消费。推理线程只做非阻塞入队,Redis 故障时立即保留本机窗口并自动重连,不影响 readiness、计费或请求处理。该窗口用于选路而非持久 SLA,长期统计仍以 PostgreSQL Usage Ledger 为准。
- 计费上游必须返回 usage。OpenAI 流请求会强制请求 `stream_options.include_usage=true`;成功响应缺少 usage 时不会按零费用放行,也不会猜测 token,而是把授权保持为 `metering_failed`、触发 readiness/Prometheus 告警,直到运营修复或通过带原因和 RBAC/审计证据的 reservation release API 处置。释放后事件保留 `usage_reported=false` 并标记 `released_unmetered`,钱包余额不变,只有冻结额返回可用余额。显式的零 token usage 仍可正常结算。
- 默认生产配置使用独立的推理、管理、支付/邮件 Webhook 和 operations listener。`/healthz` 只表示进程存活,`/readyz` 会检查 PG、快照、Stripe 对账、Webhook、退款、未收款、缺失用量、结算队列和邮件积压;Redis 是可降级传播层。`/metrics` 应仅在内网暴露;公网 TLS、WAF 和连接层限速应放在负载均衡器或边缘代理。
## PostgreSQL + Redis 控制面
控制面模式使用 [config.control.example.json](config.control.example.json)。PostgreSQL 保存事实数据:租户、项目、API Key 摘要、加密后的供应商密钥、模型和路由。Redis 保存当前 generation 并广播变更事件;它不是客户账本,PG 才是控制面事实来源。Redis 短暂不可用或未配置时,网关仍从 PostgreSQL 启动并通过周期轮询收敛,恢复后会在后台自动重连订阅。管理写入先提交 PG 并替换本机快照,Redis 广播失败只记录告警,不会把已提交的写入报告为失败。
凭证密钥必须是 base64 编码的 32 字节随机值。供应商 API Key 使用 AES-256-GCM 加密后写入 PostgreSQL,运行时解密到当前内存快照,管理接口永远不会返回它。
开发环境可以直接启动:
```bash
./scripts/start-debug.sh
```
脚本会首次生成仅当前用户可读的 `.env.debug`,其中包括随机 PostgreSQL 密码、凭证加密密钥和 bootstrap token;同时启动 Mailpit 作为本地 SMTP 收件箱。构建并等待 PostgreSQL、Redis 和网关健康后只打印后台与 Mailpit 地址和环境文件位置,不把密钥回显到终端。关闭调试服务:
```bash
./scripts/stop-debug.sh
```
关闭脚本保留 PostgreSQL/Redis 数据卷,下一次启动仍可继续使用已有控制面数据。默认 `AIGW_STRIPE_ENABLED=false`,预付余额、扣费账本和人工调账仍可使用,但 Checkout、Customer Portal、退款和 Stripe 对账关闭。需要测试 Stripe 时,把 `.env.debug` 中的 restricted key、Webhook signing secret、成功 URL 和取消 URL 设置为对应环境的值,再显式设置 `AIGW_STRIPE_ENABLED=true`。JSON 配置只保存环境变量名称,不保存外部服务 URL 或密钥。
源码未变化时可跳过镜像构建以快速重启:`AIGW_DEBUG_SKIP_BUILD=1 ./scripts/start-debug.sh`。默认构建使用 Docker host network;特殊环境可以通过 `AIGW_DOCKER_BUILD_NETWORK=default` 覆盖。
然后打开 `http://127.0.0.1:8081/admin/`,本地邮件在 `http://127.0.0.1:8025/` 查看;健康检查在 `http://127.0.0.1:9090/readyz`。开发者控制台的调用示例使用 `AIGW_INFERENCE_PUBLIC_URL` 作为网关地址,分离 listener 时不要把它误配成管理地址。Stripe CLI Webhook 转发到 `http://127.0.0.1:8082/billing/stripe/webhook`。启用注册时,新账号必须通过一次性邮件链接验证;团队成员由管理员邀请并自行设置密码。平台管理员也可以从权限为 `0600` 的环境文件读取 `AIGW_ADMIN_TOKEN`,将其作为 bootstrap/break-glass 凭证。日常操作使用邮箱/密码、TOTP 或 Passkey,服务端创建可逐设备撤销的数据库会话,所有写请求需要 CSRF token。第一套资源的创建顺序是:Tenant → Project → API key → Provider → Model route。客户 API Key 明文只在创建成功时返回一次;团队成员使用自己的账号,不共享管理员令牌。
账号邮件先在 PostgreSQL outbox 中加密持久化,再由后台 worker 发送;SMTP 临时不可用不会回滚注册、邀请或重置请求。普通失败指数退避,10 次后进入 dead-letter。生产环境把 `AIGW_PUBLIC_URL` 设置为 HTTPS 控制台 URL,把 `AIGW_SMTP_ADDRESS`、`AIGW_SMTP_FROM_ADDRESS`、`AIGW_SMTP_USERNAME`、`AIGW_SMTP_PASSWORD` 和 `AIGW_MAIL_FEEDBACK_SECRET` 通过密钥管理服务注入,并将 `admin.mail.tls_mode` 改为 `starttls` 或 `tls`。邮件供应商的 bounce/complaint 事件应由边缘适配器规范化后签名发送到 `/mail/feedback`;永久退信和投诉地址会进入抑制表。Billing 角色可以独立启停低余额和异常消费提醒,并设置低余额金额、相对前七日平均消费的倍数和最低异常金额;设置按租户保存在 PostgreSQL,并按日幂等发送给已验证的 tenant admin/billing 成员。未配置租户继承 `admin.mail.low_balance_micros`、`admin.mail.spend_anomaly_multiplier` 和 `admin.mail.spend_anomaly_min_micros` 的部署默认值。域名 DNS 仍必须在邮件供应商处配置 SPF、DKIM 和 DMARC,这不是应用代码可以代替的步骤。WebAuthn 的 `AIGW_WEBAUTHN_RP_ID` 必须是控制台有效域名,`AIGW_WEBAUTHN_ORIGINS` 是逗号分隔的 HTTPS origin。
租户登录后的 Quickstart 首屏会显示充值、密钥、可用模型和首次成功请求四步状态;模型目录按协议、输入模态和开发者筛选,并可按发布时间、名称、输入/输出价格和上下文长度排序,再生成使用 `AIGW_INFERENCE_PUBLIC_URL` 的 cURL、Python 和 Node.js 示例。新工作区可以在 Quickstart 直接为默认项目和当前模型创建 starter key,明文仍只显示一次,同时自动放入当前页面内存中的 Playground。连接面板集中列出 OpenAI/Anthropic SDK Base URL、Chat Completions、Responses、Messages、Models 端点,并可复制不含真实密钥的环境变量模板。每个模型都有客户安全的详情视图,展示协议、模态、上下文、最大输出、能力、生命周期和别名,并按当前价格版本实时估算输入、输出、缓存读取和缓存写入成本;逐供应商运行状态只暴露名称、协议、近期可用率、响应头延迟、样本量和熔断恢复时间,不暴露地址、凭证或上游模型。完全熔断的模型不能从快捷入口发起请求。API Playground 会直接从浏览器调用该推理地址,使用当前客户 API Key 经过完整鉴权、路由、余额冻结/结算和 Usage 链路;它只把 Key 保留在当前页面内存,刷新或退出立即清除。失败时页面保留结构化响应和请求 ID,并把权限、余额、模型、限流或上游错误引导到对应控制台页面。分离 listener 时推理服务只允许 `AIGW_PUBLIC_URL` 的精确 Origin,并且不带管理 Cookie。租户可把目录内可用模型保存为默认模型和不同的 fallback 模型,Quickstart 会立即采用该默认值;billing 成员可以单独配置低余额与异常消费邮件策略。
API keys 页面会展示每个 Key 今日与当月已结算费用、待结算冻结、请求数、日/月上限、剩余额度、RPM/TPM、过期时间和最后使用时间;统计直接读取 Usage Ledger 与 billing reservation,不在浏览器侧计算。Key 可以即时停用/启用、撤销或原子轮换;轮换会在一个 PostgreSQL 事务内创建继承原策略的新 Key 并撤销旧 Key,新明文仍只返回一次。即使 Redis 广播暂时失败,本机快照也会先应用并由 PG 轮询让其他实例最终收敛。
控制台角色分为:`platform_admin`、`platform_viewer`、`tenant_admin`、`tenant_billing`、`tenant_developer`、`tenant_viewer`。租户角色的查询条件在服务端下推到 PostgreSQL,不能读取其他租户的项目、密钥、余额、Usage 或审计事件;供应商凭证和路由管理只对平台角色开放。默认模型与财务告警使用独立写权限:developer 不能修改低余额阈值,billing 成员不能修改 API 默认模型。
## Usage、配额与限流
每个完成上游尝试的请求都会按 `request_id` 幂等写入 `usage_events`,并更新 `usage_monthly_rollups`。Usage 持久化独立于预付费冻结记录,因此关闭计费也不会关闭用量账本。Admin WebUI 提供稳定游标分页、本月汇总、单请求状态、模型、token、成本、未收金额、总延迟和首个有效输出延迟(TTFT)查询;同一时间、项目、API Key、模型、供应商 slug、协议、流式状态、错误类型与成功状态过滤会下推到模型、API Key 和供应商聚合,展示本期/上期费用变化、成功率、缓存命中、总延迟与 TTFT 的 P50/P95,以及缺失 usage 请求。租户视图在服务端移除供应商 ID、供应商名称和上游模型,只有平台角色能查看路由归因。每条请求可以打开详情并复制不含 prompt、响应正文或客户密钥的诊断 JSON。
Limits 页面按项目配置:
- `requests_per_minute`:固定分钟窗口请求数,`0` 表示不限。
- `tokens_per_minute`:请求体约算输入 token 加显式/默认最大输出 token,`0` 表示不限。
- `concurrent_requests`:从进入上游前到响应复制完成的并发 lease,`0` 表示不限。
- `monthly_spend_micros`:PG 事务内检查当月已计成本和未结冻结,`0` 表示不限。
API Key 还可以独立配置 `requests_per_minute`、`tokens_per_minute`、`daily_spend_micros` 和 `monthly_spend_micros`。项目与 Key 的 RPM/TPM 在同一次原子判断中取更严格的边界;任一限制拒绝时不会消耗另一层的计数。日/月金额限制在钱包行锁事务内连同待结冻结检查,不能通过并发请求超额。
Redis 可用时,RPM/TPM/并发通过 Lua 原子执行并在多实例间共享;Redis 故障时自动退回本机计数,服务继续可用,但降级期间限制是“每实例”而不是“全局”。Redis 恢复后新请求会自动重新使用分布式计数。启用预付计费时,月度消费配额和余额由 PostgreSQL 保证,不依赖 Redis。
## 余额与 Stripe 充值
模型价格在后台按“币种单位 / 100 万 token”配置,数据库使用 `amount_micros` 固定精度整数保存金额。Stripe 不直接为推理请求结账,只向 PostgreSQL 预付钱包充值;推理请求先按请求体字节数和 `max_tokens`/`max_completion_tokens` 保守冻结余额,成功响应按可信 usage 扣款,失败请求释放冻结。计量层把 OpenAI 总 input 中的 `cached_tokens`/cache-write 子集归一化为互斥的非缓存输入、缓存读取和缓存写入桶,Anthropic 已独立报告的缓存字段则保持不变,避免按输入价和缓存价重复扣费。`request_id` 是用量与扣费幂等键,余额、冻结和不可变 ledger 都在同一个 PG 事务中更新。Billing 页面中的 request 扣费引用可以直接打开对应的 token、价格、状态与延迟详情;精确 ID 查询仍由服务端强制附加当前租户边界。
Stripe 使用托管 Checkout,服务端不会接触卡号,也没有硬编码支付方式;支付方式由 Stripe Dashboard 动态配置。租户可在 Billing 页面保存开票名称、邮箱和地址,资料用稳定幂等键创建或更新 Stripe Customer;Stripe 暂时不可用时本地资料标记为待修复,下一次充值、保存支付方式或打开客户门户会再次同步。本地不保存税号,启用且确认 Stripe Tax 注册后由 Checkout/Customer Portal 托管税号。手动充值只在签名校验通过的 Webhook 确认 `payment_status=paid` 后入账,成功跳转页不会直接修改余额。自动充值先通过 Checkout Setup Session 保存支付方式;余额低于客户阈值时,后台 worker 用订单 ID 作为幂等键创建并确认 off-session PaymentIntent。同步结果、Webhook 重放和周期对账都只能生成一笔钱包入账;需要客户认证或支付方式失效时会暂停自动充值,不会循环扣款。
配置 Stripe 测试环境:
```bash
cp .env.control.example .env
# 将 AIGW_STRIPE_API_KEY 设为最小权限的 rk_test_ restricted key
# AIGW_STRIPE_CLI_API_KEY 使用另一把仅有 Debugging Tools Write 的测试 key
# 本地转发会显示 whsec_...,填入 AIGW_STRIPE_WEBHOOK_SECRET
stripe listen --api-key "$AIGW_STRIPE_CLI_API_KEY" --forward-to http://127.0.0.1:8082/billing/stripe/webhook
docker compose up --build
```
启动网关前先用只读预检检查 test restricted key。命令固定发送当前
`stripe-go` SDK 的 API 版本,不创建 Customer、Session、PaymentIntent 或退款,
也不会输出密钥和 Stripe 原始错误正文;为防误操作,它拒绝 `rk_live_`/`sk_live_`
密钥:
```bash
AIGW_STRIPE_API_KEY="$AIGW_STRIPE_API_KEY" go run ./cmd/stripe-preflight
# 已构建镜像也可直接运行:
docker compose run --rm --entrypoint stripe-preflight aigw
```
`ready=true` 表示 Customers、Checkout Sessions、Setup/Payment Intents、Refunds、
Charges、Disputes、Invoices 和 Billing Portal 配置的读取权限齐全。写权限无法在
不创建外部对象的前提下证明,仍必须通过下述真实测试模式流程验证。
Webhook 至少订阅:
- `checkout.session.completed`
- `checkout.session.async_payment_succeeded`
- `checkout.session.async_payment_failed`
- `checkout.session.expired`
- `payment_intent.succeeded`
- `payment_intent.payment_failed`
- `payment_intent.canceled`
- `charge.succeeded`
- `charge.updated`
- `charge.refunded`
- `refund.created`
- `refund.updated`
- `refund.failed`
- `charge.dispute.created`
- `charge.dispute.updated`
- `charge.dispute.closed`
- `invoice.created`
- `invoice.finalized`
- `invoice.paid`
- `invoice.payment_failed`
网关 restricted key 的最小权限按实际启用功能配置:Checkout Sessions Write、Customer Portal Sessions Write、Customers Write、Setup Intents Read、Payment Intents Write、Refunds Write,以及 Charges、Disputes、Invoices 的 Read。读取预检还需要 Checkout Sessions、Customers、Payment Intents、Refunds 和 Billing Portal Configurations 的 Read(Dashboard 中 Write 通常已包含同一资源的 Read)。不要给主网关 Debugging Tools 权限;Stripe CLI 使用独立的测试 key。Dashboard 保存权限时可能要求账户持有人完成二次验证。修改权限后必须先通过 `cmd/stripe-preflight`,再在测试模式重新跑一次手动充值、保存支付方式、自动充值、客户门户、退款和对账。
后台已覆盖 Checkout 重试、客户门户、退款队列、争议/发票/收据记录、失败重试、周期性 Stripe 对账和 CSV 财务导出。Stripe 已支付但本地 pending 的订单会自动补账且传播错误不会被忽略;Stripe 中不存在的未入账订单只能通过 RBAC/审计保护的 resolution 作废,已入账孤立充值只能用等额负向账本冲销,原记录不会删除或改写。退款与争议会先冻结/扣除本地余额,余额不足进入 `uncollected_micros`,不会静默丢账。当前没有默认启用 Stripe Tax,因为是否有有效税务注册不能由代码推断;确认注册和 canonical product tax code 后再显式打开。生产环境应把 Stripe restricted key 和 Webhook signing secret 放入云平台的密钥管理服务,并限制密钥权限和来源 IP,不要放进镜像或仓库。
模型目录支持输入/输出模态、上下文窗口、最大输出、能力集合、生命周期、弃用替代模型、区域和租户/API key allowlist;价格在 `model_price_versions` 中按生效时间版本化。每个客户 API Key 还可以独立设置模型白名单、日/月消费上限、RPM/TPM 和过期时间:限制随 PostgreSQL generation 热加载到鉴权快照,模型检查发生在路由前,金额上限在钱包行锁事务内和待结算冻结一起检查。旧 alias 只映射到 canonical model ID。
静态模式的上游 Base URL 与 API Key 分别使用 `base_url_env` 和 `api_key_env`;控制面、Redis、Stripe、SMTP、WebAuthn 和监听地址同样只通过环境变量或密钥管理服务注入。严格 JSON 解析会拒绝旧的 `address`、`base_url`、`success_url` 和 `cancel_url` 字面量字段,避免环境隔离被配置文件绕过。版本化配置只保存 `*_env` 名称,不保存外部服务密钥或部署域名。
生产环境建议把 `auto_migrate` 改为 `false`,先执行:
```bash
AIGW_DATABASE_URL="postgres://..." go run ./cmd/migrate
# 查看当前迁移版本和 checksum
AIGW_DATABASE_URL="postgres://..." go run ./cmd/migrate -status
```
管理 API 支持账号密码、TOTP、Passkey 会话和仅用于初始化/故障恢复的 bootstrap token。客户 API Key 明文只在创建或轮换响应中返回一次;后续列表仅返回已持久化的前缀和六字符后缀,历史上未保存后缀的 Key 保持前缀显示,不能从 SHA-256 摘要反推。即使已经启用 RBAC,生产环境仍应设置 HTTPS、把推理端口和管理端口分离、按需关闭公开注册,并把 bootstrap token 存入密钥管理服务;企业部署可再接 OIDC/SAML 与强制 MFA 策略。
完整的扩展边界见 [架构说明](docs/architecture.md),与 ZenMux 模型目录对照后的上线缺口见 [商业化就绪清单](docs/commercial-readiness.md)。
## 验证
```bash
go test ./...
go test -race ./...
go vet ./...
CGO_ENABLED=0 go build -buildvcs=false ./cmd/...
# 在容器网络内运行真实 PostgreSQL 集成测试
docker compose --env-file .env.debug --profile test run --rm integration-test
```
设置 `AIGW_TEST_DATABASE_URL` 和 `AIGW_TEST_REDIS_URL` 后,测试会强制执行 PostgreSQL Webhook/结算/迁移升级,以及跨实例供应商健康传播/重启回放用例。CI 还构建容器镜像、生成 SPDX SBOM、以 Trivy 阻断 HIGH/CRITICAL 漏洞,并在 `v*` 标签发布时使用 OIDC keyless Cosign 签名。负载与 Redis 降级演练分别使用 `scripts/load-smoke.sh` 和 `scripts/redis-fault-drill.sh`;后者会验证 Redis 停止期间 readiness 不受影响以及恢复后的自动重连,传入 `AIGW_METRICS_URL` 时还会严格检查共享健康连接 gauge。备份恢复演练使用 `scripts/backup-postgres.sh` 与 `scripts/restore-drill.sh`。
|