diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1c3812eae..856ebf86f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -626,7 +626,7 @@ jobs: make_latest: ${{ needs.release-info.outputs.prerelease == 'true' && 'false' || 'true' }} body: | > [!WARNING] - > **Upgrading across major versions?** Database schema and configuration are only compatible within a major version, and the one-click self-update refuses cross-major targets (releases before 3.0 lack this gate — do not use their built-in update to reach a newer major). Deploy the new major fresh (new `.runtime/` data directory) and re-import or re-authorize provider accounts and client keys. See the [upgrade notes](https://github.com/${{ github.repository }}/blob/main/deploy/README.md#镜像升级与源码构建). + > **Upgrading across major versions?** Online upgrades across major versions are not supported. Deploy the new major fresh with a new `.runtime/` data directory, then re-import or re-authorize provider accounts and client keys. See the [upgrade notes](https://github.com/${{ github.repository }}/blob/main/deploy/README.md#镜像升级与源码构建). ## 📥 Installation diff --git a/README.md b/README.md index 57ca2e40d..e581911eb 100644 --- a/README.md +++ b/README.md @@ -168,10 +168,8 @@ docker compose -f deploy/compose.yaml up -d > 运行状态。 > [!WARNING] -> 跨大版本不是原地升级:数据库 schema 与配置只在同一大版本内兼容,内置一键自更新 -> 会拒绝跨大版本目标(3.0 之前的旧版没有这道门禁,不要用旧版界面的一键更新跨大版本)。 -> 升级到新的大版本需要全新部署(全新 `.runtime/` 数据目录),并重新导入或重新授权 -> Provider 账号与客户端 Key。 +> 不支持跨大版本在线升级。升级到新的大版本时,请使用全新的 `.runtime/` 数据目录重新部署, +> 并重新导入或重新授权 Provider 账号与客户端 Key。 Compose 默认只绑定 `127.0.0.1`。公网接入应使用 HTTPS 反向代理,转发 WebSocket upgrade 与真实客户端 IP;不要暴露 PostgreSQL 或 Redis。 diff --git a/backend/crates/gateway-store/tests/postgres/mod.rs b/backend/crates/gateway-store/tests/postgres/mod.rs index f37bf92dc..2845a4c8f 100644 --- a/backend/crates/gateway-store/tests/postgres/mod.rs +++ b/backend/crates/gateway-store/tests/postgres/mod.rs @@ -58,30 +58,6 @@ impl TestDatabase { .execute(&mut *migration) .await .expect("apply terminal migration"); - sqlx::raw_sql(include_str!( - "../../../../migrations/0002_drop_opaque_response_id_index.sql" - )) - .execute(&mut *migration) - .await - .expect("apply opaque response ID migration"); - sqlx::raw_sql(include_str!( - "../../../../migrations/0003_store_opaque_response_ids_as_bytes.sql" - )) - .execute(&mut *migration) - .await - .expect("apply opaque response storage migration"); - sqlx::raw_sql(include_str!( - "../../../../migrations/0004_add_model_request_service_tier.sql" - )) - .execute(&mut *migration) - .await - .expect("apply model request service tier migration"); - sqlx::raw_sql(include_str!( - "../../../../migrations/0005_allow_quota_exhausted_cooldown.sql" - )) - .execute(&mut *migration) - .await - .expect("apply quota exhausted cooldown migration"); migration.commit().await.expect("commit terminal migration"); Some(Self { admin, @@ -177,25 +153,19 @@ async fn connect_and_migrate_should_apply_all_migrations_once_and_reopen_cleanly .expect("drop migration test database"); admin.close().await; - assert_eq!((first_table_count, migration_count), (8, 5)); + assert_eq!((first_table_count, migration_count), (8, 1)); assert_eq!(response_id_types, ["bytea", "bytea"]); assert!(!raw_response_id_index_exists); } #[test] fn migrations_should_leave_transaction_ownership_to_sqlx() { - let transaction_statements = [ - include_str!("../../../../migrations/0001_initial.sql"), - include_str!("../../../../migrations/0002_drop_opaque_response_id_index.sql"), - include_str!("../../../../migrations/0003_store_opaque_response_ids_as_bytes.sql"), - include_str!("../../../../migrations/0004_add_model_request_service_tier.sql"), - include_str!("../../../../migrations/0005_allow_quota_exhausted_cooldown.sql"), - ] - .into_iter() - .flat_map(str::lines) - .map(str::trim) - .filter(|line| matches!(*line, "begin;" | "commit;")) - .count(); + let transaction_statements = [include_str!("../../../../migrations/0001_initial.sql")] + .into_iter() + .flat_map(str::lines) + .map(str::trim) + .filter(|line| matches!(*line, "begin;" | "commit;")) + .count(); assert_eq!(transaction_statements, 0); } diff --git a/backend/migrations/.frozen-sha256 b/backend/migrations/.frozen-sha256 index 5bb9bcacb..616b5bf49 100644 --- a/backend/migrations/.frozen-sha256 +++ b/backend/migrations/.frozen-sha256 @@ -1,5 +1 @@ -0554fed953bd1440b50bc4033dc8bad96b51e132c57c2ccf1553728a991d5cb2 0001_initial.sql -0ae3945c02461347ab8dcfc64821027cd2366505778e0450d71bd82772c4e99d 0002_drop_opaque_response_id_index.sql -e608ed89991f9d6317491714447a00a61ede90caca4fb030eb30695654afb664 0003_store_opaque_response_ids_as_bytes.sql -df4e02cf2335733326c12429ced4cbd0b35b8a22b3fc24981c3c0549477d7c43 0004_add_model_request_service_tier.sql -fe8f36913c6db7053148db450f05e008dab6cdb9ca8cdcfcb22d97ad9a87bf49 0005_allow_quota_exhausted_cooldown.sql +a4681a5c040e80c5234e879293fc34972080e5251dfbc7c40e1374f8e4170a4c 0001_initial.sql diff --git a/backend/migrations/0001_initial.sql b/backend/migrations/0001_initial.sql index 0b1ac07c6..13f4fcbc3 100644 --- a/backend/migrations/0001_initial.sql +++ b/backend/migrations/0001_initial.sql @@ -166,7 +166,14 @@ create table provider_accounts ( ) ), constraint provider_accounts_cooldown_ck check ( - (availability = 'cooldown') = (cooldown_until is not null) + ( + availability != 'cooldown' + or cooldown_until is not null + ) + and ( + cooldown_until is null + or availability in ('cooldown', 'quota_exhausted') + ) ), constraint provider_accounts_time_ck check ( created_at <= updated_at @@ -208,6 +215,7 @@ create table model_requests ( requested_model_id text not null, provider_kind text, upstream_model_id text, + service_tier text, provider_account_id text, provider_account_ref text, upstream_transport text, @@ -220,9 +228,11 @@ create table model_requests ( outcome text not null default 'running', client_status_code integer, upstream_status_code integer, - client_response_id text, + -- Provider response IDs are opaque JSON strings. Bytes preserve embedded NUL + -- and avoid imposing PostgreSQL text-index limits on protocol handles. + client_response_id bytea, upstream_request_id text, - upstream_response_id text, + upstream_response_id bytea, error_kind text, provider_error_code text, error_message text, @@ -321,6 +331,13 @@ create table model_requests ( provider_observation_json is null or jsonb_typeof(provider_observation_json) = 'object' ), + constraint model_requests_service_tier_ck check ( + service_tier is null + or ( + octet_length(service_tier) between 1 and 64 + and service_tier !~ '[[:cntrl:]]' + ) + ), constraint model_requests_cost_ck check ( cost_source in ('provider_reported', 'calculated', 'unavailable') and ( @@ -414,10 +431,6 @@ create index model_requests_running_deadline_idx create index model_requests_retention_idx on model_requests (completed_at) where outcome <> 'running'; -create unique index model_requests_client_response_uq - on model_requests (client_response_id) - where client_response_id is not null; - create table ops_events ( id text primary key, model_request_id text, diff --git a/backend/migrations/0002_drop_opaque_response_id_index.sql b/backend/migrations/0002_drop_opaque_response_id_index.sql deleted file mode 100644 index fed242688..000000000 --- a/backend/migrations/0002_drop_opaque_response_id_index.sql +++ /dev/null @@ -1,3 +0,0 @@ --- Provider response IDs are opaque protocol values, not relational identifiers. --- Indexing the raw text makes PostgreSQL reject otherwise valid long handles. -drop index model_requests_client_response_uq; diff --git a/backend/migrations/0003_store_opaque_response_ids_as_bytes.sql b/backend/migrations/0003_store_opaque_response_ids_as_bytes.sql deleted file mode 100644 index ec9716bc4..000000000 --- a/backend/migrations/0003_store_opaque_response_ids_as_bytes.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Response IDs are opaque JSON strings. PostgreSQL text rejects NUL even though JSON accepts it. --- Store their UTF-8 bytes without an index; Rust decodes them only at the audit/API boundary. -alter table model_requests - alter column client_response_id type bytea - using convert_to(client_response_id, 'UTF8'), - alter column upstream_response_id type bytea - using convert_to(upstream_response_id, 'UTF8'); diff --git a/backend/migrations/0004_add_model_request_service_tier.sql b/backend/migrations/0004_add_model_request_service_tier.sql deleted file mode 100644 index ee1637e4e..000000000 --- a/backend/migrations/0004_add_model_request_service_tier.sql +++ /dev/null @@ -1,9 +0,0 @@ -alter table model_requests - add column service_tier text, - add constraint model_requests_service_tier_ck check ( - service_tier is null - or ( - octet_length(service_tier) between 1 and 64 - and service_tier !~ '[[:cntrl:]]' - ) - ); diff --git a/backend/migrations/0005_allow_quota_exhausted_cooldown.sql b/backend/migrations/0005_allow_quota_exhausted_cooldown.sql deleted file mode 100644 index 4a7f3b94d..000000000 --- a/backend/migrations/0005_allow_quota_exhausted_cooldown.sql +++ /dev/null @@ -1,12 +0,0 @@ -alter table provider_accounts - drop constraint provider_accounts_cooldown_ck, - add constraint provider_accounts_cooldown_ck check ( - ( - availability != 'cooldown' - or cooldown_until is not null - ) - and ( - cooldown_until is null - or availability in ('cooldown', 'quota_exhausted') - ) - ); diff --git a/backend/migrations/README.md b/backend/migrations/README.md index 3006980f4..fb912de0e 100644 --- a/backend/migrations/README.md +++ b/backend/migrations/README.md @@ -6,14 +6,16 @@ sqlx 在服务启动时(`serve` 监听之前)按编号顺序执行本目录 ## 冻结规则 -- 已合入 main 的迁移文件**永久冻结**,一个字节都不能改——包括注释和空白。 - 任何改动都会让所有已部署实例(含自更新链路上的存量用户)在下次重启时 - 因 checksum 失配而无法启动,且没有自动恢复路径。 -- schema 变更一律新增编号迁移(`0003_...`、`0004_...`),永远不回改旧文件。 +- 同一大版本内,已合入 main 的迁移文件**永久冻结**,一个字节都不能改——包括注释和 + 空白。任何改动都会让该大版本的已部署实例在下次重启时因 checksum 失配而无法启动, + 且没有自动恢复路径。 +- 同一大版本内的 schema 变更一律新增编号迁移(`0002_...`、`0003_...`),永远不回改 + 旧文件。切换大版本时允许把已完成升级的 schema 折叠成新的 `0001` 基线,因为跨大版本 + 不支持在线升级,目标版本必须使用全新数据目录部署。 - `.frozen-sha256` 是冻结清单(`sha256sum` 格式)。CI 会校验:清单内文件 字节未变,且目录里每个 `*.sql` 都已入册。新增迁移时在同一提交里执行 `sha256sum 000N_xxx.sql >> .frozen-sha256` 入册。 -- 若确需修正已冻结迁移里的错误,用新迁移做补偿性变更(`alter` / 回填), +- 若同一大版本内确需修正已冻结迁移里的错误,用新迁移做补偿性变更(`alter` / 回填), 不要动原文件。 ## 本地测试库 diff --git a/deploy/README.md b/deploy/README.md index 2e98a82ea..8931bc036 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -130,10 +130,8 @@ Provider schema 以明文 JSON 保存于 PostgreSQL。Redis 只保存可重建 ## 镜像升级与源码构建 > [!WARNING] -> 以下命令只适用于同一大版本内的升级。数据库 schema 与配置只在同一大版本内兼容, -> 跨大版本直接换镜像会导致服务无法启动;内置一键自更新会拒绝跨大版本目标(3.0 之前 -> 的旧版没有这道门禁,不要用旧版界面的一键更新跨大版本)。跨大版本请全新部署(全新 -> `.runtime/` 数据目录),并重新导入或重新授权 Provider 账号与客户端 Key。 +> 以下命令只适用于同一大版本内的升级,不支持跨大版本在线升级。跨大版本请使用全新的 +> `.runtime/` 数据目录重新部署,并重新导入或重新授权 Provider 账号与客户端 Key。 ```bash docker compose -f deploy/compose.yaml build codex-proxy-rs diff --git a/docs/architecture.md b/docs/architecture.md index f13590a0c..cdece0ae9 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -170,11 +170,11 @@ Admin API 不要求客户端提交配置 revision,也不向客户端暴露配 客户端可见协议结果。队列不是第二份业务权威,也不做无幂等键的盲目重试。 - retention 只删除已满足保留规则的历史事实,不改变运行中请求。 -业务 schema 由 `backend/migrations/` 下的编号迁移按序定义:`0001_initial.sql` 建表,`0002` 删除 -opaque response ID 唯一索引,`0003` 将 client/upstream response ID 改为无格式假设的 UTF-8 bytes, -`0004` 增加响应观测得到的 `model_requests.service_tier`。已合入的迁移永久冻结—— -`.frozen-sha256` 是冻结清单,CI 校验清单相对 PR base 只增不改,启动时 sqlx 重校验已应用迁移的 -checksum,不一致直接拒绝启动。规则见 `backend/migrations/README.md`。 +业务 schema 由 `backend/migrations/0001_initial.sql` 直接定义当前大版本的完整新库基线,其中 +response ID 使用无格式假设的 UTF-8 bytes,`model_requests.service_tier` 保存响应观测到的服务档位, +且额度耗尽账号可以携带冷却截止时间。同一大版本内已合入的迁移永久冻结—— +`.frozen-sha256` 是冻结清单,CI 校验清单相对 PR base 只增不改,启动时 sqlx 重校验已应用 +迁移的 checksum,不一致直接拒绝启动。规则见 `backend/migrations/README.md`。 ## 9. Redis 终态 diff --git a/skills/codex-proxy-rs/references/repo-guide.md b/skills/codex-proxy-rs/references/repo-guide.md index a79d114a7..0e7579a60 100644 --- a/skills/codex-proxy-rs/references/repo-guide.md +++ b/skills/codex-proxy-rs/references/repo-guide.md @@ -40,9 +40,9 @@ ## 存储 -- PostgreSQL 业务表只有 `0001_initial.sql` 定义的七张;后续编号迁移只做增量。当前 `0002` 删除 - opaque response ID 索引,`0003` 把 response ID 改为 bytes,`0004` 增加响应观测的 service tier。 -- 已应用迁移按字节冻结:`backend/migrations/.frozen-sha256` 是冻结清单,CI 相对 PR base 做 append-only 校验;schema 变更一律新增编号迁移,规则见 `backend/migrations/README.md`。 +- 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 是可丢失热缓存。