Files
codex-proxy-rs/docs/multi-provider-database.md
T
ZyyandCodex 06f3443e56 docs(architecture): define multi-provider gateway target
Document the provider-neutral gateway boundary and its terminal PostgreSQL and Redis data model, including configuration, credentials, routing, attempts, continuation, telemetry, retention, and migration contracts.

Co-authored-by: Codex <noreply@openai.com>
2026-07-17 17:38:11 +08:00

91 KiB
Raw Permalink Blame History

多平台统一 AI 网关终态数据模型

本文定义 多平台统一 AI 网关目标架构 对应的终态 PostgreSQL 与 Redis 数据边界。它是目标设计,不表示当前迁移已经完成;当前生效表仍以 architecture.md 和仓库迁移为准。

本文中的表名、字段归属、事实层级、删除规则和秘密存储规则是终态约束。实际迁移可以分阶段完成,但不能长期保留两套生产事实来源。

设计决策

  1. PostgreSQL 是持久事实的唯一权威来源;Redis 只保存可过期、可重建的协调状态。
  2. Provider 是编译进进程的代码,不创建 providers 表;数据库只保存可以动态配置的 Provider instance。
  3. Router 选择 Provider instance 和 model target,Provider 自己选择 credential。
  4. 调用方隔离和策略单元固定为现有 client_api_key,不提前引入 tenant、organization 或 project。
  5. 下游 client API key 只保存不可逆摘要;上游 API Key、OAuth token、Cookie 等可恢复秘密保存加密 envelope 或外部 secret reference。
  6. 稳定、需要查询的跨平台事实使用普通列;Provider 专属且不参与通用查询的内容使用经过 adapter 校验的 jsonb。
  7. 客户端的一次请求记录为 logical request;每次真实上游调用记录为 attempt。成功、失败和取消不再分散到不同事实表。
  8. logical request usage 表示客户端实际收到的用量;attempt usage 表示上游实际报告、可能产生费用的用量。
  9. 默认不保存 prompt、response body 或原始 Provider 错误正文。Portable continuation 的内容使用独立加密表。
  10. 聚合桶是派生数据,不参与调度、计费结算或请求正确性判断。

类型与命名约定

  • ID 使用带语义前缀的应用生成 text,例如 inst_、cred_、route_、req_、att_。分页始终使用时间与 ID 组合游标,不依赖 ID 的自然顺序。
  • 所有时间点使用 timestamptz,应用按 UTC 读写。
  • token、次数和毫秒使用非负 bigint;状态码使用受范围约束的 integer。
  • 金额使用 numeric(20, 10),禁止浮点;货币使用通过 ^[A-Z]{3}$ 校验的大写三字符代码。
  • jsonb 列只接受 object,并使用 jsonb_typeof(...) = 'object' check;空对象是否有效由字段语义决定,写入 RuntimeSnapshot 前再由 owner adapter 完整校验。
  • 不使用 PostgreSQL enum。稳定生命周期状态使用 text + check;Provider、协议、operation、metric 等可扩展标识由应用注册表校验。
  • 外键只表达真实所有权。配置实体优先 disable 而不是删除;历史事实不能随配置删除而消失。
  • 不为 jsonb 默认创建 GIN index。只有出现稳定查询并有执行计划证据时才增加。
  • text 不代表允许无限输入。名称、外部 ID、错误消息、URL 和 JSON 都必须由 owner 设置长度/字节上限;客户端或 Provider 提供的值在进入数据库前截断或拒绝,不能让遥测成为无界存储入口。
  • 所有数据库生命周期时间由 PostgreSQL 时钟生成;延迟由进程单调时钟计算后写入,不能用两台机器的 wall clock 相减。Provider 提供的时间只能作为带来源的观察值。
  • 所有会改变运行行为的 JSON(policy、config、capabilities、limits、options、state)必须包含正整数 schema_version,由唯一 owner 做版本迁移和完整校验。metadata_json 不是逃避 schema 演进的配置入口。
  • __...__ 前缀保留给聚合维度中的系统 sentinel;Provider、模型、tier、protocol、operation 等外部或注册标识不得使用该前缀。

主键、外键与快照字段命名

  • 表名使用复数名词,独立实体的主键默认命名为 id;只有纯关联表或严格一对一 extension table 才可用复合键或父实体 FK 作主键。
  • 外键字段使用被引用实体的完整单数名加 _id,例如 provider_instance_id、upstream_credential_id、model_route_id 和 gateway_request_id。
  • 同一父表存在多个语义角色时增加角色前缀,例如 actor_admin_user_id。
  • 复合外键字段与父键同名,例如 (provider_instance_id, upstream_model_id)。
  • _id 也可用于明确限定来源的外部协议标识,例如 upstream_request_id 和 client_response_id;这类字段必须带 upstream、client 等来源前缀,不能伪装成本地外键。
  • 配置删除后需要保留的历史维度使用 _ref,例如 provider_instance_ref、model_route_ref 和 resource_ref。_ref 永远不是外键。
  • 不使用含义不清的 route_id、request_id、credential_id、transcript_id 或历史 _key 缩写。

约束和索引显式命名:

pk_<table>
fk_<child_table>__<parent_table>
ux_<table>__<columns>
idx_<table>__<columns>
ck_<table>__<rule>

每个外键都必须有以该外键列开头的主键、唯一索引或普通索引,避免父行更新和删除时扫描子表。不能为了“可能查询”重复创建已经被主键或唯一约束覆盖的同列索引。

身份字段不可重绑定

一个 ID 一旦进入 request/attempt 历史,就不能被改造成另一项资源:

  • Provider instance 的 provider_kind、endpoint authority、region/issuer 等身份字段不可原位替换;变更这些内容创建新 instance。展示名称和非身份参数可以更新。
  • Credential 不能移动到另一 instance,resource_ref 不可修改;普通 secret 轮换只有在 adapter 确认仍是同一上游主体时才沿用原 credential,否则创建新行。
  • Provider model 的复合主键不可更新;模型重命名视为新模型。
  • Model route 的 public_model_id 不可改绑到另一个外部名称;需要改名时创建新 route 并显式迁移调用方策略。
  • Model route target 的 route、instance 和 upstream model 关联不可更新;切换目标时创建新 target,只有 priority、weight、enabled 和 options 可以修改。
  • 已发布 price version、request、attempt、audit 和 conversation item 都是 append/finalize 事实,不能通过 UPDATE 改写身份。

这些规则由 repository API 和契约测试保证;数据库 FK 只防悬空引用,不能自动判断“同一个 ID 的业务含义是否被偷换”。

关系图

admin_users
  `-- admin_audit_events
client_api_keys
  |-- gateway_requests --< request_attempts -- model_price_versions
  |     `-- optional source --> continuation_bindings
  |
  `-- continuation_bindings -- source response --> gateway_requests
          |-- native_state_envelope
          `-- portable --> conversation_transcripts
                            |-- parent --> conversation_transcripts
                            `-- conversation_items

provider_instances
  |-- upstream_credentials
  |     |-- upstream_credential_states
  |     |-- codex_accounts
  |     `-- codex_account_cookies
  |-- provider_models
  |     `-- model_price_versions
  |           `-- model_price_rates
  `-- model_route_targets -- model_routes

ops_events
request_metric_buckets     [derived]
attempt_metric_buckets     [derived]

Writer 所有权

数据 唯一 writer
admin_users、管理会话 Auth owner
client_api_keys、runtime_settings Control-plane settings owner
admin_audit_events Audit owner;控制面修改必须在业务 transaction 内追加
provider_instances、provider_models、routes、prices Control-plane config publisher
upstream_credentials secret/config Provider credential manager
upstream_credential_states Provider state owner
codex_accounts、codex_account_cookies Codex Provider owner
gateway_requests、request_attempts Gateway request/attempt lifecycle
conversation_*、continuation_bindings History/continuation owner
ops_events Ops recorder
两张 metric bucket 唯一 bucket aggregator

API handler、Dashboard query、Provider transport 和后台清理任务不能绕过 owner 直接拼 SQL 修改业务事实。Retention worker 只删除已经满足本文生命周期条件的数据。

身份与全局设置

admin_users

沿用当前管理员表:

字段 类型与约束 语义
id text primary key 管理员 ID
password_hash text not null Argon2 密码摘要
created_at timestamptz not null 创建时间
updated_at timestamptz not null 修改时间

管理员登录会话继续保存在 Redis。runtime_settings.admin_api_key_hash 继续保存高熵管理 Key 的摘要,不新增另一套管理凭据表。

admin_audit_events

控制面修改必须生成不可变、已脱敏的审计事实:

字段 类型与约束 语义
id text primary key Audit event ID
actor_kind text not null admin_session/admin_api_key/system/anonymous
actor_admin_user_id text references admin_users on delete set null 可选管理员 live join
actor_ref text not null Actor 历史伪名;不是 session/API Key 本身
admin_request_id text 管理 HTTP request ID,不是外键
action text not null create、update、disable、delete、rotate 等
entity_kind text not null provider_instance、credential、route、price 等
entity_ref text not null 被修改实体的历史引用,不是外键
config_revision bigint 本次修改发布的结构配置版本
details_json jsonb not null default '{}' 已脱敏字段变化或安全事件详情,不含 secret 值
created_at timestamptz not null 发生时间

约束和索引:

actor_kind in ('admin_session', 'admin_api_key', 'system', 'anonymous')
(created_at desc, id desc)
(entity_kind, entity_ref, created_at desc, id desc)
(actor_admin_user_id, created_at desc, id desc)
  where actor_admin_user_id is not null
(actor_ref, created_at desc, id desc)

Credential 创建、替换和轮换只记录“secret 已变化”、secret reference 类型和新 revision,绝不能记录明文、密文、fingerprint 或外部 secret 路径。管理员登录成功、失败、登出、密码/API Key 轮换和强制删除也进入本表;未认证事件使用 actor_kind = 'anonymous'、空 actor FK 和经过速率限制的安全详情,避免审计表本身成为攻击者制造的无界写入通道。

client_api_keys

下游 Key 只允许在创建时返回一次完整值:

字段 类型与约束 语义
id text primary key Key ID
name text not null 名称
label text 管理标签
prefix text not null 管理端展示前缀
key_hash text not null unique 完整 Key 的 SHA-256 摘要
enabled boolean not null default true 管理员开关
policy_json jsonb not null 带 schema version 的模型 allowlist、速率、并发和预算策略
created_at timestamptz not null 创建时间
updated_at timestamptz not null 修改时间

索引:

unique(key_hash)
(created_at desc, id desc)

该方案只接受网关生成的至少 256 bit CSPRNG Key,因此快速摘要不会把低熵口令变成可离线猜测的凭据;认证比较使用完整摘要且不记录原 Key。若未来允许用户自定义低熵 token,必须改用带独立 pepper 的 keyed hash 或 password KDF,不能继续假设普通 SHA-256 足够。

policy_json 是当前自托管产品的调用方策略边界,由 client-policy owner 校验,至少允许表达:

{
  "schema_version": 1,
  "allowed_model_ids": [],
  "max_concurrency": 0,
  "requests_per_minute": 0,
  "tokens_per_minute": 0,
  "budget": {
    "period": "month",
    "amount": "0.0000000000",
    "currency": "USD"
  }
}

缺省或零值的含义由版本化 validator 明确定义,不能由各 middleware 自行解释。若未来产品真的引入多租户、共享预算或组织权限,需要单独修订本文,而不是现在增加空的 tenant/project 表。

预算 period 默认按 UTC calendar window 计算,边界为 [window_start, window_end);不能由各应用节点本地时区推导。若未来允许调用方时区,时区 ID 本身必须进入冻结的 policy/window 事实并处理规则变更,不能只保存一个模糊的 “month”。

runtime_settings

只保留部署级设置:

字段 类型与约束 语义
id bigint primary key check (id = 1) 单例
config_revision bigint not null default 1 check (> 0) 已发布 RuntimeSnapshot 的单调版本
admin_api_key_hash text 管理 API Key 摘要
usage_retention_days bigint not null check (> 0) request/attempt 原始事实保留期
ops_event_retention_days bigint not null check (> 0) 运维事件保留期
audit_retention_days bigint not null check (> 0) 管理审计保留期
bucket_retention_days bigint not null check (> 0) 聚合桶保留期
updated_at timestamptz not null 修改时间

Client policy、Provider instance、credential 结构配置、model catalog、route 或价格发生有效变化时,控制面必须在同一事务中使用 config_revision = config_revision + 1 returning config_revision 原子推进版本,并写入对应 audit event。数据面使用 repeatable-read 只读事务读取一次 revision 和全部配置,以该一致性快照构建相同 revision 的 RuntimeSnapshot;事务结束后再用一次独立读取与当前 revision 比较,若已经推进则立即重载。Redis 只广播版本;数据面发现版本跳变或丢失通知时同样从 PostgreSQL 重载。

Redis Pub/Sub 不是可靠消息队列。每个数据面实例还必须周期性轮询 config_revision,并轻量比较 (upstream_credential_id, credential_revision, state_revision) 向量;通知只用于缩短收敛时间。即使提交后进程在发布通知前崩溃,其他实例也必须在有界时间内发现变化。轮询发现版本倒退、缺口或无法解析的新 JSON schema 时保留最后一个有效 Snapshot、停止接收依赖新配置的请求并记录运维事件,不能部分套用配置。

所有管理写请求携带读页面时看到的 expected_config_revision。更新事务只在当前 revision 相等时提交,否则返回 conflict 并要求重新读取,避免两个管理员或后台同步任务发生静默 last-write-wins。后台 catalog 同步只能修改自己拥有的 available/last_seen_at 等字段,不能覆盖管理员拥有的 enabled 和手工 metadata。

Provider kind 或 JSON schema 的发布与滚动升级协调:先部署能理解新 schema/adapter 的二进制并确认旧实例已 drain,再允许控制面发布该配置;不能依赖“旧实例碰到未知 JSON 后大概会重载”来完成兼容。这里不为部署清单新增数据库表,兼容矩阵由发布系统和启动前 Registry 校验管理。

OAuth access token 刷新和 credential 运行状态变化不递增全局 config_revision,分别使用 credential_revision 和 state_revision 通知,避免高频刷新触发全量 Snapshot 重建。

config_revision 是一致性 token,不是假装存在的历史配置外键:配置表仍可原位更新,revision 本身不能还原一份已经过期的完整 Snapshot。终态只承诺通过 request/attempt 的 route、target、provider、model、price 和安全 decision metadata 复盘实际执行;若未来出现监管级“完整配置时点回放”要求,必须新增不可变配置归档及独立 retention,不能仅凭一个 revision 数字宣称可重现。

schema_migrations 继续沿用当前严格递增版本、名称和 SQL checksum 设计,不并入业务设置表。

以下当前字段不再属于全局设置:

  • model_aliases_json 迁入 model_routes 和 model_route_targets。
  • rotation_strategy 迁入 model_routes.routing_strategy。
  • max_concurrent_per_account、request_interval_ms 迁入 credential 或 Provider instance 配置。
  • refresh_margin_seconds、refresh_concurrency 迁入 Codex Provider 配置。

Provider、凭据与模型

provider_instances

一行表示一个具体的上游部署、区域或 endpoint:

字段 类型与约束 语义
id text primary key Instance ID
provider_kind text not null 已注册 adapter 名称
name text not null 管理名称
base_url text 可选 endpoint;Bedrock 等平台可为空
enabled boolean not null default true 是否进入 RuntimeSnapshot
max_concurrency integer check (> 0) 可选 instance 总并发上限
request_interval_ms bigint not null default 0 check (>= 0) Instance 请求间隔
config_json jsonb not null 带 schema version 的 region、API version、非秘密 header 等配置
created_at timestamptz not null 创建时间
updated_at timestamptz not null 修改时间

约束与索引:

unique(provider_kind, name)
(provider_kind, enabled)

provider_kind 不设置数据库枚举或固定 check。控制面必须确认名称存在于 Provider Registry,并由对应 adapter 校验 base_url 和 config_json。HTTP endpoint 必须 canonicalize,只允许明确支持的 scheme,拒绝 URL userinfo,并阻断云 metadata 地址;私网与 link-local endpoint 只有在本地模型等场景显式启用 trusted-network policy 后才允许。Transport 在实际连接和每次 redirect 时重新校验解析 IP,默认禁用跨 origin redirect,防止 DNS rebinding 绕过控制面校验。config_json 禁止保存任何秘密。

Instance 限制约束整个 endpoint/pool,credential 限制约束单个资源;两者同时存在时必须同时取得 permit 并遵守更晚的请求间隔。无 credential Provider 仍能使用 instance 限制。

upstream_credentials

一行表示 Provider instance 可选择的一项认证资源:

字段 类型与约束 语义
id text primary key Credential ID
provider_instance_id text not null references provider_instances on delete cascade 所属 instance
name text not null 管理名称
credential_kind text not null api_key、oauth、aws、service_account 等
secret_envelope bytea AEAD 加密的 Provider 专属 secret JSON
secret_key_id text 加密密钥版本
secret_locator text Vault、KMS 或环境变量中的外部 secret 定位符
secret_version text 外部 secret manager 的不可逆版本/etag;envelope 模式为空
credential_fingerprint text Adapter 从稳定 credential 身份生成的 HMAC,用于去重
resource_ref text not null unique 遥测使用的稳定匿名资源 ID;不是外键,也不是 credential fingerprint
credential_revision bigint not null default 1 check (> 0) Secret 或 credential 配置版本
enabled boolean not null default true 管理员开关
max_concurrency integer check (> 0) 可选凭据级并发上限
request_interval_ms bigint not null default 0 check (>= 0) 凭据请求间隔
metadata_json jsonb not null default '{}' 可展示、不可含秘密的 Provider metadata
created_at timestamptz not null 创建时间
updated_at timestamptz not null 修改时间

Secret 存储必须满足二选一:

exactly one of secret_envelope and secret_locator is not null
secret_envelope is not null <-> secret_key_id is not null
secret_envelope is not null -> secret_version is null
secret_locator is not null <-> secret_version is not null
secret_locator is not null -> secret_key_id is null

没有认证要求的 instance 不创建 credential 行。一个 instance 就是一个 credential pool;如果同一 endpoint 需要互相隔离的资源池,创建两个 instance,不新增 credential group 抽象。

外部 secret_locator 不能指向一个会在数据库不知情时任意变更的“最新值”:secret manager integration 必须把可比较的版本/etag 写入 secret_version,发现变化后以 CAS 推进 credential_revision 并通知数据面;否则 in-memory lease、attempt snapshot 和审计无法知道哪一代凭据真正被使用。

credential_fingerprint 不能直接 HMAC 整个 secret envelope:OAuth access token 和 refresh token 可能轮换。Adapter 必须选择稳定身份,例如 API Key 本身或已经验证的 Provider subject;无法获得稳定身份时保持为空,并使用 Provider 专属唯一约束。

resource_ref 在 credential 创建时由 identity pseudonym key 和经过 adapter 规范化的稳定身份生成;没有稳定身份时以随机值生成并永久复制到历史事实。它不能等于 credential_fingerprint,也不能从邮箱、Key prefix 等可识别字段直接拼接。Secret 轮换不改变同一 credential 的 resource_ref;删除后重新导入是否继承历史身份只能由 adapter 在能够安全证明同一主体时决定。

稳定身份的 HMAC 输入必须包含 Provider/issuer namespace,避免两个无关 OpenAI-compatible endpoint 上碰巧相同的 Key 被视为同一资源。全局 unique(resource_ref) 同时阻止把同一上游资源复制到多个 instance 来绕过 quota/concurrency;若以后确有一个资源合法挂载多个 endpoint 的需求,应显式引入 resource 与 instance attachment 模型并按 resource 共用 lease,不能复制 credential 行。

索引:

(provider_instance_id, enabled)
unique(provider_instance_id, name)
unique(provider_instance_id, credential_fingerprint)
  where credential_fingerprint is not null
unique(id, provider_instance_id)
(updated_at, id)

Secret 刷新、任何 Provider 专属认证材料(例如 Cookie)变更或 credential 配置更新都使用 credential_revision 做 compare-and-swap,并发布 credential 级变更通知。它不修改全局 config_revision,除非 enabled、并发限制或其他会改变 Route Plan 的结构配置同时变化。

upstream_credential_states

Credential 配置和高频运行状态具有不同写入频率,必须分表:

字段 类型与约束 语义
upstream_credential_id text primary key references upstream_credentials on delete cascade Credential
availability text not null default 'unknown' unknown/ready/cooldown/exhausted/invalid
availability_reason text 已脱敏原因
cooldown_until timestamptz 跨重启仍需遵守的冷却截止时间
state_json jsonb not null 带 schema version 的 quota、reset window 等 Provider 状态
state_revision bigint not null default 1 check (> 0) 状态 CAS 版本
observed_at timestamptz not null 产生当前状态事实的时间
updated_at timestamptz not null 持久化时间

约束和索引:

availability in ('unknown', 'ready', 'cooldown', 'exhausted', 'invalid')
(availability = 'cooldown') = (cooldown_until is not null)
(availability, cooldown_until)
(updated_at, upstream_credential_id)

状态更新必须使用 state_revision CAS,并由 Provider owner 合并并发事实;旧 observed_at 的结果不能覆盖更新状态。只有 availability/cooldown/quota 等有效状态发生变化或专门的状态刷新任务取得新观察时才持久化,普通成功 attempt 不触碰本行。进程内 semaphore、在途 lease 和连接状态仍不写 PostgreSQL,Redis 只缓存这张表可恢复的热状态。

最后使用/成功/失败时间从 request_attempts 的 credential 索引或 metric bucket 查询,不回写本行;否则热门 credential 每个请求都更新同一行,会制造行锁热点、WAL 和 autovacuum 压力,也形成第二套可漂移事实。Client API key 的最后使用时间同理从 gateway_requests 查询。

创建 upstream_credentials 时必须在同一事务中创建唯一 state 行,不能用“缺少 state 行代表 unknown”形成第二套隐式状态。credential_revision 与 state_revision 各自单调递增,互不覆盖。

codex_accounts

Codex 是当前唯一需要可查询专属字段的 Provider,一行与一个 OAuth credential 一对一:

字段 类型与约束 语义
upstream_credential_id text primary key references upstream_credentials on delete cascade 对应 credential
email text ChatGPT 邮箱
chatgpt_account_id text ChatGPT account ID
chatgpt_user_id text ChatGPT user ID
plan_type text 订阅计划
access_token_expires_at timestamptz access token 到期时间
next_refresh_at timestamptz 下次计划刷新时间

唯一索引沿用当前身份约束:

unique(chatgpt_account_id, coalesce(chatgpt_user_id, ''))
  where chatgpt_account_id is not null

Access token 和 refresh token 位于 upstream_credentials.secret_envelope,本表不得保存明文。Quota、Cloudflare cooldown 和 quota verification 等运行状态归入 upstream_credential_states.state_json,避免同一事实存在两个 writer。

Codex owner 在创建 account/cookie 的同一事务中验证 credential 所属 instance 的 provider_kind = 'codex' 且 credential kind 与认证流程匹配;普通 FK 只能证明 credential 存在,不能证明它属于正确 adapter。该跨表 invariant 必须进入数据库契约测试。

codex_account_cookies

字段 类型与约束 语义
id text primary key Cookie ID
upstream_credential_id text not null references upstream_credentials on delete cascade Codex credential
domain text not null Domain
name text not null Cookie name
value_envelope bytea not null 加密 Cookie value
secret_key_id text not null 加密密钥版本
path text not null default '/' Cookie path
host_only boolean not null 无 Domain attribute 时为 true,禁止错误发送给子域
secure boolean not null 只允许通过安全 transport 发送
expires_at timestamptz 过期时间
updated_at timestamptz not null 修改时间

索引:

unique(upstream_credential_id, domain, name, path)
(expires_at) where expires_at is not null

Cookie owner 只持久化 Provider allowlist 中确有业务需要的名称。捕获 Set-Cookie 时,Domain 必须 domain-match 实际响应 origin 且不能是 public suffix;无 Domain attribute 时保存 host_only = true。重放时同时检查 domain、host-only、path、secure 和 expiry,不能因为数据库里已有一行就向另一个 Provider endpoint 发送。

Cookie upsert 与父 credential 的 credential_revision 递增必须在同一事务内完成;否则 attempt 无法准确冻结它使用的是哪一代认证材料,旧 Cookie 的失败也可能污染新会话状态。

其他 Provider 只有在出现真实、需要查询的专属状态时才增加自己的扩展表。普通 OpenAI、Anthropic、Google 或 xAI API Key 不创建空扩展表。

provider_models

一行表示一个 instance 上的真实模型:

字段 类型与约束 语义
provider_instance_id text not null references provider_instances on delete cascade 所属 instance
upstream_model_id text not null 上游真实模型 ID
display_name text 展示名称
catalog_source text not null discovered 或 manual
enabled boolean not null default true 管理员开关
available boolean not null default true 最近 catalog 是否仍存在
capabilities_json jsonb not null 带 schema version 的 operation 和 feature 能力
limits_json jsonb not null 带 schema version 的 context、输出等限制
metadata_json jsonb not null default '{}' Provider 原始目录的安全子集
last_seen_at timestamptz 最近同步出现时间
created_at timestamptz not null 创建时间
updated_at timestamptz not null 修改时间

主键:

primary key(provider_instance_id, upstream_model_id)
catalog_source in ('discovered', 'manual')

Catalog 同步发现模型消失时设置 available = false,不能删除仍被 model route target 或历史价格引用的模型。只有 enabled、available、capability 或 limits 的有效值发生变化时才递增全局 config_revision;单纯更新 last_seen_at 不触发 Snapshot 重建。

capabilities_json 不是开放式能力布尔值集合,而是带来源的声明:对每项能力区分 native/emulated/unsupported/unknown,并记录由 adapter 代码、manual override 或 probe 得到。Router 对 unknown 默认不路由;probe 失败不会把未知能力当成支持,协议 adapter 的 emulation 也必须显式进入 Route Plan。

Catalog 拉取必须先得到一个完整、成功的远端 snapshot,再在单个数据库事务中 upsert 本轮模型并只对 catalog_source = 'discovered' 的缺失行设置 unavailable。分页中断、鉴权失败或部分响应不得把未见到的模型批量下线;手工模型也不受 discovery 缺失处理影响。

Router 从编译后的 capability snapshot 过滤 instance 级目标,不在请求热路径查询 JSON。某个 credential 因套餐、授权或区域无法使用模型时,由 Provider credential selector 根据专属 metadata/state 再过滤;不为此创建一张所有 Provider 都必须填充的空映射表。

model_routes

一行表示一个对客户端公开的模型:

字段 类型与约束 语义
id text primary key Route ID
public_model_id text not null unique 客户端模型 ID
description text 管理说明
enabled boolean not null default true 是否公开
routing_strategy text not null priority/weighted/latency/cost/balanced/sticky
policy_json jsonb not null 带 schema version 的 fallback、最大 attempts、预算等策略
created_at timestamptz not null 创建时间
updated_at timestamptz not null 修改时间

约束:

unique(id, public_model_id)

routing_strategy 由应用注册表校验,不设置数据库 check,以便增加组合策略时不修改数据库类型。控制面只有在 route 至少存在一个 enabled target 且所有 target/options 都通过校验后,才能发布新的 config_revision。

model_route_targets

一行表示 route 的一个候选真实模型:

字段 类型与约束 语义
id text primary key Target ID
model_route_id text not null references model_routes on delete cascade 所属 route
provider_instance_id text not null 目标 instance
upstream_model_id text not null 真实模型 ID
priority integer not null default 100 check (>= 0) 数字越小优先级越高
weight integer not null default 1 check (> 0) 加权策略权重
enabled boolean not null default true 是否参与候选
options_json jsonb not null 带 schema version 的 service tier 等目标级非秘密选项
created_at timestamptz not null 创建时间
updated_at timestamptz not null 修改时间

约束与索引:

foreign key(provider_instance_id, upstream_model_id)
  references provider_models(provider_instance_id, upstream_model_id)
  on delete restrict

unique(model_route_id, provider_instance_id, upstream_model_id)
(model_route_id, enabled, priority, id)
(provider_instance_id, upstream_model_id)

Target 不引用 credential。Provider 必须在执行 attempt 时从 instance 的 credential pool 中选择资源。

model_price_versions

一行表示一个模型和 service tier 从某个时间点开始生效的一整套价格。价格版本发布后不可修改:

字段 类型与约束 语义
id text primary key Price version ID
provider_instance_id text not null Instance
upstream_model_id text not null 真实模型 ID
service_tier text not null default '__default__' 规范化 tier;sentinel 表示 Provider 默认档位
currency text not null 大写货币代码
effective_from timestamptz not null 生效时间
metadata_json jsonb not null default '{}' 价格来源和说明
created_at timestamptz not null 创建时间

约束与索引:

foreign key(provider_instance_id, upstream_model_id)
  references provider_models(provider_instance_id, upstream_model_id)
  on delete restrict

unique(
  provider_instance_id,
  upstream_model_id,
  service_tier,
  effective_from
)

unique(id, provider_instance_id, upstream_model_id, service_tier, currency)

currency ~ '^[A-Z]{3}$'

一次 attempt 在发送前从当时的 RuntimeSnapshot 选择 effective_from <= started_at 的最新版本,并把 version ID 写入 attempt 起始行;在途请求不受后来发布或回溯生效的版本影响。没有 effective_to,因此不存在重叠区间;价格修正通过新增版本完成,不能回写已经被 attempt 使用的历史版本。

model_price_rates

价格版本中的每个计费 metric 一行:

字段 类型与约束 语义
model_price_version_id text not null references model_price_versions on delete cascade 所属价格版本
metric text not null input_token、image、audio_second 等
unit_size numeric(20, 6) not null check (> 0) 报价单位数量
amount numeric(20, 10) not null check (>= 0) 每单位价格

主键:

primary key(model_price_version_id, metric)

Version 与至少一条 rate 必须在一个事务中创建并完整验证,再递增 config_revision 发布;空版本不能发布。没有匹配版本表示 unknown;版本存在但缺少本次 usage 所需 metric 表示 partial。零价格必须由 amount = 0 的明确 rate 表达。不同货币永远分组展示和统计,系统没有汇率事实时禁止相加。

Partial 的 estimated_cost_amount 只表示已知 metric 的下界,不能作为完整成本参与 strict budget 释放;unknown 没有金额。只有 known(包括由明确零价 rate 算出的 known zero)可以作为完整估算展示。

metric 来自 Gateway 的版本化 normalized-usage registry,并与 request/attempt 的 usage_metrics_json 使用同一名称和单位。当前 price rate 只表达 usage × unit price 的线性定价;阶梯价、包月抵扣、承诺消费和 Provider invoice adjustment 不允许塞入 metadata_json 或临时公式。真正需要这些能力时应单独设计账务边界,在此之前标记 partial/unknown。

Price repository 对已发布版本只提供读取,不提供更新;更正必须新增 version。成本路由遇到 unknown/partial price 时不能把它当作零成本,严格预算只能选择与预算 currency 相同且费率完整的目标,否则必须按 policy 明确拒绝或降级。

计费计算全程使用十进制定点数,按 usage × amount / unit_size 计算每个 metric 后求和,只在持久化 estimated_cost_amount 时按 10 位小数统一舍入;Rust 业务代码禁止经过 f32/f64。

cost_breakdown_json 只保存 rate metric、price version、非负十进制字符串数量/金额和舍入说明;它不是 Provider 原始账单响应,也不允许 JSON 浮点。

这里的 estimated_cost_amount 是按当时价格目录计算的网关成本估算,不是 Provider 发票、付款或会计结算事实。预算可以基于该保守估算执行,但 UI 和 API 必须标注 estimated;未来若接入账单对账,应新增独立的 invoice/reconciliation 模型,不能覆盖已记录的请求价格快照来伪装成实际账单。

请求、Attempt 与用量

gateway_requests

请求被接受后立即插入一行,所有终止路径只 finalize 一次:

字段 类型与约束 语义
id text primary key Logical request ID
client_api_key_id text references client_api_keys on delete set null 调用方 Key;内部任务可为空
client_api_key_ref text 调用方 Key ID 历史快照;内部任务可为空
config_revision bigint not null check (> 0) 本请求冻结 Route Plan 时的 RuntimeSnapshot 版本
protocol text not null OpenAI Responses、Anthropic Messages 等
operation text not null generate/embed/rerank/...
client_transport text not null http_json/http_sse/websocket 等入站 transport
model_route_id text 冻结时选中的 route;与 public model 组成复合 FK
model_route_ref text Route ID 历史快照
requested_model_id text not null 客户端原始模型 ID 快照
public_model_id text 成功解析后的对外模型 ID 快照;未解析时为空
continuation_binding_id text 本请求使用的 source binding;与 client key 组成复合 FK
continuation_binding_ref text Source binding ID 历史快照
stream boolean not null 客户端是否请求流式返回
outcome text not null default 'running' running/succeeded/failed/cancelled/incomplete
client_status_code integer 最终客户端 HTTP/协议状态
client_response_id text 网关暴露的 response ID
error_kind text 稳定 Gateway error taxonomy
error_message text 已脱敏的客户端安全消息
budget_reserved_amount numeric(20, 10) 接入时持久化的保守预算预留
budget_limit_amount numeric(20, 10) 本请求冻结的预算上限
budget_currency text 预留货币
budget_window_start timestamptz 本请求所属预算窗口起点
budget_window_end timestamptz 本请求所属预算窗口终点(exclusive)
input_tokens bigint 客户端逻辑响应 usage
output_tokens bigint 客户端逻辑响应 usage
cached_tokens bigint 客户端逻辑响应 usage
cache_write_tokens bigint 客户端逻辑响应 usage
reasoning_tokens bigint 客户端逻辑响应 usage
total_tokens bigint Provider 报告或规范化总量
usage_metrics_json jsonb not null default '{}' 非 token 的注册 normalized usage metric
first_token_ms bigint 从请求开始到首个客户端可见 token
latency_ms bigint 请求总耗时
metadata_json jsonb not null default '{}' 不含正文和秘密的请求事实
started_at timestamptz not null 开始时间
deadline_at timestamptz not null 接入时冻结的绝对执行截止时间
completed_at timestamptz 终止时间

约束:

outcome in ('running', 'succeeded', 'failed', 'cancelled', 'incomplete')
client_status_code is null or client_status_code between 100 and 599
foreign key(model_route_id, public_model_id)
  references model_routes(id, public_model_id)
  on delete set null (model_route_id)

foreign key(continuation_binding_id, client_api_key_id)
  references continuation_bindings(id, client_api_key_id)
  on delete set null (continuation_binding_id)
所有 token、first_token_ms、latency_ms 为 null 或 >= 0
预算五字段 reserved/limit/currency/window_start/window_end 必须同时为 null 或同时非 null
budget_reserved_amount is null or (budget_reserved_amount >= 0 and budget_limit_amount >= budget_reserved_amount)
budget_currency is null or budget_currency ~ '^[A-Z]{3}$'
budget_window_start is null or (client_api_key_ref is not null and budget_window_start < budget_window_end)
client_api_key_id is null or (client_api_key_ref is not null and client_api_key_ref = client_api_key_id)
model_route_id is null or (model_route_ref is not null and model_route_ref = model_route_id)
model_route_id is null or public_model_id is not null
continuation_binding_id is null or (continuation_binding_ref is not null and continuation_binding_ref = continuation_binding_id)
continuation_binding_id is null or client_api_key_id is not null
started_at <= deadline_at
running -> completed_at is null
非 running -> completed_at is not null and completed_at >= started_at

索引:

(started_at desc, id desc)
(client_api_key_id, started_at desc, id desc)
(client_api_key_ref, started_at desc, id desc) where client_api_key_ref is not null
(model_route_id, started_at desc, id desc) where model_route_id is not null
(public_model_id, started_at desc, id desc) where public_model_id is not null
(continuation_binding_id, client_api_key_id, started_at desc, id desc)
  where continuation_binding_id is not null
(continuation_binding_ref, started_at desc, id desc) where continuation_binding_ref is not null
(outcome, started_at desc, id desc)
(deadline_at, id) where outcome = 'running'
(client_api_key_ref, budget_window_start, budget_window_end, budget_currency)
  where budget_reserved_amount is not null
unique(client_response_id) where client_response_id is not null
unique(id, client_response_id, client_api_key_id)

client_api_key_ref、model_route_ref 和 continuation_binding_ref 在创建请求时与对应 live FK 使用同一个 ID;只要 live FK 非空,数据库 check 就要求对应 _ref 相等。配置删除后 FK 可以置空,历史维度仍然可查询。Continuation owner 只能通过当前 client key 解析 response ID 得到 continuation_binding_id,API handler 不能自行填入其他调用方的 binding。requested_model_id 是有严格长度上限、默认不建索引的诊断原值,public_model_id 是 Router 成功解析后的稳定业务维度,不能互相冒充。严格预算请求必须冻结完整预算窗口和上限,使 policy 在请求执行中途被修改、Key 被禁用或 Redis 丢失时,reservation 仍可从事实表确定性重建。Policy 更新以新窗口生效;若管理员要求当前窗口立即生效,控制面必须先在 PostgreSQL 中按新规则重算并成功重建 Redis,再发布新 revision。

incomplete 只表示 Gateway 无法确认完整终态(例如进程终止或持久化失败后的 recovery),不是 OpenAI response.incomplete、max-token finish reason 等合法 Provider 结果;后者若已按协议完整交付仍记为 succeeded,并在规范化 finish metadata 中表达。

Token 列以 null 表示 Provider/协议没有给出可信值,以 0 表示明确观测到零,不能互换。usage_metrics_json 只保存 registry 中带固定单位的 metric 到非负十进制字符串的映射,例如 {"image.output.count":"2","audio.input.second":"12.345"},禁止 JSON 浮点和 Provider 原始 usage body;它与固定 token 列一起构成价格计算输入,并继续汇总到长期 bucket。

本表不保存 Provider、credential 或 upstream model,它们属于 attempt。本表 usage 只表示最终交付给调用方的逻辑结果,不能用于计算重试产生的上游目录价格估算。

成功 request 必须存在且只存在一个 downstream-committed attempt;在鉴权后但调用上游前失败的 request 可以没有 attempt。Request usage 来自客户端实际收到的 canonical 结果,不能简单复制最后 attempt 或把多次 attempt 相加,尤其是在协议转换、截断和部分流式输出场景。

request_attempts

Provider 选定执行资源、准备发送上游请求时插入一行;未越过 commit 也必须记录:

字段 类型与约束 语义
id text primary key Attempt ID
gateway_request_id text not null references gateway_requests on delete cascade Logical request
attempt_index integer not null check (> 0) 请求内从 1 开始的序号
trigger text not null initial/credential_retry/target_retry/route_fallback
model_route_target_id text references model_route_targets on delete set null 当前可关联 route target
model_route_target_ref text Target ID 历史快照
provider_instance_id text references provider_instances on delete set null 当前可关联 instance
provider_instance_ref text not null Instance ID 历史快照
provider_kind text not null Provider 名称快照
upstream_credential_id text 当前可关联 credential;与 instance 组成复合 FK
upstream_credential_revision bigint 本 attempt 实际使用的 credential secret/config revision
resource_ref text not null 匿名资源快照;无 credential 时为 __none__
upstream_model_id text not null 实际上游模型 ID
service_tier text not null default '__default__' 规范化后的实际 tier
upstream_transport text not null HTTP/SSE/WebSocket 等上游 transport
upstream_send_state text not null default 'not_sent' not_sent/sent/ambiguous
outcome text not null default 'running' running/succeeded/failed/cancelled/incomplete
downstream_committed boolean not null default false 是否向客户端越过 commit 边界
upstream_status_code integer 上游 HTTP/协议状态
provider_error_code text Provider 原始稳定 code
failure_kind text 稳定 Provider failure taxonomy
retry_after_ms bigint 上游建议等待时间
upstream_request_id text 上游 request ID
upstream_response_id text adapter 分类为非 bearer 的上游诊断 response ID
input_tokens bigint 本 attempt 上游 usage
output_tokens bigint 本 attempt 上游 usage
cached_tokens bigint 本 attempt 上游 usage
cache_write_tokens bigint 本 attempt 上游 usage
reasoning_tokens bigint 本 attempt 上游 usage
total_tokens bigint 本 attempt 上游 usage
usage_metrics_json jsonb not null default '{}' 非 token 的注册 normalized usage metric
model_price_version_id text references model_price_versions on delete restrict 使用的价格版本;与模型/tier/currency 另组成复合 FK
cost_estimate_status text not null default 'not_applicable' not_applicable/known/partial/unknown
estimated_cost_amount numeric(20, 10) 本 attempt 已知或部分已知的目录价格估算
estimated_cost_currency text 估算货币
cost_breakdown_json jsonb not null default '{}' 各 metric 的估算明细和舍入依据
headers_ms bigint 上游 headers 延迟
first_event_ms bigint 上游首事件延迟
first_token_ms bigint 上游首 token 延迟
latency_ms bigint Attempt 总耗时
metadata_json jsonb not null default '{}' 已脱敏 transport 和诊断事实
started_at timestamptz not null 开始时间
deadline_at timestamptz not null 本 attempt 冻结的绝对截止时间
completed_at timestamptz 终止时间

约束:

unique(gateway_request_id, attempt_index)
unique(gateway_request_id) where downstream_committed

foreign key(upstream_credential_id, provider_instance_id)
  references upstream_credentials(id, provider_instance_id)
  on delete set null (upstream_credential_id)

foreign key(model_price_version_id, provider_instance_id, upstream_model_id, service_tier, estimated_cost_currency)
  references model_price_versions(id, provider_instance_id, upstream_model_id, service_tier, currency)
  on delete restrict

trigger in ('initial', 'credential_retry', 'target_retry', 'route_fallback')
upstream_send_state in ('not_sent', 'sent', 'ambiguous')
outcome in ('running', 'succeeded', 'failed', 'cancelled', 'incomplete')
upstream_status_code is null or upstream_status_code between 100 and 599
retry_after_ms、token 和延迟字段为 null 或 >= 0
cost_estimate_status in ('not_applicable', 'known', 'partial', 'unknown')
cost_estimate_status == 'not_applicable' -> upstream_send_state == 'not_sent' 且 estimated_cost_amount、estimated_cost_currency、model_price_version_id 均为 null
upstream_send_state == 'not_sent' -> cost_estimate_status == 'not_applicable'
upstream_send_state in ('sent', 'ambiguous') -> cost_estimate_status != 'not_applicable'
cost_estimate_status == 'unknown' -> estimated_cost_amount、estimated_cost_currency 均为 null;model_price_version_id 可为空或记录已经找到但 usage 不足以计算的版本
cost_estimate_status in ('known', 'partial') -> estimated_cost_amount、estimated_cost_currency、model_price_version_id 均非 null
estimated_cost_amount is null or estimated_cost_amount >= 0
estimated_cost_currency is null or estimated_cost_currency ~ '^[A-Z]{3}$'
model_route_target_id is null or (model_route_target_ref is not null and model_route_target_ref = model_route_target_id)
provider_instance_id is null or provider_instance_ref = provider_instance_id
upstream_credential_id is not null -> provider_instance_id is not null and resource_ref != '__none__'
upstream_credential_id is not null -> upstream_credential_revision is not null
upstream_credential_revision is null or upstream_credential_revision > 0
started_at <= deadline_at
running -> completed_at is null
非 running -> completed_at is not null and completed_at >= started_at

索引:

(model_route_target_id, started_at desc, id desc) where model_route_target_id is not null
(model_route_target_ref, started_at desc, id desc) where model_route_target_ref is not null
(provider_instance_id, started_at desc, id desc) where provider_instance_id is not null
(provider_instance_ref, started_at desc, id desc)
(upstream_credential_id, started_at desc, id desc) where upstream_credential_id is not null
(resource_ref, started_at desc, id desc) where resource_ref is not null
(upstream_model_id, started_at desc, id desc)
(upstream_transport, started_at desc, id desc)
(model_price_version_id) where model_price_version_id is not null
(failure_kind, started_at desc, id desc) where failure_kind is not null
(provider_instance_ref, upstream_request_id) where upstream_request_id is not null
(provider_instance_ref, upstream_response_id) where upstream_response_id is not null
(deadline_at, id) where outcome = 'running'

model_route_target_ref、provider_instance_ref、provider_kind、resource_ref 和 upstream_model_id 是刻意保存的事实快照。配置实体被删除后,历史 attempt 仍可正确统计。Attempt 从 credential 行复制 resource_ref;它不能包含邮箱、API Key prefix 或其他身份信息。

provider_kind、instance ref、target ref 和实际 model 一律由 Attempt Coordinator 从已验证的 Route Plan/Provider result 派生,不能信任 API handler 或 Provider 错误对象中的任意字符串。插入时若 live instance/target 仍存在,owner 必须验证它们的 Provider kind 与目标模型一致;删除后的历史只依赖快照。

deadline_at 由 parent request 的冻结 deadline 与 target/Provider 更短 timeout 取最小值,不能晚于 parent request;这个跨表约束由 Attempt Coordinator 的同一行锁事务验证,防止一个 retry 悄悄越过客户端已经放弃的执行时间。

Attempt 还冻结所用的 upstream_credential_revision。Provider state owner 收到认证/配额结果时必须把该 revision 一并比较:旧 revision 的失败可以记录诊断,但不能把已经轮换的新 secret 标成 invalid 或覆盖其 cooldown/quota 状态。

upstream_send_state 与 downstream_committed 是两个独立边界:前者决定上游是否可能处理或计费,后者决定网关是否还能向客户端切换 attempt。一个 downstream-committed attempt 随后仍可 failed 或 incomplete;downstream commit 不等于成功。ambiguous 表示 payload 可能已送达但结果未知,默认禁止自动重放。

发送状态采用保守转换:attempt 初始为 not_sent/not_applicable;紧邻实际网络 send 之前,先 CAS 持久化为 ambiguous/unknown;transport 明确确认发送后改为 sent,只有能够证明业务 payload 零字节发送时才允许回到 not_sent/not_applicable。因此进程在 send 边界崩溃只会留下 ambiguous,不会把可能已处理的请求误判为可安全重放。

failure_kind 使用跨 Provider 稳定分类:invalid_request、unsupported、unauthorized、permission_denied、rate_limited、quota_exhausted、timeout、transport、protocol、unavailable、cancelled 和 process_terminated。Provider 原始错误 code 只用于诊断,不能代替该分类。

Attempt insert 是请求级上游 exchange 的持久化屏障:只有 gateway_requests 和本次 request_attempts 行都已提交,Provider 才能发起携带本次 credential 的请求级 handshake 或发送可能产生费用、副作用的 payload。Provider 内部不得隐藏 credential retry;每一次可能到达上游的业务调用都必须对应独立 attempt。与业务请求无关、尚未发送 payload 的后台 preconnect 只记录 transport/ops telemetry,不伪造 attempt。

HTTP/SDK/WebSocket transport 的自动 retry 和携带 payload 的自动 redirect 默认关闭;否则底层可能产生数据库看不见的第二次上游调用。确需 redirect 时由 Provider 校验目标并交回 Attempt Coordinator,以独立 attempt 记录;仅连接建立阶段且能够证明 payload 未发送的内部重试可以留在同一 attempt。

创建 attempt 的事务和把 attempt 改为 downstream_committed = true 的事务都必须锁定对应 gateway_requests 行。前者确认 request 仍为 running、尚无 committed attempt,并在锁内分配下一个 attempt_index;后者在同一锁内再次确认没有竞争者。这样不是只依赖 partial unique index“事后发现冲突”,也不会由两个并发 retry 从 max(attempt_index) + 1 得到相同序号,commit 后的新 attempt 也被串行拒绝。锁只覆盖同一个 logical request,不是全局热路径锁。

Request 和 attempt finalize 都使用 compare-and-swap:只允许把 running 更新为一个 terminal outcome,重复 finalizer 不得再次写 usage 或 bucket。Downstream-committed partial unique index 只保证最多一个已提交客户端的 attempt;“commit 后不能创建新 attempt”仍由 Attempt Coordinator 的状态机和契约测试保证,不使用数据库 trigger。

最终 attempt、logical request、continuation binding 与 portable transcript metadata 的终态写入使用同一 PostgreSQL 事务。只有 terminal CAS 实际更新一行时,事务内才调用唯一 bucket aggregator 应用对应 delta;CAS 未命中时不能再次累加。需要 continuation 的响应在该事务成功前不能发送最后 terminal event。

进程异常可能留下 running 行。启动和 recovery worker 必须依据持久化 deadline_at,把已经超时且不存在活动 lease 的 request/attempt 收敛为 incomplete,并记录稳定的 process-termination failure;不能依赖已经变化的 timeout 配置,也不能永久把它们算作在途请求。Retention worker 只在 recovery 完成后删除终态事实,不兼任状态推断。

Continuation 与可移植历史

conversation_transcripts

每一行是一个不可变的 portable continuation canonical transcript snapshot:

字段 类型与约束 语义
id text primary key Transcript ID
client_api_key_id text not null references client_api_keys on delete restrict 所属调用方
parent_conversation_transcript_id text 上一个 snapshot;同一调用方内的自引用复合 FK
canonical_version integer not null check (> 0) Canonical item schema 版本
metadata_json jsonb not null default '{}' 不含对话正文的摘要
created_at timestamptz not null 创建时间
expires_at timestamptz not null 自动清理时间

索引:

(client_api_key_id, created_at desc, id desc)
unique(id, client_api_key_id)
(parent_conversation_transcript_id, client_api_key_id)
  where parent_conversation_transcript_id is not null
(expires_at)

foreign key(parent_conversation_transcript_id, client_api_key_id)
  references conversation_transcripts(id, client_api_key_id)
  on delete restrict

parent_conversation_transcript_id is null or parent_conversation_transcript_id != id

一个 portable continuation 从前一个 binding 的 snapshot 创建新的子 snapshot,只写入本轮 canonical input/output delta;parent 一经插入不可更新,旧 snapshot 和它的 items 永不 append 或改写。因此多个 response ID 从同一历史分支继续时,互不看到对方后续内容,也不会形成循环链。读取上下文按 parent 链从根到叶拼接,并设置受控最大深度;超过阈值时 history owner 创建新的 materialized root、解密后重新加密必要 items,绝不修改已经绑定给旧 response 的 snapshot。Materialized root 继承链上最早的 expires_at,不能借 compaction 延长用户正文保留期。

子 snapshot 的 expires_at 不得晚于 parent,且 owner 必须拒绝跨 client API key 建链。这个跨行时间规则与 parent owner FK 一起保证 retention 可以从叶子向根删除,而不会让较新的 continuation 指向已清理历史。

conversation_items

对话内容以 snapshot delta 保存,不以明文 JSON 保存:

字段 类型与约束 语义
conversation_transcript_id text not null references conversation_transcripts on delete cascade Transcript
sequence bigint not null check (>= 0) 当前 snapshot delta 内的 Canonical 顺序
item_kind text not null client_input/assistant_output/tool_result/...
item_envelope bytea not null 加密 canonical item
secret_key_id text not null 加密密钥版本
token_estimate bigint check (token_estimate is null or token_estimate >= 0) 可选上下文预算估算
created_at timestamptz not null 写入时间

主键:

primary key(conversation_transcript_id, sequence)

图片、音频和文件只保存受控资源引用,禁止把 base64 大对象写进 item_envelope。

continuation_bindings

一行表示客户端 response ID 的后续请求如何恢复:

字段 类型与约束 语义
id text primary key Binding ID
client_response_id text not null unique 网关暴露的 response ID
gateway_request_id text not null 来源 logical request;与 response/key 组成复合 FK
client_api_key_id text not null references client_api_keys on delete restrict 所属调用方
mode text not null native 或 portable
provider_instance_id text references provider_instances on delete restrict Native owner
upstream_credential_id text 需要固定资源时填写;与 instance 组成复合 FK
native_state_envelope bytea 加密的 Provider continuation handle/state
native_state_schema_version integer Provider native state schema 版本
secret_key_id text Native envelope 加密密钥版本
native_reuse_mode text not null default 'not_applicable' reusable/single_use/not_applicable
native_use_state text not null default 'not_applicable' single-use binding 的持久化消费状态
native_consumer_gateway_request_id text references gateway_requests on delete set null single-use binding 当前/最后 consumer request
native_use_revision bigint not null default 1 check (> 0) single-use state CAS 版本
conversation_transcript_id text Portable transcript;与 client key 组成复合 FK
metadata_json jsonb not null default '{}' connection scope 等非秘密绑定信息
created_at timestamptz not null 创建时间
updated_at timestamptz not null 修改时间
expires_at timestamptz not null 自动清理时间

模式约束:

mode in ('native', 'portable')

foreign key(gateway_request_id, client_response_id, client_api_key_id)
  references gateway_requests(id, client_response_id, client_api_key_id)
  on delete restrict

foreign key(conversation_transcript_id, client_api_key_id)
  references conversation_transcripts(id, client_api_key_id)
  on delete restrict

foreign key(upstream_credential_id, provider_instance_id)
  references upstream_credentials(id, provider_instance_id)
  on delete restrict

native:
  provider_instance_id != null
  native_state_envelope、native_state_schema_version、secret_key_id 均非 null
  native_state_schema_version > 0
  conversation_transcript_id == null
  native_reuse_mode in ('reusable', 'single_use')
  reusable -> native_use_state = 'reusable' 且 native_consumer_gateway_request_id is null
  single_use -> native_use_state in ('available', 'claimed', 'consumed', 'ambiguous')

portable:
  conversation_transcript_id != null
  provider_instance_id == null
  upstream_credential_id == null
  native_state_envelope == null
  native_state_schema_version == null
  secret_key_id == null
  native_reuse_mode = 'not_applicable'
  native_use_state = 'not_applicable'
  native_consumer_gateway_request_id == null

索引:

unique(gateway_request_id)
unique(id, client_api_key_id)
(client_api_key_id, expires_at)
(provider_instance_id, expires_at) where provider_instance_id is not null
(upstream_credential_id) where upstream_credential_id is not null
(native_consumer_gateway_request_id) where native_consumer_gateway_request_id is not null
(conversation_transcript_id) where conversation_transcript_id is not null
(expires_at)

Redis 可以缓存 binding 和 connection-local topology,但 PostgreSQL 是持久 binding 的事实源。Native 失败时不能自动改为 portable。

复合外键在数据库中保证 response/request、request owner、transcript owner 和 native credential/instance 一致,应用校验只是更早返回友好错误,不能作为唯一防线。API 按当前 Key 查询 binding,不能只凭可猜测的 response ID 读取其他调用方历史。删除或重新生成 client API key 不自动继承旧 Key 的 portable transcript。

Native binding 的 instance、可选 credential scope 和 continuation state 必须取自该 request 唯一 downstream-committed attempt 的终态结果,不能接受 API handler 传入的任意组合;portable binding 只能引用本次 finalization transaction 新建的不可变 canonical transcript snapshot。native_state_envelope 由对应 Provider adapter 生成和校验,可以容纳 response ID、conversation ID 或 connection scope,但通用 store 不解析其内部字段。

Portable binding 的 expires_at 不得晚于 transcript 的 expires_at;native binding 的有效期也不能超过 Provider 声明的上游 continuation 生命周期。Provider adapter 在写 binding 时声明 native state 是 reusable(必须能安全并发使用)还是 single-use,不能让 API 选择;“可重复但只能串行”不进入通用 binding 语义,Provider 必须提供自己的持久状态边界或禁用 native continuation。对 single-use binding,history owner 在创建 consumer request 时锁定 binding 行并 CAS available → claimed,同时写入 native_consumer_gateway_request_id;若 attempt 被证明 not_sent 则 CAS 释放为 available,若 sent 则变为 consumed,若 send 边界 ambiguous 则永久标为 ambiguous、禁止重用。Redis continuation lease 只缩短竞争窗口,PostgreSQL native_use_state/native_use_revision 才是 Redis 丢失后的正确性边界。新 response 产生新 binding,旧 binding 不原地变成新 continuation state。该跨行规则由 history owner 在同一事务中校验,并由 retention 契约测试覆盖。

Recovery worker 对超时 claimed binding 读取其 consumer request/attempt:没有 attempt 或全部已证明 not-sent 才释放,存在 sent/ambiguous attempt 则分别收敛为 consumed/ambiguous。它不能根据 Redis lease 是否过期擅自释放,否则进程崩溃会把已经到达上游的一次消费重新放行。

运维事件

ops_events

只记录请求事实之外的后台错误和重要运维事件。请求错误已经存在于 gateway_requests 和 request_attempts,禁止再复制一份。

字段 类型与约束 语义
id text primary key Event ID
level text not null warning 或 error
component text not null models、oauth、quota、update 等 owner
operation text not null 具体后台操作
provider_instance_id text references provider_instances on delete set null 可选 live join
provider_instance_ref text Instance 历史快照
provider_kind text Provider 名称快照
upstream_credential_id text 可选 live join;与 instance 组成复合 FK
resource_ref text 资源历史快照
failure_kind text not null 稳定错误分类
status_code integer 可选协议状态
message text not null 已脱敏安全消息
metadata_json jsonb not null default '{}' 已脱敏诊断
created_at timestamptz not null 发生时间

约束:

level in ('warning', 'error')
status_code is null or status_code between 100 and 599
foreign key(upstream_credential_id, provider_instance_id)
  references upstream_credentials(id, provider_instance_id)
  on delete set null (upstream_credential_id)
provider_instance_id is null or (provider_instance_ref is not null and provider_instance_ref = provider_instance_id)
upstream_credential_id is null or (provider_instance_id is not null and resource_ref is not null)

索引:

(created_at desc, id desc)
(component, created_at desc, id desc)
(provider_instance_id, created_at desc, id desc)
  where provider_instance_id is not null
(provider_instance_ref, created_at desc, id desc)
  where provider_instance_ref is not null
(upstream_credential_id, created_at desc, id desc)
  where upstream_credential_id is not null
(resource_ref, created_at desc, id desc)
  where resource_ref is not null
(failure_kind, created_at desc, id desc)

Ops recorder 对相同故障做有界速率限制并周期性写入已脱敏的 suppressed count,不能在 Provider outage 的每次后台 retry 都无限插入一行。限流只抑制重复诊断噪声,不得吞掉首次故障或管理审计事实。

当 live instance/credential 存在时,Ops recorder 从其受控 snapshot 填充 provider_instance_ref、provider_kind 和 resource_ref;错误对象只能提供经过 allowlist 的 failure/status 信息,不能反向决定这些维度。

派生聚合

request_metric_buckets

保存客户端视角的长期趋势,不包含 Provider 维度:

字段 类型与约束
bucket_start timestamptz not null
client_api_key_ref text not null
protocol text not null
operation text not null
client_transport text not null
model_route_ref text not null
public_model_id text not null
request_count bigint not null default 0 check (>= 0)
success_count bigint not null default 0 check (>= 0)
failure_count bigint not null default 0 check (>= 0)
cancelled_count bigint not null default 0 check (>= 0)
incomplete_count bigint not null default 0 check (>= 0)
caller_error_count bigint not null default 0 check (>= 0)
input_tokens bigint not null default 0 check (>= 0)
output_tokens bigint not null default 0 check (>= 0)
cached_tokens bigint not null default 0 check (>= 0)
cache_write_tokens bigint not null default 0 check (>= 0)
reasoning_tokens bigint not null default 0 check (>= 0)
total_tokens bigint not null default 0 check (>= 0)
usage_metrics_json jsonb not null default '{}'
first_token_latency_sum bigint not null default 0 check (>= 0)
first_token_latency_count bigint not null default 0 check (>= 0)
latency_sum bigint not null default 0 check (>= 0)
latency_count bigint not null default 0 check (>= 0)
max_latency_ms bigint not null default 0 check (>= 0)
min_latency_ms bigint check (min_latency_ms is null or min_latency_ms >= 0)
updated_at timestamptz not null

主键:

primary key(
  bucket_start,
  client_api_key_ref,
  protocol,
  operation,
  client_transport,
  model_route_ref,
  public_model_id
)

client_api_key_ref 和 model_route_ref 使用当时的 ID 快照;无对应实体时使用稳定的 __none__,未知值使用 __unknown__。无法解析的客户端模型统一聚合为 public_model_id = '__unmatched__',绝不能把任意 requested_model_id 放进 bucket 主键制造无界高基数;原始有界值只留在 gateway_requests 保留期内。

attempt_metric_buckets

保存上游视角的健康、资源用量和成本:

字段 类型与约束
bucket_start timestamptz not null
provider_kind text not null
provider_instance_ref text not null
resource_ref text not null
upstream_model_id text not null
service_tier text not null
upstream_transport text not null
currency text not null
attempt_count bigint not null default 0 check (>= 0)
success_count bigint not null default 0 check (>= 0)
failure_count bigint not null default 0 check (>= 0)
cancelled_count bigint not null default 0 check (>= 0)
incomplete_count bigint not null default 0 check (>= 0)
rate_limited_count bigint not null default 0 check (>= 0)
auth_failure_count bigint not null default 0 check (>= 0)
provider_5xx_count bigint not null default 0 check (>= 0)
not_billable_attempt_count bigint not null default 0 check (>= 0)
input_tokens bigint not null default 0 check (>= 0)
output_tokens bigint not null default 0 check (>= 0)
cached_tokens bigint not null default 0 check (>= 0)
cache_write_tokens bigint not null default 0 check (>= 0)
reasoning_tokens bigint not null default 0 check (>= 0)
total_tokens bigint not null default 0 check (>= 0)
usage_metrics_json jsonb not null default '{}'
first_token_latency_sum bigint not null default 0 check (>= 0)
first_token_latency_count bigint not null default 0 check (>= 0)
latency_sum bigint not null default 0 check (>= 0)
latency_count bigint not null default 0 check (>= 0)
max_latency_ms bigint not null default 0 check (>= 0)
min_latency_ms bigint check (min_latency_ms is null or min_latency_ms >= 0)
fully_priced_attempt_count bigint not null default 0 check (>= 0)
partially_priced_attempt_count bigint not null default 0 check (>= 0)
unpriced_attempt_count bigint not null default 0 check (>= 0)
estimated_cost_amount numeric(20, 10) not null default 0 check (>= 0)
updated_at timestamptz not null

主键:

primary key(
  bucket_start,
  provider_kind,
  provider_instance_ref,
  resource_ref,
  upstream_model_id,
  service_tier,
  upstream_transport,
  currency
)

无 credential 的 Provider 使用 resource_ref = '__none__',默认 tier 使用 service_tier = '__default__'。not_sent attempt 使用 currency = '__none__' 并增加 not_billable_attempt_count;unknown estimate 使用 currency = '__unknown__' 并增加 unpriced_attempt_count;partial estimate 使用真实 currency 并增加 partially_priced_attempt_count。Dashboard 必须同时展示 not-applicable/known/partial/unknown 覆盖率,不能通过 estimated_cost_amount = 0 伪装成免费,也不能跨 currency 求和。

两张表都使用 UTC 对齐的固定 15 分钟 bucket_start,数据库 check 要求 epoch 秒数能被 900 整除;更粗粒度由查询层 roll up,不在同一表混存多种 bucket width。表中的 request_count/attempt_count 只在事实终态时增加,总数必须等于 success、failure、cancelled 与 incomplete 四类 terminal 计数之和;caller_error_count 是 failure 的子集,不参与第二次求和。Attempt 表还必须满足 attempt_count = not_billable + fully_priced + partially_priced + unpriced。

Attempt bucket 的 currency 只能是大写三字符代码、__none__ 或 __unknown__。__none__ 行只能累计 not-billable attempt,__unknown__ 行只能累计 unpriced attempt,真实 currency 行只能累计 fully/partially priced attempt;这些关系使用 check constraint 固化,避免同一行把无价格和零价格混在一起。

usage_metrics_json 是注册 metric 到非负十进制字符串累计值的对象,由 bucket owner 使用十进制定点数逐 key 加法,用于在原始事实过期后仍保留 image、audio 等非 token 用量。它不接受任意 Provider key 或 JSON 浮点。Latency count 为 0 时对应 sum/max 必须为 0 且 min 为 null;count 大于 0 时 min 非空且 min <= max,防止“没有样本但显示 0 ms”。

两张 bucket 表必须可以从保留期内的原始事实重建。增量聚合与 terminal CAS 位于同一事务,避免没有 outbox/checkpoint 时的重复累计和漏算;周期性 rebuild 只负责校验或修复保留期内的派生数据。聚合只能由一个 owner 写入;API、Provider 和 Dashboard 不各自维护计数。account_usage 一类可漂移的累计表不进入终态。

Bucket delta 使用单条 PostgreSQL upsert 原子相加并检测 bigint/numeric overflow,不能先 select 再由应用回写。若 finalization 同时更新 request 与 attempt bucket,所有 writer 使用固定锁顺序,避免高流量下产生可预期的 bucket-row deadlock。

Rebuild 只替换已经超过最大 request deadline 与迟到宽限期的 closed bucket,并在窗口级锁内从事实表重算后原子替换;当前仍可能接收 terminal delta 的 open bucket 只做对比,不允许“先清空再重建”覆盖并发写入。

PostgreSQL 删除与保留规则

数据 删除规则
Provider instance 先 disable 并从 Snapshot 移除;存在 route target、价格或未过期 native binding 时禁止删除
Credential 先 disable、停止新 lease 并 drain;未过期 native binding 存在时禁止删除;删除时清除 secret 和 Provider 专属子表,历史 attempt 保留快照
Credential state 随 credential 级联删除,不影响历史 attempt 快照
Provider model 缺失时设为 unavailable;被 model route target 或历史价格引用时禁止删除
Model route 删除时级联 model route targets;历史 request 的 live FK 置空
Price version 发布后不可修改;被 attempt 引用后禁止删除
Client API key 先 disable、拒绝新请求并 drain/recover running request;同一删除事务先显式删除 binding/transcript,再删除 Key,使历史 request FK 置空
Gateway request 按 retention 删除并级联 attempts;只要 binding 仍存在,FK 就拒绝删除,清理任务必须先删除已过期 binding
Conversation transcript 先删除已过期 portable binding,再从 snapshot 叶子向根删除;items 随 transcript 级联删除
Admin audit event 不因 actor 或目标实体删除而删除,仅按 audit retention 清理
Ops event 按独立 retention 删除
Metric bucket 按 bucket retention 删除

gateway_requests、request_attempts 和 ops_events 初期使用普通表与 (time desc, id desc) 索引。只有生产数据量证明 retention delete 或索引维护成为瓶颈时,才按月进行时间分区;不在首轮迁移提前引入分区管理。

配置实体的 hard delete 不是普通更新:统一执行 disable → 发布 revision → 停止新租约 → 等待 running attempt/后台任务 drain → 检查 binding/引用 → delete。超时后是否强制终止必须是显式管理操作并写 audit,不能让 on delete cascade 意外中断在途请求。Client API key 的删除顺序固定为 binding → transcript snapshot/items(叶子到根)→ key,避免复合 owner FK 依赖多个 referential action 的触发顺序。Retention 按小批量、有界锁时长删除,并先运行 stale recovery;备份和只读副本也必须遵守同一数据保留与密钥退役策略,否则主库删除不等于隐私数据真正到期。

Control-plane validator 保证 usage retention 大于最大 request deadline、迟到宽限和 bucket rebuild 安全余量之和;否则可能在 request 尚未 recovery 或 bucket 尚未关闭时先删掉唯一事实。Retention cutoff 使用数据库时间并记录每批高水位,任务重跑不能越过未处理引用。

Redis 终态边界

Redis 保存:

Key 族 内容 性质
admin:session:* 管理员会话 TTL
runtime:snapshot:* 配置版本通知 可重建
runtime:credential:* Credential revision 变更通知 可从 PostgreSQL 重载
lease:provider-instance:* 多实例共享的 endpoint 并发与请求间隔 短 TTL
lease:credential:* 多实例 credential 并发或刷新租约 短 TTL
lease:provider-task:* Catalog/health/quota 等后台任务 leader lease 短 TTL、结果仍由数据库 CAS 防旧写
lease:continuation:* Provider 声明为单并发/单次消费的 native continuation 短 TTL、fencing token
cooldown:credential:* 热路径 cooldown 缓存 PostgreSQL 状态的可丢失缓存
rate:client-key:* 调用方速率和并发计数 窗口 TTL
budget:client-key:* 当前预算窗口的 estimated spend 与 in-flight reservation 可由 request/attempt facts 重建
continuation:* Binding 与 connection-local topology 缓存 只缓存 opaque ID 或加密 envelope,PostgreSQL/上游状态可恢复
circuit:provider-instance:* 多实例共享熔断摘要 TTL、可由 attempt 恢复

Redis 不保存上游 secret、对话正文、唯一模型目录或唯一计费事实。Provider model catalog 的权威数据进入 PostgreSQL,RuntimeSnapshot 在进程内使用不可变结构读取。

预算限额在 request 接入时从 client_api_keys.policy_json 冻结到 request,已发生的目录价格估算来自 request_attempts。Redis 只原子维护当前窗口的 in-flight reservation 和热聚合;接入时按可计算的保守上界预留,终止时按全部 attempts 的估算成本调整。Unknown estimate 不能按零释放 reservation,必须按 policy 保留估算、拒绝后续请求或显式降级为 soft budget。

严格预算接入顺序固定为:Redis 原子检查并预留、PostgreSQL 写入带预留金额的 running request、之后才允许建立 attempt。PostgreSQL 写入失败时释放 Redis reservation;进程在两步之间崩溃只会留下带 TTL 的保守孤儿 reservation,不会产生没有持久 request 的上游消费。

Redis 丢失后必须先按 client_api_key_ref + budget_window_start/end + budget_currency 从 terminal attempt facts 与仍为 running 的请求重建,再恢复严格预算接入。重建过程持有窗口级 fencing token,避免一边扫描旧事实一边接受新 reservation。若无法完成重建,应 fail closed 或显式降级为 soft budget,不能静默按零消费继续放行。

跨实例 semaphore、request interval、rate limit 和严格预算的多 Key 原子操作必须使用同一 Redis Cluster hash tag、Redis server time 和带 fencing token 的 Lua/function;不能依赖应用节点时钟或先 GET 后 SET。Redis 不可用时,上游并发/间隔和 strict budget 默认 fail closed;只允许经过显式配置的单节点 soft-limit 降级,并把降级状态暴露到健康检查和审计中。

Redis epoch 变化时先暂停新的受限操作:client concurrency 从 running requests 重建,RPM/TPM 从当前窗口 request facts 重建或等待窗口自然结束,provider/credential lease 从 running attempts 与仍存活 worker 的重新注册保守恢复。恢复完成前不能把“Redis 是空的”解释成“当前使用量为零”。

Secret 与隐私

当前表中的 client_api_keys.key、accounts.access_token、accounts.refresh_token 和 account_cookies.value 都是可直接使用的明文,终态不允许继续存在。

终态使用三类互相独立的密钥材料:

  1. Encryption root key:通过不同 domain 派生 AEAD key,分别加密上游 secret、Cookie、native continuation state 和 portable transcript item。
  2. Credential fingerprint key:HMAC credential 用于去重,不可用于解密。
  3. Identity pseudonym key:只用于 installation ID 和遥测资源伪名,不可兼作前两者。

加密 key 不存 PostgreSQL,应来自环境、KMS 或 secret manager。Envelope 自带 nonce、算法和格式版本,表中 secret_key_id 用于轮换;key_hash、credential fingerprint 和 resource ref 的文本值也携带算法/版本前缀,避免以后增加独立版本列。AEAD additional authenticated data 必须绑定用途、表、owner ID 和行主键/sequence,防止不同 credential、Cookie 或 conversation item 之间交换密文。轮换采用读旧写新,不修改历史迁移文件。

以下数据禁止进入任何 metadata_json、错误消息或日志:

  • API Key、OAuth token、Cookie、AWS secret。
  • 完整 request/response body。
  • 未脱敏 Provider 原始错误正文。
  • Portable conversation 明文内容和 capability-bearing native continuation handle。

每个 metadata_json 必须由该表 owner 使用字段 allowlist 构造,不能直接序列化外部 request、Provider response、Rust Debug 对象或任意 serde_json::Value。request_attempts.upstream_request_id/upstream_response_id 只允许保存 adapter 明确分类为非 bearer 的诊断 ID;任何可用于恢复、读取或劫持上游会话的值只能进入 continuation_bindings.native_state_envelope。需要新增可查询字段时优先增加普通列;只有确实属于 Provider 扩展且不参与通用查询时才进入 JSON。

当前表迁移归宿

当前表或字段 终态归宿
schema_migrations 原表保留,继续校验版本、名称和 checksum
admin_users 原表保留
管理配置变更 新增不可变 admin_audit_events
client_api_keys.key 迁移为 key_hash,完整 Key 只返回一次
runtime_settings.model_aliases_json model_routes、model_route_targets
runtime_settings.rotation_strategy model_routes.routing_strategy
runtime_settings.ops_error_retention_days ops_event_retention_days
Codex refresh/concurrency 设置 Codex Provider config 与 credentials
accounts upstream_credentials + upstream_credential_states + codex_accounts
accounts.status 管理禁用映射到 credential enabled;过期、封禁和额度状态映射到 credential state availability/state_json
accounts.quota_*、cooldown 字段 upstream_credential_states.cooldown_until/state_json
accounts.access_token/refresh_token 加密 secret_envelope
account_cookies codex_account_cookies,value 加密
account_usage 从 attempts 或 attempt_metric_buckets 查询,最终删除
usage_records gateway_requests + request_attempts
请求相关 ops_error_logs request/attempt failure 字段
后台 ops_error_logs ops_events
request_time_buckets request_metric_buckets + attempt_metric_buckets
Redis 模型快照 PostgreSQL provider_models + 进程 RuntimeSnapshot
Redis response affinity continuation_bindings 的缓存与 WS topology

迁移顺序固定为:

  1. 启动前验证独立 encryption root、credential fingerprint 和 identity pseudonym key 已就绪;密钥缺失时拒绝执行会产生明文回退的迁移。
  2. Expand:只追加新表、加密列和兼容索引,创建 Provider、credential config/state、model、route、price、request/attempt、audit、continuation 与 bucket 结构;不在同一发布中删除旧列。request 与 continuation binding 的双向 FK 先建表、回填并验证,再以 not valid 约束分阶段添加,避免循环依赖阻塞 rollout。
  3. Backfill:以有界批次和可恢复 checkpoint 加密 client key、token、Cookie,并回填唯一 Codex instance、账号和保留期内 telemetry。每批记录数量、checksum 和失败原因,允许安全重跑但不能重复生成事实;对仍会 OAuth 刷新的旧账号,cutover 前必须短暂冻结旧 writer、drain refresh lease 并做最后一轮 delta copy,不能让 backfill 与旧 writer 静默竞争。
  4. Verify:用独立查询逐项对账行数、credential 身份、usage、终态、引用和解密抽样;新二进制可 shadow-read 比较,但同一事实始终只有旧或新 writer,不能双写后靠时间戳择胜。
  5. Cutover:先切换 Codex RuntimeSnapshot 和请求执行读取,再在一个明确版本切换 request/attempt、continuation 和 bucket 的唯一 writer/readers;切换点持久化并可观测。
  6. Contract:至少经过一个不再支持旧 writer 的稳定发布和备份保留窗口后,删除明文/旧表/旧 Redis 事实;回滚只能回到仍理解新 schema 且不会重新写明文的版本。

迁移期间可以离线回填和验证,但一个生产事实在任一版本只能有一个 writer。禁止长期 dual-write 后依靠人工判断哪一份正确。DDL 必须评估表锁:大表的 default、not-null、FK validation 和索引采用分阶段 not valid/validate 或 concurrent build,不能把终态约束直接变成不可控停机。CREATE INDEX CONCURRENTLY 不能放进当前的事务型 migration runner,届时必须增加受审计、可重试的 non-transactional migration phase,而不是把命令偷偷塞进普通 migration SQL。

现有迁移文件已经受 checksum 保护,不能修改 0001_initial.sql 或 0002_cache_write_tokens.sql;所有变化只能追加新版本。Telemetry 回填按非空旧 request_id 聚合 logical request,按 attempt_index 和时间恢复 attempts;缺少 request ID、commit、usage 或 price 证据的旧记录使用新生成 ID 和 legacy metadata,并保持 unknown,不能伪造成功、目录价格估算或 continuation。account_usage 累计值不反向生成请求事实。

明确不创建的结构

  • 不创建 providers lookup 表;Provider code 由 Registry 决定。
  • 不创建包含所有平台字段的通用 accounts 大表。
  • 不创建 tenant、organization、project 等当前没有业务语义的层级。
  • 不创建 Provider capability EAV 表;能力快照使用校验后的 JSON。
  • 不创建 credential group;隔离资源池使用独立 Provider instance。
  • 不创建成功表和失败表两套请求事实。
  • 不创建通用 subject_type/subject_id usage ledger。
  • 在只做目录价格估算的阶段不创建 invoice/payment ledger;接入真实账单对账时单独建模,不能复用或篡改 attempt estimate。
  • 不保存默认 request/response payload archive。
  • 当前不引入响应缓存;如果未来要本地命中成功响应,必须显式增加 response source/cache 事实并重写 request-success invariant,不能伪造一个上游 attempt。
  • 不预留无法完成响应 replay 的空 idempotency 表;只有协议层明确承诺幂等语义时才单独设计。
  • 不用 PostgreSQL 行锁实现热路径 semaphore、lease 或 circuit breaker。

数据与并发契约测试

终态迁移和 store 必须覆盖:

  1. 空库完整迁移、从每个受支持旧版本升级、migration name/checksum 篡改拒绝。
  2. 所有 FK 的 cascade、set-null 和 restrict 行为,以及每个 FK 是否有支持索引;行为 JSON 缺少/不支持 schema_version、超长文本/JSON 或保留 sentinel 必须被拒绝。
  3. 两个并发控制面事务只能得到不同且递增的 config_revision,stale expected revision 必须 conflict;Snapshot 读取只能得到一个完整 revision,丢失 Redis 通知后轮询仍能收敛。
  4. credential_revision 与 state_revision 独立 CAS,旧 observed_at 不能覆盖新状态;同一稳定 resource 不能复制到多个 instance 绕过限制。
  5. 上游 payload 发送前 request/attempt 行已经提交;attempt target/instance 必须属于 request 冻结的 route;upstream_send_state 与 downstream_committed 不混淆;并发 create/commit 时同一 request 最多一个 downstream-committed attempt 且 commit 后没有新 attempt,重复 finalizer 只结算一次。
  6. Stale running request/attempt 依据持久 deadline 收敛为 incomplete,不能因配置变化改变判断,也不能重复写 usage 或估算成本。
  7. Price version 在 effective_from 边界正确选择;known、partial、unknown、明确零价、多货币和 estimated/invoice 语义均不混淆。
  8. Native/portable continuation check、调用方 owner、response/request、credential/instance 对应关系、native state 与 transcript 加密边界由真实 FK/transaction 验证;同一 portable history 的分支不能读到彼此后续 delta,single-use native binding 在 Redis flush/进程崩溃后也不会重复消费。
  9. Client key、Provider、credential、route 和 transcript 删除后的历史快照与隐私清理结果。
  10. 从 request/attempt facts 重建 closed bucket 后,与增量聚合逐字段一致;unknown estimate 不进入已知金额,非 token metric 不丢失,任意客户端 model ID 不制造高基数 bucket。
  11. 数据库、管理 API、Debug、错误和日志样本中不存在完整 client key、上游 secret、Cookie 或 conversation 明文。
  12. Retention 先 recovery,再清理过期 binding、从 transcript leaf 向 root 清理,最后清理 request;未过期 continuation 不会被原始事实清理提前破坏,usage facts 不会早于 bucket 安全窗口删除。
  13. Redis flush/epoch 变化时,rate、concurrency、lease 和 strict budget 在重建完成前不会把空状态当成零;预算窗口可仅从冻结 request 与 attempt facts 恢复。

涉及并发的测试必须使用真实 PostgreSQL transaction 和唯一约束,不能只用内存 mock 证明。

验收标准

  1. 新增普通 API Key Provider 只增加 Provider adapter 和 instance/credential 数据,不修改通用 schema。
  2. 新增 Provider 专属表时,只能承载真实可查询状态,不能复制通用 credential、model、request 或 attempt 字段。
  3. 一次客户端请求始终只有一行 gateway_requests,所有上游调用都能按顺序关联到 request_attempts。
  4. 最多一个 attempt 可以 downstream_committed = true,commit 后不能出现新的 attempt;ambiguous upstream send 不会自动重放。
  5. 客户端 usage、上游 usage 和上游目录价格估算可以分别对账;known、partial、unknown estimate 可区分,未知价格不会显示为零。
  6. 删除或禁用 Provider/credential 后,历史请求仍能按 Provider、instance、resource 和模型统计。
  7. PostgreSQL、备份、Redis、日志和管理 API 都不能恢复下游完整 Key 或读取上游明文秘密。
  8. Dashboard 聚合与 credential 用量可以从事实表重建,不依赖 account_usage 一类可漂移累计状态。
  9. Native continuation 不能跨 Provider;portable continuation 的正文始终加密并与调用方 Key 隔离。
  10. 所有 FK、外部 ID 与历史 _ref 均符合本文命名规则,不存在含义不明的 route_id、request_id、credential_id 或 _key。
  11. 数据库设计不要求 Gateway Engine、API adapter 或 Provider adapter 互相了解对方的存储结构。