# 架构说明 ## 设计目标 网关保持轻量且可横向扩容,并把计费、权限和控制面边界固定下来。路由和鉴权只读内存快照;启用预付计费时,请求热路径会执行余额冻结与结算事务,以财务正确性优先。 ```mermaid flowchart LR C["客户 SDK"] --> E["OpenAI / Anthropic 入口"] E --> A["Authenticator + Scope"] A --> Q["Redis/local limits"] Q --> B["Balance + monthly quota"] B --> R["Model Catalog + Router"] R --> P["共享连接池 + Provider Adapter"] P --> U1["上游 A"] P --> U2["上游 B"] P --> U3["后备上游"] P --> M["Usage settlement"] E -.-> O["Metrics + Structured Logs"] M --> L["PostgreSQL usage + ledger"] S1["Stripe Checkout"] --> W["Signed Webhook"] W --> L D["PostgreSQL 控制面"] -. "事实数据" .-> S["Redis generation / PubSub"] S -. "变更广播" .-> A P -. "非阻塞健康事件" .-> H["Redis route-health Stream"] H -. "跨实例回放/消费" .-> R D -. "启动与轮询快照" .-> A D -. "模型和路由快照" .-> R ``` ## 热路径 1. 入口生成不可预测的请求 ID,并通过 Bearer 或 `x-api-key` 解析客户身份。 2. 身份包含 `key_id`、`tenant_id`、`project_id`、scopes、密钥模型白名单、日/月上限、RPM/TPM 和过期时间;控制面模式使用 PostgreSQL 摘要快照,代理只依赖 `Authenticator` 接口。 3. 请求体在配置上限内读取一次,以提取公开模型名并支持故障转移时重放。 4. 项目和 API Key 策略从 PG 热更新快照读取。Redis Lua 在一次调用中原子占用项目/Key RPM、估算 TPM 和项目并发额度;任一层拒绝会回滚本次全部计数。Redis 不可用时退回本机窗口。 5. Router 按协议过滤路由并跳过仍处于冷却期的 route,再按优先级分组。同级 route 冷启动时按配置权重选择;积累 5 个可用性样本或 3 个 TTFT 样本后优先近期成功率和 TTFT 更好的 route,并每 20 次保留一次原权重探索。开启共享历史时,各实例从有界 Redis Stream 回放 TTL 内样本并实时消费;写入走非阻塞队列,Redis 不可用时继续使用本机窗口。请求使用 `model:provider-slug` 时,基础模型先经过租户/API Key 白名单校验,然后只保留该公开 slug 的兼容 route,绝不跨供应商回退;全部匹配 route 熔断时直接返回可重试的 503。 6. 计费开启时,请求进入上游前在锁定钱包的 PostgreSQL 事务中同时检查项目月度额度、API Key 日/月额度和未结冻结,再冻结保守估算额度;余额不足返回 402,任一额度耗尽返回 429。 7. Provider Adapter 重写上游模型名和凭证,使用进程级共享 Transport 发送请求。每次上游尝试记录响应头延迟与可重试失败,响应转发再记录最终实际 route 的首个有效输出 TTFT;连续 3 次连接失败或 429/502/503/504 后熔断该模型/供应商 route 30 秒。可选主动探测器按供应商去重调用认证 `/models`,拒绝 HTML/非标准 JSON,并把无客户流量时的可用性写入同一熔断器;它默认关闭且不发推理请求。上游返回成功头后即锁定路由;SSE 逐块 flush。 8. 请求结束后释放并发 lease,并用 `request_id` 幂等持久化 UsageEvent(含首个有效输出 TTFT)、更新月度汇总、按实际 token 结算;结构化日志仅是异步副本。 ## 已固定的扩展边界 | 边界 | 当前实现 | 下一阶段替换 | | --- | --- | --- | | 客户身份 | PostgreSQL 快照、内存 SHA-256 索引、一次性明文与前缀/六字符后缀显示、密钥过期/模型白名单/日月上限/RPM/TPM、停用与原子轮换 | IP/CIDR 策略、短期服务身份 | | 控制台权限 | 邮箱验证/邀请/重置、登录限流、设备会话、TOTP/恢复码、Passkey、CSRF、六角色 RBAC、租户 SQL scope、审计日志 | SSO/OIDC、SCIM、组织级 MFA 策略、自定义角色、审批流 | | 权限 | `Principal.Scopes` 中的 `inference`、API Key 模型限制 + 控制台 RBAC | ABAC、IP 与条件策略 | | 模型目录 | PostgreSQL 快照 + 可选 Redis generation 广播 + PG 轮询兜底;租户安全目录 API、服务端渲染公开 canonical 详情页、供应商运行状态与 Quickstart | 状态历史、灰度发布 | | 路由 | priority + weighted selection + failover + `model:provider-slug` 固定供应商 + 真实流量/可选主动探测滑动窗口 + Redis Stream 跨实例健康聚合/重启回放 + TTFT/可用率自适应择优 + 有界探索 + 熔断 | 吞吐量/成本/质量策略、客户可见状态历史 | | 计量 | PostgreSQL UsageEvent(总延迟/TTFT)+ 月度汇总 + 游标分页 + 模型/Key/供应商分位数聚合 + 异步日志副本 | 分区表、持久消息流、供应商账单对账 | | 计费 | 版本价格、预付余额、冻结/结算、不可变流水、Stripe 手动/自动充值、退款/争议/对账 | 信用额度、合同价、Metronome 企业合同 | | 限流 | Redis Lua 全局 RPM/估算 TPM/并发,故障时本机降级;PG 月度消费配额 | 滑动窗口、层级策略、边缘 token bucket | | 协议 | Chat Completions、Responses、Embeddings、Anthropic Messages 同 wire API 透传;计费单位已定义 token/image/second | Images/Audio 端点、规范化 IR + OpenAI/Anthropic/Google 双向转换 | ## 计费数据原则 真实计费不依赖请求日志。当前以 `request_id` 作为 reservation、usage 和扣费幂等键,价格快照随冻结记录保存,账本流水不可变。余额使用百万分之一币种单位,所有变更在锁定 tenant wallet 的 PostgreSQL 事务中完成。 Stripe 手动充值使用 Checkout Session:本地先创建 top-up order,Stripe 请求使用 order ID 作为幂等键;Webhook 验证签名后再次核对 event ID、order ID、session ID、金额和币种。自动充值使用 Checkout Setup Session 保存支付方式,低余额 worker 再创建 off-session PaymentIntent;同一订单的同步成功、Webhook 与对账共享幂等账本来源。浏览器成功跳转不具有入账权威性。 流式请求的 usage 可能只在最后事件出现。当前 observer 会在线解析 Chat Completions、Responses、Embeddings 和 Anthropic SSE 中的 usage,并把 OpenAI details 中属于总 input 子集的缓存 token 拆成互斥计费桶;Anthropic 独立缓存字段不做减法。如果计费上游不返回 usage,请求会进入 `metering_failed` 并保持余额冻结,而不是按零费用结算。运营只能通过要求原因且受 `billing.adjust` 保护的接口释放无法恢复的冻结;事务同时写零金额审计流水并把事件标成 `released_unmetered`,不伪造 token 或扣款。每种上游仍需使用供应商账单做日对账。 ## 扩容方式 - 进程无状态,可以在 L4/L7 负载均衡器后横向扩容。 - HTTP server 不设置 WriteTimeout,避免杀死长时间流;上游只限制响应头等待时间,客户端取消会终止上游请求。 - 连接池按进程共享,默认保留 4096 个空闲连接、单 host 1024 个,可按实例并发和上游限制调整。 - 请求体默认最多 16 MiB,响应仅保留最多 64 KiB 用于非流式 usage 提取;正文直接传输。 - 余额冻结和结算当前各需要一次 PostgreSQL 事务,以正确性优先。高吞吐阶段可把计费拆成独立服务并做账户分片,但不能用最终一致缓存替代权威账本事务。 ## 开发者上手路径 租户控制台的 Quickstart 首屏把充值、API key、可用模型和首次成功请求显示为 可追踪的四步状态。模型目录通过 `GET /admin/api/developer/models` 返回公开元数据和 当前生效价格,只保留该租户可用的模型与 wire API;它不会返回供应商地址、上游模型名、 路由权重或 allowlist。控制台可按开发者、协议和输入模态过滤,并按发布时间、价格或上下文 排序。`GET /admin/api/developer/config` 返回由 `AIGW_INFERENCE_PUBLIC_URL` 注入的推理地址和协议端点,控制台据此生成 cURL、Python 和 Node.js 示例。新建客户密钥仍只在创建响应和一次性对话框中出现,代码示例默认使用 `$AIGW_API_KEY`,不会把明文密钥写入本地存储或 HTML。 同一 Quickstart 页面提供 API Playground。它使用所选客户 Key 直接调用配置中的公开推理 地址,因此请求仍经过正式鉴权、模型限制、路由、余额冻结、结算与 Usage Ledger。Key 只 存在于当前页面内存。分离 listener 时,推理服务仅允许从 `AIGW_PUBLIC_URL` 派生出的精确 Origin,且不接受浏览器 credentials,只向页面暴露 `X-AIGW-Request-ID`。 模型详情由同一份租户安全目录数据渲染,不暴露供应商凭证、内部路由或上游模型名。成本 估算按当前版本化单价分别计算输入、输出、缓存读取和缓存写入 token;它是请求前预算工具, 实际扣款仍只认 Usage Ledger。Playground 对 `401/403`、`402`、`404`、`429` 和 `5xx` 提供不同的恢复入口,同时原样保留结构化错误与 request ID,便于开发者和支持人员定位。 目录还把当前内存 route 状态按模型投影为 `online`、`degraded` 或 `unavailable`,详情只返回 供应商公开 slug、显示名、协议、近期可用率、响应头延迟、样本量和熔断恢复时间。Quickstart 和 Playground 默认使用自动路由,也可以把所选 slug 编入 `model:provider-slug` 来固定供应商。 `GET /v1/models` 和 Anthropic 模型列表同样只公开 slug 与 wire API,不返回内部 UUID、URL、 凭证、上游模型名或权重。统计窗口最多保留最近 100 次真实尝试与可选认证 `/models` 主动探测;开启 `AIGW_PROVIDER_SHARED_HISTORY_ENABLED` 后会合并 TTL 内其他实例的 route 结果和 TTFT,详情分别标出共享样本、主动探测次数和最近探测时间。Redis Stream 只保存短期、有界的选路信号,故障时退回本机窗口,因此它不冒充持久状态历史或全局 SLA。 Usage 页不保存 prompt 或响应正文。单请求详情仅把已持久化的身份边界、模型路由、协议、 重试、延迟、token、缓存和结算字段组成可复制诊断 JSON,因此既能支持工单排障,也不会 把客户输入扩大为新的控制面敏感数据面。 `GET /admin/api/usage/analytics` 直接聚合 PostgreSQL Usage Ledger,并复用 Usage 页的租户、 项目、API Key、模型、状态和时间过滤。结果按模型与供应商返回请求量、成功率、token、 缓存命中、费用、未收金额、缺失 usage、总延迟和 TTFT 的 P50/P95,同时按 API Key 返回同口径归因,并用等长前一周期计算费用变化。 它不在推理热路径执行,也不从浏览器当前加载的有限请求列表推算财务数据。 Quickstart 的 starter key 表单复用正式 `POST /admin/api/keys` 写入链路,为当前租户、所选 项目和当前模型生成仅含 `inference` scope 的 Key。`api_keys(project_id, tenant_id)` 到 `projects(id, tenant_id)` 的复合外键保证项目不能跨租户绑定;明文 Key 仍只在创建响应中 返回一次,随后仅保存在页面内存并填入 Playground。连接面板直接消费 `GET /admin/api/developer/config`,集中输出两个 SDK Base URL、四个推理/模型端点和不含 真实凭证的环境变量模板。 密钥表单可设置模型白名单、日/月金额上限、RPM/TPM、到期时间和标签。白名单与密钥摘要在同一个 PostgreSQL 事务内创建,任何未知模型都会让事务整体回滚。`last_used_at` 在 Usage/结算 事务内单调更新;过期时间既用于快照过滤,也在每次鉴权时检查,避免长轮询间隔延迟失效。 密钥列表还通过 key/time 索引从 PostgreSQL 返回今日/本月已结算费用、待结算冻结和请求数。 停用/启用会立即切换鉴权快照;轮换在单个事务中复制策略、创建新摘要并撤销旧密钥, 从而不存在两个密钥同时有效的窗口。 `tenant_preferences` 保存租户默认模型、不同的 fallback 模型、低余额提醒阈值,以及异常消费提醒的启停、相对七日基线倍数和最低金额;异常策略为空时回退到 `admin.mail` 的部署默认值。 `GET /admin/api/developer/preferences` 返回当前租户值;两个独立的写接口分别要求 `developer.preferences.write` 与 `billing.preferences.write`,因此 developer 与 billing 角色 不能越权修改对方的设置。保存默认/fallback 时服务端会重新验证该模型当前对租户可见、 未退役且至少存在一条启用路由。邮件扫描直接读取 PostgreSQL 中的租户提醒策略,不依赖 Redis。 ## 管理面安全 bootstrap token 只映射为 `platform_admin`,用于首次建号和故障恢复,不是日常用户凭证。租户注册在同一 PostgreSQL 事务内创建租户、默认项目、钱包和待验证的 `tenant_admin` 账号;邮件动作使用仅保存摘要的一次性 token,邮件正文在 outbox 中加密。密码使用 PBKDF2-HMAC-SHA-256 哈希,TOTP secret、Passkey credential 和 WebAuthn challenge 使用 AES-256-GCM 加密。登录创建 HttpOnly、SameSite 会话 Cookie,并为所有写请求校验独立 CSRF Cookie/header;改密、密码重置和撤销成员会立即失效旧会话。平台角色没有 `tenant_id`,租户角色必须绑定一个 tenant。所有管理 API 在 handler 执行前校验 permission,租户过滤在 SQL 查询或资源所有权检查中完成,前端隐藏菜单不承担安全职责。 每个通过认证的管理请求都写入 `audit_logs`,包含 actor、角色、tenant、action、状态码、请求 ID、IP 和 User-Agent。审计写入失败不会回滚已成功的资源事务,但会输出结构化告警。 ## 建议的后续顺序 1. 将管理监听端口与公网推理端口分离,并为企业客户接入 OIDC/SAML、SCIM 与组织级强制 MFA 策略。 2. 增加供应商日账单对账和合同价/信用额度。 3. 为不返回 usage 的上游增加可靠 token 计算器,并监控 `uncollected_micros`。 4. 把 UsageEvent 做时间分区和归档,增加定时导出与报告。 5. 增加吞吐量/成本感知路由和客户可见的持久状态历史。