Files
codex-proxy-rs/docs/architecture.md
T
2026-08-30 00:46:01 +08:00

274 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Codex Proxy RS 架构
本文回答系统由哪些边界组成、请求如何流动、状态由谁拥有,以及修改时必须保持哪些不变量。
具体 HTTP 字段见 [接口文档](api.md),部署参数见 [部署文档](../deploy/README.md);上游 URL、超时、
重试间隔和 UI 布局属于源码或配置,不在架构文档重复维护。
## 1. 系统定位
Codex Proxy RS 是单进程、单副本运行的多 Provider AI 网关,同时提供:
- 面向客户端的 OpenAI Responses、Images 和模型目录协议;
- 面向管理员的 `/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
```
`backend/apps/gateway` 是唯一组合根,按 Host → Store → Provider → Core → Admin → API → Worker 的顺序
初始化具体实现。其余 crate 只暴露自己的配置、端口和 Bundle,不自行定位别的实现。
## 3. Workspace 边界
| 路径 | 责任 |
| --- | --- |
| `backend/apps/gateway` | 读取顶层配置、连接 Bundle、注册 Provider 与 Worker |
| `gateway-protocol` | 跨层共享的 wire contract 和 canonical event,不依赖其他 workspace crate |
| `gateway-core` | operation、请求快照、路由、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 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. 具体实现只在组合根相遇。
`gateway-api` 与 `apps/gateway` 的 architecture tests 冻结关键源码树和依赖禁区;其余边界由 manifest、
集成测试和评审共同维护。
## 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 accounting
```
请求开始时冻结 `RuntimeSnapshot`、Client Key 的账号范围、模型映射、Provider 候选顺序和调度策略。
运行中的请求不再拼接新旧配置,也不在热路径查询分组关系。
核心不变量:
- 一个客户端请求对应一条 `model_requests`;attempt 是请求内事实,不建立第二张权威表。
- Provider 的一次 `execute` 只选择一个 credential 并返回一个冷流;换号、重试和 fallback 由 Core 决定。
- `not_sent`、`sent`、`ambiguous` 是单调的上游发送边界;结果不明确时不能假定上游未收到请求。
- downstream commit 是不可撤回的交付承诺。commit 后禁止换号、重试和 fallback。
- 跨 Provider 只在账号范围和能力都允许,且请求尚未到达上游或已被证明可安全重放时发生。
- 可恢复观测写入失败不能替换已经确定的客户端协议结果。
Responses 按模型目录编译候选;全局模型映射是精确映射,未命中时模型名原样交给候选 Provider。
Images 是 OpenAI Provider 自有端点:它不要求模型字段、不参与文本模型映射,只在 Client Key 的账号范围
确实包含 OpenAI 账号时生成单一 OpenAI 候选。
## 5. Provider 与协议边界
Core 只理解 `Operation`、能力要求、Provider 候选、稳定错误和 canonical event,不读取 Provider SDK
类型。Provider 独占 credential schema、OAuth、账号选择、模型目录、额度投影和上游 transport。
- OpenAI 是透明边界。Responses 请求保留未知字段和字段顺序;SSE、WebSocket 与 Images 的业务正文
按原始字节转发。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 | 由官方目录或发布源刷新,不写成第二份业务配置 |
账号对外状态不是独立列,而是 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: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 的业务降级等同于整个进程失活。
关闭分为两段:先停止接收新连接并 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`,架构不变量保留在本文。