diff --git a/README.md b/README.md index f4fe04efa..9a6e5fce7 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ | 协议 | OpenAI Responses JSON、SSE、WebSocket 与模型目录 | | Provider | 固定 OpenAI、xAI 两个 Provider;各自管理 OAuth、账号、额度与模型目录 | | 路由 | Client Key 绑定 Provider、全局模型映射、会话亲和与同 Provider 安全 fallback | -| 透明边界 | OpenAI 保持 Responses 语义透明;xAI 的协议适配只驻留在 `providers/xai` | +| 透明边界 | OpenAI SSE 与 WebSocket 均原样转发上游字节;xAI 在 `providers/xai` 内做 Grok 与 Responses 协议转换 | | 延续 | OpenAI native/replay continuation;xAI 使用客户端提交的完整历史 | | 管理 | Client Key、账号、模型目录、设置、观测、系统更新与 OAuth | | 计量 | 模型请求 Token、费用、延迟、账号与 Provider 归因 | @@ -91,7 +91,7 @@ curl -i http://127.0.0.1:8080/healthz 2. 在账号页选择对应 Provider:OpenAI 支持 OAuth、账号文件与 Agent Identity;xAI 支持 OAuth 和兼容账号文件导入。 3. 按需配置客户端模型到上游模型的全局映射;未命中映射的模型名原样透传。 -4. 创建 `sk_...` Client Key,绑定 `openai` 或 `xai`,并设置模型范围、速率与并发限制。 +4. 创建 `sk_...` Client Key,绑定 `openai` 或 `xai`,并设置速率与并发限制。 > [!IMPORTANT] > xAI 使用 OAuth session,不支持把 xAI API Key 作为上游 credential。 @@ -158,7 +158,7 @@ docker compose -f deploy/compose.yaml up -d ``` > [!IMPORTANT] -> `.runtime/` 保存 PostgreSQL、Redis、日志、OpenAI 会话锚点和自更新状态。删除该目录会永久清除 +> `.runtime/` 保存 PostgreSQL、Redis、日志、OpenAI 会话锚点密钥和自更新状态。删除该目录会永久清除 > 运行状态。 > [!WARNING] diff --git a/deploy/README.md b/deploy/README.md index 5a06d2207..fecf692c2 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -5,7 +5,7 @@ - `config.yaml`:应用行为与真实凭据,由 `config.example.yaml` 复制得到并被 Git 忽略。 - `compose.yaml`:镜像、容器网络、端口、目录映射、健康检查和资源限制。 -项目不使用 `.env` 配置文件。Compose 中的少量环境变量只描述容器内部拓扑,不是用户配置入口。 +项目不使用 `.env` 配置文件。Compose 中的少量环境变量只描述容器内部拓扑与发布链路装配,不是用户配置入口。 ## 准备 @@ -70,6 +70,17 @@ curl -i http://127.0.0.1:8080/healthz 不要把未脱敏的 `docker compose config` 或 `docker inspect` 输出上传到工单;它们会包含 PostgreSQL/Redis 启动密码。日常校验使用 `config --quiet`。 +## 优雅关停 + +收到停止信号后,应用先停止接收新连接并 drain 存量连接;整个 drain 共享一个从停止信号 +起算的绝对截止点(`host.drain_timeout_seconds`,默认 30 秒),逾期放弃等待,存量连接随 +进程退出终止。drain 结束后才关停后台 worker,预算为 +`host.worker_shutdown_timeout_seconds`(默认 30 秒),两段预算按最坏情况串联。 + +Compose 的 `stop_grace_period` 为 30 秒,覆盖空闲与常规负载下的关停;若存量长连接把两段 +预算全部用满(合计 60 秒),Docker 会在宽限期结束时 SIGKILL。要求最坏情况下也完整走完 +drain 与 worker 收尾时,把 `stop_grace_period` 调到大于两个超时之和。 + ## 本地开发 本地 PostgreSQL 和 Redis 可继续由 Compose 启动: @@ -81,7 +92,9 @@ cargo run -p codex-proxy-rs ``` 后端会从当前目录向上查找 `deploy/config.yaml`。相对数据和日志目录以该文件所在目录解析; -Compose 只把监听地址和数据库、Redis 地址固定覆盖为容器内部服务名。 +Compose 把监听地址和数据库、Redis 地址固定覆盖为容器内部服务名,并把前端静态目录指向 +容器内构建产物。PG/Redis 集成测试所需的 `CPR_TEST_DATABASE_URL` / `CPR_TEST_REDIS_URL` +约定见 [迁移文档](../backend/migrations/README.md)。 ## 持久化与备份 diff --git a/docs/api.md b/docs/api.md index 60589daaf..7da1a279d 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,6 +1,6 @@ # Codex Proxy RS 接口 -本文列出 v3.0.0 当前公开 HTTP 接口。路由事实以 +本文列出 v3 当前公开 HTTP 接口。路由事实以 `backend/crates/gateway-api/src` 中的 router 为准。 ## 1. 鉴权与公共约定 @@ -22,8 +22,12 @@ Client Key 固定绑定一个 Provider:`openai` 或 `xai`。同一次请求不 - 浏览器登录后得到的 `cpr_admin_session` Cookie; - `x-api-key: `。 -已鉴权的管理请求还必须携带非空 `x-request-id`。管理端响应统一带 -`Cache-Control: no-store`。 +请求无需自带 `x-request-id`;缺失时服务端自动生成 UUID 并在响应头回传同一 request ID。 +`api.request_id_header` 只改变注入与回传的 header 名,而管理端鉴权固定读取 `x-request-id`, +因此启用管理端时该配置必须保持默认值。管理端响应统一带 `Cache-Control: no-store`。 + +配置了 CORS 白名单 origin 时,跨域请求以凭据模式放行,仅允许 `GET`/`POST` 方法和 +`authorization`、`content-type`、`x-api-key` 与 request ID 四个请求头,不使用通配符。 普通成功响应使用以下信封: @@ -36,8 +40,8 @@ Client Key 固定绑定一个 Provider:`openai` 或 `xai`。同一次请求不 ``` 错误响应仍使用 `code`、`message`、`data`,其中 `data` 为 `null`。稳定业务码包括参数错误 -`40001`、会话缺失 `40101`、资源不存在 `40401`、revision 冲突 `40901`、上游失败 `50201` -和依赖不可用 `50301`。 +`40001`、会话缺失 `40101`、登录凭据错误 `40102`、管理 API Key 错误 `40103`、资源不存在 +`40401`、revision 冲突 `40901`、内部错误 `50001`、上游失败 `50201` 和依赖不可用 `50301`。 ### 配置 revision @@ -60,12 +64,17 @@ Client Key 固定绑定一个 Provider:`openai` 或 `xai`。同一次请求不 | `POST` | `/v1/responses` | OpenAI Responses JSON;`stream=true` 返回 SSE,否则返回完整 JSON | | `GET` | `/v1/responses` | 通过 HTTP Upgrade 建立 Responses WebSocket | | `POST` | `/v1/responses/review` | 使用同一 Responses 合同发起 review 子代理请求 | -| `GET` | `/v1/models` | 返回当前 Client Key 所属 Provider 的可用公开模型 | +| `GET` | `/v1/models` | 返回当前 Client Key 所属 Provider 的可用公开模型;有两种响应形态,见下 | | `GET` | `/v1/models/catalog` | 返回 Codex 客户端使用的模型目录 | | `GET` | `/v1/models/{model_id}/info` | 返回 Codex 客户端使用的单模型信息 | | `GET` | `/v1/models/{model_id}` | 返回 OpenAI 兼容的单模型详情 | -OpenAI 路径保留客户端 Responses wire 语义;xAI 所需的协议转换只在 xAI Provider 内完成。模型映射 +`GET /v1/models` 默认返回 OpenAI 兼容列表 `{"object": "list", "data": [...]}`;请求携带非空 +`client_version` query 参数(Codex 客户端)时改为返回 Codex 专用目录合同 `{"models": [...]}`。 + +OpenAI 路径保留客户端 Responses wire 语义,上游事件字节在 HTTP SSE 与 WebSocket 两条链路上都 +原样转发;xAI 是 Grok wire 与 Responses wire 之间的协议转换层,转换只在 xAI Provider 内完成, +上游结构化错误的 message/code/type 会透传给客户端,其中内嵌的账号指纹 UUID 已脱敏。模型映射 是全局精确映射,未命中时模型名原样交给所属 Provider。 ## 4. 管理员认证 @@ -111,6 +120,7 @@ OpenAI 路径保留客户端 Responses wire 语义;xAI 所需的协议转换 - OpenAI 接受 OAuth 账号文档、账号 bundle 和 Agent Identity; - xAI 从单账号 object 或 `accounts` 数组中提取 OAuth token;包装中的代理、并发、优先级等字段不参与认证; +- xAI 批量导入逐条独立校验:失败条目跳过并记录日志,不中断其余条目,仅当没有任何条目成功时整个导入才报错; - xAI API Key 不是受支持的账号 credential; - 导入仍会通过 Provider 的身份或刷新链路校验,不能只凭文件外形写入账号。 @@ -220,6 +230,9 @@ auditRetentionDays request/response/upstream ID、outcome 与搜索文本。诊断 `dimension` 可取 `model`、`account`、 `apiKey`、`provider`、`transport`、`failureClass`、`status`。 +汇总与洞察中的请求数与 outcome 分布覆盖筛选范围内全部请求;token、缓存、延迟与成本聚合仅统计 +已完整交付客户端的成功响应。 + ## 9. 版本、更新与重启 | 方法 | 路由 | 主要 query/body | 说明 | @@ -232,5 +245,6 @@ request/response/upstream ID、outcome 与搜索文本。诊断 `dimension` 可 | `POST` | `/api/admin/system/rollback` | 无 | 回滚到保留的上一版本 | | `POST` | `/api/admin/system/restart` | 无 | 请求进程重启 | -在线更新仅在当前部署模式、Release 资产和进程重启能力都满足要求时可用。仓库发版流程见 +在线更新仅在当前部署模式、Release 资产和进程重启能力都满足要求时可用,且只在同一 major 版本内 +提供:跨大版本目标会以 `40901` 冲突拒绝,需按发布说明手动迁移。仓库发版流程见 [部署文档](../deploy/README.md) 与根目录 [README](../README.md)。 diff --git a/docs/architecture.md b/docs/architecture.md index 3a10dfaa1..0ce1a0cc7 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -21,16 +21,16 @@ Gateway Engine 不识别具体 Provider 协议;Provider 不拥有客户端 adm ```text backend/ -├── apps/gateway/ composition root、HTTP server、后台任务 +├── apps/gateway/ composition root(Bundle 装配) ├── crates/gateway-core/ operation、routing、engine、policy、accounting ├── crates/gateway-protocol/ 可共享 wire contract 与 canonical event ├── crates/gateway-admin/ 管理领域、用例与抽象端口 ├── crates/gateway-store/ PostgreSQL、Redis adapter ├── crates/gateway-api/ OpenAI Responses 与 Admin HTTP adapter -├── crates/gateway-host/ host/update/system 能力 +├── crates/gateway-host/ 配置与日志、HTTP serve/drain、worker 运行时、自更新 ├── crates/providers/openai/ OpenAI credential、catalog、transport ├── crates/providers/xai/ xAI/Grok credential、catalog、transport -└── migrations/0001_initial.sql 唯一终态初始迁移 +└── migrations/ 编号迁移与冻结清单 .frozen-sha256 ``` 依赖规则: @@ -42,7 +42,7 @@ backend/ - `gateway-store` 实现 Admin/Core 端口,但不拥有业务策略。 - 只有 `apps/gateway` 组合具体实现。 -各 crate 的 architecture 测试强制验证 crate 集合、依赖 DAG、生产源码边界与测试树。 +依赖 DAG 主要由上述约定与评审维护,机器校验只覆盖两处:`gateway-api` 的 architecture 测试冻结自身源码树与测试树,并禁止 manifest 出现 store/Provider/基础设施依赖(禁止清单不含 `gateway-host`);`apps/gateway` 的 architecture 测试冻结应用源码树、禁止生产测试挂载并约束 bootstrap 只做装配。其余 crate 没有 architecture 测试。 ## 3. 请求执行链 @@ -64,7 +64,7 @@ API decode/auth 1. 每个客户端请求只有一条 `model_requests`;attempt 明细保存在其 JSON 事实中,不另建 attempt 表。 2. 任何可能到达上游的调用都必须先有持久请求事实。 -3. `not_sent`、`sent`、`ambiguous` 是不同的上游发送边界;`ambiguous` 不自动重放。 +3. `not_sent`、`sent`、`ambiguous` 是不同的上游发送边界;发送状态是请求级单调水位(`sent` > `ambiguous` > `not_sent`),跨 attempt 只升不降,终态写回不得低于水位;`ambiguous` 不自动重放。 4. `downstream_committed_at` 表示网关已作出不可撤回的交付承诺,从该时刻禁止 retry/fallback;它在实际首字节写出前持久化,不宣称字节已经到达客户端。 5. Provider 每次 `execute` 只能选择一个 credential 并准备一个 cold stream,不得隐藏换号或业务 retry。 6. 下游 commit 前,Core 可按冻结策略处理同一 Provider 内的账号 fallback;禁止跨 Provider fallback。 @@ -78,7 +78,7 @@ Fallback 只允许在同一 Provider 内更换可用账号,不跨 Provider。 明确的上游认证、封禁、额度或 cooldown 错误会更新对应账号状态,使下一次选择排除该账号;满足重放安全条件且尚未 downstream commit 时,可以换号。传输结果不明确时不得假定请求未到达上游。 -重试矩阵属于独立策略,本次终态迁移保持 OpenAI 原正式行为与 xAI 参考行为,不在 Provider transport 内增加额外重试。 +重试矩阵属于独立策略,OpenAI 保持原正式行为、xAI 保持参考行为,不在 Provider transport 内增加额外重试。 ## 5. Continuation @@ -103,7 +103,12 @@ Provider 独占以下职责: - HTTP/WebSocket transport 和错误分类; - quota 投影与 Provider 私有观测字段。 -credential 更新使用 `credential_revision` CAS。认证永久失败、封禁、额度耗尽和带截止时间的 cooldown 是账号运行时事实。cooldown 到期后,Core 的有效调度谓词会自然允许账号重新参与选择;Redis cooldown 只是热缓存。成功调用后才可把账号状态重新观测为 `ready`。 +客户端可见正文的立场按 Provider 不同: + +- OpenAI 是逐字节透明代理:上游 wire 是客户端可见的事实来源,HTTP SSE 帧以原始字节原样下发,WebSocket 上游 JSON 文本原样下发;canonical facts 只从同一帧旁路解析,用于观测、亲和与计费。仅网关内部帧(rate limits、metadata)被就地消费,不向客户端转发。 +- xAI 是翻译层:把 Grok wire 解码后重新合成 Responses wire 事件,不承诺字节透明;上游错误按结构化 message/code/type 透出,message 中的 UUID 账号指纹替换为占位符。 + +credential 更新使用 `credential_revision` CAS。认证永久失败、封禁、额度耗尽和带截止时间的 cooldown 是账号运行时事实。cooldown 到期后,Core 的有效调度谓词会自然允许账号重新参与选择;Redis cooldown 只是热缓存。成功调用后才可把账号状态重新观测为 `ready`。自动 refresh 的瞬态失败(限流、5xx、超时、畸形响应)保留现有凭据并按指数退避推迟下一次刷新,不把账号标记为失效;只有上游明确的永久错误(`invalid_grant`、封禁)才写入终态失效。 `refresh_token_expires_at` 不是公共 SQL 列或 Core 权威状态。xAI 可在 `provider_credentials_json` 内保存它作为 Provider 私有提示;真正失效以 refresh endpoint 返回的永久错误为准。 @@ -141,14 +146,16 @@ credential 更新使用 `credential_revision` CAS。认证永久失败、封禁 - credential JSON 以 Provider 自己的 schema 明文保存在 `provider_credentials_json`;日志、Debug、API 和 audit 不输出其中的 secret,Provider 只在需要发往上游时把值包装为运行时 secret。 - stale recovery 只把超时 `running` 请求收敛为 `incomplete`,不重放业务请求。 -- `ops_events` 当前逐条同步持久化,`occurrence_count=1`;OpsFlush 可禁用,不存在隐藏内存聚合权威。 +- `ops_events` 逐条同步持久化,`occurrence_count=1`;OpsFlush worker 以 Disabled 声明贡献(没有需要刷写的内存缓冲),不存在隐藏内存聚合权威。 - retention 只删除已满足保留规则的历史事实,不改变运行中请求。 -业务 schema 由唯一初始迁移 `0001_initial.sql` 定义。 +业务 schema 由 `backend/migrations/` 下的编号迁移按序定义:`0001_initial.sql` 建表,后续迁移只做增量变更。已合入的迁移永久冻结——`.frozen-sha256` 是冻结清单,CI 校验清单相对 PR base 只增不改,启动时 sqlx 重校验已应用迁移的 checksum,不一致直接拒绝启动。规则见 `backend/migrations/README.md`。 ## 9. Redis 终态 -Redis 保存 admission、lease、circuit/cooldown 热状态、OAuth pending flow 与 runtime change 通知。Redis 丢失后必须从 PostgreSQL 或 Provider 事实恢复;恢复完成前需要保护的 acquire 路径 fail closed。 +Redis 保存的协调状态恰好是:客户端 admission 热状态、credential lease/fencing 与调度信号、账号 cooldown 与 availability 热缓存、catalog 缓存、Provider circuit、会话亲和与会话级账号排除、continuation pin、OAuth pending flow、worker leader lease 以及 runtime change 通知。Redis 丢失后必须从 PostgreSQL 或 Provider 事实恢复;恢复完成前需要保护的 acquire 路径 fail closed。 + +多副本部署时,跨副本协调面只有上述 Redis 状态与 PostgreSQL 事实;WebSocket 连接池、请求画像与 RuntimeSnapshot 副本都是进程内状态,其中 RuntimeSnapshot 依靠周期对账收敛,请求画像依靠各自的官方版本检查收敛。 OAuth pending key: @@ -167,13 +174,17 @@ create/take 使用 Lua 原子执行。owner mismatch 不删除记录;匹配 ow ## 10. 后台任务与恢复 -后台任务由 Provider bundle 和 host bundle 显式贡献,bootstrap 统一启动: +HTTP serve/drain 与 worker 运行时都在 `gateway-host`:Host 负责注册校验、leader lease、失败退避、健康暴露与关闭;`apps/gateway` 的 bootstrap 只装配。WorkerContribution 由 Store、Core 与两个 Provider bundle 显式贡献,host bundle 自身不贡献任何 worker: -- RuntimeSnapshot revision 对账与 Redis change wake-up; -- credential refresh、catalog、quota 等 Provider worker; -- stale execution recovery; -- retention 与必要的 Redis 热状态重建; -- host update/system worker。 +- Store:stale execution recovery 与 retention;OpsFlush 以 Disabled 声明贡献。 +- Core:RuntimeSnapshot revision 周期对账(无 lease,逐副本运行)与 Redis change 长驻订阅(daemon,逐副本运行)。 +- Provider:credential refresh、quota/catalog 健康,以及模型 etag 与官方版本 release 检查(OpenAI CLI、OpenAI Desktop、xAI CLI)。 + +带 lease 的周期任务在每个周期开始向 Redis 申请一次 leader lease,周期内续租,周期结束即释放。这只是单周期互斥,不是跨周期 leader 选举:各副本独立计时,N 副本部署下同一任务的实际执行频率最坏可达单副本的 N 倍。 + +自更新、回滚与进程重启不是 worker:它们是 Admin API 触发的 Host 系统操作,只替换并重启处理该请求的那个副本。 + +关闭时序:收到取消信号后,HTTP drain(axum 优雅关闭与游离连接等待)共享同一个从信号时刻起算的绝对截止点,逾期放弃等待存量连接;随后 worker 在独立的 shutdown 超时内 join。 worker 不得通过导入 Admin use case 绕过边界,也不得维护第二份业务状态。 diff --git a/skills/codex-proxy-rs/SKILL.md b/skills/codex-proxy-rs/SKILL.md index 523af6584..3990b8af8 100644 --- a/skills/codex-proxy-rs/SKILL.md +++ b/skills/codex-proxy-rs/SKILL.md @@ -19,10 +19,12 @@ description: Codex Proxy RS 仓库开发指南。Use when changing or auditing i - `gateway-api` 只做协议适配;`gateway-core` 编排;Provider 独占 credential、catalog 与 transport;`gateway-store` 实现持久化端口。 - Provider 每次调用只选择一个 credential,不隐藏换号、业务 retry 或跨 Provider fallback。 - PostgreSQL 是持久化权威;Redis 只保存可恢复协调状态和 OAuth pending flow。 +- 已应用的数据库迁移按字节冻结(清单 `backend/migrations/.frozen-sha256`,CI 做 append-only 校验); + schema 变更一律新增编号迁移,规则见 `backend/migrations/README.md`。 - 当前只有 `openai` 与 `xai` 两个 Provider;账号归属 Provider,不存在 Provider Instance 层。 - OpenAI 的 OAuth 与 Agent Identity credential 由 `providers/openai` 独占解析;账号导出使用 CPR 文档,Agent Identity 只输出运行所需身份材料,不伪造 OAuth 字段。 -- 测试放在各 package 的 `tests/`。禁止在生产 `src/` 写 test-only 代码。 +- 测试放在各 package 的 `tests/`,统一挂在单一 `main` 集成测试目标下(`--test main`)。禁止在生产 `src/` 写 test-only 代码。 - Vue 使用 `