mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 16:06:08 +00:00
Move Dashboard quota projection to the backend, share pagination request state, and fix independent usage loads while keeping trend switches free of loading overlays. Stream billing facts within the query budget, share pending authorization and usage validation, and separate Core value ownership and response observations. Remove the unused theme helper and strengthen dependency checks. Validated with workspace integration tests against isolated PostgreSQL and Redis, strict Clippy, Rust formatting, and frontend typecheck, lint and build. Co-authored-by: Codex <noreply@openai.com>
332 lines
20 KiB
Markdown
332 lines
20 KiB
Markdown
# Codex Proxy RS 架构
|
||
|
||
本文回答系统由哪些边界组成、请求如何流动、状态由谁拥有,以及修改时必须保持哪些不变量。
|
||
具体 HTTP 字段见 [接口文档](api.md),部署参数见 [部署文档](../deploy/README.md);上游 URL、超时、
|
||
重试间隔和 UI 布局属于源码或配置,不在架构文档重复维护。
|
||
|
||
## 1. 系统定位
|
||
|
||
Codex Proxy RS 是单进程、单副本运行的多 Provider AI 网关,同时提供:
|
||
|
||
- 面向客户端的 OpenAI Responses、Images、standalone Search 和模型目录协议;
|
||
- 面向管理员的 `/api/admin/*` 控制面和 Vue 管理端;
|
||
- OpenAI 与 xAI 两个编译期 Provider;
|
||
- PostgreSQL 持久化、Redis 协调状态以及 S3/R2 数据库备份。
|
||
|
||
系统不提供 `/v1/chat/completions`,不存在 Provider Instance 层,也不支持通过复制应用容器进行多副本
|
||
扩容。Client Key 限定账号分组,而不是绑定某个 Provider;一次请求的 Provider 候选由账号范围、模型
|
||
能力和运行时健康共同决定。
|
||
|
||
## 2. 运行拓扑
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
Client[API Client] --> API[gateway-api]
|
||
Browser[Vue Admin] --> API
|
||
|
||
API --> Core[gateway-core]
|
||
API --> Admin[gateway-admin]
|
||
|
||
Core --> Registry[Provider Registry]
|
||
Admin --> Registry
|
||
Registry --> OpenAI[provider-openai]
|
||
Registry --> XAI[provider-xai]
|
||
OpenAI --> OpenAIUpstream[OpenAI upstream]
|
||
XAI --> XAIUpstream[xAI upstream]
|
||
|
||
Core --> Store[gateway-store]
|
||
Admin --> Store
|
||
OpenAI --> Store
|
||
XAI --> Store
|
||
Store --> PG[(PostgreSQL)]
|
||
Store --> Redis[(Redis)]
|
||
Store --> Object[(S3 / R2)]
|
||
|
||
Host[gateway-host] -. lifecycle / workers / update .-> API
|
||
Host -. lazy client distribution resolver .-> Admin
|
||
Host --> StoreLinks[RG-Adguard / Microsoft CDN]
|
||
```
|
||
|
||
`backend/apps/gateway` 是唯一组合根,按 Host → Store → Provider → Core → Admin → API → Worker 的顺序
|
||
初始化具体实现。其余 crate 只暴露自己的配置、端口和 Bundle,不自行定位别的实现。
|
||
|
||
## 3. Workspace 边界
|
||
|
||
| 路径 | 责任 |
|
||
| --- | --- |
|
||
| `backend/apps/gateway` | 读取顶层配置、连接 Bundle、注册 Provider 与 Worker |
|
||
| `gateway-protocol` | 跨层共享的 OpenAI wire contract、SSE 编解码与无业务 owner 的解析事实,不依赖其他 workspace crate |
|
||
| `gateway-core` | operation、canonical event、请求快照、路由、admission、attempt 协调、交付边界和计量 |
|
||
| `gateway-admin` | 管理领域、用例、Provider/Store 端口、审计语义和备份策略 |
|
||
| `gateway-api` | HTTP/WS/SSE 解码与交付、Admin wire、静态管理端;不直接访问 Store 或具体 Provider |
|
||
| `gateway-store` | PostgreSQL、Redis、S3/R2、`pg_dump` 适配器;不拥有业务策略 |
|
||
| `gateway-host` | 配置加载、日志、HTTP 生命周期、Worker 监督和系统更新 |
|
||
| `providers/openai` | OpenAI OAuth、账号选择、目录、额度、Responses/Images/Search transport |
|
||
| `providers/xai` | xAI OAuth session、账号选择、目录、额度和 Grok/Responses 转换 |
|
||
| `frontend` | Vue 管理端,仅通过 Admin API 读写状态 |
|
||
|
||
依赖方向遵守四条规则:
|
||
|
||
1. Core 不依赖 HTTP、数据库、Redis 或具体 Provider。
|
||
2. Provider 之间不互相依赖,也不决定跨 Provider fallback。
|
||
3. API 只调用 Core/Admin 抽象;Store 只实现端口。
|
||
4. 具体实现只在组合根相遇。
|
||
|
||
组合根的 workspace architecture tests 冻结成员清单、依赖 DAG、公开模块面、源码纪律以及生产/测试模块
|
||
镜像关系;具体行为边界由各 crate 的集成测试维护。
|
||
|
||
### 3.1 Rust 模块组织约定
|
||
|
||
后端采用目录模块的 `mod.rs` 风格;以下规则由组合根的 workspace architecture tests 扫描全部生产源码与
|
||
测试模块树:
|
||
|
||
1. crate root 作为顶层门面,声明模块并暴露该 crate 的配置、Bundle、端口或稳定领域合同;目录节点使用
|
||
`目录/mod.rs`,叶子模块使用 `名称.rs`,不混用 `名称.rs` 与 `名称/` 两套入口。
|
||
2. 生产源码不使用内联 `mod name { ... }`、`#[path]` 或 `include!` 隐藏文件归属。`lib.rs` 和各级
|
||
`mod.rs` 是模块门面,负责声明子模块、选择性 re-export 与少量同层协调。
|
||
3. 子模块默认私有;只有真实的跨 crate 生产合同才能使用 `pub`。adapter/provider 的公开根模块由
|
||
allowlist 冻结,变更必须同步完成边界审计,不能为测试或兼容路径创建第二 owner。
|
||
4. owner 级依赖保持单向。两个模块互相引用时,应把共享值对象、端口或生命周期能力提升到共同 owner,
|
||
不能依靠同一 crate 内可见性掩盖循环边界。
|
||
5. 每个 crate 的 `src/` 与 `tests/` 平级,生产源码不承载测试。`tests/` 镜像 `src/` 的模块目录形态:例如
|
||
`src/foo/mod.rs` 对应 `tests/foo/mod.rs`,`src/foo/bar.rs` 对应 `tests/foo/bar.rs`。一个生产模块可以没有
|
||
测试;额外场景测试必须放在最近的生产 owner 目录下。根级 `support` 和冻结的 crate/workspace 架构场景
|
||
是明确例外。
|
||
|
||
较大的模块门面只负责组合和 re-export:账号 Admin HTTP 边界按 `wire`、`credentials`、`handlers`、
|
||
`presenter` 划分;Provider 执行按 continuation、stream、failure、observation 和 worker 等职责拆分;xAI
|
||
请求转换按 response、tools 与 history 拆分。各子模块之间只使用 owner 内最小可见性。
|
||
|
||
### 3.2 `gateway-core` 内部 owner
|
||
|
||
`gateway-core` 内共享事实按语义 owner 划分,不再借用 `engine` 作为通用命名空间:
|
||
|
||
| 模块 | 唯一责任 |
|
||
| --- | --- |
|
||
| `validation` | 纯值对象校验错误和文本约束,不依赖事件、执行错误或路由 |
|
||
| `identity` | Provider 身份值 `ProviderKind`,只依赖纯校验 |
|
||
| `account` | Provider 账号/credential/quota 值对象、持久化端口与请求级账号选择;`scope` 持有分组、账号目录和冻结账号范围 |
|
||
| `metering` | 标准化 Usage、金额、费用估算与费用明细;不表示账号或开票系统 |
|
||
| `upstream` | 跨 Engine、Event、Error 与 Provider 共用的 transport 名称、发送状态和不透明上游值 |
|
||
| `lifecycle` | 取消信号、连接注册与 drain 合同 |
|
||
| `engine` | attempt、发送/提交屏障、执行编排和持久化调用时序 |
|
||
| `routing` | 冻结路由事实、请求计划以及运行时快照的表示与编译 |
|
||
| `runtime` | 当前快照的发布、读取、revision 订阅与周期对账任务 |
|
||
|
||
`event` 通过 `validation` / `upstream` 使用基础值,不再依赖承载原始事件的执行错误;账号值对象和
|
||
选择策略通过 `identity` / `account::scope` 使用身份与范围,不依赖路由计划。`routing` 和 `error` 的
|
||
既有公开类型路径保留 re-export,定义与 Core 内部使用均归属上述 owner。架构测试约束这些叶子依赖。
|
||
|
||
`engine::observation` 统一维护单次响应的用量、费用、时间和响应 ID,并负责重试前清理;协调器继续
|
||
独占发送、提交、重试和终结顺序。Provider 上报费用优先于本地估算,丢弃的 attempt 不得污染最终计量。
|
||
|
||
## 4. 数据面请求生命周期
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant C as Client
|
||
participant A as API Adapter
|
||
participant E as Core Engine
|
||
participant P as Provider
|
||
participant S as Store / Ops queue
|
||
|
||
C->>A: authenticated request
|
||
A->>E: Operation + client context
|
||
E->>E: freeze snapshot and compile routing plan
|
||
E->>S: enqueue request / attempt observations
|
||
E->>P: one candidate, one credential, one attempt
|
||
P-->>E: cold canonical stream + raw wire
|
||
E->>E: enforce send and downstream commit barriers
|
||
E-->>A: committed response stream
|
||
A-->>C: JSON / SSE / WebSocket
|
||
E->>S: enqueue terminal observation and metering
|
||
```
|
||
|
||
请求开始时冻结 `RuntimeSnapshot`、Client Key 的账号范围、模型映射、Codex 客户端最低版本、Provider
|
||
候选顺序和调度策略。
|
||
运行中的请求不再拼接新旧配置,也不在热路径查询分组关系。
|
||
|
||
Client Key 鉴权完成后,API adapter 从有界请求头识别 Codex Desktop/CLI,Core 使用同一请求冻结的
|
||
`RuntimeSnapshot` 比较对应最低版本。Desktop 优先于其 User-Agent 内嵌的 CLI/Core 标记;未知客户端不
|
||
应用门禁。低版本或已识别但版本不可用时,在进入 Provider 前返回稳定的 `426` 合同。
|
||
|
||
核心不变量:
|
||
|
||
- 一个客户端请求对应一条 `model_requests`;attempt 是请求内事实,不建立第二张权威表。
|
||
- Provider 的一次 `execute` 只选择一个 credential 并返回一个冷流;换号、重试和 fallback 由 Core 决定。
|
||
- `not_sent`、`sent`、`ambiguous` 是单调的上游发送边界;结果不明确时不能假定上游未收到请求。
|
||
- downstream commit 是不可撤回的交付承诺。commit 后禁止换号、重试和 fallback。
|
||
- 跨 Provider 只在账号范围和能力都允许,且请求尚未到达上游或已被证明可安全重放时发生。
|
||
- 可恢复观测写入失败不能替换已经确定的客户端协议结果。
|
||
|
||
Responses 按模型目录编译候选;全局模型映射是精确映射,未命中时模型名原样交给候选 Provider。
|
||
Images 与 standalone Search 是 OpenAI Provider 自有端点:两者都不参与文本模型映射,只在 Client Key
|
||
的账号范围确实包含 OpenAI 账号时生成单一 OpenAI 候选。Images 不要求模型字段;Search body 中的模型
|
||
及其他字段保持原始 bytes 并由上游解释。
|
||
|
||
## 5. Provider 与协议边界
|
||
|
||
Core 只理解 `Operation`、能力要求、Provider 候选、稳定错误和 canonical event,不读取 Provider SDK
|
||
类型。Provider 独占 credential schema、OAuth、账号选择、模型目录、额度投影和上游 transport。
|
||
|
||
- OpenAI 是透明边界。Responses 请求保留未知字段和字段顺序;SSE、WebSocket、Images 与 standalone
|
||
Search 的业务正文按原始字节转发。canonical facts 从同一数据旁路提取,只用于路由、观测和计费。
|
||
- xAI 是翻译边界。Provider 把 Grok wire 转换为 Responses wire;上游结构化错误的 message/code/type
|
||
可以透出,但账号指纹会先脱敏。
|
||
- response ID 是不透明 UTF-8 bytes,不假设 UUID、固定长度或跨 Provider 可复用。
|
||
- 请求画像以配置为启动基线。OpenAI Desktop 与 xAI CLI 的官方版本检查只更新各自负责的运行时画像,
|
||
不回写 `config.yaml`。
|
||
|
||
### 错误与诊断三层边界
|
||
|
||
错误信息按用途分成三层,不能用同一个 `message` 同时承担协议、界面和诊断职责:
|
||
|
||
1. **数据面协议错误**:`/v1/*` 继续遵守 OpenAI/xAI wire 合同。可交付原始上游响应时保留其状态、headers、
|
||
content type 和 body;本地 fallback 使用数据面稳定机器码与安全英文,不受管理端中文化影响。
|
||
2. **控制面展示错误**:`/api/admin/*` 由 API 统一 HTTP 状态与数值业务码,由 Admin/API owner 提供安全中文
|
||
文案。extractor rejection、namespace 404 和 method 405 也使用同一 JSON 信封;任意 Store、Serde、
|
||
Provider `Display` 不得直接跨越 HTTP 边界。
|
||
3. **原始上游诊断**:只在连接测试、运维错误详情等明确诊断界面展示已经捕获的原始字段,不翻译、不补造,
|
||
也不塞入普通 Admin 错误信封。原始 body 不进入 Debug、普通日志或持久化错误消息。
|
||
|
||
连接测试的 `gateway` / `provider` / `upstream` 来源以及 `not_sent` / `sent` / `ambiguous` 发送状态由 Core 在
|
||
仍持有完整执行错误时一次判定;Vue 只能根据稳定字段生成摘要,不能匹配英文错误句子反推来源。
|
||
|
||
## 6. 路由、账号范围与 continuation
|
||
|
||
Client Key 与账号分组形成授权范围:
|
||
|
||
- 没有分组关联表示 `AllAccounts`;
|
||
- 有关联时只允许已启用分组成员的并集;
|
||
- 已绑定分组为空或全部禁用时得到空池,绝不回退为全部账号;
|
||
- 分组可以包含多个 Provider,账号也可以属于多个分组。
|
||
|
||
账号选择综合启停状态、credential/quota 事实、Redis cooldown、并发上限、权重、请求间隔和会话亲和。
|
||
账号编辑在一个事务中替换完整调度事实;导入、重新授权和后台刷新不会覆盖已有分组、权重或并发设置。
|
||
|
||
Continuation 仍受原请求的 Client Key、账号范围、Provider 和发送/交付边界约束:
|
||
|
||
- native continuation 固定创建它的 Provider 与账号;
|
||
- OpenAI 按 native → replay owner → replay any 推进,并保留官方 `previous_response_id` 语义;
|
||
- xAI 使用客户端提交的完整历史作为重放输入;
|
||
- scope 外账号、跨 Key 复用或不明确发送结果均 fail closed。
|
||
|
||
会话亲和是优先选择提示,不是硬账号绑定;native continuation 才携带不可跨越的 owner 约束。
|
||
|
||
## 7. 控制面与 revision
|
||
|
||
管理写入遵循统一流程:
|
||
|
||
```text
|
||
HTTP validation
|
||
-> Admin use case
|
||
-> Provider prepare/verify when needed
|
||
-> PostgreSQL transaction + audit
|
||
-> publish committed runtime snapshot
|
||
-> notify Provider-derived caches
|
||
```
|
||
|
||
会改变路由快照或安全配置的 mutation 在同一 PostgreSQL 事务中提交业务事实、推进内部
|
||
`config_revision` 并写入脱敏审计。Admin mutation 不要求客户端提交 revision;少数账号/分组响应
|
||
返回已提交的 `configRevision`,不将它当作乐观并发前置条件。
|
||
|
||
额度、cooldown、目录 generation、请求统计和自动 credential refresh 属于运行时观测,不推进全局
|
||
revision;credential 轮换只推进账号自己的 `credential_revision`。Redis 通知用于缩短收敛延迟,
|
||
PostgreSQL 周期对账才是正确性基础。
|
||
|
||
## 8. 状态所有权
|
||
|
||
| 状态 | 唯一权威 | 说明 |
|
||
| --- | --- | --- |
|
||
| 账号、credential、分组、Client Key、设置、审计、请求与备份记录 | PostgreSQL | 业务持久化事实 |
|
||
| admission、lease、cooldown、circuit、会话亲和、continuation、OAuth pending、目录 cache | Redis | 可重建、可过期的协调状态 |
|
||
| 日志、OAuth 恢复记录、在线更新状态、备份暂存 | `.runtime/` | 部署节点本地运行文件 |
|
||
| 重置卡库存与消费结果 | OpenAI upstream | 后端不建立本地卡库存;前端只保留当前浏览器会话的最近查询 |
|
||
| Provider 公开模型与请求画像 | Provider/runtime cache | 由官方目录或发布源刷新,不写成第二份业务配置 |
|
||
| Windows 安装包临时直链 | Host 进程内短缓存 | 按需解析、严格校验、到期前丢弃;不写 PostgreSQL/Redis,也不代理包字节 |
|
||
|
||
账号对外状态不是独立列,而是 PostgreSQL credential/quota 事实与 Redis cooldown 的统一投影:
|
||
`normal`、`quota_exhausted`、`rate_limited`、`disabled`、`error`。只有明确上游证据才能恢复或终态化账号,
|
||
本地时钟和不确定响应不能伪造事实。
|
||
|
||
PostgreSQL schema 由迁移目录按编号管理。已应用迁移按字节冻结,后续 schema 变化只能新增编号迁移,
|
||
详见 [迁移规则](../backend/migrations/README.md)。
|
||
|
||
## 9. Credential、额度与主动重置
|
||
|
||
credential 与 quota 是两组独立事实:credential refresh 不等于 quota refresh,额度接口的 401/403
|
||
也不能单独证明 refresh token 永久失效。
|
||
|
||
- OpenAI 支持 OAuth、AT/RT 与 OAuth JSON;RT-only 导入先换取 AT,AT-only 导入没有
|
||
自动续期能力。OAuth 身份只从官方 JWT claims 投影,不信任导入文档顶层身份字段。
|
||
- xAI 使用 OAuth session;API Key 不是受支持的账号 credential。
|
||
- 新账号导入和首次 OAuth 在 credential 提交后尽力读取一次额度;失败只留下观测,不回滚账号事务。
|
||
- quota refresh、正常推理返回的 rate-limit headers 和后台健康任务汇入同一额度事实;套餐只用于展示与
|
||
目录 cache 隔离,不创建套餐专属状态机。
|
||
|
||
主动额度重置是 OpenAI Provider 的不可逆上游操作:列表查询和消费都直接使用当前 Desktop 请求画像;
|
||
卡片不写 PostgreSQL/Redis。消费请求携带调用方生成的 UUIDv4 幂等键,同一账号的消费在进程内串行;
|
||
发送结果不明确时必须复用原键。确认成功后管理端再显式刷新卡片与 quota,不能直接改本地重置时间。
|
||
|
||
## 10. 观测与后台任务
|
||
|
||
请求观测通过有界进程内队列异步投影到 PostgreSQL。队列满、Store 暂不可用或进程退出超时时,允许丢失
|
||
可恢复观测并累计指标,但不允许改变客户端响应。Usage 详情中的 attempt 因此是 best-effort,并通过
|
||
`attemptsComplete: false` 明示不完整性。
|
||
|
||
只有完整交付客户端的成功响应进入 Token、延迟和成本聚合。实际 `service_tier` 只接受上游响应事件确认,
|
||
不能用请求期望值替代。
|
||
|
||
Worker 由各 Bundle 贡献、由 Host 统一监督:
|
||
|
||
- Store:过期请求恢复、历史保留和 PostgreSQL/Redis 观测队列;
|
||
- Core:`runtime` owner 的 RuntimeSnapshot 周期对账和 Redis change 订阅;
|
||
- Admin:S3/R2 备份 daemon,负责调度、执行、删除收敛与保留清理;
|
||
- Provider:credential refresh、quota/catalog 健康和官方版本/etag 检查。
|
||
|
||
周期任务的 Redis lease 只保证单周期互斥,不构成多副本 leader 选举。备份 daemon 依赖单副本部署边界,
|
||
自更新也只替换处理请求的当前进程,因此整个应用必须保持单副本。
|
||
|
||
## 11. 生命周期、安全与恢复
|
||
|
||
启动只有在配置、PostgreSQL、Redis、Provider、Core、Admin、API 和 Worker 全部初始化成功后才进入服务。
|
||
健康检查综合 Core、Store 与 Worker 状态,但不会把单个 Provider 的业务降级等同于整个进程失活。
|
||
组合根在启动时只把 Host 的 `ClientDistributionResolver` 能力注入 Admin;此时不访问 RG-Adguard,下载
|
||
HTTP Client 构造失败也不会阻断网关启动。外部解析在已认证管理员首次打开客户端下载弹窗时惰性执行,
|
||
失败时按架构使用 OpenAI 官方稳定地址。Microsoft Store 内容通道返回的 HTTP/80 临时链接保留原始 scheme,
|
||
不会被错误改写到该 host 不保证支持的 HTTPS 虚拟主机。
|
||
|
||
关闭分为两段:先停止接收新连接并 drain HTTP/WS,再取消并等待 Worker;两段各有独立预算,Compose 的
|
||
`stop_grace_period` 必须覆盖二者之和。超时后只丢弃仍未落盘的可恢复观测,不执行隐式业务重放。
|
||
|
||
安全边界:
|
||
|
||
- Provider credential 以 Provider schema 的明文 JSON 保存在 PostgreSQL;数据库和备份必须按敏感数据保护。
|
||
- OAuth 恢复日志包含原始 AT/RT,`.runtime/logs` 同样属于敏感数据。
|
||
- 真实 secret 不进入普通日志、Debug、fixture 或 audit details;明文只能通过账号导出、Key reveal、
|
||
备份设置等明确的敏感 Admin 合同返回。
|
||
- OAuth pending flow 使用有期限、带 owner 的一次性 claim;事务成功后才消费,失败释放 claim。
|
||
- 在线更新校验 Release host、大小、SHA-256 和归档路径,并只允许同一大版本内更新。
|
||
|
||
PostgreSQL 备份恢复属于人工维护操作:恢复时停用备份 Worker,清理快照中的非终态任务,重算计划游标并
|
||
重新验证对象存储;不会自动删除恢复前已经存在的远端对象。
|
||
|
||
## 12. 修改与验收
|
||
|
||
变更应落在拥有该事实的边界:协议适配进 API,执行策略进 Core,Provider 差异进对应 Provider,持久化
|
||
实现进 Store,生命周期进 Host,管理规则进 Admin。不要用兼容 shim、第二套状态机或跨层旁路绕开 owner。
|
||
|
||
后端验证从 `backend/Cargo.toml` 执行,仓库根目录没有 Cargo manifest:
|
||
|
||
```bash
|
||
cargo +1.97.0 fmt --all --manifest-path backend/Cargo.toml -- --check
|
||
cargo +1.97.0 clippy --manifest-path backend/Cargo.toml --all-targets --all-features --locked -- -D warnings
|
||
cargo +1.97.0 test --manifest-path backend/Cargo.toml --test main --locked
|
||
pnpm --dir frontend format:check
|
||
pnpm --dir frontend build
|
||
docker compose -f deploy/compose.yaml config --quiet
|
||
```
|
||
|
||
行为、配置或边界变化必须同步其唯一文档 owner:用户入口写入根 README,HTTP 合同写入 `docs/api.md`,
|
||
部署操作写入 `deploy/README.md`,架构不变量保留在本文。
|