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

15 KiB
Raw Blame History

Codex Proxy RS 架构

本文回答系统由哪些边界组成、请求如何流动、状态由谁拥有,以及修改时必须保持哪些不变量。 具体 HTTP 字段见 接口文档,部署参数见 部署文档;上游 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. 运行拓扑

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. 数据面请求生命周期

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

管理写入遵循统一流程:

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 变化只能新增编号迁移, 详见 迁移规则。

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:

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,架构不变量保留在本文。