mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
docs: refresh architecture and project guides
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -12,31 +12,35 @@
|
||||
[](https://github.com/zyycn/codex-proxy-rs/pkgs/container/codex-proxy-rs)
|
||||
[](https://opensource.org/license/mit)
|
||||
|
||||
[快速开始](#快速开始) · [客户端接入](#客户端接入) · [运维](#运维) · [部署](deploy/README.md) · [接口](docs/api.md) · [架构](docs/architecture.md)
|
||||
[快速开始](#快速开始) · [客户端接入](#客户端接入) · [运行维护](#运行维护) · [部署](deploy/README.md) · [接口](docs/api.md) · [架构](docs/architecture.md)
|
||||
|
||||
</div>
|
||||
|
||||
Codex Proxy RS 在一个网关中管理 OpenAI 与 xAI 账号,为客户端提供统一鉴权、模型路由、额度状态、
|
||||
请求观测和管理控制面。
|
||||
|
||||
> [!NOTE]
|
||||
> 只支持 OpenAI Responses API,不提供 `/v1/chat/completions`。
|
||||
> 数据面支持 OpenAI Responses、Images 和模型目录协议,不提供 `/v1/chat/completions`。
|
||||
|
||||
## 能力
|
||||
## 功能
|
||||
|
||||
| 领域 | 实现 |
|
||||
| -------- | ----------------------------------------------------------------------------------------------------- |
|
||||
| 协议 | OpenAI Responses JSON、SSE、WebSocket 与模型目录 |
|
||||
| Provider | 固定 OpenAI、xAI 两个 Provider;各自管理 OAuth、账号、额度与模型目录 |
|
||||
| 路由 | Client Key 绑定 Provider、全局模型映射、会话亲和与同 Provider 安全 fallback |
|
||||
| 透明边界 | OpenAI 请求保留未知字段和字段顺序,SSE 与 WebSocket 原样转发业务字节;xAI 在 Provider 内做协议转换 |
|
||||
| 延续 | OpenAI native/replay continuation;xAI 使用客户端提交的完整历史 |
|
||||
| 账号 | 导入、OAuth、credential 刷新、主动额度刷新,以及正常响应中的被动额度/状态观测 |
|
||||
| 管理 | Client Key、账号、模型目录、设置、观测、系统更新与 OAuth |
|
||||
| 计量 | 模型请求 Token、费用、延迟、账号与 Provider 归因,并记录响应确认的实际 `service_tier` |
|
||||
| 领域 | 当前能力 |
|
||||
| --- | --- |
|
||||
| 数据面 | Responses JSON、SSE、WebSocket、review 子代理、Images 生成/编辑与模型目录 |
|
||||
| Provider | 固定 `openai`、`xai` 两个 Provider,各自拥有 OAuth、credential、额度、目录与 transport |
|
||||
| 路由 | Client Key 限定账号分组;按模型能力编译 Provider 候选;支持全局精确模型映射、会话亲和和安全 fallback |
|
||||
| 透明边界 | OpenAI 保留未知请求字段及字段顺序,并原样转发 SSE、WebSocket 与 Images 业务字节;xAI 在 Provider 内转换协议 |
|
||||
| 账号 | OAuth、OpenAI AT/RT、账号文件与 Agent Identity 导入,credential/额度刷新、账号恢复及主动额度重置卡 |
|
||||
| 管理 | 账号与分组、Client Key、运行设置、用量/错误观测、S3/R2 备份、系统更新与回滚 |
|
||||
| 计量 | Token、图片、费用、延迟、账号与 Provider 归因,并记录上游响应确认的实际 `service_tier` |
|
||||
|
||||
架构边界、状态所有权和请求不变量见 [架构文档](docs/architecture.md)。
|
||||
|
||||
## 快速开始
|
||||
|
||||
需要 Docker Engine 与 Docker Compose Plugin。
|
||||
|
||||
### 1. 准备
|
||||
### 1. 准备配置和目录
|
||||
|
||||
```bash
|
||||
git clone https://github.com/zyycn/codex-proxy-rs.git
|
||||
@@ -56,16 +60,16 @@ openssl rand -hex 24
|
||||
openssl rand -hex 24
|
||||
```
|
||||
|
||||
把两个结果写入 `deploy/config.yaml`,并单独设置管理员初始密码:
|
||||
把两个结果分别写入 `deploy/config.yaml`,再设置管理员初始密码:
|
||||
|
||||
| 配置 | 约束 |
|
||||
| -------------------------- | ---------------------------- |
|
||||
| `store.database.password` | 48 位十六进制 |
|
||||
| `store.redis.password` | 48 位十六进制 |
|
||||
| `admin.default_password` | 至少 12 位,不能包含 `$` |
|
||||
| 配置 | 约束 |
|
||||
| --- | --- |
|
||||
| `store.database.password` | 48 位十六进制 |
|
||||
| `store.redis.password` | 48 位十六进制 |
|
||||
| `admin.default_password` | 至少 12 位,不能包含 `$` |
|
||||
|
||||
PostgreSQL、Redis 容器和应用通过同一份 `config.yaml` 使用这两个基础设施密码,不需要额外
|
||||
导出环境变量。管理员密码只在首次创建管理员时生效。
|
||||
PostgreSQL、Redis 和应用从同一份 `config.yaml` 读取基础设施密码,不需要额外导出环境变量。
|
||||
管理员密码只在首次创建管理员时生效。
|
||||
|
||||
Linux 需要允许容器组写入运行目录:
|
||||
|
||||
@@ -83,37 +87,36 @@ docker compose -f deploy/compose.yaml up -d --no-build
|
||||
curl -i http://127.0.0.1:8080/healthz
|
||||
```
|
||||
|
||||
`204 No Content` 表示应用、PostgreSQL 与 Redis 均可用。管理端地址:
|
||||
`204 No Content` 表示应用、PostgreSQL 与 Redis 均可用。管理端地址为
|
||||
`http://127.0.0.1:8080`。
|
||||
|
||||
### 3. 初始化
|
||||
### 3. 初始化网关
|
||||
|
||||
1. 使用 `admin@cpr.local` 与初始密码登录。
|
||||
2. 在账号页选择对应 Provider:OpenAI 支持 OAuth、账号文件与 Agent Identity;xAI 支持 OAuth
|
||||
和兼容账号文件导入。
|
||||
3. 按需配置客户端模型到上游模型的全局映射;未命中映射的模型名原样透传。
|
||||
4. 创建 `sk_...` Client Key,绑定 `openai` 或 `xai`,并设置速率与并发限制。
|
||||
2. 导入或授权 Provider 账号:OpenAI 支持 OAuth、逐行 AT/RT、OAuth JSON 与 Agent Identity;
|
||||
xAI 支持 OAuth 和账号 JSON。
|
||||
3. 创建账号分组并按需配置全局模型映射。未命中的模型名原样交给候选 Provider。
|
||||
4. 创建 `sk_...` Client Key,选择可用账号分组,并设置速率和并发限制;空分组范围表示全部账号。
|
||||
|
||||
OpenAI 账号导入和首次 OAuth 创建在 credential 提交后会立即尝试一次额度观测;额度接口临时失败不会
|
||||
回滚已提交的账号。Free、K12 等套餐共用同一套额度与状态转换逻辑,套餐字段只作为账号事实并隔离
|
||||
对应的模型目录 cache。credential 刷新、额度刷新和正常请求中的被动额度观测是三条独立链路,详见
|
||||
[账号接口](docs/api.md#5-账号)。
|
||||
OpenAI RT 导入会先向官方 token endpoint 换取 AT;仅 AT 的账号不具备自动续期能力,
|
||||
能否参与调度仍取决于 token 中可验证的身份与后续上游事实。账号导入和首次 OAuth 创建会尽力读取一次额度,
|
||||
额度上游暂时失败不会回滚已提交的账号。
|
||||
|
||||
OpenAI 账号的主动重置卡只在用户打开相应弹窗或手工刷新时查询。消费成功后管理端重新查询卡片与额度;
|
||||
系统不会通过修改本地 `reset_at` 伪造重置。完整账号合同见 [账号接口](docs/api.md#5-账号)。
|
||||
|
||||
> [!IMPORTANT]
|
||||
> xAI 使用 OAuth session,不支持把 xAI API Key 作为上游 credential。
|
||||
|
||||
> [!NOTE]
|
||||
> `admin.default_password` 只在首次创建管理员时生效。
|
||||
|
||||
完整部署、权限和密码轮换规则见 [deploy/README.md](deploy/README.md)。
|
||||
完整的权限、密码轮换、备份和升级规则见 [部署文档](deploy/README.md)。
|
||||
|
||||
## 客户端接入
|
||||
|
||||
| 配置 | 值 |
|
||||
| -------- | ---------------------------------------- |
|
||||
| Base URL | `http://127.0.0.1:8080/v1` |
|
||||
| API Key | 管理端创建的 `sk_...` |
|
||||
| 鉴权 | `Authorization: Bearer <client-api-key>` |
|
||||
| 配置 | 值 |
|
||||
| --- | --- |
|
||||
| Base URL | `http://127.0.0.1:8080/v1` |
|
||||
| API Key | 管理端创建的 `sk_...` |
|
||||
| 鉴权 | `Authorization: Bearer <client-api-key>` |
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:8080/v1/models \
|
||||
@@ -130,69 +133,70 @@ curl http://127.0.0.1:8080/v1/responses \
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>API 路由</summary>
|
||||
<summary>公开数据面路由</summary>
|
||||
|
||||
| 路由 | 用途 |
|
||||
| ---------------------------------- | ------------------------------ |
|
||||
| `POST /v1/responses` | JSON 响应或 SSE Responses 流 |
|
||||
| `GET /v1/responses` | Responses WebSocket 升级 |
|
||||
| `POST /v1/responses/review` | review 子代理请求 |
|
||||
| `POST /v1/images/generations` | 图像生成 JSON 透明转发 |
|
||||
| `POST /v1/images/edits` | 图像编辑 JSON 透明转发 |
|
||||
| `GET /v1/models` | 当前 Client Key 可用的模型列表 |
|
||||
| `GET /v1/models/catalog` | Codex 客户端展示用模型目录 |
|
||||
| `GET /v1/models/{model_id}/info` | Codex 客户端展示用模型详情 |
|
||||
| `GET /v1/models/{model_id}` | OpenAI 兼容模型详情 |
|
||||
| 路由 | 用途 |
|
||||
| --- | --- |
|
||||
| `POST /v1/responses` | JSON 响应或 SSE Responses 流 |
|
||||
| `GET /v1/responses` | Responses WebSocket 升级 |
|
||||
| `POST /v1/responses/review` | review 子代理请求 |
|
||||
| `POST /v1/images/generations` | OpenAI Images 生成 JSON 透明转发 |
|
||||
| `POST /v1/images/edits` | OpenAI Images 编辑 JSON 透明转发 |
|
||||
| `GET /v1/models` | 当前 Client Key 账号范围内的模型列表 |
|
||||
| `GET /v1/models/catalog` | Codex 客户端展示用模型目录 |
|
||||
| `GET /v1/models/{model_id}/info` | Codex 客户端展示用模型详情 |
|
||||
| `GET /v1/models/{model_id}` | OpenAI 兼容模型详情 |
|
||||
|
||||
所有 `/v1/*` 路由都需要客户端 API Key。
|
||||
所有 `/v1/*` 路由都需要 Client Key。Images 是 OpenAI Provider 自有端点,不读取模型字段,也不参与
|
||||
文本模型映射。
|
||||
|
||||
</details>
|
||||
|
||||
完整的客户端与管理端路由、鉴权和 mutation 规则见 [接口文档](docs/api.md)。
|
||||
完整的客户端与管理端路由、鉴权、请求字段和 mutation 语义见 [接口文档](docs/api.md)。
|
||||
|
||||
## 运维
|
||||
## 运行维护
|
||||
|
||||
```bash
|
||||
# 实例升级
|
||||
# 同一大版本内升级发布镜像
|
||||
docker compose -f deploy/compose.yaml pull codex-proxy-rs
|
||||
docker compose -f deploy/compose.yaml up -d --no-build
|
||||
|
||||
# 日志
|
||||
# 查看日志
|
||||
docker compose -f deploy/compose.yaml logs -f codex-proxy-rs
|
||||
|
||||
# 从源码构建
|
||||
# 从当前源码构建
|
||||
docker compose -f deploy/compose.yaml build codex-proxy-rs
|
||||
docker compose -f deploy/compose.yaml up -d
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> `.runtime/` 保存 PostgreSQL、Redis、日志、OpenAI 会话锚点密钥和自更新状态。删除该目录会永久清除
|
||||
> 运行状态。
|
||||
> `.runtime/` 保存 PostgreSQL、Redis、日志、OpenAI 会话锚点和在线更新状态。删除该目录会永久清除
|
||||
> 本地运行状态。
|
||||
|
||||
> [!WARNING]
|
||||
> 不支持跨大版本在线升级。升级到新的大版本时,请使用全新的 `.runtime/` 数据目录重新部署,
|
||||
> 并重新导入或重新授权 Provider 账号与客户端 Key。
|
||||
> 不支持跨大版本在线升级。升级到新的大版本时请使用全新的 `.runtime/` 数据目录,并重新导入或授权
|
||||
> Provider 账号与 Client Key。
|
||||
|
||||
Compose 默认只绑定 `127.0.0.1`。公网接入应使用 HTTPS 反向代理,转发 WebSocket
|
||||
upgrade 与真实客户端 IP;不要暴露 PostgreSQL 或 Redis。
|
||||
Compose 默认只绑定 `127.0.0.1`。公网接入应使用 HTTPS 反向代理,转发 WebSocket upgrade 与真实客户端
|
||||
IP;不要暴露 PostgreSQL 或 Redis。当前运行拓扑为单副本,不能通过复制应用容器扩容。
|
||||
|
||||
## 维护与发布
|
||||
|
||||
发布仓库版本必须从干净、已同步上游的发布分支运行:
|
||||
仓库版本只能从干净、已同步上游的发布分支发布:
|
||||
|
||||
```bash
|
||||
release/publish <version>
|
||||
```
|
||||
|
||||
脚本依赖已登录的 GitHub CLI(`gh auth login`)。它会更新 `release/version.yaml`、创建约定提交与带
|
||||
注释的 `v<version>` tag,原子推送分支和 tag,再从发布分支触发 Release 构建,使构建缓存可以跨版本
|
||||
复用。不要手工修改版本后单独打 tag。该脚本不会登录服务器,也不会改变任何运行实例;源码提交、
|
||||
仓库发版和实例升级是三个独立状态。
|
||||
脚本依赖已登录的 GitHub CLI(`gh auth login`),会更新 `release/version.yaml`、创建约定提交和带注释的
|
||||
`v<version>` tag,原子推送分支与 tag,再触发 Release 构建。源码提交、仓库发版和运行实例升级是三个
|
||||
独立状态;脚本不会登录服务器或改变任何运行实例。
|
||||
|
||||
## 文档
|
||||
|
||||
- [部署](deploy/README.md)
|
||||
- [部署与运维](deploy/README.md)
|
||||
- [客户端与管理端接口](docs/api.md)
|
||||
- [架构与数据边界](docs/architecture.md)
|
||||
- [系统架构](docs/architecture.md)
|
||||
- [数据库迁移规则](backend/migrations/README.md)
|
||||
- [Release](https://github.com/zyycn/codex-proxy-rs/releases)
|
||||
- [容器镜像](https://github.com/zyycn/codex-proxy-rs/pkgs/container/codex-proxy-rs)
|
||||
|
||||
@@ -119,11 +119,11 @@ pub struct RetentionDecision {
|
||||
|
||||
/// 从按完成时间倒序的 completed 计划备份中决定纳入删除流程的记录。
|
||||
///
|
||||
/// 规则(`docs/s3-backup-design-audit.md` §4.4):
|
||||
/// 自动保留规则:
|
||||
/// - `retentionDays = 0` 时不按天数清理;`retentionCount = 0` 时不按份数清理。
|
||||
/// - 自动计划备份满足任一启用阈值即可进入删除。
|
||||
/// - 无论阈值如何,至少保留最近一个成功的计划备份(倒序下标 0)。
|
||||
/// - 手动备份不受自动阈值影响;无 TTL,仅由管理员手动删除。
|
||||
/// - 手动备份不进入本规则;手工/计划记录各自的 `expires_at` 由到期扫描处理。
|
||||
#[must_use]
|
||||
pub fn decide_retention(
|
||||
retention_days: u32,
|
||||
|
||||
@@ -288,7 +288,7 @@ pub struct BackupRecord {
|
||||
pub error_message: Option<String>,
|
||||
pub started_at: Option<DateTime<Utc>>,
|
||||
pub completed_at: Option<DateTime<Utc>>,
|
||||
/// 手动过期时间;创建时确定,到期自动清理。
|
||||
/// 创建时确定的过期时间;手工和计划备份均可设置,到期自动清理。
|
||||
pub expires_at: Option<DateTime<Utc>>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Admin API 的展示层辅助:数值格式化。
|
||||
//!
|
||||
//! 按 docs/project-redundancy-boundary-audit.md BE-02 的目标,display 字段最终
|
||||
//! 迁移到前端;过渡期统一由本模块提供,避免 accounts/observability 各自维护。
|
||||
//! 当前 wire 仍保留 display 字段;统一由本模块提供,避免 accounts/observability
|
||||
//! 各自维护格式化规则。
|
||||
|
||||
use gateway_core::accounting::Decimal;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! 多 Provider 网关的 PostgreSQL 持久化与 Redis 协调 adapter。
|
||||
//!
|
||||
//! 业务规则与 port 由 `gateway-core` 拥有。本 crate 只负责把终态的七张业务表
|
||||
//! 业务规则与 port 由 `gateway-core` / `gateway-admin` 拥有。本 crate 只负责把当前十二张业务表
|
||||
//! 和可丢失 Redis 状态映射为明确的基础设施操作。
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! 九张终态业务表的 PostgreSQL adapters。
|
||||
//! 当前十二张业务表的 PostgreSQL adapters。
|
||||
|
||||
use async_trait::async_trait;
|
||||
use sqlx::{PgPool, postgres::PgPoolOptions};
|
||||
|
||||
@@ -4,12 +4,15 @@ sqlx 在服务启动时(`serve` 监听之前)按编号顺序执行本目录
|
||||
文件的 checksum 记入 `_sqlx_migrations`。此后每次启动都会重新校验:**已应用
|
||||
迁移的文件字节与记录不一致时,服务直接拒绝启动**。
|
||||
|
||||
当前大版本只有两份冻结迁移:`0001_initial.sql` 建立十二张业务表的完整新库基线,
|
||||
`0002_nullable_requested_model.sql` 允许 Images 等无模型端点把请求模型保存为 `NULL`。
|
||||
|
||||
## 冻结规则
|
||||
|
||||
- 同一大版本内,已合入 main 的迁移文件**永久冻结**,一个字节都不能改——包括注释和
|
||||
空白。任何改动都会让该大版本的已部署实例在下次重启时因 checksum 失配而无法启动,
|
||||
且没有自动恢复路径。
|
||||
- 同一大版本内的 schema 变更一律新增编号迁移(`0002_...`、`0003_...`),永远不回改
|
||||
- 同一大版本内的 schema 变更一律新增编号迁移(下一份从 `0003_...` 开始),永远不回改
|
||||
旧文件。切换大版本时允许把已完成升级的 schema 折叠成新的 `0001` 基线,因为跨大版本
|
||||
不支持在线升级,目标版本必须使用全新数据目录部署。
|
||||
- `.frozen-sha256` 是冻结清单(`sha256sum` 格式)。CI 会校验:清单内文件
|
||||
|
||||
+8
-3
@@ -112,6 +112,9 @@ PostgreSQL 是账号、Client Key、运行设置、请求记录与审计的权
|
||||
Provider schema 以明文 JSON 保存于 PostgreSQL。Redis 只保存可重建、可过期的协调状态,例如
|
||||
会话亲和、lease、cooldown、OAuth pending flow 与套餐模型目录 cache。
|
||||
|
||||
OpenAI 主动额度重置卡及其消费结果由上游持有,不写入 PostgreSQL/Redis,也不属于本地备份内容;
|
||||
管理端只在当前浏览器会话中保留最近一次查询结果。
|
||||
|
||||
备份必须包含 `.runtime/postgres`。要保留 OAuth 的 AT/RT 恢复能力,必须保持
|
||||
`host.logging.file.enabled: true` 并备份 `.runtime/logs`;恢复记录位于独立文件集,仍按普通日志的
|
||||
`retention_days` 和 `max_files` 分别约束。若希望保留短期 Redis 状态和会话锚点,也同时备份 `.runtime/redis` 与
|
||||
@@ -205,8 +208,8 @@ Release 必须提供当前 OS/架构的 `codex-proxy-rs_<version>_<os>_<arch>.ta
|
||||
- 备份暂存目录为 `host.runtime_data_dir/backup-staging`;Compose 默认对应
|
||||
`/app/.runtime/data/backup-staging`,由 `.runtime/data` 卷持久化,权限 `0700`(仅 `cpr`
|
||||
用户可读写)。部署卷至少预留一个最大数据库归档的空间。
|
||||
- OAuth 恢复记录通道使用 `oauth_recovery` 结构化日志事件;当前 OpenAI provider 每次成功取得
|
||||
AT/RT 后,都会在账号资料补全、过期时间计算和数据库写入前写入独立的
|
||||
- OAuth 恢复记录通道使用 `oauth_recovery` 结构化日志事件;当前 OpenAI Provider 每次成功取得
|
||||
AT(以及存在时的 RT)后,都会在账号资料补全、过期时间计算和数据库写入前写入独立的
|
||||
`codex-proxy-rs-oath.YYYY-MM-DD[.N].log` 文件集。事件含原始 AT/RT,并以 `provider`
|
||||
字段标记来源,且与普通 `.runtime/logs` 完全相同地按日、按大小分割,并分别按相同 `retention_days` 与 `max_files`
|
||||
清理;文件日志开启时不会被普通日志级别筛掉。它遵循普通日志现有的非阻塞写入机制,不会阻断
|
||||
@@ -214,6 +217,8 @@ Release 必须提供当前 OS/架构的 `codex-proxy-rs_<version>_<os>_<arch>.ta
|
||||
`.runtime/logs` 因此属于敏感数据,必须按现有普通日志的访问控制和加密备份策略处理。
|
||||
- S3/R2 存储、Cron 计划、保留策略与备份记录都保存在 PostgreSQL(`backup_settings` /
|
||||
`backup_records`),备份记录行在删除成功后硬删除,操作历史进入 `admin_audit_events`。
|
||||
- 手工备份的 `expiresInDays` 在创建时生成独立 `expires_at`;计划备份按当前 `retentionDays` 生成
|
||||
`expires_at`,并同时受 `retentionCount` 清理规则约束。到期只进入删除流程,不构成在线恢复点。
|
||||
|
||||
### 人工恢复数据库
|
||||
|
||||
@@ -224,7 +229,7 @@ Release 必须提供当前 OS/架构的 `codex-proxy-rs_<version>_<os>_<arch>.ta
|
||||
pg_restore --no-owner --no-privileges --dbname=<target> <archive>.dump
|
||||
```
|
||||
|
||||
恢复期间与恢复后的边界(见 `docs/s3-backup-design-audit.md` §8):
|
||||
恢复期间与恢复后的边界见 [架构文档](../docs/architecture.md#11-生命周期安全与恢复)。操作顺序为:
|
||||
|
||||
1. 恢复期间以维护模式启动,不运行备份 Worker(避免把快照中的中间状态当作当前任务继续处理)。
|
||||
2. 恢复完成后清理备份控制记录的非终态(`queued/dumping/uploading/deleting`),并重新计算
|
||||
|
||||
+75
-9
@@ -47,7 +47,9 @@ Client Key 通过账号分组限定路由范围:未绑定分组时可使用全
|
||||
|
||||
### 管理写入一致性
|
||||
|
||||
Admin API 不暴露全局配置版本,mutation 请求也不要求客户端提供全局配置版本。会改变路由快照或安全配置的写入由后端在事务内推进内部配置 revision,并用于快照发布与审计。
|
||||
mutation 请求不要求客户端提供全局配置版本。会改变路由快照或安全配置的写入由后端在事务内推进
|
||||
内部 `config_revision`,并用于快照发布与审计。账号更新和分组查询/写入的部分响应会返回
|
||||
`configRevision` 作为已提交事实,但它不是客户端 mutation 的前置条件。
|
||||
|
||||
## 2. 健康检查
|
||||
|
||||
@@ -64,8 +66,8 @@ Responses 和 Images HTTP body、WebSocket message 和 frame 不设置网关私
|
||||
| `POST` | `/v1/responses` | OpenAI Responses JSON;`stream=true` 返回 SSE,否则返回完整 JSON |
|
||||
| `GET` | `/v1/responses` | 通过 HTTP Upgrade 建立 Responses WebSocket |
|
||||
| `POST` | `/v1/responses/review` | 使用同一 Responses 合同发起 review 子代理请求 |
|
||||
| `POST` | `/v1/images/generations` | 按当前 Codex Provider 发起图像生成;JSON 请求与响应正文原样转发 |
|
||||
| `POST` | `/v1/images/edits` | 按当前 Codex Provider 发起图像编辑;JSON 请求与响应正文原样转发 |
|
||||
| `POST` | `/v1/images/generations` | 通过 OpenAI Provider 发起图像生成;JSON 请求与响应正文原样转发 |
|
||||
| `POST` | `/v1/images/edits` | 通过 OpenAI Provider 发起图像编辑;JSON 请求与响应正文原样转发 |
|
||||
| `GET` | `/v1/models` | 返回当前 Client Key 账号范围内各 Provider 的可用公开模型并集;有两种响应形态,见下 |
|
||||
| `GET` | `/v1/models/catalog` | 返回 Codex 客户端使用的模型目录 |
|
||||
| `GET` | `/v1/models/{model_id}/info` | 返回 Codex 客户端使用的单模型信息 |
|
||||
@@ -77,9 +79,9 @@ Responses 和 Images HTTP body、WebSocket message 和 frame 不设置网关私
|
||||
OpenAI 路径保留客户端 Responses wire 语义:请求 body 的未知字段和字段顺序保持不变(受控模型
|
||||
映射除外),HTTP SSE 与 WebSocket 的上游业务事件字节原样转发,response ID 按 opaque 值处理而不
|
||||
假设 UUID 或固定长度;OpenAI 上游错误 envelope 和允许下发的 opaque header 值也不由 canonical
|
||||
观测结果重写。Images 请求不读取或重建 JSON,只在原始字节之外完成账号选择、鉴权头替换和端点路由;
|
||||
成功与失败响应正文同样保持原始字节。xAI 是 Grok wire 与 Responses wire 之间的协议转换层,转换只在
|
||||
xAI Provider 内完成。
|
||||
观测结果重写。Images 请求不读取或重建 JSON,也不要求或映射模型字段;它固定使用 OpenAI Provider,
|
||||
只在原始字节之外完成账号选择、鉴权头替换和端点路由,成功与失败响应正文同样保持原始字节。xAI 是
|
||||
Grok wire 与 Responses wire 之间的协议转换层,转换只在 xAI Provider 内完成。
|
||||
上游结构化错误的 message/code/type 会透传给客户端,其中内嵌的账号指纹 UUID 已脱敏。模型映射是
|
||||
全局精确映射,未命中时模型名原样交给候选 Provider;分组只限定账号集合,不参与模型改名。
|
||||
|
||||
@@ -103,12 +105,15 @@ xAI Provider 内完成。
|
||||
| `GET` | `/api/admin/accounts/export` | `accountIds`、`confirm=export_sensitive_accounts` | 显式导出最多 200 个账号的敏感 Provider 文档 |
|
||||
| `POST` | `/api/admin/accounts/import` | `{ provider, data }` | 导入或按上游身份更新账号;新账号保持未分组,已有账号保留所属分组 |
|
||||
| `POST` | `/api/admin/accounts/refresh` | `{ accountId }` | 手工刷新 OAuth credential(`idToken` / `accessToken` / `refreshToken`),不刷新额度 |
|
||||
| `POST` | `/api/admin/accounts/recover` | `{ accountId }` | 管理员显式清除该账号的本地错误/额度/cooldown 事实并重新启用,不访问上游 |
|
||||
| `POST` | `/api/admin/accounts/rotate` | OpenAI rotation 字段 | 手工替换 OpenAI OAuth token |
|
||||
| `POST` | `/api/admin/accounts/update` | `{ accountId, enabled, concurrencyLimit, weight, groupIds }` | 一次更新账号调度状态、并发上限(`null` 表示继承运行参数)、权重(1–100)与所属分组 |
|
||||
| `POST` | `/api/admin/accounts/batch-update` | `{ accountIds, enabled, concurrencyLimit, weight, groupIds }` | 一次事务统一更新所选账号的全部调度字段与完整分组集合 |
|
||||
| `POST` | `/api/admin/accounts/delete` | `{ provider, accountIds }` | 批量删除 1–200 个账号 |
|
||||
| `GET` | `/api/admin/accounts/quota` | `accountId` | 读取当前额度,不强制访问上游 |
|
||||
| `POST` | `/api/admin/accounts/quota/refresh` | `{ accountId }` | 访问 Provider 并刷新额度,同时同步额度所属状态 |
|
||||
| `GET` | `/api/admin/accounts/reset-credits` | `accountId` | 查询 OpenAI 上游主动额度重置卡,不读取本地库存 |
|
||||
| `POST` | `/api/admin/accounts/reset-credits` | `{ accountId, creditId?, redeemRequestId }` | 使用 UUIDv4 幂等键消费一张 OpenAI 上游重置卡 |
|
||||
| `GET` | `/api/admin/accounts/models` | `accountId` | 优先读取该 Provider + 套餐的模型 cache,缺失时有限实时拉取 |
|
||||
| `POST` | `/api/admin/accounts/models/refresh` | `{ accountId }` | 强制拉取最新模型并覆盖 cache |
|
||||
| `GET` | `/api/admin/accounts/connection-test` | `accountId`、`modelId` | 通过 SSE 返回实时连接测试事件,不作为业务 Responses 用量记录 |
|
||||
@@ -123,14 +128,34 @@ xAI Provider 内完成。
|
||||
- `sortBy`: `email`、`status`、`planType`、`usage`、`lastUsedAt`、`expiresAt`;
|
||||
- `sortDirection`: `asc`、`desc`。
|
||||
|
||||
导入的 `data` 必须是 JSON object,内部 schema 由目标 Provider 独占解释:
|
||||
导入的 `data` 必须是 JSON object,Admin API 请求上限为 64 MiB;Provider 可以收紧限制,
|
||||
当前 xAI 导入上限为 16 MiB。内部 schema 由目标 Provider 独占解释:
|
||||
|
||||
- OpenAI 接受 OAuth 账号文档、账号 bundle 和 Agent Identity;
|
||||
- OpenAI 接受单账号 OAuth 文档、`accounts` 数组(最多 200 项)、CPR 账号 bundle 和 Agent Identity;
|
||||
- OpenAI OAuth token 字段只识别 `accessToken`、`refreshToken`、`idToken`,可以嵌套在账号 object 内;
|
||||
每项至少包含 AT 或 RT。RT-only 会在导入时换取 AT,AT-only 不具备自动续期能力;
|
||||
- xAI 从单账号 object 或 `accounts` 数组中提取 OAuth token;包装中的代理、并发、优先级等字段不参与认证;
|
||||
- xAI 批量导入逐条独立校验:失败条目跳过并记录日志,不中断其余条目,仅当没有任何条目成功时整个导入才报错;
|
||||
- xAI API Key 不是受支持的账号 credential;
|
||||
- 导入不会只凭文件外形写入账号;目标 Provider 使用认证材料完成必要的 token exchange 或已认证账号资料补全。
|
||||
|
||||
管理端的 OpenAI `AT` / `RT` 标签是同一导入 API 的输入便利层:每行一个 token,最多 200 行,提交前
|
||||
转换为对应的 `accounts` JSON。Admin API 本身不接收纯文本 token 列表。例如:
|
||||
|
||||
```json
|
||||
{
|
||||
"provider": "openai",
|
||||
"data": {
|
||||
"accounts": [
|
||||
{ "accessToken": "eyJ..." },
|
||||
{ "accessToken": "eyJ...", "refreshToken": "rt_...", "idToken": "eyJ..." }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
RT-only 使用同一形状,只提交 `refreshToken`。不得把真实 token 写入日志、issue、fixture 或文档。
|
||||
|
||||
账号导入与 OAuth complete 不接收 `groupIds`。首次创建的账号保持未分组;按既有上游身份重新导入、
|
||||
重新授权以及普通 credential refresh/rotation 均保留已有分组。分组关系只通过账号编辑维护。
|
||||
账号列表的每个 item 返回轻量 `groups: [{ id, name, enabled }]`。
|
||||
@@ -200,6 +225,9 @@ OAuth start 使用:
|
||||
但上游 authorization code 本身通常只能交换一次;已完成过 token exchange 时应重新创建 OAuth flow。
|
||||
- `GET /accounts/quota` 只读取最后一次落库快照;`POST /accounts/quota/refresh` 才访问上游。access token
|
||||
已过期时,额度刷新要求先走 credential 刷新或重新授权,不会拿过期 token 探测额度。
|
||||
- `POST /accounts/recover` 是管理员对本地事实的强制恢复:它清除 Redis cooldown 和已保存的额度/错误,
|
||||
把账号重新启用并恢复为可调度 credential;它不验证上游账号是否已经恢复,下一次真实请求仍可重新写入
|
||||
失败事实。
|
||||
- 成功额度观测会 revision-fenced 写入 quota;明确 `Allowed` 投影为 `normal`,明确耗尽投影为
|
||||
`quota_exhausted`。额度观测不会清除凭据过期、无效或封禁事实;这些事实统一投影为 `error`,并由
|
||||
`errorReason` 区分。额度接口的 401/403 也不足以判定 refresh token 永久失效,credential 终态只由
|
||||
@@ -214,6 +242,43 @@ OAuth start 使用:
|
||||
新状态不符合当前筛选,该行从当前页移除。请求驱动或后台任务产生的状态变化,需要下一次显式查询账号
|
||||
列表后才会显示。
|
||||
|
||||
### OpenAI 主动额度重置卡
|
||||
|
||||
`GET /api/admin/accounts/reset-credits?accountId=...` 每次都查询 OpenAI 上游;后端不把卡片列表写入
|
||||
PostgreSQL 或 Redis。管理端只在用户打开弹窗或点击刷新时调用,并在当前浏览器会话内缓存最近一次成功
|
||||
结果,用于账号行上的 `xN` 提示。
|
||||
|
||||
查询响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"availableCount": 1,
|
||||
"credits": [{
|
||||
"id": "credit_...",
|
||||
"status": "available",
|
||||
"title": "...",
|
||||
"expiresAt": "2026-08-31T12:00:00Z",
|
||||
"resetType": "..."
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
消费请求的 `redeemRequestId` 必须是小写、带连字符的 canonical UUIDv4;`creditId` 可省略,由上游选择
|
||||
可用卡。一次请求发出后若传输结果不明确,重试必须复用完全相同的 `redeemRequestId`、`creditId` 和
|
||||
账号。服务在单副本进程内按账号串行消费,并在 credential 需要刷新时以同一命令重试一次;它不会对不明
|
||||
结果自动创建新消费。
|
||||
|
||||
```json
|
||||
{
|
||||
"accountId": "acct_...",
|
||||
"creditId": "credit_...",
|
||||
"redeemRequestId": "8fbf302d-11df-4bd5-82e4-08e4b3df7874"
|
||||
}
|
||||
```
|
||||
|
||||
消费响应只返回上游结果 `code` 和可选 `credit`。消费端确认成功后应重新 GET 卡片列表,并显式调用
|
||||
`POST /api/admin/accounts/quota/refresh` 回读官方额度;不得直接改写本地 `resetAt`。xAI 不支持该能力。
|
||||
|
||||
## 6. 账号分组
|
||||
|
||||
分组是 Provider-neutral 的账号集合;一个组可包含任意 Provider 账号,一个账号也可属于多个组。
|
||||
@@ -327,7 +392,8 @@ id, triggerKind, status, scheduledAt, objectKey, sizeBytes, sha256, attemptCount
|
||||
errorCode, errorMessage, startedAt, completedAt, expiresAt, createdAt, updatedAt
|
||||
```
|
||||
|
||||
`expiresAt` 为创建时确定的手动过期时间;到期后由 Worker 自动清理(手动与计划备份均可设置)。
|
||||
`expiresAt` 在创建时确定:手动备份来自 `expiresInDays`,计划备份来自当时的
|
||||
`retentionDays`;到期后由 Worker 进入删除流程。
|
||||
|
||||
连接测试响应:
|
||||
|
||||
|
||||
+208
-255
@@ -1,306 +1,259 @@
|
||||
# Codex Proxy RS 架构
|
||||
|
||||
本文是仓库唯一架构文档,只描述当前架构。
|
||||
本文回答系统由哪些边界组成、请求如何流动、状态由谁拥有,以及修改时必须保持哪些不变量。
|
||||
具体 HTTP 字段见 [接口文档](api.md),部署参数见 [部署文档](../deploy/README.md);上游 URL、超时、
|
||||
重试间隔和 UI 布局属于源码或配置,不在架构文档重复维护。
|
||||
|
||||
## 1. 系统边界
|
||||
## 1. 系统定位
|
||||
|
||||
Codex Proxy RS 是单进程、单副本部署的多 Provider AI 网关:
|
||||
Codex Proxy RS 是单进程、单副本运行的多 Provider AI 网关,同时提供:
|
||||
|
||||
- 客户端面提供 OpenAI Responses 兼容的 JSON、SSE、WebSocket 与模型目录协议。
|
||||
- 管理面提供 `/api/admin/*` 和 Vue 静态管理端。
|
||||
- 当前 Provider 为 OpenAI 与 xAI;账号 credential 均由所属 Provider 独占解释。
|
||||
- OpenAI 行为以重构前正式实现为语义基准;xAI/Grok 行为以 `grok2api` 验证结果为基准。
|
||||
- 两个 Provider 的请求画像都由启动配置提供基线,并通过进程级共享状态向每次新请求发布一致快照。OpenAI Desktop 与 xAI CLI 的官方版本检查会分别原子更新自己负责的版本字段。
|
||||
- OpenAI Desktop 使用官方 appcast,并从同一 ZIP 的内嵌 Core 做有界 Range 探测;xAI CLI 使用官方 npm 包 `@xai-official/grok`。检查失败保留上一份成功画像;检查成功后 OAuth、catalog、quota/billing、inference 与 Dashboard 自动采用最新版本。OpenAI 制品画像写入 Redis 单键并使用 TTL,避免按发布次数堆积。
|
||||
- PostgreSQL 是业务与配置事实的唯一权威存储。
|
||||
- Redis 只保存可丢失、可重建或有自然过期时间的协调状态。
|
||||
- 面向客户端的 OpenAI Responses、Images 和模型目录协议;
|
||||
- 面向管理员的 `/api/admin/*` 控制面和 Vue 管理端;
|
||||
- OpenAI 与 xAI 两个编译期 Provider;
|
||||
- PostgreSQL 持久化、Redis 协调状态以及 S3/R2 数据库备份。
|
||||
|
||||
部署假定:单副本。带 lease 的周期任务只做单周期互斥而非跨周期 leader 选举,N 副本会让同一任务最坏以 N 倍频率执行(见第 10 节);自更新只替换并重启处理该请求的那个副本,其余副本停留在旧版本。不要以多副本方式运行本网关。
|
||||
系统不提供 `/v1/chat/completions`,不存在 Provider Instance 层,也不支持通过复制应用容器进行多副本
|
||||
扩容。Client Key 限定账号分组,而不是绑定某个 Provider;一次请求的 Provider 候选由账号范围、模型
|
||||
能力和运行时健康共同决定。
|
||||
|
||||
Gateway Engine 不识别具体 Provider 协议;Provider 不拥有客户端 admission、跨 Provider 路由或业务重试预算。
|
||||
## 2. 运行拓扑
|
||||
|
||||
## 2. Workspace 与依赖方向
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Client[API Client] --> API[gateway-api]
|
||||
Browser[Vue Admin] --> API
|
||||
|
||||
```text
|
||||
backend/
|
||||
├── apps/gateway/ composition root(Bundle 装配)
|
||||
├── crates/gateway-core/ operation、routing、engine、policy、accounting
|
||||
├── crates/gateway-protocol/ 可共享 wire contract 与 canonical event
|
||||
├── crates/gateway-admin/ 管理领域、用例、抽象端口与备份 Worker 策略
|
||||
├── crates/gateway-store/ PostgreSQL、Redis、S3/R2 与 pg_dump adapter
|
||||
├── crates/gateway-api/ OpenAI Responses 与 Admin HTTP adapter
|
||||
├── crates/gateway-host/ 配置与日志、HTTP serve/drain、worker 运行时、自更新
|
||||
├── crates/providers/openai/ OpenAI credential、catalog、transport
|
||||
├── crates/providers/xai/ xAI/Grok credential、catalog、transport
|
||||
└── migrations/ 编号迁移与冻结清单 .frozen-sha256
|
||||
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,不自行定位别的实现。
|
||||
|
||||
- `gateway-core` 不依赖 HTTP、数据库、Redis 或具体 Provider。
|
||||
- `gateway-protocol` 不依赖其他 workspace crate。
|
||||
- Provider crate 之间禁止互相依赖。
|
||||
- `gateway-api` 只面向 Admin/Core/Protocol 抽象,不导入具体 Provider。
|
||||
- `gateway-store` 实现 Admin/Core 端口与备份基础设施(S3/R2、`pg_dump`、受控暂存),但不拥有业务策略。
|
||||
- 只有 `apps/gateway` 组合具体实现。
|
||||
## 3. Workspace 边界
|
||||
|
||||
依赖 DAG 主要由上述约定与评审维护,机器校验只覆盖两处:`gateway-api` 的 architecture 测试冻结自身源码树与测试树,并禁止 manifest 出现 store/Provider/基础设施依赖(禁止清单不含 `gateway-host`);`apps/gateway` 的 architecture 测试冻结应用源码树、禁止生产测试挂载并约束 bootstrap 只做装配。其余 crate 没有 architecture 测试。
|
||||
| 路径 | 责任 |
|
||||
| --- | --- |
|
||||
| `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/Agent Identity、账号选择、目录、额度、Responses/Images transport |
|
||||
| `providers/xai` | xAI OAuth session、账号选择、目录、额度和 Grok/Responses 转换 |
|
||||
| `frontend` | Vue 管理端,仅通过 Admin API 读写状态 |
|
||||
|
||||
## 3. 请求执行链
|
||||
依赖方向遵守四条规则:
|
||||
|
||||
```text
|
||||
API decode/auth
|
||||
-> freeze RuntimeSnapshot
|
||||
-> compile RoutePlan
|
||||
-> admission
|
||||
-> enqueue model_request observation
|
||||
-> select Provider/account
|
||||
-> enqueue attempt/send observations
|
||||
-> cross upstream send barrier
|
||||
-> canonical stream
|
||||
-> cross in-memory downstream delivery barrier and enqueue observation
|
||||
-> enqueue terminal observation and accounting
|
||||
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 候选顺序和调度策略。
|
||||
运行中的请求不再拼接新旧配置,也不在热路径查询分组关系。
|
||||
|
||||
核心不变量:
|
||||
|
||||
1. 每个客户端请求只有一条 `model_requests`;attempt 明细保存在其 JSON 事实中,不另建 attempt 表。
|
||||
2. 任何可能到达上游的调用都必须先按序提交 request/attempt 观测到有界队列;PostgreSQL 投影是可恢复
|
||||
观测,不是数据面发送的同步前置条件。3. `not_sent`、`sent`、`ambiguous` 是不同的上游发送边界;发送状态是请求级单调水位(`sent` > `ambiguous` > `not_sent`),跨 attempt 只升不降,终态写回不得低于水位;`ambiguous` 不自动重放。
|
||||
4. 内存中的 downstream commit 表示网关已作出不可撤回的交付承诺,从该时刻禁止 retry/fallback;
|
||||
它在实际首字节写出前越过并按序入队。`downstream_committed_at` 是该事实的 PostgreSQL 观测投影,
|
||||
不宣称字节已经到达客户端,也不反向决定协议交付。
|
||||
5. Provider 每次 `execute` 只能选择一个 credential 并准备一个 cold stream,不得隐藏换号或业务 retry。
|
||||
6. 下游 commit 前,Core 先在当前 Provider 内按冻结策略换账号;只有调用仍处于 `not_sent`、错误属于
|
||||
可安全推进的资源/基础设施类别且 operation 允许重放时,才推进到路由计划中的下一个 Provider。
|
||||
7. 本架构不通过隐式连接复用承载业务身份。HTTP client 可安全复用 transport 连接,但账号、credential revision、cookie/session binding 必须显式绑定到本次调用。
|
||||
- 一个客户端请求对应一条 `model_requests`;attempt 是请求内事实,不建立第二张权威表。
|
||||
- Provider 的一次 `execute` 只选择一个 credential 并返回一个冷流;换号、重试和 fallback 由 Core 决定。
|
||||
- `not_sent`、`sent`、`ambiguous` 是单调的上游发送边界;结果不明确时不能假定上游未收到请求。
|
||||
- downstream commit 是不可撤回的交付承诺。commit 后禁止换号、重试和 fallback。
|
||||
- 跨 Provider 只在账号范围和能力都允许,且请求尚未到达上游或已被证明可安全重放时发生。
|
||||
- 可恢复观测写入失败不能替换已经确定的客户端协议结果。
|
||||
|
||||
管理端 Usage 详情中的 attempt 列表是 best-effort 观测(见 adr-002):中间失败来自 `ops_events`,最终尝试由 `model_requests` 合成,二者都不承诺完整(队列丢弃、写入失败、非失败观测不落库会造成缺口)。API 通过 `attemptsComplete: false` 显式标注这一语义,不再声称「全部尝试」。
|
||||
Responses 按模型目录编译候选;全局模型映射是精确映射,未命中时模型名原样交给候选 Provider。
|
||||
Images 是 OpenAI Provider 自有端点:它不要求模型字段、不参与文本模型映射,只在 Client Key 的账号范围
|
||||
确实包含 OpenAI 账号时生成单一 OpenAI 候选。
|
||||
|
||||
## 4. 路由、fallback 与错误处理
|
||||
## 5. Provider 与协议边界
|
||||
|
||||
RuntimeSnapshot 冻结 Provider 集合、模型能力、全部账号及 membership 索引、每个 Client Key 的账号
|
||||
范围、运行时策略和全局 `config_revision`。Key 零分组关联表示 `AllAccounts`;存在关联时表示
|
||||
`Restricted`,只允许已启用绑定组成员的并集。已绑定组全部禁用或为空会得到空池,绝不回退到全部账号。
|
||||
一次请求始终使用认证时取得的同一账号范围和快照,不在执行中拼接新旧配置,热路径也不查询 membership。
|
||||
账号调度事实包含可空的并发上限覆盖与 1–100 的权重;并发上限为空时继承运行参数,权重默认 1。
|
||||
管理员单账号或批量编辑在同一事务中替换启停、并发上限、权重和完整分组集合;credential 导入、
|
||||
重新授权与后台刷新不覆盖已有调度事实。
|
||||
Core 只理解 `Operation`、能力要求、Provider 候选、稳定错误和 canonical event,不读取 Provider SDK
|
||||
类型。Provider 独占 credential schema、OAuth、账号选择、模型目录、额度投影和上游 transport。
|
||||
|
||||
模型先经过全局精确映射,再按账号范围内实际存在的 Provider 及其明确模型/operation 能力编译有序候选。
|
||||
普通请求和 `ReplayAny` 只有在 Provider 尚未发出请求,且返回无可用账号、账号容量不足或 Provider
|
||||
基础设施不可用时,才可推进到下一 Provider;已建立流后的重试先在当前 Provider 内换账号。任何发送结果
|
||||
不明确或 downstream 已 commit 的请求都不跨目标重放。
|
||||
- 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`。
|
||||
|
||||
明确的上游认证、封禁、额度或 cooldown 错误会更新对应账号状态,使下一次选择排除该账号;满足重放安全条件且尚未 downstream commit 时,可以换号。传输结果不明确时不得假定请求未到达上游。
|
||||
## 6. 路由、账号范围与 continuation
|
||||
|
||||
重试矩阵属于独立策略,OpenAI 保持原正式行为、xAI 保持参考行为,不在 Provider transport 内增加额外重试。
|
||||
Client Key 与账号分组形成授权范围:
|
||||
|
||||
## 5. Continuation
|
||||
- 没有分组关联表示 `AllAccounts`;
|
||||
- 有关联时只允许已启用分组成员的并集;
|
||||
- 已绑定分组为空或全部禁用时得到空池,绝不回退为全部账号;
|
||||
- 分组可以包含多个 Provider,账号也可以属于多个分组。
|
||||
|
||||
系统在 `model_requests.client_response_id` 与 `upstream_response_id` 中记录响应生命周期确认的 ID,
|
||||
两者都按 opaque UTF-8 bytes 保存,不做格式、长度或唯一性推断;不新增 conversation、transcript、
|
||||
continuation 或 claim 表。
|
||||
账号选择综合启停状态、credential/quota 事实、Redis cooldown、并发上限、权重、请求间隔和会话亲和。
|
||||
账号编辑在一个事务中替换完整调度事实;导入、重新授权和后台刷新不会覆盖已有分组、权重或并发设置。
|
||||
|
||||
- `store=true`:使用 Provider 持久化的 native handle。
|
||||
- `store=false`:opaque replay state 仅存在于活连接内,不落 PostgreSQL。
|
||||
- OpenAI continuation 顺序为 native、replay owner、replay any。`replay any` 是
|
||||
Provider 定义的恢复阶段,不代表 Core 已持有完整 input:OpenAI 在跨账号时保留
|
||||
canonical `previous_response_id` 与原始增量 input 做 continuation probe;上游返回
|
||||
`previous_response_not_found` 时原样交给官方客户端触发 full retry。只有确实持有
|
||||
可携带 transcript 的 Provider 才能清除 native handle 后在代理内部重放。
|
||||
- xAI 使用客户端提交的完整历史作为已验证 continuation 路径。
|
||||
- native continuation pin 同时绑定创建它的 Client Key、冻结账号范围、Provider 和账号;跨 Key 复用、
|
||||
scope 外账号或计划中不存在该 Provider 都 fail closed。`ReplayOwner` 同样固定原 Provider 和账号,
|
||||
不参与跨 Provider fallback。
|
||||
- continuation 失败仍受 send barrier、downstream commit 和 Provider kind 边界约束。
|
||||
Continuation 仍受原请求的 Client Key、账号范围、Provider 和发送/交付边界约束:
|
||||
|
||||
## 6. Provider 与 credential owner
|
||||
- native continuation 固定创建它的 Provider 与账号;
|
||||
- OpenAI 按 native → replay owner → replay any 推进,并保留官方 `previous_response_id` 语义;
|
||||
- xAI 使用客户端提交的完整历史作为重放输入;
|
||||
- scope 外账号、跨 Key 复用或不明确发送结果均 fail closed。
|
||||
|
||||
`Provider` 接收 canonical `Operation + ProviderCandidate + AttemptContext`,返回携带冻结 metadata 的 canonical cold stream。Registry 使用稳定 `ProviderKind` 查找实现。
|
||||
会话亲和是优先选择提示,不是硬账号绑定;native continuation 才携带不可跨越的 owner 约束。
|
||||
|
||||
Provider 独占以下职责:
|
||||
## 7. 控制面与 revision
|
||||
|
||||
- credential 文档的编码、解码与校验;
|
||||
- OAuth 登录、导入、刷新与轮换;
|
||||
- 账号选择所需的 Provider 私有事实;
|
||||
- catalog 查询与能力编译;
|
||||
- HTTP/WebSocket transport 和错误分类;
|
||||
- quota 投影与 Provider 私有观测字段。
|
||||
|
||||
客户端可见正文的立场按 Provider 不同:
|
||||
|
||||
- OpenAI 是逐字节透明代理:请求 body 的未知字段和字段顺序保持不变(受控模型映射除外),上游 wire 是客户端可见的事实来源,HTTP SSE 帧以原始字节原样下发,WebSocket 上游 JSON 文本原样下发;response ID 以 opaque bytes 存储,不假设 UUID 或固定长度。canonical facts 只从同一帧旁路解析,用于观测、亲和与计费。仅网关内部帧(rate limits、metadata)被就地消费,不向客户端转发。
|
||||
- xAI 是翻译层:把 Grok wire 解码后重新合成 Responses wire 事件,不承诺字节透明;上游错误按结构化 message/code/type 透出,message 中的 UUID 账号指纹替换为占位符。
|
||||
|
||||
credential 更新使用 `credential_revision` CAS。PostgreSQL 分别保存凭据事实与额度访问事实;带截止时间的
|
||||
429 cooldown 只保存在 Redis,是唯一的临时冷却运行时事实,不进入 Provider quota 展示模型。Store 把三类
|
||||
事实交给 Core 的唯一解析器,投影为 `normal`、`quota_exhausted`、`rate_limited`、`disabled` 或 `error`;
|
||||
投影为 `rate_limited` 时同时携带有效的 `rate_limited_until`,到期重算后该时间和状态一起消失。若没有其它
|
||||
阻塞事实,账号才恢复为 `normal`。成功推理或明确的成功额度观测可以写入 `Allowed`;本地时钟与不确定响应
|
||||
不能伪造恢复。推理请求的账号/model cooldown 只约束数据面调度,不参与 OAuth refresh;OpenAI 与 xAI 的
|
||||
OAuth refresh 请求状态不明确时只按 OAuth 自身的有界退避推进该 credential 的 `next_refresh_at`:前五次
|
||||
约为 5/15/45/135/300 秒,之后约每 10 分钟恢复;仍有效 Access Token 时不晚于其到期时刻,Access Token
|
||||
到期后仍按该节奏恢复,至“到期 + 24 小时”才终态化。自动 refresh 的瞬态失败(限流、5xx、超时、畸形
|
||||
响应)保留现有凭据并按指数退避推迟下一次刷新,不把账号标记为失效;上游明确的永久错误
|
||||
(`invalid_grant`、封禁)仍立即写入终态失效。
|
||||
|
||||
`refresh_token_expires_at` 不是公共 SQL 列或 Core 权威状态。xAI 可在 `provider_credentials_json` 内保存它作为 Provider 私有提示;真正失效以 refresh endpoint 返回的永久错误为准。
|
||||
|
||||
OpenAI OAuth 导入先取得 access token(仅有 refresh token 时先交换),再复用官方
|
||||
`token_data.rs::parse_chatgpt_jwt_claims` 的本地 JWT payload 解析逻辑:优先读取 `idToken`,字段缺失时
|
||||
由 `accessToken` 补齐。解析器只读取 `email`、`https://api.openai.com/profile` 与
|
||||
`https://api.openai.com/auth` 中的官方 claims,不调用 `whoami`,也不使用导入文档顶层的
|
||||
`userId/accountId` 补身份。两个令牌都没有用户身份 claims 时,账号以未验证错误状态入库。首次 OAuth
|
||||
返回与当前 Codex Desktop 一致的两层登录地址:内层使用 `auth.openai.com/oauth/authorize` 的
|
||||
官方参数顺序,外层使用 `https://chatgpt.com/codex/desktop-auth` 并开启
|
||||
`codex_streamlined_login`。`codex_app_version` 每次从共享 Desktop 画像快照读取;
|
||||
首次授权在 10 分钟 pending flow 中生成账号级 `installation_id`,换码成功后把同一个值写入 credential。
|
||||
`source_surface_stable_id` 与 `codex_origin_stable_id` 由该 ID 经过带版本域的 SHA-256 确定性派生,并映射为
|
||||
UUIDv4;重新授权读取既有 `installation_id`,因此得到相同的 surface ID。系统不额外持久化 surface ID,
|
||||
也不再从全局 HMAC 身份锚点派生;它不等同于 DeviceCheck attestation。为了对齐 macOS Desktop
|
||||
交给外部浏览器的最终 `chatgpt.com` URL,Provider 同样携带 `no_universal_links=1` 浏览器路由参数。
|
||||
服务端随后仍以 `state`、PKCE
|
||||
与官方 token exchange 成功后解析并保存同一 token set。重新授权的目标绑定
|
||||
只保存目标账号 ID,不接受或冻结客户端提供的 credential revision;complete 阶段重新读取目标账号及其当前 revision,并以当前 revision 做最终 CAS。
|
||||
重新授权和手工或后台 RT refresh 只轮换目标账号的 token,保留既有账号资料与 OAuth principal。
|
||||
|
||||
credential 与 quota 是两组独立事实,不是两套对外状态。主动 quota refresh 会拒绝过期 Access Token;
|
||||
quota 探测的 401/403 不具备判定 Refresh Token 永久失效的证据。成功主动观测和正常推理响应中的
|
||||
rate-limit header 观测都 revision-fenced 写入同一 quota;只有明确的访问证据才写 `Allowed` 或
|
||||
`Exhausted`。只有明确的 deactivated workspace 才能由额度链路写入 `Banned` 凭据事实,且额度观测不能
|
||||
清除 `Invalid`、`Expired`、`Banned`。这些凭据事实对外统一投影为 `error` 并携带稳定原因。Free、K12 等
|
||||
套餐复用该逻辑;`plan_type` 在此不参与额度分支,只作为 Provider 事实并用于隔离不同套餐的 catalog cache。
|
||||
账号展开用量不读取 retention 范围累计值,而是选择带完整边界且可归属到账号的代表性额度窗口,并按
|
||||
`[reset_at - window_seconds, reset_at)` 聚合请求、Token、模型和成本;额度重置边界变化后,同一查询随刷新
|
||||
结果重算。模型专属窗口不会错误复用账号总用量,边界不完整时也不伪造“当前窗口”统计。
|
||||
|
||||
## 7. 控制面与内部 revision
|
||||
|
||||
Admin API 不要求客户端提交配置 revision,也不向客户端暴露配置 revision。会改变调度快照或安全配置的写入,必须在同一 PostgreSQL 事务中:
|
||||
|
||||
- 执行业务 mutation;
|
||||
- 推进 `runtime_settings.config_revision`;
|
||||
- 写入脱敏 `admin_audit_events`。
|
||||
|
||||
推进全局 revision 的 mutation 包括 runtime settings、账号分组 CRUD、客户端 Key 及其分组
|
||||
替换、账号编辑、账号导入/创建/删除、管理员显式启停和管理员 credential rotation。上述分组关系
|
||||
写入与业务 mutation、单次 revision 推进、脱敏审计处于同一 PostgreSQL 事务;审计会明确记录
|
||||
`routing_scope: groups -> all` 等权限范围变化。
|
||||
|
||||
不推进全局 revision 的运行时观测包括 quota、cooldown、catalog generation、请求统计以及自动 credential refresh;自动 refresh 只推进 `credential_revision`。提交后 Redis 通知只负责缩短其他副本的收敛延迟,周期性 PostgreSQL 对账才是正确性基础。
|
||||
|
||||
## 8. PostgreSQL 终态
|
||||
|
||||
当前 schema 由冻结的编号迁移演进:`0001_initial.sql` 创建九张基线业务表,
|
||||
`0005_account_groups.sql` 再增加三张 Provider-neutral 分组关系表,合计十二张业务表:
|
||||
|
||||
| 表 | 权威事实 |
|
||||
| --- | --- |
|
||||
| `admin_users` | 管理员身份与密码摘要 |
|
||||
| `admin_audit_events` | 管理 mutation 审计 |
|
||||
| `client_api_keys` | 客户端鉴权、限额与授权范围 |
|
||||
| `account_groups` | Provider-neutral 分组资料与启停事实 |
|
||||
| `account_group_accounts` | 分组到账号的多对多 membership |
|
||||
| `client_api_key_groups` | Client Key 到分组的多对多授权;零行表示全部账号 |
|
||||
| `runtime_settings` | 全局配置与 `config_revision` |
|
||||
| `provider_accounts` | 账号资料、Provider-owned 明文 credential JSON、revision、凭据事实与规范化 quota 事实 |
|
||||
| `model_requests` | 请求、attempt、计费、交付、恢复事实及请求开始时冻结的路由范围快照 |
|
||||
| `ops_events` | 脱敏运行事件 |
|
||||
| `backup_settings` | S3/R2 存储、Cron 计划与保留策略单例配置 |
|
||||
| `backup_records` | 备份任务状态与归档事实(`queued/dumping/uploading/completed/failed/deleting`) |
|
||||
|
||||
备份设计边界见 `docs/s3-backup-design-audit.md`:单副本部署下备份由单个可取消
|
||||
`DaemonTask` 承担调度、执行、删除收敛与保留清理,不需要 leader lease、fencing token 或
|
||||
heartbeat;计划时间点冲突只记录日志/指标并推进游标,不产生 skipped 记录;删除成功后
|
||||
记录硬删除,操作历史进入 `admin_audit_events`。管理员身份事实不重复写入
|
||||
`backup_records`。存储身份在存在记录时锁定:endpoint/region/bucket/path-style 不允许
|
||||
变化,只允许轮换凭据与修改 prefix。每份备份可设置创建时确定的手动过期时间
|
||||
`expires_at`(手动与计划备份均可,`expiresInDays` 天数生成),到期由 Worker 自动清理。
|
||||
|
||||
设计规则:
|
||||
|
||||
- 一个事实只存一次;可表达关系使用真实 FK 与支持索引。
|
||||
- Provider 差异只进入受 schema/version 校验的 JSONB 边界。
|
||||
- credential JSON 以 Provider 自己的 schema 明文保存在 `provider_credentials_json`;日志、Debug、API 和 audit
|
||||
不输出其中的 secret,Provider 只在需要发往上游时把值包装为运行时 secret。
|
||||
- stale recovery 只把超时 `running` 请求收敛为 `incomplete`,不重放业务请求。
|
||||
- 数据面 execution observation 通过有界、带总字节预算的进程内队列交给 PostgreSQL OpsFlush worker;
|
||||
入队不等待数据库,队列满、worker 不可用或写入失败时记录累计统计并丢弃该可恢复观测,不能替换
|
||||
客户端可见协议结果。队列不是第二份业务权威,也不做无幂等键的盲目重试。
|
||||
- retention 只删除已满足保留规则的历史事实,不改变运行中请求。
|
||||
|
||||
业务 schema 由编号迁移定义;`0001_initial.sql` 是当前大版本基线,后续冻结迁移只向前演进。其中
|
||||
response ID 使用无格式假设的 UTF-8 bytes,`model_requests.service_tier` 保存响应观测到的服务档位,
|
||||
`routing_scope` 与 group ID/name 快照保存请求开始时的 `all`/`groups` 授权事实,迁移前历史明确标为
|
||||
`legacy_provider`,
|
||||
账号对外五态由 PostgreSQL 凭据/额度事实与 Redis 429 冷却通过 Core 唯一解析器动态派生,
|
||||
不保存重复的 `status` 列。同一大版本内已合入的迁移永久冻结——
|
||||
`.frozen-sha256` 是冻结清单,CI 校验清单相对 PR base 只增不改,启动时 sqlx 重校验已应用
|
||||
迁移的 checksum,不一致直接拒绝启动。规则见 `backend/migrations/README.md`。
|
||||
|
||||
## 9. Redis 终态
|
||||
|
||||
Redis 保存的协调状态恰好是:客户端 admission 热状态、credential lease/fencing 与调度信号、账号 429 cooldown 与 credential state 可重建缓存、catalog 缓存、Provider circuit、会话亲和与会话级账号排除、continuation pin、OAuth pending flow、worker leader lease以及 runtime change 通知。会话亲和和 round-robin 游标按 Client Key + Provider 隔离;continuation payload 带 Client Key 所有权并在读取时校验。账号 lease、quota、cooldown 和请求间隔仍按真实账号全局共享。Redis 丢失后必须从 PostgreSQL 或 Provider 事实恢复;恢复完成前需要保护的 acquire 路径 fail closed。
|
||||
|
||||
多副本部署时,跨副本协调面只有上述 Redis 状态与 PostgreSQL 事实;WebSocket 连接池、请求画像与 RuntimeSnapshot 副本都是进程内状态,其中 RuntimeSnapshot 依靠周期对账收敛,请求画像依靠各自的官方版本检查收敛。
|
||||
|
||||
OAuth pending key:
|
||||
管理写入遵循统一流程:
|
||||
|
||||
```text
|
||||
codex-proxy-rs:oauth-pending:v1:{provider_kind}:{flow_fingerprint}
|
||||
flow_fingerprint = SHA-256(provider_kind || 0x00 || raw_flow_binding)
|
||||
HTTP validation
|
||||
-> Admin use case
|
||||
-> Provider prepare/verify when needed
|
||||
-> PostgreSQL transaction + audit
|
||||
-> publish committed runtime snapshot
|
||||
-> notify Provider-derived caches
|
||||
```
|
||||
|
||||
创建时的基础 Hash 字段为:
|
||||
会改变路由快照或安全配置的 mutation 在同一 PostgreSQL 事务中提交业务事实、推进内部
|
||||
`config_revision` 并写入脱敏审计。Admin mutation 不要求客户端提交 revision;少数账号/分组响应
|
||||
返回已提交的 `configRevision`,不将它当作乐观并发前置条件。
|
||||
|
||||
- `owner_fingerprint`
|
||||
- `expires_at_epoch_seconds`
|
||||
- `provider_payload`
|
||||
额度、cooldown、目录 generation、请求统计和自动 credential refresh 属于运行时观测,不推进全局
|
||||
revision;credential 轮换只推进账号自己的 `credential_revision`。Redis 通知用于缩短收敛延迟,
|
||||
PostgreSQL 周期对账才是正确性基础。
|
||||
|
||||
complete 先通过 Lua 原子 claim,并临时增加 `claim_fingerprint` 与
|
||||
`claim_expires_at_epoch_millis`。owner mismatch 不删除记录;有效 claim 阻止并发 complete,失败时只有
|
||||
claim owner 可以释放,账号事务提交后才原子删除整个 pending key。OpenAI TTL 为 10 分钟,xAI TTL
|
||||
为 30 分钟。
|
||||
## 8. 状态所有权
|
||||
|
||||
## 10. 后台任务与恢复
|
||||
| 状态 | 唯一权威 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 账号、credential、分组、Client Key、设置、审计、请求与备份记录 | PostgreSQL | 业务持久化事实 |
|
||||
| admission、lease、cooldown、circuit、会话亲和、continuation、OAuth pending、目录 cache | Redis | 可重建、可过期的协调状态 |
|
||||
| 日志、OAuth 恢复记录、在线更新状态、备份暂存 | `.runtime/` | 部署节点本地运行文件 |
|
||||
| 重置卡库存与消费结果 | OpenAI upstream | 后端不建立本地卡库存;前端只保留当前浏览器会话的最近查询 |
|
||||
| Provider 公开模型与请求画像 | Provider/runtime cache | 由官方目录或发布源刷新,不写成第二份业务配置 |
|
||||
|
||||
HTTP serve/drain 与 worker 运行时都在 `gateway-host`:Host 负责注册校验、leader lease、失败退避、健康暴露与关闭;`apps/gateway` 的 bootstrap 只装配。WorkerContribution 由 Store、Core、Admin 与两个 Provider bundle 显式贡献,host bundle 自身不贡献任何 worker:
|
||||
账号对外状态不是独立列,而是 PostgreSQL credential/quota 事实与 Redis cooldown 的统一投影:
|
||||
`normal`、`quota_exhausted`、`rate_limited`、`disabled`、`error`。只有明确上游证据才能恢复或终态化账号,
|
||||
本地时钟和不确定响应不能伪造事实。
|
||||
|
||||
- Store:stale execution recovery、retention,以及 PostgreSQL execution observation、Redis client
|
||||
admission release 和 Provider circuit feedback 三个 OpsFlush daemon。native continuation 直接写 Redis,
|
||||
单次 Lua 记录只清理有界数量的过期/超限索引项,避免无界阻塞。
|
||||
- Core:RuntimeSnapshot revision 周期对账(无 lease,逐副本运行)与 Redis change 长驻订阅(daemon,逐副本运行)。
|
||||
- Admin:备份 daemon(`WorkerKind::Backup`)。单副本边界下它是一个可取消 `DaemonTask`,内部循环推进
|
||||
Cron 游标、恢复中间状态、完成删除收敛、领取并执行一个 queued 任务、执行一小批保留清理;长时间
|
||||
`pg_dump` 与 multipart 上传由 daemon 自身持有,Host 只负责 panic 后重启、健康与关闭。它不使用
|
||||
Redis leader lease(单副本无竞争),只依赖 PostgreSQL 部分唯一约束防御并发 API/调度创建。
|
||||
- Provider:credential refresh、quota/catalog 健康,以及模型 etag 与官方版本 release 检查(OpenAI Desktop、xAI CLI)。
|
||||
当前大版本的十二张业务表由 `0001_initial.sql` 建立;`0002_nullable_requested_model.sql` 允许 Images
|
||||
等无模型端点把 `requested_model_id` 保存为 `NULL`。已应用迁移按字节冻结,后续 schema 变化只能新增
|
||||
编号迁移,详见 [迁移规则](../backend/migrations/README.md)。
|
||||
|
||||
带 lease 的周期任务在每个周期开始向 Redis 申请一次 leader lease,周期内续租,周期结束即释放。这只是单周期互斥,不是跨周期 leader 选举:各副本独立计时,N 副本部署下同一任务的实际执行频率最坏可达单副本的 N 倍。
|
||||
## 9. Credential、额度与主动重置
|
||||
|
||||
自更新、回滚与进程重启不是 worker:它们是 Admin API 触发的 Host 系统操作,只替换并重启处理该请求的那个副本。
|
||||
credential 与 quota 是两组独立事实:credential refresh 不等于 quota refresh,额度接口的 401/403
|
||||
也不能单独证明 refresh token 永久失效。
|
||||
|
||||
关闭时序:收到取消信号后,HTTP drain(axum 优雅关闭与游离连接等待)共享同一个从信号时刻起算的绝对截止点,逾期放弃等待存量连接;随后 worker 在独立的 shutdown 超时内 join。有界观测队列会在该阶段按各自预算 drain,逾期的可恢复观测被计数并丢弃。
|
||||
- OpenAI 支持 OAuth、AT/RT、OAuth JSON 与 Agent Identity;RT-only 导入先换取 AT,AT-only 导入没有
|
||||
自动续期能力。OAuth 身份只从官方 JWT claims 投影,不信任导入文档顶层身份字段。
|
||||
- xAI 使用 OAuth session;API Key 不是受支持的账号 credential。
|
||||
- 新账号导入和首次 OAuth 在 credential 提交后尽力读取一次额度;失败只留下观测,不回滚账号事务。
|
||||
- quota refresh、正常推理返回的 rate-limit headers 和后台健康任务汇入同一额度事实;套餐只用于展示与
|
||||
目录 cache 隔离,不创建套餐专属状态机。
|
||||
|
||||
worker 不得通过导入 Admin use case 绕过边界,也不得维护第二份业务状态。
|
||||
主动额度重置是 OpenAI Provider 的不可逆上游操作:列表查询和消费都直接使用当前 Desktop 请求画像;
|
||||
卡片不写 PostgreSQL/Redis。消费请求携带调用方生成的 UUIDv4 幂等键,同一账号的消费在进程内串行;
|
||||
发送结果不明确时必须复用原键。确认成功后管理端再显式刷新卡片与 quota,不能直接改本地重置时间。
|
||||
|
||||
## 11. 测试与验收
|
||||
## 10. 观测与后台任务
|
||||
|
||||
- 生产 `src` 禁止 `#[cfg(test)]`、`#[path]` 和 `include!` 测试挂载。
|
||||
- 测试位于各 package 的 `tests/`,目录镜像生产模块。
|
||||
- Core 规则使用确定性测试;Provider 使用 contract/fixture 测试。
|
||||
- FK、CAS、revision、recovery 使用真实 PostgreSQL 测试。
|
||||
- admission、lease、cooldown、OAuth pending 使用真实 Redis 测试。
|
||||
- 自动化测试不得发送真实对话或轮换生产 refresh token;仓库不提供破坏性 live fixture。
|
||||
- 可选的外部 `CODEX_REAL_ACCOUNT_FILE` 官方 JWKS 签名检查仅用于显式测试,环境变量未设置时直接跳过;
|
||||
它不属于 OpenAI OAuth 导入、首次授权、重新授权或 RT refresh 流程,也不经过 selector/transport。
|
||||
请求观测通过有界进程内队列异步投影到 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
|
||||
cd backend
|
||||
cargo fmt --check
|
||||
cargo clippy --all-targets --all-features --locked -- -D warnings
|
||||
cargo test --test main --locked
|
||||
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`,架构不变量保留在本文。
|
||||
|
||||
@@ -48,10 +48,11 @@ const modalDescription = computed(() => showConfirm.value
|
||||
const triggerLabel = computed(() => {
|
||||
if (ambiguous.value)
|
||||
return '查看主动重置卡,有一项操作待确认'
|
||||
if (loadError.value)
|
||||
if (loadError.value) {
|
||||
return hasSnapshot.value
|
||||
? `查看主动重置卡,查询失败,最近查询 ${availableCount.value} 张可用`
|
||||
: '查看主动重置卡,查询失败'
|
||||
}
|
||||
return hasSnapshot.value
|
||||
? `查看主动重置卡,最近查询 ${availableCount.value} 张可用`
|
||||
: '查看主动重置卡'
|
||||
|
||||
@@ -1,46 +1,71 @@
|
||||
---
|
||||
name: codex-proxy-rs
|
||||
description: Codex Proxy RS 仓库开发指南。Use when changing or auditing its Rust backend, Vue frontend, PostgreSQL/Redis state, account scheduling, Responses transport and recovery, telemetry, Docker deployment, CI, release, configuration, or project documentation.
|
||||
description: Codex Proxy RS 仓库开发与审计指南。Use for its Rust gateway, Vue admin, OpenAI/xAI providers, account routing, Responses/Images transport, PostgreSQL/Redis state, workers, deployment, release, CI, or project documentation.
|
||||
---
|
||||
|
||||
# Codex Proxy RS
|
||||
|
||||
## 事实来源
|
||||
## 开始工作
|
||||
|
||||
1. 以当前源码、配置和测试为准。
|
||||
2. 非简单改动读取 `references/repo-guide.md`。
|
||||
3. 涉及目录、生命周期、存储或后台任务时读取 `docs/architecture.md`。
|
||||
4. skill 或文档与实现冲突时,在同一改动中修正。
|
||||
1. 从仓库根目录检查 `git status --short`,保留用户已有改动。
|
||||
2. `.codegraph/` 存在时先用 CodeGraph 定位 owner、调用链和动态分发,再读未覆盖的源码。
|
||||
3. 先确定事实属于 API、Core、Admin、Store、Host、Provider 还是 frontend,不跨 owner 打补丁。
|
||||
4. 行为与文档冲突时以当前源码、配置和测试为准,并在同一任务中修正文档。
|
||||
|
||||
存在 `.codegraph/` 时可先定位调用链;结论必须回到源码和测试验证。
|
||||
仓库根目录没有 Cargo manifest;后端命令必须进入 `backend/` 或显式传
|
||||
`--manifest-path backend/Cargo.toml`。
|
||||
|
||||
## 边界
|
||||
## 文档路由
|
||||
|
||||
- `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/`,统一挂在单一 `main` 集成测试目标下(`--test main`)。禁止在生产 `src/` 写 test-only 代码。
|
||||
- Vue 使用 `<script setup lang="ts">`、现有基础组件和主题 token。
|
||||
- README 面向使用者,保持简短;长期架构只写入 `docs/architecture.md`。
|
||||
- 完整 HTTP/Admin 路由与敏感导入/导出格式写入 `docs/api.md`。
|
||||
- 不添加兼容 shim、重复状态机、第二套配置或补丁式旁路。
|
||||
| 需要了解 | 权威入口 |
|
||||
| --- | --- |
|
||||
| 用户能力与快速开始 | `README.md` |
|
||||
| HTTP 路由、DTO、敏感导入/导出 | `docs/api.md` |
|
||||
| 系统边界、数据流、状态 owner、不变量 | `docs/architecture.md` |
|
||||
| Compose、密码、备份、更新与恢复 | `deploy/README.md` |
|
||||
| 迁移冻结与测试库 | `backend/migrations/README.md` |
|
||||
| 具体目录和常见改动入口 | `references/repo-guide.md` |
|
||||
|
||||
## 工作流
|
||||
README 保持用户导向;不要把上游 URL、重试常量、数据库字段清单或实现时间线堆进 README/架构文档。
|
||||
|
||||
1. 检查工作树,区分用户改动与当前任务。
|
||||
2. 追踪入口、owner、状态变化和输出,再决定修改点。
|
||||
3. 在最小所有权边界内实现;行为变化用外置集成测试固定。
|
||||
4. 同步受影响的架构、配置或 skill。
|
||||
5. 按风险运行验证,最后检查 staged 与 unstaged 范围。
|
||||
## 不可破坏的边界
|
||||
|
||||
- `gateway-api` 只做 HTTP/WS/SSE 适配;`gateway-core` 编排请求;Provider 独占 credential、catalog、
|
||||
quota 与 transport;`gateway-store` 实现持久化端口;具体实现只在 `apps/gateway` 组合。
|
||||
- Provider 的一次 `execute` 只选择一个 credential。换号、业务 retry 和跨 Provider fallback 只能由 Core
|
||||
在发送/交付边界内决定。
|
||||
- PostgreSQL 是持久化权威;Redis 只保存可重建、可过期的协调状态。
|
||||
- 当前只有 `openai` 与 `xai`,不存在 Provider Instance 层;Client Key 绑定账号分组而不是 Provider。
|
||||
- 已应用迁移按字节冻结;schema 变化新增编号迁移,并同步 `.frozen-sha256`。
|
||||
- 真实 secret 不进入普通日志、Debug、fixture、audit details 或文档示例;明文 Admin 响应只能出现在
|
||||
账号导出、Key reveal、备份设置等明确敏感合同中。
|
||||
- 测试放在各 package 的 `tests/`,统一挂载到单一 `main` 集成测试目标;生产 `src` 不写 test-only 代码。
|
||||
- 不添加兼容 shim、第二套状态机、重复配置或跨层旁路。
|
||||
|
||||
## 当前关键合同
|
||||
|
||||
- 数据面公开 Responses JSON/SSE/WS、review、Images generation/edit 和模型目录;不提供 Chat Completions。
|
||||
- OpenAI Responses/Images 保持业务 wire 透明;xAI 在 Provider 内完成 Grok/Responses 转换。
|
||||
- Images 固定走 OpenAI Provider 自有端点,不要求模型,也不参与文本模型映射。
|
||||
- OpenAI 账号支持 OAuth、AT/RT、OAuth JSON 和 Agent Identity;xAI 只接受 OAuth session/账号 JSON,
|
||||
不接受 API Key。
|
||||
- 主动额度重置卡只由 OpenAI 上游持有。查询由用户显式触发,消费使用 UUIDv4 幂等键;不得写本地卡库存
|
||||
或直接改 quota reset 时间。
|
||||
- credential 与 quota 是独立事实;刷新 token 不等于刷新额度,quota 401/403 也不等于 RT 永久失效。
|
||||
- 当前业务 schema 是 `0001_initial.sql` 的十二张表,加 `0002_nullable_requested_model.sql` 的无模型请求
|
||||
兼容变更。
|
||||
- 运行拓扑是单副本;worker lease 不是完整多副本 leader election,自更新也只替换当前进程。
|
||||
|
||||
## 前端约束
|
||||
|
||||
- 使用 Vue 3、`<script setup lang="ts">`、现有基础组件和主题 token。
|
||||
- `frontend/src/api` 只保留 wire DTO 与请求函数;页面查询、缓存和交互状态留在对应 view/composable。
|
||||
- 修改页面前先检查共享组件和相邻调用方;复用既有视觉语言,保持紧凑、低噪声和键盘可访问。
|
||||
- 上游查询不得由列表渲染或隐藏轮询意外触发。不可逆动作需要明确确认、loading 和不确定结果恢复路径。
|
||||
|
||||
## 验证
|
||||
|
||||
从仓库根目录执行:
|
||||
按改动风险选择相关命令;完整门禁为:
|
||||
|
||||
```bash
|
||||
cargo +1.97.0 fmt --all --manifest-path backend/Cargo.toml -- --check
|
||||
@@ -51,14 +76,12 @@ pnpm --dir frontend build
|
||||
docker compose -f deploy/compose.yaml config --quiet
|
||||
```
|
||||
|
||||
PostgreSQL/Redis 集成测试需要指向本地实例的 `CPR_TEST_DATABASE_URL`、
|
||||
`CPR_TEST_REDIS_URL`;本地未设置时这些测试静默跳过,CI 缺失则直接失败。
|
||||
`gateway-store` 的 PostgreSQL/Redis 集成测试使用 `CPR_TEST_DATABASE_URL` 和 `CPR_TEST_REDIS_URL`;
|
||||
本地未设置时静默跳过,CI 缺失则失败。自动化测试不发送真实对话、不轮换生产 RT。
|
||||
|
||||
## Git 与发布
|
||||
|
||||
- 使用简短 Conventional Commit subject。
|
||||
- 提交前检查 `git status --short`、cached/unstaged diff 和 `git diff --check`。
|
||||
- 提交带 `Co-authored-by: Codex <noreply@openai.com>`。
|
||||
- 发布以 `release/version.yaml` 和带注释的 `vX.Y.Z` tag 为准。
|
||||
- 正式发布使用 `release/publish <version>`,不要手工绕过发布脚本。
|
||||
- 发布完成后核对远端 `main`、tag、Actions、Release asset 与 GHCR。
|
||||
- 提交前检查 status、cached/unstaged diff 和 `git diff --check`。
|
||||
- 使用简短 Conventional Commit subject;按用户要求添加 `Co-authored-by: Codex <noreply@openai.com>`。
|
||||
- 发布权威是 `release/version.yaml` 与带注释的 `vX.Y.Z` tag。
|
||||
- 正式发布使用 `release/publish <version>`;提交、Release 和实例升级是三个独立状态。
|
||||
|
||||
@@ -1,82 +1,168 @@
|
||||
# Codex Proxy RS 项目指南
|
||||
|
||||
## 权威来源
|
||||
这是面向仓库修改者的导航,不复制完整 API 或架构说明。行为以当前源码和测试为准;长期边界见
|
||||
`docs/architecture.md`。
|
||||
|
||||
- 行为:当前源码和测试。
|
||||
- 架构与数据边界:`docs/architecture.md`。
|
||||
- 部署:`deploy/config.example.yaml`、`deploy/compose.yaml`、`deploy/README.md`。
|
||||
- 发布:`release/version.yaml`、`release/platforms.yaml`、Release workflow。
|
||||
## 权威入口
|
||||
|
||||
## 结构与 owner
|
||||
|
||||
| 路径 | 责任 |
|
||||
| 主题 | 入口 |
|
||||
| --- | --- |
|
||||
| `backend/apps/gateway` | composition root、server、worker 启动 |
|
||||
| `backend/crates/gateway-core` | operation、routing、attempt coordinator、policy、accounting |
|
||||
| `backend/crates/gateway-protocol` | wire contract、canonical event |
|
||||
| `backend/crates/gateway-admin` | 管理领域、用例和抽象端口 |
|
||||
| `backend/crates/gateway-store` | PostgreSQL/Redis adapter |
|
||||
| `backend/crates/gateway-api` | Responses/Admin HTTP adapter |
|
||||
| `backend/crates/gateway-host` | host、update、system 能力 |
|
||||
| `backend/crates/providers/openai` | OpenAI OAuth、catalog、transport |
|
||||
| `backend/crates/providers/xai` | xAI/Grok OAuth session、catalog、transport |
|
||||
| `frontend` | Vue 管理端 |
|
||||
| `deploy` | Compose、镜像与配置模板 |
|
||||
| `docs/architecture.md` | 唯一长期架构文档 |
|
||||
| 顶层装配与启动顺序 | `backend/apps/gateway/src/bootstrap.rs` |
|
||||
| 客户端路由 | `backend/crates/gateway-api/src/openai/router.rs` |
|
||||
| 管理路由 | `backend/crates/gateway-api/src/admin/` |
|
||||
| 请求生命周期 | `backend/crates/gateway-core/src/engine/` |
|
||||
| 快照与路由计划 | `backend/crates/gateway-core/src/routing/snapshot.rs` |
|
||||
| OpenAI Provider | `backend/crates/providers/openai/src/` |
|
||||
| xAI Provider | `backend/crates/providers/xai/src/` |
|
||||
| PostgreSQL/Redis adapter | `backend/crates/gateway-store/src/` |
|
||||
| Worker 贡献与监督 | 各 Bundle 的 `worker_contributions`、`gateway-host/src/workers/` |
|
||||
| Vue 管理端 | `frontend/src/views/`、`frontend/src/components/base/` |
|
||||
| 部署与发布 | `deploy/`、`release/` |
|
||||
|
||||
仓库根目录没有 Cargo manifest;后端命令进入 `backend/` 或传入 `--manifest-path backend/Cargo.toml`。
|
||||
存在 `.codegraph/` 时,先查询符号和调用链;CodeGraph 未覆盖的目标再用 `rg` 和定点读取补齐。
|
||||
|
||||
## 执行与 Provider 边界
|
||||
## Package owner
|
||||
|
||||
- `gateway-api` 不写具体 Provider 分支。
|
||||
- Core 冻结 RuntimeSnapshot、RoutePlan、retry/fallback 与 downstream commit 边界。
|
||||
- Provider 每次 `execute` 只选择一个 credential 并返回 cold canonical stream。
|
||||
- OpenAI 数据面是透明代理:请求 body 逐字段(含未知字段与顺序)原样透传,HTTP SSE 与 WebSocket 的上游业务事件逐字节原样转发;xAI 是 Grok wire 与 Responses wire 的翻译层,向客户端转发结构化上游错误 message/code/type,message 先擦除 UUID 账号指纹。
|
||||
- 换号只由 Core 在下游 commit 前按重放安全性决定。
|
||||
- fallback 只允许同一 Provider kind 内的账号;不跨 Provider,也不存在 Provider Instance 层。
|
||||
- continuation 升级链由 Core 冻结:native(原生 handle 绑定原账号)→ replay owner(原账号完整 transcript 重放)→ replay any(transcript 已可携带,允许换账号);两个 Provider 都实现全链,xAI 的 transcript 由不透明 session state 承载,跨账号重放前做脱敏投影。
|
||||
- Provider wire profile 以配置为启动基线,并由共享运行时状态统一发布。OpenAI Desktop 读取官方 appcast,并从同一 ZIP 的内嵌 Core 做有界 Range 探测;xAI CLI 读取 `@xai-official/grok`。发现新版本后自动更新对应版本字段,所有消费边界不得维护独立常量。
|
||||
- `downstream_committed_at` 是不可撤回交付承诺,不是首字节已经写达的证明。
|
||||
| Package | 拥有的事实 | 不应拥有 |
|
||||
| --- | --- | --- |
|
||||
| `gateway-protocol` | 共享 wire/canonical contract | 数据库、Provider、业务策略 |
|
||||
| `gateway-core` | operation、routing、attempt、commit/send、accounting | HTTP DTO、SQL、具体上游协议 |
|
||||
| `gateway-admin` | 管理命令、用例、审计、备份策略、抽象端口 | Axum、具体数据库实现 |
|
||||
| `gateway-api` | Axum route、鉴权解码、wire presenter、响应交付 | SQL、具体 Provider 分支 |
|
||||
| `gateway-store` | PostgreSQL、Redis、S3、`pg_dump` adapter | 路由/重试/Provider 业务规则 |
|
||||
| `gateway-host` | 配置、日志、serve/drain、worker、update | 数据面路由与 Provider 逻辑 |
|
||||
| `provider-openai` | OpenAI credential/quota/catalog/transport | Client Key admission、跨 Provider fallback |
|
||||
| `provider-xai` | xAI credential/quota/catalog/transport 与协议转换 | Client Key admission、跨 Provider fallback |
|
||||
| `apps/gateway` | 具体实现装配 | 业务状态机 |
|
||||
|
||||
## 存储
|
||||
仓库根目录没有 Cargo manifest。后端 workspace 是 `backend/Cargo.toml`,Rust 版本为 1.97,edition 2024。
|
||||
|
||||
- PostgreSQL 业务表只有 `0001_initial.sql` 定义的七张;该文件直接表达当前大版本的完整新库基线,
|
||||
包括 bytes response ID、响应观测 service tier 与额度耗尽冷却约束。
|
||||
- 同一大版本内已应用迁移按字节冻结:`backend/migrations/.frozen-sha256` 是冻结清单,CI 相对 PR base 做 append-only 校验;schema 变更一律新增编号迁移,规则见 `backend/migrations/README.md`。
|
||||
- `config_revision` 只用于会改变调度快照或安全配置的管理 mutation。
|
||||
- quota、cooldown、catalog generation、自动 refresh 不推进全局 revision。
|
||||
- refresh 只推进账号 `credential_revision`;Redis cooldown 是可丢失热缓存。
|
||||
- OAuth pending 使用 Provider 域隔离 SHA-256 key;基础 Hash 保存 owner、过期时间和 Provider payload,
|
||||
complete 先原子 claim,失败释放 claim,账号事务提交后才消费。
|
||||
- 数据面 PostgreSQL execution observation 与 Redis admission release/circuit feedback 使用有界 OpsFlush
|
||||
队列;队列可丢失、可观测且不是权威状态,不得让 Store 失败改写客户端协议结果。native
|
||||
continuation 直接写 Redis,并限制每次记录的索引清理量。
|
||||
- 真实 secret 不进入日志、Debug、fixture、文档或 audit details;默认测试使用合成值,外部 access-token
|
||||
签名检查是唯一显式 opt-in 例外。
|
||||
## 数据面入口
|
||||
|
||||
## 前端
|
||||
公开路由由 `gateway-api/src/openai/router.rs` 固定:
|
||||
|
||||
- Vue 3 Composition API 与 `<script setup lang="ts">`。
|
||||
- API 位于 `frontend/src/api`,页面状态留在对应 view/composable。
|
||||
- 复用现有基础组件和主题 token,保持紧凑低噪声。
|
||||
- Responses:`POST /v1/responses`、WebSocket `GET /v1/responses`、`POST /v1/responses/review`;
|
||||
- Images:`POST /v1/images/generations`、`POST /v1/images/edits`;
|
||||
- 模型:`/v1/models`、catalog、info、detail。
|
||||
|
||||
## 验证
|
||||
Responses adapter 解析参与路由的最少语义,并保留原始请求;Images adapter 不解析模型或重建 JSON,
|
||||
直接创建 `GenerateImage` operation。两者都进入同一 Core execution lifecycle。
|
||||
|
||||
定位请求问题时按以下顺序追踪:
|
||||
|
||||
```text
|
||||
gateway-api adapter
|
||||
-> ExecutionService
|
||||
-> RuntimeSnapshot::plan / plan_provider_endpoint
|
||||
-> AttemptCoordinator
|
||||
-> Provider::execute
|
||||
-> API delivery
|
||||
-> asynchronous observations
|
||||
```
|
||||
|
||||
重点边界:
|
||||
|
||||
- `UpstreamSendState` 区分 `not_sent/sent/ambiguous`;
|
||||
- downstream commit 后不能重试;
|
||||
- Provider execute 一次只返回一个账号的一条冷流;
|
||||
- account retry 与跨 Provider 候选推进由 Core 管理;
|
||||
- response ID 和原始 wire 不因观测模型而重写。
|
||||
|
||||
OpenAI 保持 Responses/SSE/WS/Images 业务字节透明;xAI 在自己的 Provider 内完成 Grok/Responses 转换。
|
||||
Images 固定调用 OpenAI Provider 自有端点,候选不带 `upstream_model`。
|
||||
|
||||
## 路由与 continuation
|
||||
|
||||
`RuntimeSnapshot` 聚合运行设置、账号目录、分组 membership、Client Key scope、Provider 模型目录和
|
||||
`config_revision`。Client Key 零分组表示全部账号,非空分组表示已启用成员并集;空池 fail closed。
|
||||
|
||||
文本模型先做全局精确映射,再在 scope 内按 Provider 能力生成候选。普通 retry 需要同时满足发送状态、
|
||||
重放安全、attempt 预算和未 commit;传输不明确不自动重放。
|
||||
|
||||
OpenAI session affinity 是粘性提示;native continuation pin 才固定 Client Key、scope、Provider 与账号。
|
||||
OpenAI continuation 按 native/replay-owner/replay-any 推进,xAI 使用客户端提交的完整历史。
|
||||
|
||||
## 账号控制面
|
||||
|
||||
账号路由和 DTO 在 `gateway-api/src/admin/accounts.rs`,业务编排在
|
||||
`gateway-admin/src/use_case/accounts.rs`,Provider 解析/验证后由 Store 事务提交。
|
||||
|
||||
### 导入与授权
|
||||
|
||||
- 公共导入合同始终是 `{ provider, data }`,其中 `data` 为 Provider-owned JSON object。
|
||||
- OpenAI 支持 OAuth JSON、`accounts` 数组、AT-only、RT-only、AT+RT+可选 ID Token 和 Agent Identity;
|
||||
最多 200 项。RT-only 先交换 AT,AT-only 不会凭空获得刷新能力。
|
||||
- OpenAI OAuth 身份来自 `parse_chatgpt_jwt_claims`,不信任导入 object 顶层的 user/account ID。
|
||||
- 前端 AT/RT 标签只把“每行一个 token”转换成 `accounts` JSON,没有第二条导入 API。
|
||||
- xAI 接受 OAuth 账号 object/数组,逐项验证;API Key 不是 credential。
|
||||
- 新账号未分组;匹配既有上游身份的导入/重新授权保留调度事实。
|
||||
|
||||
### Credential、quota 与恢复
|
||||
|
||||
- credential refresh、quota refresh、正常请求的 rate-limit 观测是独立链路。
|
||||
- 账号五态由 credential/quota + Redis cooldown 统一投影,不保存重复 `status`。
|
||||
- `/accounts/recover` 是管理员清除本地错误、quota 和 cooldown 的强制动作,不证明上游已经恢复。
|
||||
- 展开区用量按代表性额度窗口 `[reset_at - window_seconds, reset_at)` 聚合,不回退到全历史累计。
|
||||
|
||||
### 主动重置卡
|
||||
|
||||
- `GET/POST /api/admin/accounts/reset-credits` 只对 OpenAI 有效。
|
||||
- 列表和消费直接访问上游;不建 PostgreSQL/Redis 卡库存。
|
||||
- POST 使用 canonical UUIDv4 `redeemRequestId`,账号级串行;401 credential refresh 后复用同一命令。
|
||||
- 发送后结果不明确时,调用方只能复用原键重试;成功后另行刷新卡列表和 quota。
|
||||
- 前端只在弹窗打开/手工刷新时查询,最近结果仅缓存于当前浏览器会话。
|
||||
|
||||
## 存储与迁移
|
||||
|
||||
PostgreSQL 的十二张业务表全部在 `0001_initial.sql` 建立;`0002_nullable_requested_model.sql` 让
|
||||
`model_requests.requested_model_id` 可空,以记录 Images 等无模型端点。`.frozen-sha256` 覆盖全部迁移,
|
||||
已应用文件不可回改。
|
||||
|
||||
主要状态边界:
|
||||
|
||||
- PostgreSQL:账号、credential、quota、分组、Client Key、设置、请求、审计、备份;
|
||||
- Redis:admission、credential/worker lease、cooldown、circuit、session affinity、continuation、OAuth
|
||||
pending、catalog/cache 和 runtime change;
|
||||
- `.runtime/data`:会话身份、更新状态与备份暂存;
|
||||
- `.runtime/logs`:普通日志和含原始 AT/RT 的独立 OAuth 恢复文件集。
|
||||
|
||||
数据面 observation 使用有界 OpsFlush 队列;它可丢失、可观测,但不是第二份业务权威。native
|
||||
continuation 等正确性依赖的协调写入不经该 best-effort 队列。
|
||||
|
||||
## Worker 与生命周期
|
||||
|
||||
组合根按 Store → Core → OpenAI → xAI → Admin 收集 worker contributions,Host 统一校验、监督、健康
|
||||
暴露和关闭:
|
||||
|
||||
- Store:stale request recovery、retention、PostgreSQL/Redis flush daemons;
|
||||
- Core:snapshot reconciliation 和 change subscription;
|
||||
- OpenAI/xAI:OAuth refresh、quota/catalog 和版本/etag 检查;
|
||||
- Admin:单个备份 daemon。
|
||||
|
||||
应用只支持单副本。周期 lease 是单周期互斥,不是集群 leader election;备份和在线更新同样依赖此边界。
|
||||
关闭先 drain HTTP/WS,再 join workers。
|
||||
|
||||
## 前端定位
|
||||
|
||||
- API DTO/请求:`frontend/src/api/modules/`;
|
||||
- 页面状态:对应 `views/*/composables/`;
|
||||
- 页面展示转换:对应组件或相邻 presenter/util;
|
||||
- 通用交互:`components/base/`;
|
||||
- 主题:`styles/tokens.css` 与既有 `cp-*` token。
|
||||
|
||||
不要把页面状态塞进 API module,也不要为单一页面创造第二套通用层。修改共享组件前检查所有调用方;
|
||||
异步上游动作必须区分 loading、明确失败和结果不确定。
|
||||
|
||||
## 验证与发布
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
cargo fmt --check
|
||||
cargo clippy --all-targets --all-features --locked -- -D warnings
|
||||
cargo test --test main --locked
|
||||
cd ..
|
||||
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
|
||||
```
|
||||
|
||||
每个 package 的集成测试统一挂在单一 `main` 测试目标下(`autotests = false`)。
|
||||
`gateway-store` 的 PostgreSQL/Redis 集成测试需要 `CPR_TEST_DATABASE_URL`、
|
||||
`CPR_TEST_REDIS_URL`;本地未设置时静默跳过,CI 缺失则直接失败。
|
||||
|
||||
自动化测试不发送真实对话,也不轮换生产 refresh token。OpenAI identity 有一个可选的
|
||||
`CODEX_REAL_ACCOUNT_FILE` 外部文件签名检查,只验证官方 JWKS,不经过 selector 或数据面;未设置时
|
||||
直接跳过。线上链路验收使用隔离的操作员请求,并按 request ID 对照服务日志和观测记录。
|
||||
PostgreSQL/Redis 集成测试需要 `CPR_TEST_DATABASE_URL`、`CPR_TEST_REDIS_URL`。发布使用
|
||||
`release/publish <version>`;不要把本地 commit、GitHub Release 和在线实例 revision 混为同一状态。
|
||||
|
||||
@@ -1,59 +1,77 @@
|
||||
---
|
||||
name: engineering-standards
|
||||
description: 证据优先的长期维护工作流。用于代码审计、问题修复、重构、生产故障、参考项目对比,以及用户强调干净架构、禁止兼容写法、先审计再实现或拒绝代码异味的任务。
|
||||
description: 证据优先、owner 清晰的长期维护工作流。Use for audits, bug fixes, refactors, production incidents, reference-project comparisons, documentation synchronization, or requests that reject compatibility shims and code smell.
|
||||
---
|
||||
|
||||
# 工程规范
|
||||
|
||||
## 证据顺序
|
||||
## 先确定任务合同
|
||||
|
||||
1. 当前运行结果与可复现输入。
|
||||
2. 当前源码、配置、数据库和日志。
|
||||
3. 当前测试与公开契约。
|
||||
4. 架构文档。
|
||||
5. 记忆和参考项目。
|
||||
| 用户要求 | 默认交付 |
|
||||
| --- | --- |
|
||||
| 解释、审计、诊断 | 查明证据和边界,只报告结论,不擅自修改 |
|
||||
| 修复、实现、重构 | 在已证明的 owner 内修改,并按风险验证 |
|
||||
| 文档、计划 | 只更新被授权的文档/计划,不顺手实现功能 |
|
||||
| 生产排障 | 先核对实际 revision、配置、数据和日志;写操作需要明确授权 |
|
||||
|
||||
后两项只能辅助定位,不能覆盖当前事实。
|
||||
最新的用户范围修正优先。不要用“顺便优化”扩大 mutation、部署、数据清理或外部操作范围。
|
||||
|
||||
## 事实优先级
|
||||
|
||||
1. 当前可复现输入与运行结果。
|
||||
2. 当前部署 revision、配置、数据库和日志。
|
||||
3. 当前源码、测试与公开合同。
|
||||
4. 架构和项目文档。
|
||||
5. 记忆、历史提交和参考项目。
|
||||
|
||||
低优先级来源只能帮助定位,不能覆盖当前事实。观察到相关性时标为线索;只有调用链、状态变化或可复现
|
||||
实验闭合后才称为根因。
|
||||
|
||||
## 工作流
|
||||
|
||||
1. 追踪入口、调用链、状态变化、持久化、读取和输出。
|
||||
2. 区分展示、接口、领域规则、存储、异步任务和外部依赖。
|
||||
3. 找到现有 owner,证明断点位于该边界。
|
||||
4. 在 owner 内直接修复;必要重构必须减少分支或重复职责。
|
||||
5. 通过公开接口或外置集成测试固定行为。
|
||||
6. 运行项目既定格式、lint 和相关测试,再检查最终 diff。
|
||||
1. 检查工作树和运行环境,区分用户改动与本任务范围。
|
||||
2. 找到入口,沿调用链追踪状态的创建、持久化、读取、转换和输出。
|
||||
3. 标出协议、领域、存储、异步任务和外部依赖之间的 owner。
|
||||
4. 用最小证据证明断点;如果现象已经由环境/数据解释,不先写代码。
|
||||
5. 在 owner 内做最小完整修复;行为变化通过公开接口或外置集成测试固定。
|
||||
6. 同步受影响的 API、架构、部署、迁移或 skill 文档。
|
||||
7. 运行与风险相称的格式、lint、测试和静态检查,再复核最终 diff。
|
||||
|
||||
简单文案和机械修改直接完成,不套审计模板。结论先行,只报告影响判断的证据。
|
||||
简单文案和机械修改直接完成,不套审计模板;高风险或不可逆操作先做只读检查并明确影响范围。
|
||||
|
||||
## 实现约束
|
||||
## 设计约束
|
||||
|
||||
- 不添加兼容 shim、重复 API、双状态机或临时旁路。
|
||||
- 不用大范围重构掩盖尚未理解的问题。
|
||||
- 不为局部问题修改无关模块。
|
||||
- 不在没有复用或所有权收益时增加抽象。
|
||||
- 用户只要求审计、文档或方案时,不扩大到实现。
|
||||
- 已确认且属于当前范围的问题,先修复再继续测试。
|
||||
- 测试遵守仓库目录边界,不混入生产源码。
|
||||
- 行为语义变化应在修改前明确说明。
|
||||
- 一个事实只能有一个权威 owner;缓存、projection 和 display 必须能追溯到它。
|
||||
- 不添加兼容 shim、重复 API、双状态机、第二套配置或临时旁路。
|
||||
- 不用大范围重构掩盖尚未理解的问题,也不为局部问题修改无关模块。
|
||||
- 抽象必须减少重复职责或稳定边界;没有复用/所有权收益时保留直接实现。
|
||||
- 错误应保留作出判断所需的原始上游事实,同时在日志和响应边界脱敏 secret。
|
||||
- 不把可恢复观测升级成业务权威,也不让遥测失败改写已经确定的业务结果。
|
||||
- 并发和重试必须先定义幂等键、发送边界、commit 边界和结果不确定语义。
|
||||
- 测试遵守生产目录边界;不为测试把 mock、fixture 或开关塞进生产源码。
|
||||
|
||||
## 参考项目
|
||||
|
||||
只有同时满足以下条件才采用参考实现:
|
||||
采用外部实现前同时证明:
|
||||
|
||||
1. 当前项目已证明缺少该行为。
|
||||
2. 当前架构存在匹配 owner。
|
||||
3. 不会复制现有链路。
|
||||
4. 用户授权的范围包含实现。
|
||||
1. 当前项目确实缺少该行为;
|
||||
2. 当前架构存在匹配 owner;
|
||||
3. 参考行为与当前上游/版本仍一致;
|
||||
4. 引入后不会复制已有链路;
|
||||
5. 用户授权范围包含这项实现。
|
||||
|
||||
参考代码用于验证机制,不用于替代当前项目证据。
|
||||
参考代码用于验证机制和请求画像,不用于替代当前项目证据。若参考项目与当前源码冲突,先解释差异,
|
||||
不要静默移植。
|
||||
|
||||
## 收尾
|
||||
## 验证与收尾
|
||||
|
||||
交付只包含:
|
||||
验证目标是证明改动的合同,而不是堆命令。优先运行最窄相关测试,再按风险扩展到 package、workspace、
|
||||
frontend build 或部署配置;用户明确要求不运行验证时,只做必要的只读 diff/一致性复核并如实说明。
|
||||
|
||||
- 已改变的行为与边界。
|
||||
- 能证明结果的验证。
|
||||
- 明确未做或仍受外部状态阻塞的事项。
|
||||
最终交付只报告:
|
||||
|
||||
禁止把排查时间线、空洞原则或未证实猜测包装成深度。
|
||||
- 已改变的行为和 owner;
|
||||
- 能证明结果的验证;
|
||||
- 明确未做、未验证或仍受外部状态阻塞的事项。
|
||||
|
||||
不要把排查时间线、空洞原则、未证实猜测或工具输出堆成结论。
|
||||
|
||||
Reference in New Issue
Block a user