diff --git a/.env.example b/.env.example index 771056ec6..bc3aeb1b5 100644 --- a/.env.example +++ b/.env.example @@ -209,8 +209,11 @@ DETACHED_STREAM_METERING_RESERVE_BYTES=16777216 # 内置多进程间分摊)。 # 预算不足时请求等待现有前缀被消费,不会关闭门禁或误触发供应商切换。 # 未设置时使用统一自动内存预算;显式设置仍作为门控子项上限。 +# 该上限仅在「系统设置 > 内存准入」开启时生效。 # STREAM_GATE_GLOBAL_PREBUFFER_BYTE_CAP=268435456 +# 以下内存准入相关配置仅在「系统设置 > 内存准入」开启时生效(默认关闭)。 +# 关闭时请求正文与流式门控前缀全部留在内存,不排队、不落盘、不返回本地 429。 # 受管业务内存总预算:默认根据可用 RAM + 0.5 * 可用 swap 自动计算。 # 同时遵守 cgroup v1/v2 可见祖先配额、memory.high,以及热点的物理余量。 # 该预算是应用软上限,不等于容器 RSS 硬限制。 diff --git a/docs/research/ttft-memory-aware-admission.md b/docs/research/ttft-memory-aware-admission.md index fbf98ed42..6eb5a3ada 100644 --- a/docs/research/ttft-memory-aware-admission.md +++ b/docs/research/ttft-memory-aware-admission.md @@ -2,11 +2,15 @@ 关联 #1473,部分涉及 #1466。v0.9.5 的门控按每请求 40 MiB 预留,默认四个 worker 各分到 64 MiB,因此每个 worker 同时只有一个请求能等待首内容。慢上游占住这个位置,其他请求即使上游很快也会被本地队列阻塞。 +## 开关 + +内存准入由系统设置 `enableMemoryAdmission` 控制,默认关闭。关闭时分配器只记账不设限:租约与增长总是成功,不向 primary 申请授权;请求正文与门控前缀全部留在内存,不落盘;门控子限额 `STREAM_GATE_GLOBAL_PREBUFFER_BYTE_CAP` 与物化前的 V8 堆余量检查不生效;不会产生 `local_capacity_exceeded`。开启后下文全部机制生效。每个 worker 在读取入站正文前按系统设置缓存同步开关,保存设置后随缓存失效广播在各进程生效。 + ## 请求与门控 - 门控从 128 KiB 工作集开始,随自有字节块和解析堆栈实际增长。扩容不持有部分正文排队,不能立即取得容量就把前缀逐块暂存到磁盘。首内容之后按原顺序回放。 - 增量 JSON/SSE 分类只保存协议路径事实和有界错误预览,不为大工具参数、图片或回显帧创建完整对象。重复 JSON 键、错误优先级、compaction、终止帧和合法深层 JSON 保持既有语义。 -- 入站正文只有一个流消费者;压缩输入和解压输出逐块检查大小、必要时暂存。JSON 物化前按字节和对象结构估算容量,密集小对象的预算高于同样长度的长文本。 +- 入站正文只有一个流消费者;压缩输入和解压输出逐块检查大小、必要时暂存。JSON 物化前按字节和对象结构估算容量,密集小对象的预算高于同样长度的长文本。物化与解析期间按峰值 `8192 + 8 × 字节 + 结构` 预留;解析完成后收缩到请求持续持有的 `8192 + 4 × 字节 + 结构`(原始字节、UTF-16 字符串、对象结构与一份出站副本),流式响应与后台消费者期间只占用该持有量。按峰值持有到请求结束时,长时间流式请求积压会让账面占用达到实际常驻内存的 3 到 4 倍,额度耗尽后新请求全部在正文准入阶段返回 429。 - 过滤、重试和 multipart 仍保留完整语义。日志视图改为按需生成;未启用调试正文或 Langfuse 时不长期保存出站序列化副本。 - 首次向上游发请求前取得门控基础容量。本地准入最多等待 20 秒,容量不足返回本地 `429`、`local_capacity_exceeded`、`Retry-After: 1`;不触发供应商 failover、健康扣分或熔断。 @@ -30,7 +34,7 @@ hotBudget = min(autoBudget, headroom) hotBudget 只在启动(多进程为全部 worker 就绪)时计算一次,作为固定上限。运行期每秒的收紧条件是 `limit = min(启动上限, 在用量 + headroom)`:0.6 折扣已体现在启动上限里,不再对剩余内存重复折扣。否则进程自身常驻内存(Next.js 基础堆等非受管分配)增长会在零租约时持续压低正文额度;只有真实物理余量低于启动上限或出现内存压力时才收紧。 -Linux 使用 MemAvailable、SwapFree,同时检查可见 cgroup v2/v1 的成员和祖先限制,遵守 memory.high、swap 禁用及 v1 memory+swap 联合限额。无法确认容器 swap 额度时不增加 swap 容量;非 Linux 以可用物理内存保守估计。 +Linux 使用 MemAvailable、SwapFree,同时检查可见 cgroup v2/v1 的成员和祖先限制,遵守 memory.high、swap 禁用及 v1 memory+swap 联合限额。cgroup 已用量按 working set 计算:v2 为 `memory.current - inactive_file`,v1 为 `memory.usage_in_bytes - total_inactive_file`(memsw 联合用量同样扣除),与 kubelet/cAdvisor 一致;溢写文件、读取过的代码与日志产生的可回收 page cache 不计入占用,否则设置了内存上限的容器运行一段时间后额度会被 cache 压到 0。无法确认容器 swap 额度时不增加 swap 容量;非 Linux 以可用物理内存保守估计。 `CCH_MEMORY_BUDGET_BYTES` 可明确指定受管分配预算,替代自动比例;显式模式保留 10% 当前 RAM 余量并遵守热点物理容量。该值约束正文物化与门控工作集,并非进程 RSS 硬上限;需要硬上限时应使用容器/cgroup 内存限制。DB、Replay、detached drain 和异步写队列继续使用原有独立限额,尚未全部迁入同一个分配器。 diff --git a/drizzle/0124_system_settings_memory_admission.sql b/drizzle/0124_system_settings_memory_admission.sql new file mode 100644 index 000000000..87658698f --- /dev/null +++ b/drizzle/0124_system_settings_memory_admission.sql @@ -0,0 +1 @@ +ALTER TABLE "system_settings" ADD COLUMN "enable_memory_admission" boolean DEFAULT false NOT NULL; \ No newline at end of file diff --git a/drizzle/meta/0124_snapshot.json b/drizzle/meta/0124_snapshot.json new file mode 100644 index 000000000..864cf66ed --- /dev/null +++ b/drizzle/meta/0124_snapshot.json @@ -0,0 +1,5888 @@ +{ + "id": "bd6f4b40-3271-4735-b93c-e3afeaa2d3dc", + "prevId": "631bcd2f-bab9-4088-8813-e5e66585e20a", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "action_category": { + "name": "action_category", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "action_type": { + "name": "action_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "target_id": { + "name": "target_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "target_name": { + "name": "target_name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "before_value": { + "name": "before_value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "after_value": { + "name": "after_value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "operator_user_id": { + "name": "operator_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "operator_user_name": { + "name": "operator_user_name", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "operator_key_id": { + "name": "operator_key_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "operator_key_name": { + "name": "operator_key_name", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "operator_ip": { + "name": "operator_ip", + "type": "varchar(45)", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_audit_log_category_created_at": { + "name": "idx_audit_log_category_created_at", + "columns": [ + { + "expression": "action_category", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_log_operator_user_created_at": { + "name": "idx_audit_log_operator_user_created_at", + "columns": [ + { + "expression": "operator_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"audit_log\".\"operator_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_log_operator_ip_created_at": { + "name": "idx_audit_log_operator_ip_created_at", + "columns": [ + { + "expression": "operator_ip", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"audit_log\".\"operator_ip\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_log_target": { + "name": "idx_audit_log_target", + "columns": [ + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"audit_log\".\"target_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_log_created_at_id": { + "name": "idx_audit_log_created_at_id", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.avail_bucket_1m": { + "name": "avail_bucket_1m", + "schema": "", + "columns": { + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "bucket_start": { + "name": "bucket_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "success_cnt": { + "name": "success_cnt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failure_cnt": { + "name": "failure_cnt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "excluded_cnt": { + "name": "excluded_cnt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "latency_cnt": { + "name": "latency_cnt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "latency_sum_ms": { + "name": "latency_sum_ms", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_request_at": { + "name": "last_request_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_avail_bucket_1m_time": { + "name": "idx_avail_bucket_1m_time", + "columns": [ + { + "expression": "bucket_start", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "avail_bucket_1m_pkey": { + "name": "avail_bucket_1m_pkey", + "columns": [ + "provider_id", + "bucket_start" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.avail_current": { + "name": "avail_current", + "schema": "", + "columns": { + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "availability": { + "name": "availability", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_request_at": { + "name": "last_request_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cloud_pricing_catalog": { + "name": "cloud_pricing_catalog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "version": { + "name": "version", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "refreshed_at": { + "name": "refreshed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "providers": { + "name": "providers", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "vendors": { + "name": "vendors", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "model_count": { + "name": "model_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "synced_at": { + "name": "synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.error_rules": { + "name": "error_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "pattern": { + "name": "pattern", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "match_type": { + "name": "match_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'regex'" + }, + "category": { + "name": "category", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "override_response": { + "name": "override_response", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "override_status_code": { + "name": "override_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_error_rules_enabled": { + "name": "idx_error_rules_enabled", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "unique_pattern": { + "name": "unique_pattern", + "columns": [ + { + "expression": "pattern", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_category": { + "name": "idx_category", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_match_type": { + "name": "idx_match_type", + "columns": [ + { + "expression": "match_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.keys": { + "name": "keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "can_login_web_ui": { + "name": "can_login_web_ui", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "limit_5h_usd": { + "name": "limit_5h_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_5h_reset_mode": { + "name": "limit_5h_reset_mode", + "type": "daily_reset_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'rolling'" + }, + "limit_daily_usd": { + "name": "limit_daily_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "daily_reset_mode": { + "name": "daily_reset_mode", + "type": "daily_reset_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'fixed'" + }, + "daily_reset_time": { + "name": "daily_reset_time", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'00:00'" + }, + "limit_weekly_usd": { + "name": "limit_weekly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_monthly_usd": { + "name": "limit_monthly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_total_usd": { + "name": "limit_total_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "cost_reset_at": { + "name": "cost_reset_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "limit_concurrent_sessions": { + "name": "limit_concurrent_sessions", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "provider_group": { + "name": "provider_group", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false, + "default": "'default'" + }, + "cache_ttl_preference": { + "name": "cache_ttl_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_keys_user_id": { + "name": "idx_keys_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_keys_key": { + "name": "idx_keys_key", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_keys_created_at": { + "name": "idx_keys_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_keys_deleted_at": { + "name": "idx_keys_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.message_request": { + "name": "message_request", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cost_usd": { + "name": "cost_usd", + "type": "numeric(21, 15)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "cost_multiplier": { + "name": "cost_multiplier", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": false + }, + "group_cost_multiplier": { + "name": "group_cost_multiplier", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": false + }, + "cost_breakdown": { + "name": "cost_breakdown", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "session_identity": { + "name": "session_identity", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "session_identity_kind": { + "name": "session_identity_kind", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "affinity_scope_tag": { + "name": "affinity_scope_tag", + "type": "varchar(16)", + "primaryKey": false, + "notNull": false + }, + "affinity_fingerprint": { + "name": "affinity_fingerprint", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "affinity_fingerprint_chain": { + "name": "affinity_fingerprint_chain", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_replay": { + "name": "is_replay", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "replay_source_request_id": { + "name": "replay_source_request_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_sequence": { + "name": "request_sequence", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "provider_chain": { + "name": "provider_chain", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "routing_trace": { + "name": "routing_trace", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "api_type": { + "name": "api_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "endpoint": { + "name": "endpoint", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "original_model": { + "name": "original_model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "actual_response_model": { + "name": "actual_response_model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "ttfb_ms": { + "name": "ttfb_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "first_byte_ms": { + "name": "first_byte_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_read_input_tokens": { + "name": "cache_read_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_creation_5m_input_tokens": { + "name": "cache_creation_5m_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_creation_1h_input_tokens": { + "name": "cache_creation_1h_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_ttl_applied": { + "name": "cache_ttl_applied", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "context_1m_applied": { + "name": "context_1m_applied", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "swap_cache_ttl_applied": { + "name": "swap_cache_ttl_applied", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "special_settings": { + "name": "special_settings", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "hedge_losers": { + "name": "hedge_losers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_stack": { + "name": "error_stack", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_cause": { + "name": "error_cause", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "blocked_by": { + "name": "blocked_by", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "blocked_reason": { + "name": "blocked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "client_ip": { + "name": "client_ip", + "type": "varchar(45)", + "primaryKey": false, + "notNull": false + }, + "messages_count": { + "name": "messages_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cache_compatibility_key": { + "name": "cache_compatibility_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "cache_score_eligible": { + "name": "cache_score_eligible", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cache_score_excluded_reason": { + "name": "cache_score_excluded_reason", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "theoretical_cache_tokens": { + "name": "theoretical_cache_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_ttl_bucket": { + "name": "cache_ttl_bucket", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_message_request_user_date_cost": { + "name": "idx_message_request_user_date_cost", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_user_created_at_cost_stats": { + "name": "idx_message_request_user_created_at_cost_stats", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_user_query": { + "name": "idx_message_request_user_query", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_provider_created_at_active": { + "name": "idx_message_request_provider_created_at_active", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_provider_created_at_finalized_active": { + "name": "idx_message_request_provider_created_at_finalized_active", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"status_code\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_proxy_status_active": { + "name": "idx_message_request_proxy_status_active", + "columns": [ + { + "expression": "\"created_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"is_replay\" = false AND \"message_request\".\"status_code\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_proxy_status_latest": { + "name": "idx_message_request_proxy_status_latest", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"updated_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"is_replay\" = false AND \"message_request\".\"status_code\" IS NOT NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_session_id": { + "name": "idx_message_request_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_session_id_prefix": { + "name": "idx_message_request_session_id_prefix", + "columns": [ + { + "expression": "\"session_id\" varchar_pattern_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_session_seq": { + "name": "idx_message_request_session_seq", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_session_identity_created_at": { + "name": "idx_message_request_session_identity_created_at", + "columns": [ + { + "expression": "COALESCE(\"session_identity\", \"session_id\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"created_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_session_identity_prefix": { + "name": "idx_message_request_session_identity_prefix", + "columns": [ + { + "expression": "COALESCE(\"session_identity\", \"session_id\") varchar_pattern_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_session_id_prefix_cover": { + "name": "idx_message_request_session_id_prefix_cover", + "columns": [ + { + "expression": "\"session_id\" varchar_pattern_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_endpoint": { + "name": "idx_message_request_endpoint", + "columns": [ + { + "expression": "endpoint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_blocked_by": { + "name": "idx_message_request_blocked_by", + "columns": [ + { + "expression": "blocked_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_provider_id": { + "name": "idx_message_request_provider_id", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_user_id": { + "name": "idx_message_request_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key": { + "name": "idx_message_request_key", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key_created_at_id": { + "name": "idx_message_request_key_created_at_id", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key_model_active": { + "name": "idx_message_request_key_model_active", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"model\" IS NOT NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key_endpoint_active": { + "name": "idx_message_request_key_endpoint_active", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "endpoint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"endpoint\" IS NOT NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_created_at_id_active": { + "name": "idx_message_request_created_at_id_active", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_model_active": { + "name": "idx_message_request_model_active", + "columns": [ + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"model\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_status_code_active": { + "name": "idx_message_request_status_code_active", + "columns": [ + { + "expression": "status_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"status_code\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_created_at": { + "name": "idx_message_request_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_deleted_at": { + "name": "idx_message_request_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key_last_active": { + "name": "idx_message_request_key_last_active", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key_cost_active": { + "name": "idx_message_request_key_cost_active", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_session_user_info": { + "name": "idx_message_request_session_user_info", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_client_ip_created_at": { + "name": "idx_message_request_client_ip_created_at", + "columns": [ + { + "expression": "client_ip", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"client_ip\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_prices": { + "name": "model_prices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "model_name": { + "name": "model_name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "price_data": { + "name": "price_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'cloud'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_model_prices_latest": { + "name": "idx_model_prices_latest", + "columns": [ + { + "expression": "model_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_model_prices_model_name": { + "name": "idx_model_prices_model_name", + "columns": [ + { + "expression": "model_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_model_prices_created_at": { + "name": "idx_model_prices_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_model_prices_source": { + "name": "idx_model_prices_source", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_model_prices_vendor": { + "name": "idx_model_prices_vendor", + "columns": [ + { + "expression": "((\"price_data\" ->> 'vendor'))", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_model_prices_aliases": { + "name": "idx_model_prices_aliases", + "columns": [ + { + "expression": "((\"price_data\" -> 'aliases'))", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_settings": { + "name": "notification_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "use_legacy_mode": { + "name": "use_legacy_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "circuit_breaker_enabled": { + "name": "circuit_breaker_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "circuit_breaker_webhook": { + "name": "circuit_breaker_webhook", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "daily_leaderboard_enabled": { + "name": "daily_leaderboard_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "daily_leaderboard_webhook": { + "name": "daily_leaderboard_webhook", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "daily_leaderboard_time": { + "name": "daily_leaderboard_time", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false, + "default": "'09:00'" + }, + "daily_leaderboard_top_n": { + "name": "daily_leaderboard_top_n", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5 + }, + "cost_alert_enabled": { + "name": "cost_alert_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cost_alert_webhook": { + "name": "cost_alert_webhook", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "cost_alert_threshold": { + "name": "cost_alert_threshold", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false, + "default": "'0.80'" + }, + "cost_alert_check_interval": { + "name": "cost_alert_check_interval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 60 + }, + "cache_hit_rate_alert_enabled": { + "name": "cache_hit_rate_alert_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cache_hit_rate_alert_webhook": { + "name": "cache_hit_rate_alert_webhook", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "cache_hit_rate_alert_window_mode": { + "name": "cache_hit_rate_alert_window_mode", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false, + "default": "'auto'" + }, + "cache_hit_rate_alert_check_interval": { + "name": "cache_hit_rate_alert_check_interval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5 + }, + "cache_hit_rate_alert_historical_lookback_days": { + "name": "cache_hit_rate_alert_historical_lookback_days", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 7 + }, + "cache_hit_rate_alert_min_eligible_requests": { + "name": "cache_hit_rate_alert_min_eligible_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 20 + }, + "cache_hit_rate_alert_min_eligible_tokens": { + "name": "cache_hit_rate_alert_min_eligible_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "cache_hit_rate_alert_abs_min": { + "name": "cache_hit_rate_alert_abs_min", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "cache_hit_rate_alert_drop_rel": { + "name": "cache_hit_rate_alert_drop_rel", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.3'" + }, + "cache_hit_rate_alert_drop_abs": { + "name": "cache_hit_rate_alert_drop_abs", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.1'" + }, + "cache_hit_rate_alert_cooldown_minutes": { + "name": "cache_hit_rate_alert_cooldown_minutes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30 + }, + "cache_hit_rate_alert_top_n": { + "name": "cache_hit_rate_alert_top_n", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 10 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_target_bindings": { + "name": "notification_target_bindings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "notification_type": { + "name": "notification_type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "schedule_cron": { + "name": "schedule_cron", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "schedule_timezone": { + "name": "schedule_timezone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "template_override": { + "name": "template_override", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "unique_notification_target_binding": { + "name": "unique_notification_target_binding", + "columns": [ + { + "expression": "notification_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_notification_bindings_type": { + "name": "idx_notification_bindings_type", + "columns": [ + { + "expression": "notification_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_notification_bindings_target": { + "name": "idx_notification_bindings_target", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notification_target_bindings_target_id_webhook_targets_id_fk": { + "name": "notification_target_bindings_target_id_webhook_targets_id_fk", + "tableFrom": "notification_target_bindings", + "tableTo": "webhook_targets", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outbox_events": { + "name": "outbox_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "aggregate_type": { + "name": "aggregate_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "aggregate_id": { + "name": "aggregate_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_outbox_events_unpublished": { + "name": "idx_outbox_events_unpublished", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"outbox_events\".\"published_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "outbox_events_event_id_key": { + "name": "outbox_events_event_id_key", + "nullsNotDistinct": false, + "columns": [ + "event_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outbox_processed": { + "name": "outbox_processed", + "schema": "", + "columns": { + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.proj_applied_requests": { + "name": "proj_applied_requests", + "schema": "", + "columns": { + "request_id": { + "name": "request_id", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projection_meta": { + "name": "projection_meta", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_batch_apply_operations": { + "name": "provider_batch_apply_operations", + "schema": "", + "columns": { + "claim_key": { + "name": "claim_key", + "type": "varchar(256)", + "primaryKey": true, + "notNull": true + }, + "preview_token": { + "name": "preview_token", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "payload_fingerprint": { + "name": "payload_fingerprint", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "operation_id": { + "name": "operation_id", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "undo_token": { + "name": "undo_token", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "undo_expires_at": { + "name": "undo_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "undo_consumed_at": { + "name": "undo_consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uniq_provider_batch_apply_operations_preview_token": { + "name": "uniq_provider_batch_apply_operations_preview_token", + "columns": [ + { + "expression": "preview_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "uniq_provider_batch_apply_operations_operation_id": { + "name": "uniq_provider_batch_apply_operations_operation_id", + "columns": [ + { + "expression": "operation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "uniq_provider_batch_apply_operations_undo_token": { + "name": "uniq_provider_batch_apply_operations_undo_token", + "columns": [ + { + "expression": "undo_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_batch_apply_operations_expires_at": { + "name": "idx_provider_batch_apply_operations_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_cache_effectiveness": { + "name": "provider_cache_effectiveness", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "cache_ttl_bucket": { + "name": "cache_ttl_bucket", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true + }, + "window_start": { + "name": "window_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "window_end": { + "name": "window_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "sample_count": { + "name": "sample_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "eligible_count": { + "name": "eligible_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "theoretical_cache_tokens": { + "name": "theoretical_cache_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "observed_cache_read_tokens": { + "name": "observed_cache_read_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "raw_effectiveness_bp": { + "name": "raw_effectiveness_bp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "confidence_bp": { + "name": "confidence_bp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "effectiveness_bp": { + "name": "effectiveness_bp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_provider_cache_effectiveness_window": { + "name": "idx_provider_cache_effectiveness_window", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_start", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_endpoint_probe_logs": { + "name": "provider_endpoint_probe_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "endpoint_id": { + "name": "endpoint_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'scheduled'" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error_type": { + "name": "error_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_provider_endpoint_probe_logs_endpoint_created_at": { + "name": "idx_provider_endpoint_probe_logs_endpoint_created_at", + "columns": [ + { + "expression": "endpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoint_probe_logs_created_at": { + "name": "idx_provider_endpoint_probe_logs_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "provider_endpoint_probe_logs_endpoint_id_provider_endpoints_id_fk": { + "name": "provider_endpoint_probe_logs_endpoint_id_provider_endpoints_id_fk", + "tableFrom": "provider_endpoint_probe_logs", + "tableTo": "provider_endpoints", + "columnsFrom": [ + "endpoint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_endpoints": { + "name": "provider_endpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provider_type": { + "name": "provider_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'claude'" + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_probed_at": { + "name": "last_probed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_probe_ok": { + "name": "last_probe_ok", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "last_probe_status_code": { + "name": "last_probe_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_probe_latency_ms": { + "name": "last_probe_latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_probe_error_type": { + "name": "last_probe_error_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "last_probe_error_message": { + "name": "last_probe_error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "uniq_provider_endpoints_vendor_type_url": { + "name": "uniq_provider_endpoints_vendor_type_url", + "columns": [ + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"provider_endpoints\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_vendor_type": { + "name": "idx_provider_endpoints_vendor_type", + "columns": [ + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"provider_endpoints\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_enabled": { + "name": "idx_provider_endpoints_enabled", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"provider_endpoints\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_pick_enabled": { + "name": "idx_provider_endpoints_pick_enabled", + "columns": [ + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"provider_endpoints\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_created_at": { + "name": "idx_provider_endpoints_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_deleted_at": { + "name": "idx_provider_endpoints_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "provider_endpoints_vendor_id_provider_vendors_id_fk": { + "name": "provider_endpoints_vendor_id_provider_vendors_id_fk", + "tableFrom": "provider_endpoints", + "tableTo": "provider_vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_groups": { + "name": "provider_groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "cost_multiplier": { + "name": "cost_multiplier", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": true, + "default": "'1.0'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "provider_groups_name_unique": { + "name": "provider_groups_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_vendors": { + "name": "provider_vendors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "website_domain": { + "name": "website_domain", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "website_url": { + "name": "website_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "favicon_url": { + "name": "favicon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "uniq_provider_vendors_website_domain": { + "name": "uniq_provider_vendors_website_domain", + "columns": [ + { + "expression": "website_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_vendors_created_at": { + "name": "idx_provider_vendors_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.providers": { + "name": "providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "new_api_access_token": { + "name": "new_api_access_token", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "new_api_user_id": { + "name": "new_api_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "provider_vendor_id": { + "name": "provider_vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "weight": { + "name": "weight", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "group_priorities": { + "name": "group_priorities", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'null'::jsonb" + }, + "cost_multiplier": { + "name": "cost_multiplier", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": false, + "default": "'1.0'" + }, + "group_tag": { + "name": "group_tag", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "provider_type": { + "name": "provider_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'claude'" + }, + "preserve_client_ip": { + "name": "preserve_client_ip", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "disable_session_reuse": { + "name": "disable_session_reuse", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "model_redirects": { + "name": "model_redirects", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "allowed_models": { + "name": "allowed_models", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'null'::jsonb" + }, + "allowed_clients": { + "name": "allowed_clients", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "blocked_clients": { + "name": "blocked_clients", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "active_time_start": { + "name": "active_time_start", + "type": "varchar(5)", + "primaryKey": false, + "notNull": false + }, + "active_time_end": { + "name": "active_time_end", + "type": "varchar(5)", + "primaryKey": false, + "notNull": false + }, + "codex_instructions_strategy": { + "name": "codex_instructions_strategy", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "default": "'auto'" + }, + "mcp_passthrough_type": { + "name": "mcp_passthrough_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "mcp_passthrough_url": { + "name": "mcp_passthrough_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "limit_5h_usd": { + "name": "limit_5h_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_5h_reset_mode": { + "name": "limit_5h_reset_mode", + "type": "daily_reset_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'rolling'" + }, + "limit_daily_usd": { + "name": "limit_daily_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "daily_reset_mode": { + "name": "daily_reset_mode", + "type": "daily_reset_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'fixed'" + }, + "daily_reset_time": { + "name": "daily_reset_time", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'00:00'" + }, + "limit_weekly_usd": { + "name": "limit_weekly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_monthly_usd": { + "name": "limit_monthly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_total_usd": { + "name": "limit_total_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "total_cost_reset_at": { + "name": "total_cost_reset_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "limit_concurrent_sessions": { + "name": "limit_concurrent_sessions", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "max_retry_attempts": { + "name": "max_retry_attempts", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "circuit_breaker_failure_threshold": { + "name": "circuit_breaker_failure_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5 + }, + "circuit_breaker_open_duration": { + "name": "circuit_breaker_open_duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1800000 + }, + "circuit_breaker_half_open_success_threshold": { + "name": "circuit_breaker_half_open_success_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 2 + }, + "proxy_url": { + "name": "proxy_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "proxy_fallback_to_direct": { + "name": "proxy_fallback_to_direct", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "custom_headers": { + "name": "custom_headers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "first_byte_timeout_streaming_ms": { + "name": "first_byte_timeout_streaming_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "streaming_idle_timeout_ms": { + "name": "streaming_idle_timeout_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "request_timeout_non_streaming_ms": { + "name": "request_timeout_non_streaming_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "website_url": { + "name": "website_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "favicon_url": { + "name": "favicon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cache_ttl_preference": { + "name": "cache_ttl_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "swap_cache_ttl_billing": { + "name": "swap_cache_ttl_billing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "context_1m_preference": { + "name": "context_1m_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "codex_reasoning_effort_preference": { + "name": "codex_reasoning_effort_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "codex_reasoning_summary_preference": { + "name": "codex_reasoning_summary_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "codex_text_verbosity_preference": { + "name": "codex_text_verbosity_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "codex_parallel_tool_calls_preference": { + "name": "codex_parallel_tool_calls_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "codex_image_generation_preference": { + "name": "codex_image_generation_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "codex_service_tier_preference": { + "name": "codex_service_tier_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "anthropic_max_tokens_preference": { + "name": "anthropic_max_tokens_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "anthropic_thinking_budget_preference": { + "name": "anthropic_thinking_budget_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "anthropic_adaptive_thinking": { + "name": "anthropic_adaptive_thinking", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'null'::jsonb" + }, + "gemini_google_search_preference": { + "name": "gemini_google_search_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "tpm": { + "name": "tpm", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "rpm": { + "name": "rpm", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "rpd": { + "name": "rpd", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "cc": { + "name": "cc", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_providers_enabled_priority": { + "name": "idx_providers_enabled_priority", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "weight", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"providers\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_group": { + "name": "idx_providers_group", + "columns": [ + { + "expression": "group_tag", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"providers\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_vendor_type_url_active": { + "name": "idx_providers_vendor_type_url_active", + "columns": [ + { + "expression": "provider_vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"providers\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_created_at": { + "name": "idx_providers_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_deleted_at": { + "name": "idx_providers_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_vendor_type": { + "name": "idx_providers_vendor_type", + "columns": [ + { + "expression": "provider_vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"providers\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_enabled_vendor_type": { + "name": "idx_providers_enabled_vendor_type", + "columns": [ + { + "expression": "provider_vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"providers\".\"deleted_at\" IS NULL AND \"providers\".\"is_enabled\" = true AND \"providers\".\"provider_vendor_id\" IS NOT NULL AND \"providers\".\"provider_vendor_id\" > 0", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "providers_provider_vendor_id_provider_vendors_id_fk": { + "name": "providers_provider_vendor_id_provider_vendors_id_fk", + "tableFrom": "providers", + "tableTo": "provider_vendors", + "columnsFrom": [ + "provider_vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.replay_payloads": { + "name": "replay_payloads", + "schema": "", + "columns": { + "replay_id": { + "name": "replay_id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "verifier": { + "name": "verifier", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "scope_tag": { + "name": "scope_tag", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "format": { + "name": "format", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "headers_json": { + "name": "headers_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "source_message_request_id": { + "name": "source_message_request_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_replay_payloads_key_id": { + "name": "idx_replay_payloads_key_id", + "columns": [ + { + "expression": "key_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_replay_payloads_expires_at": { + "name": "idx_replay_payloads_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.request_filters": { + "name": "request_filters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true + }, + "match_type": { + "name": "match_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "target": { + "name": "target", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "replacement": { + "name": "replacement", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "binding_type": { + "name": "binding_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'global'" + }, + "provider_ids": { + "name": "provider_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "group_tags": { + "name": "group_tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rule_mode": { + "name": "rule_mode", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'simple'" + }, + "execution_phase": { + "name": "execution_phase", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'guard'" + }, + "operations": { + "name": "operations", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_request_filters_enabled": { + "name": "idx_request_filters_enabled", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_request_filters_scope": { + "name": "idx_request_filters_scope", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_request_filters_action": { + "name": "idx_request_filters_action", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_request_filters_binding": { + "name": "idx_request_filters_binding", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "binding_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_request_filters_phase": { + "name": "idx_request_filters_phase", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_phase", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sensitive_words": { + "name": "sensitive_words", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "word": { + "name": "word", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "match_type": { + "name": "match_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'contains'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_sensitive_words_enabled": { + "name": "idx_sensitive_words_enabled", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "match_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sensitive_words_created_at": { + "name": "idx_sensitive_words_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.system_settings": { + "name": "system_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "site_title": { + "name": "site_title", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "default": "'CC Hub'" + }, + "allow_global_usage_view": { + "name": "allow_global_usage_view", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "currency_display": { + "name": "currency_display", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "billing_model_source": { + "name": "billing_model_source", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'original'" + }, + "codex_priority_billing_source": { + "name": "codex_priority_billing_source", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'requested'" + }, + "bill_non_successful_requests": { + "name": "bill_non_successful_requests", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "bill_hedge_losers": { + "name": "bill_hedge_losers", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "legacy_hedge_max_in_flight": { + "name": "legacy_hedge_max_in_flight", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 2 + }, + "discovery_enabled": { + "name": "discovery_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "discovery_concurrency": { + "name": "discovery_concurrency", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 2 + }, + "max_discovery_rounds": { + "name": "max_discovery_rounds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 2 + }, + "discovery_sla_ms": { + "name": "discovery_sla_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10000 + }, + "sticky_sla_ms": { + "name": "sticky_sla_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 20000 + }, + "racing_total_timeout_ms": { + "name": "racing_total_timeout_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 60000 + }, + "sticky_timeout_cooldown_ms": { + "name": "sticky_timeout_cooldown_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 300000 + }, + "timezone": { + "name": "timezone", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "enable_auto_cleanup": { + "name": "enable_auto_cleanup", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "cleanup_retention_days": { + "name": "cleanup_retention_days", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30 + }, + "cleanup_schedule": { + "name": "cleanup_schedule", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "default": "'0 2 * * *'" + }, + "cleanup_batch_size": { + "name": "cleanup_batch_size", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 10000 + }, + "enable_client_version_check": { + "name": "enable_client_version_check", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "verbose_provider_error": { + "name": "verbose_provider_error", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pass_through_upstream_error_message": { + "name": "pass_through_upstream_error_message", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_http2": { + "name": "enable_http2", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_openai_responses_websocket": { + "name": "enable_openai_responses_websocket", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_high_concurrency_mode": { + "name": "enable_high_concurrency_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "intercept_anthropic_warmup_requests": { + "name": "intercept_anthropic_warmup_requests", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_thinking_signature_rectifier": { + "name": "enable_thinking_signature_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_thinking_budget_rectifier": { + "name": "enable_thinking_budget_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_thinking_effort_conflict_rectifier": { + "name": "enable_thinking_effort_conflict_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_gemini_function_id_rectifier": { + "name": "enable_gemini_function_id_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_billing_header_rectifier": { + "name": "enable_billing_header_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_response_input_rectifier": { + "name": "enable_response_input_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_non_conversation_endpoint_provider_fallback": { + "name": "allow_non_conversation_endpoint_provider_fallback", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "fake_streaming_whitelist": { + "name": "fake_streaming_whitelist", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "enable_codex_session_id_completion": { + "name": "enable_codex_session_id_completion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_claude_metadata_user_id_injection": { + "name": "enable_claude_metadata_user_id_injection", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_response_fixer": { + "name": "enable_response_fixer", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "response_fixer_config": { + "name": "response_fixer_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{\"fixTruncatedJson\":true,\"fixSseFormat\":true,\"fixEncoding\":true,\"maxJsonDepth\":200,\"maxFixSize\":1048576}'::jsonb" + }, + "quota_db_refresh_interval_seconds": { + "name": "quota_db_refresh_interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 10 + }, + "quota_lease_percent_5h": { + "name": "quota_lease_percent_5h", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "quota_lease_percent_daily": { + "name": "quota_lease_percent_daily", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "quota_lease_percent_weekly": { + "name": "quota_lease_percent_weekly", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "quota_lease_percent_monthly": { + "name": "quota_lease_percent_monthly", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "quota_lease_cap_usd": { + "name": "quota_lease_cap_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "ip_extraction_config": { + "name": "ip_extraction_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ip_geo_lookup_enabled": { + "name": "ip_geo_lookup_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "public_status_window_hours": { + "name": "public_status_window_hours", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 24 + }, + "public_status_aggregation_interval_minutes": { + "name": "public_status_aggregation_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "stream_gate_mode": { + "name": "stream_gate_mode", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true, + "default": "'enforce'" + }, + "affinity_ignore_client_session_id": { + "name": "affinity_ignore_client_session_id", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "replay_enabled": { + "name": "replay_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "replay_cache_ttl_minutes": { + "name": "replay_cache_ttl_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 30 + }, + "cache_effectiveness_enabled": { + "name": "cache_effectiveness_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enable_memory_admission": { + "name": "enable_memory_admission", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "system_settings_legacy_hedge_max_in_flight_range": { + "name": "system_settings_legacy_hedge_max_in_flight_range", + "value": "\"system_settings\".\"legacy_hedge_max_in_flight\" >= 1 AND \"system_settings\".\"legacy_hedge_max_in_flight\" <= 4" + } + }, + "isRLSEnabled": false + }, + "public.usage_ledger": { + "name": "usage_ledger", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "final_provider_id": { + "name": "final_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "original_model": { + "name": "original_model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "actual_response_model": { + "name": "actual_response_model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "endpoint": { + "name": "endpoint", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "api_type": { + "name": "api_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "session_identity": { + "name": "session_identity", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "session_identity_kind": { + "name": "session_identity_kind", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "affinity_scope_tag": { + "name": "affinity_scope_tag", + "type": "varchar(16)", + "primaryKey": false, + "notNull": false + }, + "affinity_fingerprint": { + "name": "affinity_fingerprint", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "affinity_fingerprint_chain": { + "name": "affinity_fingerprint_chain", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_replay": { + "name": "is_replay", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "replay_source_request_id": { + "name": "replay_source_request_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_success": { + "name": "is_success", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "success_rate_outcome": { + "name": "success_rate_outcome", + "type": "varchar(16)", + "primaryKey": false, + "notNull": false + }, + "blocked_by": { + "name": "blocked_by", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "cost_usd": { + "name": "cost_usd", + "type": "numeric(21, 15)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "cost_multiplier": { + "name": "cost_multiplier", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": false + }, + "group_cost_multiplier": { + "name": "group_cost_multiplier", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_read_input_tokens": { + "name": "cache_read_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_creation_5m_input_tokens": { + "name": "cache_creation_5m_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_creation_1h_input_tokens": { + "name": "cache_creation_1h_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_ttl_applied": { + "name": "cache_ttl_applied", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "context_1m_applied": { + "name": "context_1m_applied", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "swap_cache_ttl_applied": { + "name": "swap_cache_ttl_applied", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ttfb_ms": { + "name": "ttfb_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "first_byte_ms": { + "name": "first_byte_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_ip": { + "name": "client_ip", + "type": "varchar(45)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_usage_ledger_request_id": { + "name": "idx_usage_ledger_request_id", + "columns": [ + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_user_created_at": { + "name": "idx_usage_ledger_user_created_at", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL AND \"usage_ledger\".\"is_replay\" = false", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_user_id_reset": { + "name": "idx_usage_ledger_user_id_reset", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_key_created_at": { + "name": "idx_usage_ledger_key_created_at", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL AND \"usage_ledger\".\"is_replay\" = false", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_provider_created_at": { + "name": "idx_usage_ledger_provider_created_at", + "columns": [ + { + "expression": "final_provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL AND \"usage_ledger\".\"is_replay\" = false", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_created_at_minute": { + "name": "idx_usage_ledger_created_at_minute", + "columns": [ + { + "expression": "date_trunc('minute', \"created_at\" AT TIME ZONE 'UTC')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_created_at_desc_id": { + "name": "idx_usage_ledger_created_at_desc_id", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_session_id": { + "name": "idx_usage_ledger_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"session_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_session_identity_created_at": { + "name": "idx_usage_ledger_session_identity_created_at", + "columns": [ + { + "expression": "COALESCE(\"session_identity\", \"session_id\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"created_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL AND \"usage_ledger\".\"is_replay\" = false", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_session_identity": { + "name": "idx_usage_ledger_session_identity", + "columns": [ + { + "expression": "COALESCE(\"session_identity\", \"session_id\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_session_identity_prefix": { + "name": "idx_usage_ledger_session_identity_prefix", + "columns": [ + { + "expression": "COALESCE(\"session_identity\", \"session_id\") varchar_pattern_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_session_id_prefix": { + "name": "idx_usage_ledger_session_id_prefix", + "columns": [ + { + "expression": "\"session_id\" varchar_pattern_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_model": { + "name": "idx_usage_ledger_model", + "columns": [ + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"model\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_key_cost": { + "name": "idx_usage_ledger_key_cost", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "endpoint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL AND \"usage_ledger\".\"is_replay\" = false", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_user_cost_cover": { + "name": "idx_usage_ledger_user_cost_cover", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "endpoint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL AND \"usage_ledger\".\"is_replay\" = false", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_provider_cost_cover": { + "name": "idx_usage_ledger_provider_cost_cover", + "columns": [ + { + "expression": "final_provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "endpoint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL AND \"usage_ledger\".\"is_replay\" = false", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_key_created_at_desc_cover": { + "name": "idx_usage_ledger_key_created_at_desc_cover", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"created_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "final_provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL AND \"usage_ledger\".\"is_replay\" = false", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "varchar", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "rpm_limit": { + "name": "rpm_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "daily_limit_usd": { + "name": "daily_limit_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "provider_group": { + "name": "provider_group", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false, + "default": "'default'" + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "limit_5h_usd": { + "name": "limit_5h_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_5h_reset_mode": { + "name": "limit_5h_reset_mode", + "type": "daily_reset_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'rolling'" + }, + "limit_weekly_usd": { + "name": "limit_weekly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_monthly_usd": { + "name": "limit_monthly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_total_usd": { + "name": "limit_total_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "cost_reset_at": { + "name": "cost_reset_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "limit_5h_cost_reset_at": { + "name": "limit_5h_cost_reset_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "limit_concurrent_sessions": { + "name": "limit_concurrent_sessions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "daily_reset_mode": { + "name": "daily_reset_mode", + "type": "daily_reset_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'fixed'" + }, + "daily_reset_time": { + "name": "daily_reset_time", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'00:00'" + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "allowed_clients": { + "name": "allowed_clients", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "allowed_models": { + "name": "allowed_models", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "blocked_clients": { + "name": "blocked_clients", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_users_active_role_sort": { + "name": "idx_users_active_role_sort", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"users\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_users_enabled_expires_at": { + "name": "idx_users_enabled_expires_at", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"users\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_users_tags_gin": { + "name": "idx_users_tags_gin", + "columns": [ + { + "expression": "tags", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"users\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_users_created_at": { + "name": "idx_users_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_users_deleted_at": { + "name": "idx_users_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_targets": { + "name": "webhook_targets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "provider_type": { + "name": "provider_type", + "type": "webhook_provider_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "webhook_url": { + "name": "webhook_url", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": false + }, + "telegram_bot_token": { + "name": "telegram_bot_token", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "telegram_chat_id": { + "name": "telegram_chat_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "dingtalk_secret": { + "name": "dingtalk_secret", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "custom_template": { + "name": "custom_template", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "custom_headers": { + "name": "custom_headers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "proxy_url": { + "name": "proxy_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "proxy_fallback_to_direct": { + "name": "proxy_fallback_to_direct", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_test_at": { + "name": "last_test_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_test_result": { + "name": "last_test_result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.daily_reset_mode": { + "name": "daily_reset_mode", + "schema": "public", + "values": [ + "fixed", + "rolling" + ] + }, + "public.notification_type": { + "name": "notification_type", + "schema": "public", + "values": [ + "circuit_breaker", + "daily_leaderboard", + "cost_alert", + "cache_hit_rate_alert" + ] + }, + "public.webhook_provider_type": { + "name": "webhook_provider_type", + "schema": "public", + "values": [ + "wechat", + "feishu", + "dingtalk", + "telegram", + "custom" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 8ff8c5b24..d87039e76 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -869,6 +869,13 @@ "when": 1790352239389, "tag": "0123_provider_new_api_access_token", "breakpoints": true + }, + { + "idx": 124, + "version": "7", + "when": 1790674741240, + "tag": "0124_system_settings_memory_admission", + "breakpoints": true } ] } \ No newline at end of file diff --git a/messages/en/settings/config.json b/messages/en/settings/config.json index 61d257756..4a3fe3cea 100644 --- a/messages/en/settings/config.json +++ b/messages/en/settings/config.json @@ -60,6 +60,8 @@ "enableOpenaiResponsesWebsocketDesc": "When enabled, if a client opens a WebSocket connection to /v1/responses and the selected provider is a Codex type, CCH will attempt a sibling WebSocket to the upstream. If the upstream does not support WebSocket or the handshake fails, CCH gracefully falls back to standard HTTP Responses while keeping the client WebSocket open; the fallback is not counted toward circuit breakers. Non-WebSocket clients and non-Codex providers are unaffected.", "enableHighConcurrencyMode": "Enable High-Concurrency Mode", "enableHighConcurrencyModeDesc": "When enabled, CCH disables part of the Redis debug snapshots and real-time session observability writes to reduce CPU and IO pressure under high RPM. Forwarding, rectifiers, fake-200 detection, billing, and quota enforcement remain unchanged, but Sessions debugging details may be reduced or delayed.", + "enableMemoryAdmission": "Enable Memory Admission", + "enableMemoryAdmissionDesc": "When enabled, request bodies and the stream buffer before first content are admitted against this machine's available memory. When the budget runs short they are written to disk, and if that is not enough CCH returns a local 429 asking the client to retry later. When disabled, this data stays in memory without queuing and no local 429 is returned.", "enableResponseFixer": "Enable Response Fixer", "enableResponseFixerDesc": "Automatically repairs common upstream response issues (encoding, SSE, truncated JSON). Enabled by default.", "enableThinkingSignatureRectifier": "Enable Thinking Signature Rectifier", diff --git a/messages/ja/settings/config.json b/messages/ja/settings/config.json index cd699b56e..fb2f24142 100644 --- a/messages/ja/settings/config.json +++ b/messages/ja/settings/config.json @@ -60,6 +60,8 @@ "enableOpenaiResponsesWebsocketDesc": "有効にすると、クライアントが /v1/responses に WebSocket 接続し、かつ Codex タイプのプロバイダーが選択された場合、CCH は上流にも WebSocket 接続を試みます。上流が WebSocket をサポートしない、またはハンドシェイクに失敗した場合は、クライアント WebSocket を開いたまま通常の HTTP Responses に優雅にフォールバックします。このフォールバックはサーキットブレーカーにカウントされません。非 WebSocket クライアントと非 Codex プロバイダーの動作は変わりません。", "enableHighConcurrencyMode": "高並行モードを有効化", "enableHighConcurrencyModeDesc": "有効にすると、高 RPM 時の CPU / IO 負荷を下げるため、Redis の一部デバッグスナップショットとリアルタイム Session 観測書き込みを停止します。転送、整流、fake 200 検知、課金、制限処理は維持されますが、Sessions のデバッグ詳細は減少または遅延する場合があります。", + "enableMemoryAdmission": "メモリ受付制御を有効化", + "enableMemoryAdmissionDesc": "有効にすると、リクエスト本文とストリームの最初のコンテンツまでのバッファを、このマシンの空きメモリに基づく枠で受け付けます。枠が足りない場合はディスクに一時保存し、それでも足りない場合はローカル 429 を返してクライアントに再試行を促します。無効の場合、これらのデータはすべてメモリ上に保持され、待機もローカル 429 も発生しません。", "enableResponseFixer": "レスポンス整流を有効化", "enableResponseFixerDesc": "上流応答の一般的な形式問題(エンコーディング、SSE、途切れた JSON)を自動修復します(既定で有効)。", "enableThinkingSignatureRectifier": "thinking 署名整流を有効化", diff --git a/messages/ru/settings/config.json b/messages/ru/settings/config.json index 3395f2b9d..9d1ef0fcf 100644 --- a/messages/ru/settings/config.json +++ b/messages/ru/settings/config.json @@ -60,6 +60,8 @@ "enableOpenaiResponsesWebsocketDesc": "Если включено, то когда клиент открывает WebSocket-соединение с /v1/responses и выбирается провайдер типа Codex, CCH попытается установить WebSocket-соединение с вышестоящим сервером. Если сервер не поддерживает WebSocket или рукопожатие не удастся, CCH плавно переключится на обычный HTTP Responses, сохраняя WebSocket клиента открытым; этот fallback не учитывается в circuit breaker. Клиенты без WebSocket и провайдеры, отличные от Codex, работают без изменений.", "enableHighConcurrencyMode": "Включить режим высокой нагрузки", "enableHighConcurrencyModeDesc": "Если включено, CCH отключит часть Redis-снимков для отладки и записи real-time Session-наблюдения, чтобы снизить нагрузку на CPU и IO при высоком RPM. Пересылка, rectifier-логика, обнаружение fake 200, биллинг и лимиты сохраняются, но детализация отладки в Sessions может уменьшиться или запаздывать.", + "enableMemoryAdmission": "Включить контроль памяти", + "enableMemoryAdmissionDesc": "Если включено, тела запросов и буфер потока до первого содержимого принимаются в пределах доступной памяти этой машины. При нехватке бюджета данные временно записываются на диск, а если и этого мало, CCH возвращает локальный 429 с просьбой повторить запрос позже. Если выключено, эти данные целиком хранятся в памяти, без очереди и без локального 429.", "enableResponseFixer": "Включить исправление ответов", "enableResponseFixerDesc": "Автоматически исправляет распространённые проблемы ответа у провайдеров (кодировка, SSE, обрезанный JSON). Включено по умолчанию.", "enableThinkingSignatureRectifier": "Включить исправление thinking-signature", diff --git a/messages/zh-CN/settings/config.json b/messages/zh-CN/settings/config.json index ba46f1bfa..a51f53c59 100644 --- a/messages/zh-CN/settings/config.json +++ b/messages/zh-CN/settings/config.json @@ -75,6 +75,8 @@ "enableOpenaiResponsesWebsocketDesc": "启用后,当客户端以 WebSocket 连接 /v1/responses 且选中 Codex 类型供应商时,CCH 会尝试与上游建立 WebSocket。若上游不支持或握手失败,将优雅降级到普通 HTTP Responses,客户端 WebSocket 保持打开;降级不计入熔断。非 WebSocket 客户端与非 Codex 供应商行为不变。", "enableHighConcurrencyMode": "启用高并发模式", "enableHighConcurrencyModeDesc": "开启后,将关闭部分 Redis 调试快照与实时会话观测写入,以降低高并发下的 CPU 与 IO 开销。不会影响转发、整流、fake 200 检测、计费与限额,但 Sessions 调试详情会减少或延后。", + "enableMemoryAdmission": "启用内存准入", + "enableMemoryAdmissionDesc": "开启后,请求正文与流式首内容缓冲按本机可用内存分配额度,额度不足时暂存到磁盘,仍然不足时返回本地 429 并提示客户端稍后重试。关闭时这些数据全部保留在内存中,不排队,也不返回本地 429。", "interceptAnthropicWarmupRequests": "拦截 Warmup 请求(Anthropic)", "interceptAnthropicWarmupRequestsDesc": "开启后,识别到 Claude Code 的 Warmup 探测请求将由 CCH 直接抢答短响应,避免访问上游供应商;该请求会记录在日志中,但不计费、不限流、不计入统计。", "enableThinkingSignatureRectifier": "启用 thinking 签名整流器", diff --git a/messages/zh-TW/settings/config.json b/messages/zh-TW/settings/config.json index baf97527f..8322cd782 100644 --- a/messages/zh-TW/settings/config.json +++ b/messages/zh-TW/settings/config.json @@ -60,6 +60,8 @@ "enableOpenaiResponsesWebsocketDesc": "啟用後,當客戶端以 WebSocket 連線 /v1/responses 且命中 Codex 類型供應商時,CCH 會嘗試與上游建立 WebSocket 連線。若上游不支援或握手失敗,將優雅降級為一般 HTTP Responses,客戶端 WebSocket 保持開啟;降級不計入熔斷。非 WebSocket 客戶端與非 Codex 供應商行為不變。", "enableHighConcurrencyMode": "啟用高並發模式", "enableHighConcurrencyModeDesc": "開啟後,將關閉部分 Redis 除錯快照與即時 Session 觀測寫入,以降低高並發下的 CPU 與 IO 開銷。轉發、整流、fake 200 偵測、計費與限額不受影響,但 Sessions 除錯詳情會減少或延後。", + "enableMemoryAdmission": "啟用記憶體准入", + "enableMemoryAdmissionDesc": "開啟後,請求本文與串流首內容緩衝依本機可用記憶體分配額度,額度不足時暫存到磁碟,仍然不足時回傳本地 429 並提示用戶端稍後重試。關閉時這些資料全部保留在記憶體中,不排隊,也不回傳本地 429。", "enableResponseFixer": "啟用回應整流", "enableResponseFixerDesc": "自動修復上游回應中常見的編碼、SSE 與 JSON 格式問題(預設開啟)。", "enableThinkingSignatureRectifier": "啟用 thinking 簽名整流器", diff --git a/server-lib/memory-governor.js b/server-lib/memory-governor.js index 379ea44b2..9c436bf89 100644 --- a/server-lib/memory-governor.js +++ b/server-lib/memory-governor.js @@ -27,6 +27,9 @@ class MemoryGovernor { this.plan = createMemoryPlan({ env: this.env, snapshot: this.readSnapshot() }); this.limit = options.limit ?? this.plan.hotBudgetBytes; this.ceiling = this.limit; + // 关闭时只记账不设限:租约与增长总是成功,不排队、不申请跨进程授权、不返回本地 429。 + this.enabled = options.enabled ?? false; + this.enabledListeners = new Set(); this.used = 0; this.waiting = 0; this.peak = 0; @@ -90,8 +93,21 @@ class MemoryGovernor { } } + setEnabled(enabled) { + const next = enabled === true; + if (next === this.enabled) return; + this.enabled = next; + for (const listener of this.enabledListeners) listener(next); + } + + /** 开关变化时通知子限额等外部队列立即重新调度排队者。 */ + onEnabledChange(listener) { + this.enabledListeners.add(listener); + return () => this.enabledListeners.delete(listener); + } + snapshot() { - return { usedBytes: this.used, limitBytes: this.remote ? this.credits : this.limit, waiting: this.waiting, peakBytes: this.peak, rejected: this.rejected, source: this.plan.source, stages: this.stages, leases: this.leaseLedger() }; + return { enabled: this.enabled, usedBytes: this.used, limitBytes: this.remote ? this.credits : this.limit, waiting: this.waiting, peakBytes: this.peak, rejected: this.rejected, source: this.plan.source, stages: this.stages, leases: this.leaseLedger() }; } /** 仅供诊断:按标签汇总在账租约,定位长期不归还的所有者。 */ @@ -166,7 +182,7 @@ class MemoryGovernor { tryLease(bytes, tag = "untagged") { if (!Number.isSafeInteger(bytes) || bytes < 0) throw new RangeError("Invalid memory lease size"); const limit = this.remote ? this.credits : this.limit; - if (bytes > limit - this.used) return null; + if (this.enabled && bytes > limit - this.used) return null; this.used += bytes; this.peak = Math.max(this.peak, this.used); let size = bytes; @@ -178,7 +194,7 @@ class MemoryGovernor { if (released) return false; if (target <= size) return true; const delta = target - size; - if (delta > (this.remote ? this.credits : this.limit) - this.used) { + if (this.enabled && delta > (this.remote ? this.credits : this.limit) - this.used) { if (requestCredits) void this.requestCredits(delta); return false; } diff --git a/server-lib/resource-snapshot.js b/server-lib/resource-snapshot.js index 2cf76fd36..02d578d82 100644 --- a/server-lib/resource-snapshot.js +++ b/server-lib/resource-snapshot.js @@ -62,7 +62,13 @@ function readResourceSnapshot(options = {}) { } while (directory === mount || directory.startsWith(`${mount}/`)) { const number = (file) => finiteBytes(safeRead(path.join(directory, file))); - const current = number(v2 ? "memory.current" : "memory.usage_in_bytes"); + // cgroup 用量包含可回收的 page cache(溢写文件、读取过的代码与日志),不扣除时 + // 运行一段时间后剩余额度会被 cache 压到 0。与 kubelet/cAdvisor 的 working set 一致, + // 扣除 inactive file 后作为实际占用。 + const stat = safeRead(path.join(directory, "memory.stat")) || ""; + const inactiveFile = finiteBytes(stat.match(v2 ? /^inactive_file (\d+)$/m : /^total_inactive_file (\d+)$/m)?.[1]) ?? 0; + const usage = number(v2 ? "memory.current" : "memory.usage_in_bytes"); + const current = usage === null ? null : Math.max(0, usage - inactiveFile); if (current !== null) hasMemoryController = true; for (const limitFile of v2 ? ["memory.max", "memory.high"] : ["memory.limit_in_bytes"]) { const limit = number(limitFile); @@ -79,7 +85,8 @@ function readResourceSnapshot(options = {}) { if (limit !== null) swap = Math.min(swap, used === null ? 0 : Math.max(0, limit - used)); } else { const limit = number("memory.memsw.limit_in_bytes"); - const used = number("memory.memsw.usage_in_bytes"); + const memswUsage = number("memory.memsw.usage_in_bytes"); + const used = memswUsage === null ? null : Math.max(0, memswUsage - inactiveFile); if (limit !== null && used !== null) { hasSwapAccounting = true; combinedRemaining = Math.min(combinedRemaining, Math.max(0, limit - used)); diff --git a/src/actions/system-config.ts b/src/actions/system-config.ts index b17954044..96f53163a 100644 --- a/src/actions/system-config.ts +++ b/src/actions/system-config.ts @@ -117,6 +117,7 @@ export async function saveSystemSettings(formData: { replayEnabled?: boolean | null; replayCacheTtlMinutes?: number; cacheEffectivenessEnabled?: boolean | null; + enableMemoryAdmission?: boolean; enableCodexSessionIdCompletion?: boolean; enableClaudeMetadataUserIdInjection?: boolean; enableResponseFixer?: boolean; @@ -209,6 +210,7 @@ export async function saveSystemSettings(formData: { replayEnabled: validated.replayEnabled, replayCacheTtlMinutes: validated.replayCacheTtlMinutes, cacheEffectivenessEnabled: validated.cacheEffectivenessEnabled, + enableMemoryAdmission: validated.enableMemoryAdmission, enableCodexSessionIdCompletion: validated.enableCodexSessionIdCompletion, enableClaudeMetadataUserIdInjection: validated.enableClaudeMetadataUserIdInjection, enableResponseFixer: validated.enableResponseFixer, diff --git a/src/app/[locale]/settings/config/_components/system-settings-form.tsx b/src/app/[locale]/settings/config/_components/system-settings-form.tsx index 09212da22..28747d412 100644 --- a/src/app/[locale]/settings/config/_components/system-settings-form.tsx +++ b/src/app/[locale]/settings/config/_components/system-settings-form.tsx @@ -12,6 +12,7 @@ import { Gauge, Globe, MapPin, + MemoryStick, Network, Pencil, Repeat, @@ -94,6 +95,7 @@ interface SystemSettingsFormProps { | "enableHttp2" | "enableOpenaiResponsesWebsocket" | "enableHighConcurrencyMode" + | "enableMemoryAdmission" | "interceptAnthropicWarmupRequests" | "enableThinkingSignatureRectifier" | "enableBillingHeaderRectifier" @@ -198,6 +200,9 @@ export function SystemSettingsForm({ const [enableHighConcurrencyMode, setEnableHighConcurrencyMode] = useState( initialSettings.enableHighConcurrencyMode ); + const [enableMemoryAdmission, setEnableMemoryAdmission] = useState( + initialSettings.enableMemoryAdmission + ); const [interceptAnthropicWarmupRequests, setInterceptAnthropicWarmupRequests] = useState( initialSettings.interceptAnthropicWarmupRequests ); @@ -425,6 +430,7 @@ export function SystemSettingsForm({ enableHttp2, enableOpenaiResponsesWebsocket, enableHighConcurrencyMode, + enableMemoryAdmission, interceptAnthropicWarmupRequests, enableThinkingSignatureRectifier, enableBillingHeaderRectifier, @@ -488,6 +494,7 @@ export function SystemSettingsForm({ setEnableHttp2(result.data.enableHttp2); setEnableOpenaiResponsesWebsocket(result.data.enableOpenaiResponsesWebsocket); setEnableHighConcurrencyMode(result.data.enableHighConcurrencyMode); + setEnableMemoryAdmission(result.data.enableMemoryAdmission); setInterceptAnthropicWarmupRequests(result.data.interceptAnthropicWarmupRequests); setEnableThinkingSignatureRectifier(result.data.enableThinkingSignatureRectifier); setEnableBillingHeaderRectifier(result.data.enableBillingHeaderRectifier); @@ -1013,6 +1020,28 @@ export function SystemSettingsForm({ /> + {/* Memory Admission */} +
+
+
+ +
+
+

{t("enableMemoryAdmission")}

+

+ {t("enableMemoryAdmissionDesc")} +

+
+
+ setEnableMemoryAdmission(checked)} + disabled={isPending} + /> +
+ {/* Intercept Anthropic Warmup Requests */}
diff --git a/src/app/[locale]/settings/config/page.tsx b/src/app/[locale]/settings/config/page.tsx index 37a02e503..af3dcfdb9 100644 --- a/src/app/[locale]/settings/config/page.tsx +++ b/src/app/[locale]/settings/config/page.tsx @@ -71,6 +71,7 @@ async function SettingsConfigContent({ locale }: { locale: string }) { enableHttp2: settings.enableHttp2, enableOpenaiResponsesWebsocket: settings.enableOpenaiResponsesWebsocket, enableHighConcurrencyMode: settings.enableHighConcurrencyMode, + enableMemoryAdmission: settings.enableMemoryAdmission, interceptAnthropicWarmupRequests: settings.interceptAnthropicWarmupRequests, enableThinkingSignatureRectifier: settings.enableThinkingSignatureRectifier, enableThinkingBudgetRectifier: settings.enableThinkingBudgetRectifier, diff --git a/src/app/api/admin/system-config/route.ts b/src/app/api/admin/system-config/route.ts index a5f56127a..153c7415e 100644 --- a/src/app/api/admin/system-config/route.ts +++ b/src/app/api/admin/system-config/route.ts @@ -110,6 +110,7 @@ export async function POST(req: Request) { enableResponseInputRectifier: validated.enableResponseInputRectifier, streamGateMode: validated.streamGateMode, affinityIgnoreClientSessionId: validated.affinityIgnoreClientSessionId, + enableMemoryAdmission: validated.enableMemoryAdmission, enableCodexSessionIdCompletion: validated.enableCodexSessionIdCompletion, enableClaudeMetadataUserIdInjection: validated.enableClaudeMetadataUserIdInjection, enableResponseFixer: validated.enableResponseFixer, diff --git a/src/app/v1/_lib/proxy-handler.ts b/src/app/v1/_lib/proxy-handler.ts index c87f19042..fc4cec3a4 100644 --- a/src/app/v1/_lib/proxy-handler.ts +++ b/src/app/v1/_lib/proxy-handler.ts @@ -1,9 +1,9 @@ import type { Context } from "hono"; import { isRawPassthroughEndpointPolicy } from "@/app/v1/_lib/proxy/endpoint-policy"; import { findSafeDatabaseError } from "@/drizzle/admitted-client"; -import { getCachedSystemSettings } from "@/lib/config"; +import { getCachedSystemSettings, getCachedSystemSettingsOnlyCache } from "@/lib/config"; import { logger } from "@/lib/logger"; -import { isLocalCapacityError } from "@/lib/memory/governor"; +import { getMemoryGovernor, isLocalCapacityError } from "@/lib/memory/governor"; import { buildLocalCapacityResponse } from "@/lib/memory/http"; import { withRequestMemoryLifetime } from "@/lib/memory/request-lifetime"; import { ProxyStatusTracker } from "@/lib/proxy-status-tracker"; @@ -61,15 +61,13 @@ async function handleOwnedProxyRequest(c: Context): Promise { return identity.identity; }; try { - session = await ProxySession.fromContext(c); try { cachedSystemSettings = await getCachedSystemSettings(); - session.setHighConcurrencyModeEnabled( - cachedSystemSettings.enableHighConcurrencyMode ?? false - ); - session.setRawCrossProviderFallbackEnabled( - cachedSystemSettings.allowNonConversationEndpointProviderFallback ?? true - ); + // 正文读取受内存准入约束,必须在读取入站正文之前同步开关。只同步进程缓存中的 + // 当前设置:缓存失效期间完成的旧查询与读取失败时的默认对象不能改变全进程状态。 + if (cachedSystemSettings === getCachedSystemSettingsOnlyCache()) { + getMemoryGovernor().setEnabled(cachedSystemSettings.enableMemoryAdmission); + } } catch (settingsError) { const databaseError = findSafeDatabaseError(settingsError); logger.warn( @@ -81,9 +79,14 @@ async function handleOwnedProxyRequest(c: Context): Promise { databaseCode: databaseError?.code, } ); - session.setHighConcurrencyModeEnabled(false); - session.setRawCrossProviderFallbackEnabled(false); } + session = await ProxySession.fromContext(c); + session.setHighConcurrencyModeEnabled(cachedSystemSettings?.enableHighConcurrencyMode ?? false); + session.setRawCrossProviderFallbackEnabled( + cachedSystemSettings + ? (cachedSystemSettings.allowNonConversationEndpointProviderFallback ?? true) + : false + ); // 自动检测请求格式(端点优先,请求体补充) if (session.originalFormat === "claude") { diff --git a/src/app/v1/_lib/proxy/session.ts b/src/app/v1/_lib/proxy/session.ts index e2b2c28bb..3663aa2f5 100644 --- a/src/app/v1/_lib/proxy/session.ts +++ b/src/app/v1/_lib/proxy/session.ts @@ -1772,6 +1772,7 @@ async function parseRequestBody(c: Context): Promise { ? getOpenAIImageMultipartSummary(imageRequestMetadata) : "(multipart image request)"; requestBodyLogNote = "图片 multipart 请求已记录结构化摘要。"; + loaded.lease.shrinkTo(loaded.retainedBytes); return { requestMessage, @@ -1798,8 +1799,9 @@ async function parseRequestBody(c: Context): Promise { requestBodyLog = requestBodyText; requestBodyLogNote = "请求体不是合法 JSON,已记录原始文本。"; } - // 保留按正文结构估算的工作集,覆盖后续过滤、重试和异步消费者; - // 不能只按字节数缩至固定倍数,密集小对象的 V8 开销可能更大。 + // 解析峰值已结束。请求剩余生命周期(含长时间流式响应、过滤、重试与后台消费者)按正文结构 + // 估算的实际持有量占用额度;密集小对象的 V8 开销计入 structureBytes,不能只按字节数估算。 + loaded.lease.shrinkTo(loaded.retainedBytes); return { requestMessage, diff --git a/src/app/v1/_lib/proxy/stream-gate/prebuffer-budget.ts b/src/app/v1/_lib/proxy/stream-gate/prebuffer-budget.ts index 8a111849c..16fc64e29 100644 --- a/src/app/v1/_lib/proxy/stream-gate/prebuffer-budget.ts +++ b/src/app/v1/_lib/proxy/stream-gate/prebuffer-budget.ts @@ -55,7 +55,10 @@ export class StreamGatePrebufferBudget { constructor( private readonly resolveLimit: () => number, private readonly governor?: MemoryGovernor - ) {} + ) { + // 内存准入关闭时子限额变为不限,已在队列中的请求需要立即放行。 + governor?.onEnabledChange(() => this.drainWaiters()); + } async acquire(reservedBytes: number, signal?: AbortSignal): Promise { const started = performance.now(); @@ -274,12 +277,14 @@ export function getStreamGatePrebufferBudget(): StreamGatePrebufferBudget { const globalState = globalThis as typeof globalThis & { [STREAM_GATE_PREBUFFER_BUDGET_SYMBOL]?: StreamGatePrebufferBudget; }; + const governor = getMemoryGovernor(); + // 门控子限额属于内存准入;准入关闭时不限额,排队者在下一次归还时全部放行。 globalState[STREAM_GATE_PREBUFFER_BUDGET_SYMBOL] ??= new StreamGatePrebufferBudget( () => - process.env.STREAM_GATE_GLOBAL_PREBUFFER_BYTE_CAP + governor.enabled && process.env.STREAM_GATE_GLOBAL_PREBUFFER_BYTE_CAP ? resolveStreamGateGlobalPrebufferByteCap() : Number.MAX_SAFE_INTEGER, - getMemoryGovernor() + governor ); return globalState[STREAM_GATE_PREBUFFER_BUDGET_SYMBOL]; } diff --git a/src/drizzle/schema.ts b/src/drizzle/schema.ts index 4ea75e08b..ba7d80909 100644 --- a/src/drizzle/schema.ts +++ b/src/drizzle/schema.ts @@ -1087,6 +1087,10 @@ export const systemSettings = pgTable('system_settings', { // F3b 最长前缀匹配缓存模拟开关覆写(null = 跟随环境变量 ENABLE_CACHE_EFFECTIVENESS) cacheEffectivenessEnabled: boolean('cache_effectiveness_enabled'), + // 内存准入(默认关闭) + // 开启后:请求正文与流式门控前缀按可用内存预算准入,超出时落盘或返回本地 429 + enableMemoryAdmission: boolean('enable_memory_admission').notNull().default(false), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(), }, (table) => ({ diff --git a/src/lib/api-client/v1/openapi-types.gen.ts b/src/lib/api-client/v1/openapi-types.gen.ts index 037675e5e..76e147c1d 100644 --- a/src/lib/api-client/v1/openapi-types.gen.ts +++ b/src/lib/api-client/v1/openapi-types.gen.ts @@ -12744,6 +12744,8 @@ export interface operations { streamGateMode: "off" | "shadow" | "enforce"; /** @description Whether fingerprintable requests force longest-prefix affinity for provider stickiness, skipping client session id binding. */ affinityIgnoreClientSessionId: boolean; + /** @description Whether local memory admission is enabled. When enabled, request bodies and stream gate prefixes are admitted against the available memory budget, spill to disk, and return a local 429 when capacity runs out. When disabled, they stay in memory without queuing or local 429. */ + enableMemoryAdmission: boolean; /** @description Request replay (response caching and upstream connection reuse) override. Null follows the ENABLE_REQUEST_REPLAY environment variable. */ replayEnabled: boolean | null; /** @description Replay completed payload reuse window in minutes. */ @@ -13034,6 +13036,8 @@ export interface operations { streamGateMode?: "off" | "shadow" | "enforce"; /** @description Whether fingerprintable requests force longest-prefix affinity for provider stickiness, skipping client session id binding. */ affinityIgnoreClientSessionId?: boolean; + /** @description Whether local memory admission is enabled. When enabled, request bodies and stream gate prefixes are admitted against the available memory budget, spill to disk, and return a local 429 when capacity runs out. When disabled, they stay in memory without queuing or local 429. */ + enableMemoryAdmission?: boolean; /** @description Request replay (response caching and upstream connection reuse) override. Null follows the ENABLE_REQUEST_REPLAY environment variable. */ replayEnabled?: boolean | null; /** @description Replay completed payload reuse window in minutes. */ @@ -13199,6 +13203,8 @@ export interface operations { streamGateMode: "off" | "shadow" | "enforce"; /** @description Whether fingerprintable requests force longest-prefix affinity for provider stickiness, skipping client session id binding. */ affinityIgnoreClientSessionId: boolean; + /** @description Whether local memory admission is enabled. When enabled, request bodies and stream gate prefixes are admitted against the available memory budget, spill to disk, and return a local 429 when capacity runs out. When disabled, they stay in memory without queuing or local 429. */ + enableMemoryAdmission: boolean; /** @description Request replay (response caching and upstream connection reuse) override. Null follows the ENABLE_REQUEST_REPLAY environment variable. */ replayEnabled: boolean | null; /** @description Replay completed payload reuse window in minutes. */ diff --git a/src/lib/api/v1/schemas/system-config.ts b/src/lib/api/v1/schemas/system-config.ts index 2b61000d3..4ced04e5f 100644 --- a/src/lib/api/v1/schemas/system-config.ts +++ b/src/lib/api/v1/schemas/system-config.ts @@ -234,6 +234,11 @@ export const SystemSettingsSchema = z .describe( "Whether fingerprintable requests force longest-prefix affinity for provider stickiness, skipping client session id binding." ), + enableMemoryAdmission: z + .boolean() + .describe( + "Whether local memory admission is enabled. When enabled, request bodies and stream gate prefixes are admitted against the available memory budget, spill to disk, and return a local 429 when capacity runs out. When disabled, they stay in memory without queuing or local 429." + ), replayEnabled: z .boolean() .nullable() diff --git a/src/lib/body-store/allocation-estimate.ts b/src/lib/body-store/allocation-estimate.ts index 8f60b93e6..74d638965 100644 --- a/src/lib/body-store/allocation-estimate.ts +++ b/src/lib/body-store/allocation-estimate.ts @@ -23,4 +23,13 @@ export class AllocationEstimate { // 明文字节、UTF-16 解析临时值、对象中的字符串及出站序列化工作集。 return 8192 + this.bytes * 8 + this.structureBytes; } + + /** + * 解析完成后请求在整个生命周期内持续持有的工作集:原始字节、对象中的字符串 + * (UTF-16 最多两倍)、对象结构与一份出站序列化副本。解析临时值已可回收, + * 长时间流式响应期间不能继续按解析峰值占用额度。 + */ + get retainedBytes(): number { + return 8192 + this.bytes * 4 + this.structureBytes; + } } diff --git a/src/lib/body-store/byte-store.ts b/src/lib/body-store/byte-store.ts index 32692f4b8..1666081c0 100644 --- a/src/lib/body-store/byte-store.ts +++ b/src/lib/body-store/byte-store.ts @@ -2,7 +2,7 @@ import { type FileHandle, mkdir, mkdtemp, open, rm, statfs } from "node:fs/promi import path from "node:path"; import { BufferedByteChunks } from "@/app/v1/_lib/proxy/buffered-byte-chunks"; import { logger } from "@/lib/logger"; -import { LocalCapacityError, type MemoryLease } from "@/lib/memory/governor"; +import { getMemoryGovernor, LocalCapacityError, type MemoryLease } from "@/lib/memory/governor"; import { getSpoolBudget, getSpoolRoot, spoolPrefix } from "../../../server-lib/spool-directory"; export const STORE_SCRATCH_BYTES = 128 * 1024; @@ -93,11 +93,10 @@ export class ByteStore { private async appendInternal(chunk: Uint8Array): Promise { if (this.disposed) throw new Error("Body store disposed"); const capacity = Math.ceil((this.byteLength + chunk.byteLength) / BLOCK_BYTES) * BLOCK_BYTES; - if ( - !this.file && - capacity <= (this.options.hotBytes ?? HOT_BYTES) && - (await this.tryGrow(this.scratchBytes + capacity)) - ) { + // 内存准入关闭时正文全部留在内存,不落盘。 + const hotBytes = + this.options.hotBytes ?? (getMemoryGovernor().enabled ? HOT_BYTES : Number.POSITIVE_INFINITY); + if (!this.file && capacity <= hotBytes && (await this.tryGrow(this.scratchBytes + capacity))) { this.memory.append(chunk); this.byteLength += chunk.byteLength; return; diff --git a/src/lib/body-store/request-body-store.ts b/src/lib/body-store/request-body-store.ts index 02676b608..eacdaa6db 100644 --- a/src/lib/body-store/request-body-store.ts +++ b/src/lib/body-store/request-body-store.ts @@ -36,6 +36,8 @@ export async function loadRequestBody( originalByteLength: number; encoding: string | null; lease: MemoryLease; + /** 调用方完成解析后应把 lease 收缩到该值。 */ + retainedBytes: number; }> { const governor = getMemoryGovernor(); const started = performance.now(); @@ -193,7 +195,10 @@ export async function loadRequestBody( source.restoreMemoryAfterAdmission(); } const heap = getHeapStatistics(); - if (estimate.capacityBytes > (heap.heap_size_limit - heap.used_heap_size) * 0.5) { + if ( + governor.enabled && + estimate.capacityBytes > (heap.heap_size_limit - heap.used_heap_size) * 0.5 + ) { throw new LocalCapacityError(); } const buffer = await source.arrayBuffer(); @@ -201,7 +206,7 @@ export async function loadRequestBody( governor.observe("body_materialize", performance.now() - materializeStarted, source.byteLength); const lease = materializedLease; materializedLease = null; - return { buffer, encoding, originalByteLength, lease }; + return { buffer, encoding, originalByteLength, lease, retainedBytes: estimate.retainedBytes }; } finally { await Promise.allSettled([ raw.dispose(() => rawLease.release()), diff --git a/src/lib/config/system-settings-cache.ts b/src/lib/config/system-settings-cache.ts index 15d814c03..c28d5f7fb 100644 --- a/src/lib/config/system-settings-cache.ts +++ b/src/lib/config/system-settings-cache.ts @@ -128,6 +128,7 @@ export const DEFAULT_SETTINGS: Pick< | "racingTotalTimeoutMs" | "stickyTimeoutCooldownMs" | "legacyHedgeMaxInFlight" + | "enableMemoryAdmission" > = { enableHttp2: false, enableOpenaiResponsesWebsocket: true, @@ -169,6 +170,7 @@ export const DEFAULT_SETTINGS: Pick< racingTotalTimeoutMs: 60_000, stickyTimeoutCooldownMs: 300_000, legacyHedgeMaxInFlight: 2, + enableMemoryAdmission: false, }; /** @@ -263,6 +265,7 @@ export async function getCachedSystemSettings(): Promise { replayEnabled: null, replayCacheTtlMinutes: DEFAULT_SETTINGS.replayCacheTtlMinutes, cacheEffectivenessEnabled: null, + enableMemoryAdmission: DEFAULT_SETTINGS.enableMemoryAdmission, discoveryEnabled: DEFAULT_SETTINGS.discoveryEnabled, discoveryConcurrency: DEFAULT_SETTINGS.discoveryConcurrency, maxDiscoveryRounds: DEFAULT_SETTINGS.maxDiscoveryRounds, diff --git a/src/lib/memory/governor.ts b/src/lib/memory/governor.ts index 5561b3e1d..7def0b41d 100644 --- a/src/lib/memory/governor.ts +++ b/src/lib/memory/governor.ts @@ -25,6 +25,10 @@ export interface MemoryLeaseLedger { byTag: Record; } export interface MemoryGovernor { + /** 系统设置「内存准入」开关;关闭时租约与增长总是成功。 */ + readonly enabled: boolean; + setEnabled(enabled: boolean): void; + onEnabledChange(listener: (enabled: boolean) => void): () => void; observe( stage: "admission" | "body_read" | "body_decode" | "body_materialize" | "gate", milliseconds: number, @@ -38,6 +42,7 @@ export interface MemoryGovernor { ): Promise; tryLease(bytes: number, tag?: MemoryLeaseTag): MemoryLease | null; snapshot(): { + enabled: boolean; usedBytes: number; limitBytes: number; waiting: number; diff --git a/src/lib/validation/schemas.ts b/src/lib/validation/schemas.ts index 701b36e40..1bdc236f3 100644 --- a/src/lib/validation/schemas.ts +++ b/src/lib/validation/schemas.ts @@ -1171,6 +1171,8 @@ export const UpdateSystemSettingsSchema = z .optional(), // 忽略客户端 Session ID(可选) affinityIgnoreClientSessionId: z.boolean().optional(), + // 内存准入(可选) + enableMemoryAdmission: z.boolean().optional(), // F2 Replay 响应缓存与复用(可选;null = 跟随环境变量) replayEnabled: z.boolean().nullable().optional(), // F2 Replay 完成 payload 可重放窗口(分钟) diff --git a/src/repository/_shared/transformers.ts b/src/repository/_shared/transformers.ts index 8c3567140..2bf75328d 100644 --- a/src/repository/_shared/transformers.ts +++ b/src/repository/_shared/transformers.ts @@ -345,6 +345,7 @@ export function toSystemSettings(dbSettings: any): SystemSettings { replayEnabled: dbSettings?.replayEnabled ?? null, replayCacheTtlMinutes: normalizedReplayCacheTtlMinutes, cacheEffectivenessEnabled: dbSettings?.cacheEffectivenessEnabled ?? null, + enableMemoryAdmission: dbSettings?.enableMemoryAdmission ?? false, createdAt: dbSettings?.createdAt ? new Date(dbSettings.createdAt) : new Date(), updatedAt: dbSettings?.updatedAt ? new Date(dbSettings.updatedAt) : new Date(), }; diff --git a/src/repository/system-config.ts b/src/repository/system-config.ts index 22e7a9f98..a400c3cea 100644 --- a/src/repository/system-config.ts +++ b/src/repository/system-config.ts @@ -207,6 +207,7 @@ function createFallbackSettings(): SystemSettings { replayEnabled: null, replayCacheTtlMinutes: REPLAY_CACHE_TTL_MINUTES_DEFAULT, cacheEffectivenessEnabled: null, + enableMemoryAdmission: false, createdAt: now, updatedAt: now, }; @@ -285,6 +286,12 @@ const RECENT_COLUMN_LADDER: ReadonlyArray<{ // 本层更新失败(仍有列缺失)时记录的告警 updateWarn: string; }> = [ + { + key: "enableMemoryAdmission", + column: systemSettings.enableMemoryAdmission, + selectWarn: "system_settings 缺少 enableMemoryAdmission,回退到上一代字段集。", + updateWarn: "system_settings 缺少 enableMemoryAdmission,继续降级更新。", + }, { key: "legacyHedgeMaxInFlight", column: systemSettings.legacyHedgeMaxInFlight, @@ -927,6 +934,11 @@ export async function updateSystemSettings( updates.cacheEffectivenessEnabled = payload.cacheEffectivenessEnabled; } + // 内存准入开关(如果提供) + if (payload.enableMemoryAdmission !== undefined) { + updates.enableMemoryAdmission = payload.enableMemoryAdmission; + } + let updated; try { [updated] = await executor diff --git a/src/types/system-config.ts b/src/types/system-config.ts index 693845799..4aa59806c 100644 --- a/src/types/system-config.ts +++ b/src/types/system-config.ts @@ -171,6 +171,10 @@ export interface SystemSettings { // null = 跟随环境变量 ENABLE_CACHE_EFFECTIVENESS(默认 true) cacheEffectivenessEnabled: boolean | null; + // 内存准入(默认关闭) + // 开启后:请求正文与流式门控前缀按可用内存预算准入,超出时落盘或返回本地 429 + enableMemoryAdmission: boolean; + /** Bounded streaming Discovery settings. */ discoveryEnabled: boolean; discoveryConcurrency: number; @@ -308,4 +312,7 @@ export interface UpdateSystemSettingsInput { // F3b 缓存模拟开关(可选;null = 清除覆写跟随环境变量) cacheEffectivenessEnabled?: boolean | null; + + // 内存准入开关(可选) + enableMemoryAdmission?: boolean; } diff --git a/tests/integration/proxy-hedge-lifecycle.test.ts b/tests/integration/proxy-hedge-lifecycle.test.ts index 84c945a2d..ec6743109 100644 --- a/tests/integration/proxy-hedge-lifecycle.test.ts +++ b/tests/integration/proxy-hedge-lifecycle.test.ts @@ -96,6 +96,7 @@ vi.mock("@/lib/memory/governor", async (importOriginal) => { limit: 64 * 1024 ** 2, remote: false, monitor: false, + enabled: true, }); return state.bodyGovernor; }, @@ -753,7 +754,12 @@ describe("proxy hedge transport/lifecycle integration (persistence and control-p const client = new AbortController(); const now = vi.spyOn(Date, "now"); const neutralPrefixConsumption = watchNeutralResponsesPrefixConsumption(); - const governor = new MemoryGovernor({ limit: 8 * 1024 ** 2, remote: false, monitor: false }); + const governor = new MemoryGovernor({ + limit: 8 * 1024 ** 2, + remote: false, + monitor: false, + enabled: true, + }); const budget = new StreamGatePrebufferBudget(() => 8 * 1024 ** 2, governor); const admission = vi .spyOn(getStreamGatePrebufferBudget(), "acquire") @@ -837,7 +843,12 @@ describe("proxy hedge transport/lifecycle integration (persistence and control-p async ({ sticky, waitMs, cancel }) => { const [first, second] = await Promise.all([startUpstream(), startUpstream()]); const client = new AbortController(); - const governor = new MemoryGovernor({ limit: 1024 ** 2, remote: false, monitor: false }); + const governor = new MemoryGovernor({ + limit: 1024 ** 2, + remote: false, + monitor: false, + enabled: true, + }); const budget = new StreamGatePrebufferBudget(() => 1024 ** 2, governor); const occupied = await budget.acquire(1024 ** 2); const admission = vi @@ -916,7 +927,12 @@ describe("proxy hedge transport/lifecycle integration (persistence and control-p it("pauses Sticky and race deadlines during compatibility response admission", async () => { const upstream = await startUpstream("application/json"); const client = new AbortController(); - const governor = new MemoryGovernor({ limit: 1024 ** 2, remote: false, monitor: false }); + const governor = new MemoryGovernor({ + limit: 1024 ** 2, + remote: false, + monitor: false, + enabled: true, + }); const budget = new StreamGatePrebufferBudget(() => 1024 ** 2, governor); const entered = Promise.withResolvers(); let occupied: Awaited> | undefined; @@ -1000,7 +1016,12 @@ describe("proxy hedge transport/lifecycle integration (persistence and control-p it("Discovery 解析容量不足返回本地 429,取消所有候选且不惩罚供应商", async () => { const [first, second] = await Promise.all([startUpstream(), startUpstream()]); const client = new AbortController(); - const governor = new MemoryGovernor({ limit: 256 * 1024, remote: false, monitor: false }); + const governor = new MemoryGovernor({ + limit: 256 * 1024, + remote: false, + monitor: false, + enabled: true, + }); const budget = new StreamGatePrebufferBudget(() => 256 * 1024, governor); const admission = vi .spyOn(getStreamGatePrebufferBudget(), "acquire") diff --git a/tests/unit/actions/system-config-memory-admission-settings.test.ts b/tests/unit/actions/system-config-memory-admission-settings.test.ts new file mode 100644 index 000000000..3c541f304 --- /dev/null +++ b/tests/unit/actions/system-config-memory-admission-settings.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, test, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +describe("enableMemoryAdmission system setting", () => { + test("defaults to disabled in the DB-row transformer and keeps an explicit value", async () => { + const { toSystemSettings } = await import("@/repository/_shared/transformers"); + + expect(toSystemSettings(undefined).enableMemoryAdmission).toBe(false); + expect(toSystemSettings({ id: 1, siteTitle: "CC Hub" }).enableMemoryAdmission).toBe(false); + expect(toSystemSettings({ id: 1, enableMemoryAdmission: true }).enableMemoryAdmission).toBe( + true + ); + }); + + test("is accepted by the settings update validation schema", async () => { + const { UpdateSystemSettingsSchema } = await import("@/lib/validation/schemas"); + + expect(UpdateSystemSettingsSchema.parse({ enableMemoryAdmission: true })).toMatchObject({ + enableMemoryAdmission: true, + }); + expect(UpdateSystemSettingsSchema.parse({}).enableMemoryAdmission).toBeUndefined(); + expect(() => UpdateSystemSettingsSchema.parse({ enableMemoryAdmission: "yes" })).toThrow(); + }); + + test("is exposed by the v1 system settings response and update schemas", async () => { + const { SystemSettingsSchema, SystemSettingsUpdateSchema } = await import( + "@/lib/api/v1/schemas/system-config" + ); + + expect(Object.keys(SystemSettingsSchema.shape)).toContain("enableMemoryAdmission"); + expect(SystemSettingsUpdateSchema.parse({ enableMemoryAdmission: true })).toEqual({ + enableMemoryAdmission: true, + }); + }); + + test("defaults to disabled in the settings cache fallback", async () => { + const { DEFAULT_SETTINGS } = await import("@/lib/config/system-settings-cache"); + + expect(DEFAULT_SETTINGS.enableMemoryAdmission).toBe(false); + }); + + test("is stored as a non-null column that defaults to false", async () => { + const { systemSettings } = await import("@/drizzle/schema"); + + expect(systemSettings.enableMemoryAdmission.name).toBe("enable_memory_admission"); + expect(systemSettings.enableMemoryAdmission.notNull).toBe(true); + expect(systemSettings.enableMemoryAdmission.default).toBe(false); + }); +}); diff --git a/tests/unit/actions/system-config-save.test.ts b/tests/unit/actions/system-config-save.test.ts index df00e73db..28a46a49e 100644 --- a/tests/unit/actions/system-config-save.test.ts +++ b/tests/unit/actions/system-config-save.test.ts @@ -420,4 +420,18 @@ describe("saveSystemSettings", () => { }) ); }); + + it("should pass enableMemoryAdmission through validation and save, then invalidate the cache", async () => { + const result = await saveSystemSettings({ + enableMemoryAdmission: true, + }); + + expect(result.ok).toBe(true); + expect(updateSystemSettingsMock).toHaveBeenCalledWith( + expect.objectContaining({ + enableMemoryAdmission: true, + }) + ); + expect(invalidateSystemSettingsCacheMock).toHaveBeenCalledTimes(1); + }); }); diff --git a/tests/unit/proxy/memory-aware-body-store.test.ts b/tests/unit/proxy/memory-aware-body-store.test.ts index 5392826f9..4f470eea0 100644 --- a/tests/unit/proxy/memory-aware-body-store.test.ts +++ b/tests/unit/proxy/memory-aware-body-store.test.ts @@ -18,11 +18,17 @@ let testDirectory: string; beforeEach(async () => { // Exercise the real admission implementation without depending on the host's // instantaneous free RAM when the parallel test worker first loads it. - const governor = new MemoryGovernor({ limit: 64 * 1024 ** 2, remote: false, monitor: false }); + const governor = new MemoryGovernor({ + limit: 64 * 1024 ** 2, + remote: false, + monitor: false, + enabled: true, + }); vi.spyOn(getMemoryGovernor(), "acquire").mockImplementation((...args) => governor.acquire(...args) ); vi.spyOn(getMemoryGovernor(), "tryLease").mockImplementation((bytes) => governor.tryLease(bytes)); + getMemoryGovernor().setEnabled(true); // 使用工作区所在磁盘,避免 Linux 的 /tmp 挂载为 tmpfs。 const root = path.join(process.cwd(), "tmp"); await mkdir(root, { recursive: true }); @@ -31,6 +37,7 @@ beforeEach(async () => { }); afterEach(async () => { vi.unstubAllEnvs(); + getMemoryGovernor().setEnabled(false); await rm(testDirectory, { recursive: true, force: true }); }); describe("内存/磁盘共享正文", () => { @@ -40,6 +47,7 @@ describe("内存/磁盘共享正文", () => { limit: STORE_SCRATCH_BYTES, monitor: false, remote: false, + enabled: true, }); const lease = await governor.acquire(STORE_SCRATCH_BYTES); const store = new ByteStore(lease, { directory, ioTimeoutMs: 5000 }); @@ -67,6 +75,7 @@ describe("内存/磁盘共享正文", () => { limit: STORE_SCRATCH_BYTES, monitor: false, remote: false, + enabled: true, }); const lease = await governor.acquire(STORE_SCRATCH_BYTES); const store = new ByteStore(lease, { directory, maxDiskBytes: 32 }); @@ -81,7 +90,12 @@ describe("内存/磁盘共享正文", () => { } }); it("小正文保持内存并持有自有块,避免小视图挂住大 backing buffer", async () => { - const governor = new MemoryGovernor({ limit: 1024 * 1024, monitor: false, remote: false }); + const governor = new MemoryGovernor({ + limit: 1024 * 1024, + monitor: false, + remote: false, + enabled: true, + }); const lease = await governor.acquire(STORE_SCRATCH_BYTES); const store = new ByteStore(lease); const parent = new Uint8Array(16 * 1024 * 1024); diff --git a/tests/unit/proxy/memory-aware-contention.test.ts b/tests/unit/proxy/memory-aware-contention.test.ts index af0a994c6..4ecc65665 100644 --- a/tests/unit/proxy/memory-aware-contention.test.ts +++ b/tests/unit/proxy/memory-aware-contention.test.ts @@ -11,7 +11,10 @@ import { getMemoryGovernor } from "@/lib/memory/governor"; import { createMemoryCoordinator } from "../../../server-lib/memory-coordinator"; import { MemoryGovernor } from "../../../server-lib/memory-governor"; -afterEach(() => vi.unstubAllEnvs()); +afterEach(() => { + vi.unstubAllEnvs(); + getMemoryGovernor().setEnabled(false); +}); describe("request capacity contention", () => { it("negotiates remote credits before rejecting a valid Discovery prefix", async () => { @@ -32,7 +35,12 @@ describe("request capacity contention", () => { }); coordinator.attach(worker); coordinator.resetBaseline(); - const governor = new MemoryGovernor({ processRef: child, remote: true, monitor: false }); + const governor = new MemoryGovernor({ + processRef: child, + remote: true, + monitor: false, + enabled: true, + }); const budget = new StreamGatePrebufferBudget(() => Number.MAX_SAFE_INTEGER, governor); const prefix = new DiscoveryPrebuffer(); try { @@ -66,7 +74,9 @@ describe("request capacity contention", () => { limit: compressed ? 384 * 1024 : 1024 ** 2, remote: false, monitor: false, + enabled: true, }); + getMemoryGovernor().setEnabled(true); vi.spyOn(getMemoryGovernor(), "acquire").mockImplementation((...args) => governor.acquire(...args) ); diff --git a/tests/unit/proxy/memory-aware-discovery.test.ts b/tests/unit/proxy/memory-aware-discovery.test.ts index fa81f114b..a14a8e962 100644 --- a/tests/unit/proxy/memory-aware-discovery.test.ts +++ b/tests/unit/proxy/memory-aware-discovery.test.ts @@ -14,7 +14,12 @@ const frame = (payload: object) => encoder.encode(`data: ${JSON.stringify(payloa describe("Discovery 前缀与解析容量", () => { it("中性帧实际增长、EOF 不提前归还,赢家回放后释放", async () => { - const governor = new MemoryGovernor({ limit: 8 * 1024 ** 2, remote: false, monitor: false }); + const governor = new MemoryGovernor({ + limit: 8 * 1024 ** 2, + remote: false, + monitor: false, + enabled: true, + }); const budget = new StreamGatePrebufferBudget(() => 8 * 1024 ** 2, governor); const neutral = frame({ type: "response.created", @@ -59,7 +64,12 @@ describe("Discovery 前缀与解析容量", () => { }); it("增长不足在解析与复制前返回本地 429,取消后两级额度归零", async () => { - const governor = new MemoryGovernor({ limit: 256 * 1024, remote: false, monitor: false }); + const governor = new MemoryGovernor({ + limit: 256 * 1024, + remote: false, + monitor: false, + enabled: true, + }); const budget = new StreamGatePrebufferBudget(() => 256 * 1024, governor); const buffer = new DiscoveryPrebuffer(); buffer.attachLease(await budget.acquire(128 * 1024)); diff --git a/tests/unit/proxy/memory-aware-disk-timeout.test.ts b/tests/unit/proxy/memory-aware-disk-timeout.test.ts index 9e19c43fb..192925d5d 100644 --- a/tests/unit/proxy/memory-aware-disk-timeout.test.ts +++ b/tests/unit/proxy/memory-aware-disk-timeout.test.ts @@ -1,8 +1,8 @@ // @vitest-environment node -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ByteStore, STORE_SCRATCH_BYTES } from "@/lib/body-store/byte-store"; import { runStreamContentGate } from "@/app/v1/_lib/proxy/stream-gate/stream-content-gate"; -import { LocalCapacityError } from "@/lib/memory/governor"; +import { getMemoryGovernor, LocalCapacityError } from "@/lib/memory/governor"; import { logger } from "@/lib/logger"; import { MemoryGovernor } from "../../../server-lib/memory-governor"; import { getSpoolBudget } from "../../../server-lib/spool-directory"; @@ -19,9 +19,11 @@ const disk = vi.hoisted(() => ({ })); vi.mock("node:fs/promises", () => disk); vi.mock("@/lib/logger", () => ({ logger: { warn: vi.fn() } })); +beforeEach(() => getMemoryGovernor().setEnabled(true)); afterEach(() => { vi.useRealTimers(); vi.unstubAllEnvs(); + getMemoryGovernor().setEnabled(false); }); describe("磁盘故障与取消", () => { @@ -33,6 +35,7 @@ describe("磁盘故障与取消", () => { limit: STORE_SCRATCH_BYTES, remote: false, monitor: false, + enabled: true, }); const lease = governor.tryLease(STORE_SCRATCH_BYTES)!; return { governor, lease }; @@ -90,6 +93,7 @@ describe("磁盘故障与取消", () => { limit: STORE_SCRATCH_BYTES * 2 + 4096, remote: false, monitor: false, + enabled: true, }); const lease = governor.tryLease(STORE_SCRATCH_BYTES)!; const store = new ByteStore(lease); diff --git a/tests/unit/proxy/memory-aware-lifetime.test.ts b/tests/unit/proxy/memory-aware-lifetime.test.ts index cf4c4bfe6..a109353b2 100644 --- a/tests/unit/proxy/memory-aware-lifetime.test.ts +++ b/tests/unit/proxy/memory-aware-lifetime.test.ts @@ -20,7 +20,12 @@ afterEach(() => { describe("请求内存确定性回收", () => { it("小预算连续处理大正文无需 GC,额度在响应 EOF 后归零", async () => { - const governor = new MemoryGovernor({ limit: 4 * 1024 ** 2, remote: false, monitor: false }); + const governor = new MemoryGovernor({ + limit: 4 * 1024 ** 2, + remote: false, + monitor: false, + enabled: true, + }); vi.spyOn(getMemoryGovernor(), "acquire").mockImplementation((bytes, signal, wait) => governor.acquire(bytes, signal, wait) ); @@ -45,7 +50,12 @@ describe("请求内存确定性回收", () => { }); it.each(["no-body", "throw", "read-error", "cancel"])("%s 路径只归还一次", async (mode) => { - const governor = new MemoryGovernor({ limit: 100, remote: false, monitor: false }); + const governor = new MemoryGovernor({ + limit: 100, + remote: false, + monitor: false, + enabled: true, + }); const lease = governor.tryLease(100)!; const release = vi.spyOn(lease, "release"); const result = withRequestMemoryLifetime(async () => { @@ -71,7 +81,12 @@ describe("请求内存确定性回收", () => { }); it("取消响应后,后台消费者实际结束才释放;多个消费者不重复归还", async () => { - const governor = new MemoryGovernor({ limit: 100, remote: false, monitor: false }); + const governor = new MemoryGovernor({ + limit: 100, + remote: false, + monitor: false, + enabled: true, + }); const held = Promise.withResolvers(); let background!: Promise; let releaseOther!: () => void; @@ -93,7 +108,12 @@ describe("请求内存确定性回收", () => { it("任务管理器取消不提前归还仍执行的消费者", async () => { vi.stubEnv("CI", "true"); - const governor = new MemoryGovernor({ limit: 100, remote: false, monitor: false }); + const governor = new MemoryGovernor({ + limit: 100, + remote: false, + monitor: false, + enabled: true, + }); const held = Promise.withResolvers(); const response = await withRequestMemoryLifetime(async () => { attachRequestMemory(governor.tryLease(100)!); @@ -108,7 +128,7 @@ describe("请求内存确定性回收", () => { }); it("作用域外使用兜底,已结束作用域的元数据回调不重复释放", async () => { - const governor = new MemoryGovernor({ limit: 1, remote: false, monitor: false }); + const governor = new MemoryGovernor({ limit: 1, remote: false, monitor: false, enabled: true }); const lease = governor.tryLease(1)!; expect(attachRequestMemory(lease)).toBe(false); retainCurrentRequestMemory()(); @@ -128,7 +148,12 @@ describe("请求内存确定性回收", () => { }); it("永不结束的后台所有者在宽限到期后被强制归还,并丢弃正文引用", async () => { vi.useFakeTimers(); - const governor = new MemoryGovernor({ limit: 100, remote: false, monitor: false }); + const governor = new MemoryGovernor({ + limit: 100, + remote: false, + monitor: false, + enabled: true, + }); const disposed = vi.fn(); const forcedBefore = getRequestMemoryLifetimeStats().forcedTotal; const response = await withRequestMemoryLifetime(async () => { @@ -156,7 +181,12 @@ describe("请求内存确定性回收", () => { it("仍在推进的后台消费者刷新宽限;响应未结束时不计时", async () => { vi.useFakeTimers(); - const governor = new MemoryGovernor({ limit: 100, remote: false, monitor: false }); + const governor = new MemoryGovernor({ + limit: 100, + remote: false, + monitor: false, + enabled: true, + }); let retention!: ReturnType; const body = new ReadableStream(); const response = await withRequestMemoryLifetime(async () => { @@ -184,7 +214,12 @@ describe("请求内存确定性回收", () => { it("正常结束不触发强制路径,也不调用丢弃回调", async () => { vi.useFakeTimers(); - const governor = new MemoryGovernor({ limit: 100, remote: false, monitor: false }); + const governor = new MemoryGovernor({ + limit: 100, + remote: false, + monitor: false, + enabled: true, + }); const disposed = vi.fn(); const held = Promise.withResolvers(); const forcedBefore = getRequestMemoryLifetimeStats().forcedTotal; @@ -204,7 +239,12 @@ describe("请求内存确定性回收", () => { }); it("作用域结束后再挂载返回 false,由调用方走 GC 兜底而不是抛错", async () => { - const governor = new MemoryGovernor({ limit: 10, remote: false, monitor: false }); + const governor = new MemoryGovernor({ + limit: 10, + remote: false, + monitor: false, + enabled: true, + }); let attachLate!: () => boolean; const response = await withRequestMemoryLifetime(async () => { const lease = governor.tryLease(10)!; diff --git a/tests/unit/proxy/memory-aware-retained-lease.test.ts b/tests/unit/proxy/memory-aware-retained-lease.test.ts new file mode 100644 index 000000000..d3b53c09e --- /dev/null +++ b/tests/unit/proxy/memory-aware-retained-lease.test.ts @@ -0,0 +1,159 @@ +import { mkdir, mkdtemp, rm } from "node:fs/promises"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("@/drizzle/db", () => ({ + db: { + insert: vi.fn(() => ({ + values: vi.fn(async () => undefined), + })), + }, +})); + +vi.mock("@/lib/logger", () => ({ + logger: { + debug: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + trace: vi.fn(), + fatal: vi.fn(), + }, +})); + +import { ProxySession } from "@/app/v1/_lib/proxy/session"; +import { AllocationEstimate } from "@/lib/body-store/allocation-estimate"; +import { withRequestMemoryLifetime } from "@/lib/memory/request-lifetime"; +import { MemoryGovernor } from "../../../server-lib/memory-governor"; + +const GOVERNOR_KEY = Symbol.for("cch.memoryGovernor"); +const globals = globalThis as unknown as Record; +const previousGovernor = globals[GOVERNOR_KEY]; +let spoolDirectory: string; + +beforeEach(async () => { + // 使用工作区所在磁盘,避免 Linux 的 /tmp 挂载为 tmpfs。 + const root = path.join(process.cwd(), "tmp"); + await mkdir(root, { recursive: true }); + spoolDirectory = await mkdtemp(path.join(root, "cch-retained-lease-")); + vi.stubEnv("CCH_MEMORY_SPILL_DIR", spoolDirectory); +}); + +afterEach(async () => { + globals[GOVERNOR_KEY] = previousGovernor; + vi.unstubAllEnvs(); + await rm(spoolDirectory, { recursive: true, force: true }); +}); + +function installGovernor(): MemoryGovernor { + const governor = new MemoryGovernor({ + limit: 256 * 1024 ** 2, + remote: false, + monitor: false, + enabled: true, + }); + globals[GOVERNOR_KEY] = governor; + return governor; +} + +function createContext(request: Request) { + return { + req: { + method: request.method, + url: request.url, + raw: request, + header(name?: string) { + if (name) return request.headers.get(name) ?? undefined; + return Object.fromEntries(request.headers.entries()); + }, + }, + } as never; +} + +function estimateOf(bytes: Uint8Array): AllocationEstimate { + const estimate = new AllocationEstimate(); + estimate.feed(bytes); + return estimate; +} + +describe("解析后的请求额度按实际持有量占用", () => { + it("持有量估算为原始字节、UTF-16 字符串、结构与出站副本,低于解析峰值", () => { + const bytes = new TextEncoder().encode( + JSON.stringify({ messages: [{ role: "user", content: "x".repeat(100_000) }] }) + ); + const estimate = estimateOf(bytes); + const structure = estimate.capacityBytes - 8192 - bytes.length * 8; + expect(estimate.retainedBytes).toBe(8192 + bytes.length * 4 + structure); + expect(estimate.retainedBytes).toBeLessThan(estimate.capacityBytes); + }); + + it("JSON 请求解析完成后 body_materialize 额度收缩到持有量,响应结束后归零", async () => { + const governor = installGovernor(); + const text = JSON.stringify({ + model: "claude-sonnet-4", + messages: Array.from({ length: 200 }, (_, index) => ({ + role: index % 2 ? "assistant" : "user", + content: [{ type: "text", text: `turn ${index} `.repeat(200) }], + })), + }); + const estimate = estimateOf(new TextEncoder().encode(text)); + + const response = await withRequestMemoryLifetime(async () => { + const request = new Request("http://localhost/v1/messages", { method: "POST", body: text }); + const session = await ProxySession.fromContext(createContext(request)); + expect(session.request.model).toBe("claude-sonnet-4"); + const ledger = governor.snapshot().leases!; + expect(ledger.byTag.body_materialize).toMatchObject({ + count: 1, + bytes: estimate.retainedBytes, + }); + // 解析期间仍按完整峰值预留。 + expect(governor.snapshot().peakBytes).toBeGreaterThanOrEqual(estimate.capacityBytes); + return new Response("OK"); + }); + expect(await response.text()).toBe("OK"); + expect(governor.snapshot().usedBytes).toBe(0); + }); + + it("非 JSON 正文保留原始文本时同样收缩到持有量", async () => { + const governor = installGovernor(); + const text = "not json ".repeat(50_000); + const estimate = estimateOf(new TextEncoder().encode(text)); + + const response = await withRequestMemoryLifetime(async () => { + const request = new Request("http://localhost/v1/messages", { method: "POST", body: text }); + const session = await ProxySession.fromContext(createContext(request)); + expect(session.request.message).toEqual({ raw: text }); + expect(governor.snapshot().leases!.byTag.body_materialize.bytes).toBe(estimate.retainedBytes); + return new Response("OK"); + }); + await response.text(); + expect(governor.snapshot().usedBytes).toBe(0); + }); + + it("multipart 图片请求解析完成后同样收缩到持有量", async () => { + const governor = installGovernor(); + const formData = new FormData(); + formData.append("model", "gpt-image-1.5"); + formData.append("prompt", "draw a cat"); + formData.append( + "image[]", + new File([new Uint8Array(200_000).fill(7)], "image.png", { type: "image/png" }), + "image.png" + ); + const request = new Request("https://proxy.example.com/v1/images/edits", { + method: "POST", + body: formData, + }); + const bytes = new Uint8Array(await request.clone().arrayBuffer()); + const estimate = estimateOf(bytes); + + const response = await withRequestMemoryLifetime(async () => { + await ProxySession.fromContext(createContext(request)); + expect(governor.snapshot().leases!.byTag.body_materialize.bytes).toBe(estimate.retainedBytes); + return new Response("OK"); + }); + await response.text(); + expect(governor.snapshot().usedBytes).toBe(0); + }); +}); diff --git a/tests/unit/proxy/memory-aware-switch.test.ts b/tests/unit/proxy/memory-aware-switch.test.ts new file mode 100644 index 000000000..917182e37 --- /dev/null +++ b/tests/unit/proxy/memory-aware-switch.test.ts @@ -0,0 +1,220 @@ +import { EventEmitter } from "node:events"; +import { gzipSync } from "node:zlib"; +import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; +import { + getStreamGatePrebufferBudget, + resolveStreamGateGlobalPrebufferByteCap, +} from "@/app/v1/_lib/proxy/stream-gate/prebuffer-budget"; +import { ByteStore, STORE_SCRATCH_BYTES } from "@/lib/body-store/byte-store"; +import { loadRequestBody } from "@/lib/body-store/request-body-store"; +import { getMemoryGovernor, LocalCapacityError } from "@/lib/memory/governor"; +import { MemoryGovernor } from "../../../server-lib/memory-governor"; + +const GATE_CAP = 128 * 1024; +// getEnvConfig 在进程内只解析一次,门控上限必须在任何模块读取环境变量之前设置。 +const previousGateCap = vi.hoisted(() => { + const previous = process.env.STREAM_GATE_GLOBAL_PREBUFFER_BYTE_CAP; + process.env.STREAM_GATE_GLOBAL_PREBUFFER_BYTE_CAP = String(128 * 1024); + return previous; +}); +afterAll(() => { + if (previousGateCap === undefined) delete process.env.STREAM_GATE_GLOBAL_PREBUFFER_BYTE_CAP; + else process.env.STREAM_GATE_GLOBAL_PREBUFFER_BYTE_CAP = previousGateCap; +}); + +const GOVERNOR_KEY = Symbol.for("cch.memoryGovernor"); +const BUDGET_KEY = Symbol.for("cch.streamGatePrebufferBudget"); +const globals = globalThis as unknown as Record; + +function installGovernor(governor: MemoryGovernor) { + const previousGovernor = globals[GOVERNOR_KEY]; + const previousBudget = globals[BUDGET_KEY]; + globals[GOVERNOR_KEY] = governor; + delete globals[BUDGET_KEY]; + return () => { + globals[GOVERNOR_KEY] = previousGovernor; + globals[BUDGET_KEY] = previousBudget; + }; +} + +afterEach(() => { + vi.useRealTimers(); + vi.unstubAllEnvs(); +}); + +describe("内存准入开关", () => { + it("默认关闭:超出额度的租约与增长立即成功,只记账", async () => { + const governor = new MemoryGovernor({ limit: 0, remote: false, monitor: false }); + expect(governor.enabled).toBe(false); + expect(governor.snapshot().enabled).toBe(false); + + const lease = governor.tryLease(1024 * 1024, "body_read"); + expect(lease).not.toBeNull(); + expect(lease!.tryGrow(4 * 1024 * 1024)).toBe(true); + await expect(lease!.tryGrowAsync!(8 * 1024 * 1024)).resolves.toBe(true); + const acquired = await governor.acquire(2 * 1024 * 1024); + expect(governor.snapshot()).toMatchObject({ + usedBytes: 10 * 1024 * 1024, + waiting: 0, + rejected: 0, + }); + + governor.setEnabled(true); + expect(governor.snapshot().enabled).toBe(true); + expect(governor.tryLease(1)).toBeNull(); + expect(lease!.tryGrow(16 * 1024 * 1024)).toBe(false); + await expect(governor.acquire(1, undefined, 0)).rejects.toBeInstanceOf(LocalCapacityError); + + lease!.release(); + acquired.release(); + expect(governor.snapshot().usedBytes).toBe(0); + }); + + it("关闭时多进程 worker 不向 primary 申请授权", async () => { + const child = Object.assign(new EventEmitter(), { + env: {}, + connected: true, + send: vi.fn((_message: unknown, callback?: (error?: Error) => void) => callback?.()), + }); + const governor = new MemoryGovernor({ processRef: child, remote: true, monitor: false }); + + const lease = await governor.acquire(4 * 1024 * 1024); + expect(lease.tryGrow(8 * 1024 * 1024)).toBe(true); + await expect(lease.tryGrowAsync!(16 * 1024 * 1024)).resolves.toBe(true); + expect(child.send).not.toHaveBeenCalled(); + expect(governor.snapshot().limitBytes).toBe(0); + lease.release(); + }); + + it("排队中的请求在开关关闭后立即放行", async () => { + vi.useFakeTimers(); + const governor = new MemoryGovernor({ limit: 0, remote: false, monitor: false, enabled: true }); + let admitted = false; + const waiting = governor.acquire(1024).then((lease) => { + admitted = true; + return lease; + }); + await vi.advanceTimersByTimeAsync(1000); + expect(admitted).toBe(false); + expect(governor.snapshot().waiting).toBe(1); + + governor.setEnabled(false); + await vi.advanceTimersByTimeAsync(50); + expect(admitted).toBe(true); + (await waiting).release(); + expect(governor.snapshot()).toMatchObject({ waiting: 0, rejected: 0, usedBytes: 0 }); + }); + + it("关闭时正文全部留在内存,不创建暂存文件", async () => { + const restore = installGovernor( + new MemoryGovernor({ limit: 0, remote: false, monitor: false }) + ); + vi.stubEnv("CCH_MEMORY_SPILL_DIR", "/nonexistent/cch-spool-must-not-be-used"); + try { + const lease = getMemoryGovernor().tryLease(STORE_SCRATCH_BYTES)!; + const store = new ByteStore(lease); + const body = new Uint8Array(3 * 1024 * 1024).map((_, index) => index % 251); + for (let offset = 0; offset < body.byteLength; offset += 100_000) { + await store.append(body.subarray(offset, offset + 100_000)); + } + expect(store.spilled).toBe(false); + expect(Buffer.from(await store.arrayBuffer()).equals(Buffer.from(body))).toBe(true); + await store.dispose(() => lease.release()); + } finally { + restore(); + } + }); + + it("关闭时零额度也能读取并解压大正文", async () => { + const restore = installGovernor( + new MemoryGovernor({ limit: 0, remote: false, monitor: false }) + ); + vi.stubEnv("CCH_MEMORY_SPILL_DIR", "/nonexistent/cch-spool-must-not-be-used"); + try { + const text = JSON.stringify({ model: "claude-test", input: "y".repeat(2 * 1024 * 1024) }); + const loaded = await loadRequestBody( + new Request("http://localhost/v1/messages", { + method: "POST", + headers: { "content-encoding": "gzip" }, + body: gzipSync(Buffer.from(text)), + }) + ); + expect(new TextDecoder().decode(loaded.buffer)).toBe(text); + expect(loaded.encoding).toBe("gzip"); + loaded.lease.release(); + } finally { + restore(); + } + }); + + it("开关变化只在状态实际改变时通知,取消订阅后不再通知", () => { + const governor = new MemoryGovernor({ limit: 0, remote: false, monitor: false }); + const seen: boolean[] = []; + const unsubscribe = governor.onEnabledChange((enabled) => seen.push(enabled)); + governor.setEnabled(false); + governor.setEnabled(true); + governor.setEnabled(true); + governor.setEnabled(false); + unsubscribe(); + governor.setEnabled(true); + expect(seen).toEqual([true, false]); + expect(governor.enabled).toBe(true); + }); + + it("关闭开关时立即放行门控子限额队列中的请求", async () => { + vi.useFakeTimers(); + const governor = new MemoryGovernor({ + limit: 64 * 1024 ** 2, + remote: false, + monitor: false, + enabled: true, + }); + const restore = installGovernor(governor); + try { + const budget = getStreamGatePrebufferBudget(); + const first = await budget.acquire(GATE_CAP); + let admitted = false; + const queued = budget.acquire(GATE_CAP).then((lease) => { + admitted = true; + return lease; + }); + await vi.advanceTimersByTimeAsync(1000); + expect(admitted).toBe(false); + expect(budget.snapshot().waiting).toBe(1); + + governor.setEnabled(false); + await vi.advanceTimersByTimeAsync(0); + expect(admitted).toBe(true); + expect(budget.snapshot().waiting).toBe(0); + (await queued).release(); + first.release(); + } finally { + restore(); + } + }); + + it("门控子限额只在开启时生效", async () => { + const cap = GATE_CAP; + expect(resolveStreamGateGlobalPrebufferByteCap()).toBe(cap); + const governor = new MemoryGovernor({ limit: 0, remote: false, monitor: false }); + const restore = installGovernor(governor); + try { + const budget = getStreamGatePrebufferBudget(); + expect(budget.snapshot().limit).toBe(Number.MAX_SAFE_INTEGER); + const first = await budget.acquire(cap); + const second = await budget.acquire(cap); + expect(first.tryGrow(32 * cap)).toBe(true); + expect(budget.snapshot()).toMatchObject({ waiting: 0, reservedBytes: 33 * cap }); + + governor.setEnabled(true); + expect(budget.snapshot().limit).toBe(cap); + expect(first.tryGrow(64 * cap)).toBe(false); + + first.release(); + second.release(); + expect(budget.snapshot().reservedBytes).toBe(0); + } finally { + restore(); + } + }); +}); diff --git a/tests/unit/proxy/proxy-handler-public-errors.test.ts b/tests/unit/proxy/proxy-handler-public-errors.test.ts index 5d543823e..ae455c3eb 100644 --- a/tests/unit/proxy/proxy-handler-public-errors.test.ts +++ b/tests/unit/proxy/proxy-handler-public-errors.test.ts @@ -10,6 +10,7 @@ type ProxySettingsFixture = { readonly fakeStreamingWhitelist: FakeStreamingWhitelistEntry[]; readonly passThroughUpstreamErrorMessage: boolean; readonly verboseProviderError: boolean; + readonly enableMemoryAdmission: boolean; }; const boundary = vi.hoisted(() => ({ @@ -21,6 +22,8 @@ const boundary = vi.hoisted(() => ({ incrementConcurrentCount: vi.fn<(sessionId: string) => Promise>(), incrementObservedConcurrentCount: vi.fn<(identity: string) => Promise>(), loadSettings: vi.fn<() => Promise>(), + // 进程缓存中的当前设置对象;只有与之相同的读取结果才会同步到内存准入开关。 + processCache: { current: null as ProxySettingsFixture | null }, recordLocalCapacityRejection: vi.fn(), recordPreAuthLocalCapacityRejection: vi.fn(), runGuards: vi.fn<(session: ProxySession) => Promise>(), @@ -32,13 +35,20 @@ const boundary = vi.hoisted(() => ({ vi.mock("@/lib/config", async (importOriginal) => ({ ...(await importOriginal()), getCachedSystemSettings: boundary.loadSettings, + getCachedSystemSettingsOnlyCache: () => boundary.processCache.current, })); vi.mock("@/lib/config/system-settings-cache", async (importOriginal) => ({ ...(await importOriginal()), getCachedSystemSettings: boundary.loadSettings, + getCachedSystemSettingsOnlyCache: () => boundary.processCache.current, })); +function cacheSettings(value: ProxySettingsFixture): void { + boundary.processCache.current = value; + boundary.loadSettings.mockResolvedValue(value); +} + vi.mock("@/app/v1/_lib/proxy/guard-pipeline", () => ({ GuardPipelineBuilder: { fromSession: () => ({ run: boundary.runGuards }), @@ -103,6 +113,7 @@ const settings: ProxySettingsFixture = { fakeStreamingWhitelist: [], passThroughUpstreamErrorMessage: false, verboseProviderError: false, + enableMemoryAdmission: false, }; describe("handleProxyRequest public error behavior", () => { @@ -122,7 +133,7 @@ describe("handleProxyRequest public error behavior", () => { boundary.recordPreAuthLocalCapacityRejection.mockReset(); boundary.recordLocalCapacityRejection.mockResolvedValue(true); boundary.recordPreAuthLocalCapacityRejection.mockResolvedValue(true); - boundary.loadSettings.mockResolvedValue(settings); + cacheSettings(settings); boundary.getErrorOverride.mockResolvedValue(null); boundary.incrementConcurrentCount.mockResolvedValue(undefined); boundary.decrementConcurrentCount.mockResolvedValue(undefined); @@ -298,6 +309,7 @@ describe("handleProxyRequest public error behavior", () => { const state = globalThis as unknown as Record; const previous = state[key]; state[key] = new MemoryGovernor({ limit: 0, remote: false, monitor: false }); + cacheSettings({ ...settings, enableMemoryAdmission: true }); try { const request = new Request("http://localhost/v1/messages", { method: "POST", @@ -329,4 +341,105 @@ describe("handleProxyRequest public error behavior", () => { vi.useRealTimers(); } }); + + it("内存准入关闭时请求体读取不排队,零额度也照常进入守卫链", async () => { + const key = Symbol.for("cch.memoryGovernor"); + const state = globalThis as unknown as Record; + const previous = state[key]; + const governor = new MemoryGovernor({ limit: 0, remote: false, monitor: false, enabled: true }); + state[key] = governor; + boundary.runGuards.mockResolvedValue(new Response("guarded", { status: 403 })); + try { + const response = await handleProxyRequest( + new Context( + new Request("http://localhost/v1/messages", { + method: "POST", + body: JSON.stringify({ model: "claude-test", messages: [], input: "x".repeat(4096) }), + }) + ) + ); + expect(governor.enabled).toBe(false); + expect(response.status).toBe(403); + expect(boundary.runGuards).toHaveBeenCalledTimes(1); + const session = boundary.runGuards.mock.calls[0]?.[0]; + expect(session?.request.model).toBe("claude-test"); + expect(boundary.recordPreAuthLocalCapacityRejection).not.toHaveBeenCalled(); + } finally { + state[key] = previous; + } + }); + + it("读取系统设置失败时保持进程当前的内存准入状态", async () => { + const key = Symbol.for("cch.memoryGovernor"); + const state = globalThis as unknown as Record; + const previous = state[key]; + const governor = new MemoryGovernor({ + limit: 64 * 1024 ** 2, + remote: false, + monitor: false, + enabled: true, + }); + state[key] = governor; + boundary.loadSettings.mockRejectedValue(new Error("settings unavailable")); + boundary.runGuards.mockResolvedValue(new Response("guarded", { status: 403 })); + try { + const response = await handleProxyRequest( + new Context( + new Request("http://localhost/v1/messages", { + method: "POST", + body: JSON.stringify({ model: "claude-test", messages: [] }), + }) + ) + ); + expect(response.status).toBe(403); + expect(governor.enabled).toBe(true); + const session = boundary.runGuards.mock.calls[0]?.[0]; + // 设置不可用时 raw 跨供应商回退按既有约定关闭。 + expect(session?.isRawCrossProviderFallbackEnabled()).toBe(false); + } finally { + state[key] = previous; + } + }); + + it("缓存失效期间完成的旧查询与默认对象不改变进程的内存准入状态", async () => { + const key = Symbol.for("cch.memoryGovernor"); + const state = globalThis as unknown as Record; + const previous = state[key]; + const governor = new MemoryGovernor({ + limit: 64 * 1024 ** 2, + remote: false, + monitor: false, + enabled: true, + }); + state[key] = governor; + boundary.runGuards.mockImplementation(async () => new Response("guarded", { status: 403 })); + const send = () => + handleProxyRequest( + new Context( + new Request("http://localhost/v1/messages", { + method: "POST", + body: JSON.stringify({ model: "claude-test", messages: [] }), + }) + ) + ); + try { + // 失效后缓存为空:读取结果未写入缓存,不能用于全进程开关。 + boundary.processCache.current = null; + boundary.loadSettings.mockResolvedValue({ ...settings, enableMemoryAdmission: false }); + expect((await send()).status).toBe(403); + expect(governor.enabled).toBe(true); + + // 缓存已被更新的设置替换,旧查询结果与之不是同一对象。 + boundary.processCache.current = { ...settings, enableMemoryAdmission: true }; + expect((await send()).status).toBe(403); + expect(governor.enabled).toBe(true); + + // 当前缓存对象正常同步。 + cacheSettings({ ...settings, enableMemoryAdmission: false }); + expect((await send()).status).toBe(403); + expect(governor.enabled).toBe(false); + } finally { + state[key] = previous; + } + }); }); diff --git a/tests/unit/proxy/routing-trace.test.ts b/tests/unit/proxy/routing-trace.test.ts index eb2cb6d99..ac91c709f 100644 --- a/tests/unit/proxy/routing-trace.test.ts +++ b/tests/unit/proxy/routing-trace.test.ts @@ -327,7 +327,12 @@ describe("ProxySession routing trace recorder", () => { it.each([false, true])( "响应 EOF 后保留慢观测刷新和删除的请求额度,删除失败=%s", async (failed) => { - const governor = new MemoryGovernor({ limit: 100, remote: false, monitor: false }); + const governor = new MemoryGovernor({ + limit: 100, + remote: false, + monitor: false, + enabled: true, + }); const write = Promise.withResolvers(); const deletion = Promise.withResolvers(); liveChainMocks.writeLiveRoutingTrace.mockImplementationOnce(() => write.promise); diff --git a/tests/unit/proxy/stream-gate-content-gate.test.ts b/tests/unit/proxy/stream-gate-content-gate.test.ts index bac1270e4..4fb6d0d64 100644 --- a/tests/unit/proxy/stream-gate-content-gate.test.ts +++ b/tests/unit/proxy/stream-gate-content-gate.test.ts @@ -103,7 +103,12 @@ describe("runStreamContentGate", () => { it("已提交前缀被丢弃(既未读完也未取消)时,请求作用域结束兜底归还两级额度", async () => { const reservation = GATE_OPTIONS.prebufferByteCap * 4; - const governor = new MemoryGovernor({ limit: reservation, remote: false, monitor: false }); + const governor = new MemoryGovernor({ + limit: reservation, + remote: false, + monitor: false, + enabled: true, + }); const budget = new StreamGatePrebufferBudget(() => reservation, governor); const response = await withRequestMemoryLifetime(async () => { const committed = await runStreamContentGate(readerFromChunks([TEXT_DELTA]), { diff --git a/tests/unit/proxy/stream-gate-forwarder-integration.test.ts b/tests/unit/proxy/stream-gate-forwarder-integration.test.ts index 1bc4cb6d6..18249b4d8 100644 --- a/tests/unit/proxy/stream-gate-forwarder-integration.test.ts +++ b/tests/unit/proxy/stream-gate-forwarder-integration.test.ts @@ -597,7 +597,12 @@ describe("F1 stream content gate x ProxyForwarder paths", () => { }); const session = createSession(); session.setProvider(provider); - const governor = new MemoryGovernor({ limit: 0, remote: false, monitor: false }); + const governor = new MemoryGovernor({ + limit: 0, + remote: false, + monitor: false, + enabled: true, + }); vi.spyOn(getStreamGatePrebufferBudget(), "acquire").mockImplementation((bytes, signal) => governor.acquire(bytes, signal) ); @@ -637,8 +642,18 @@ describe("F1 stream content gate x ProxyForwarder paths", () => { const session = createSession(); session.setProvider(first); mocks.pickRandomProviderWithExclusion.mockResolvedValueOnce(second); - const free = new MemoryGovernor({ limit: 1024 * 1024, remote: false, monitor: false }); - const occupied = new MemoryGovernor({ limit: 0, remote: false, monitor: false }); + const free = new MemoryGovernor({ + limit: 1024 * 1024, + remote: false, + monitor: false, + enabled: true, + }); + const occupied = new MemoryGovernor({ + limit: 0, + remote: false, + monitor: false, + enabled: true, + }); vi.spyOn(getStreamGatePrebufferBudget(), "acquire") .mockImplementationOnce((bytes, signal) => free.acquire(bytes, signal)) .mockImplementation((bytes, signal) => occupied.acquire(bytes, signal)); diff --git a/tests/unit/proxy/stream-gate-ttft-regression.test.ts b/tests/unit/proxy/stream-gate-ttft-regression.test.ts index acdbccfa3..a76b8e3f5 100644 --- a/tests/unit/proxy/stream-gate-ttft-regression.test.ts +++ b/tests/unit/proxy/stream-gate-ttft-regression.test.ts @@ -21,7 +21,12 @@ const options = { afterEach(() => vi.useRealTimers()); describe("TTFT 固定预占回归", () => { it("等待门控子限额的请求不占用全局正文额度", async () => { - const governor = new MemoryGovernor({ limit: 2 * 1024 ** 2, remote: false, monitor: false }); + const governor = new MemoryGovernor({ + limit: 2 * 1024 ** 2, + remote: false, + monitor: false, + enabled: true, + }); const budget = new StreamGatePrebufferBudget(() => 128 * 1024, governor); const first = await budget.acquire(128 * 1024); const controllers = Array.from({ length: 24 }, () => new AbortController()); @@ -42,7 +47,12 @@ describe("TTFT 固定预占回归", () => { it("本地与全局等待共用 20 秒期限,失败后归还本地子额度", async () => { vi.useFakeTimers(); - const governor = new MemoryGovernor({ limit: 256, remote: false, monitor: false }); + const governor = new MemoryGovernor({ + limit: 256, + remote: false, + monitor: false, + enabled: true, + }); const budget = new StreamGatePrebufferBudget(() => 128, governor); const first = await budget.acquire(128); const occupied = governor.tryLease(128)!; diff --git a/tests/unit/repository/system-config-degradation-ladder.test.ts b/tests/unit/repository/system-config-degradation-ladder.test.ts index 08b0ce198..9ee318bb3 100644 --- a/tests/unit/repository/system-config-degradation-ladder.test.ts +++ b/tests/unit/repository/system-config-degradation-ladder.test.ts @@ -7,6 +7,7 @@ import type { UpdateSystemSettingsInput } from "@/types/system-config"; // 近代新增列(最新在前),降级链按引入顺序逐层累计剥离。 const RECENT_COLUMNS = [ + "enableMemoryAdmission", "legacyHedgeMaxInFlight", "replayCacheTtlMinutes", "cacheEffectivenessEnabled", @@ -31,6 +32,7 @@ const RECENT_COLUMNS = [ // 全量字段集. const FULL_COLUMNS = [ + "enableMemoryAdmission", "replayCacheTtlMinutes", "cacheEffectivenessEnabled", "replayEnabled", diff --git a/tests/unit/repository/system-config-update-missing-columns.test.ts b/tests/unit/repository/system-config-update-missing-columns.test.ts index fb0596f82..17664c1f7 100644 --- a/tests/unit/repository/system-config-update-missing-columns.test.ts +++ b/tests/unit/repository/system-config-update-missing-columns.test.ts @@ -299,12 +299,13 @@ describe("SystemSettings:数据库缺列时的保存兜底", () => { vi.setSystemTime(now); // 第一次 select(fullSelection) 因新列缺失而抛 42703; - // The new legacy hedge column is the newest rung, so it is stripped before replay columns. + // The memory admission and legacy hedge columns are the newest rungs, so they are stripped before replay columns. const selectMock = vi .fn() .mockReturnValueOnce(createRejectedThenableQuery({ code: "42703" })) .mockReturnValueOnce(createRejectedThenableQuery({ code: "42703" })) .mockReturnValueOnce(createRejectedThenableQuery({ code: "42703" })) + .mockReturnValueOnce(createRejectedThenableQuery({ code: "42703" })) .mockReturnValueOnce( createThenableQuery([ { @@ -337,23 +338,25 @@ describe("SystemSettings:数据库缺列时的保存兜底", () => { const result = await getSystemSettings(); // 降级读取成功(未抛错),缺失列由 transformer 落默认值。 - expect(selectMock).toHaveBeenCalledTimes(4); + expect(selectMock).toHaveBeenCalledTimes(5); expect(result.siteTitle).toBe("CC Hub"); expect(result.enableHttp2).toBe(true); expect(result.affinityIgnoreClientSessionId).toBe(true); expect(result.streamGateMode).toBe("enforce"); - - const fourthSelection = selectMock.mock.calls[3]?.[0] as Record; - expect(fourthSelection).not.toHaveProperty("legacyHedgeMaxInFlight"); - expect(fourthSelection).not.toHaveProperty("replayCacheTtlMinutes"); - expect(fourthSelection).not.toHaveProperty("cacheEffectivenessEnabled"); - expect(fourthSelection).toHaveProperty("replayEnabled"); - expect(fourthSelection).toHaveProperty("affinityIgnoreClientSessionId"); - expect(fourthSelection).toHaveProperty("streamGateMode"); - expect(fourthSelection).toHaveProperty("stickyTimeoutCooldownMs"); - expect(fourthSelection).toHaveProperty("racingTotalTimeoutMs"); - expect(fourthSelection).toHaveProperty("enableGeminiFunctionIdRectifier"); - expect(fourthSelection).toHaveProperty("enableThinkingEffortConflictRectifier"); + expect(result.enableMemoryAdmission).toBe(false); + + const successfulSelection = selectMock.mock.calls[4]?.[0] as Record; + expect(successfulSelection).not.toHaveProperty("enableMemoryAdmission"); + expect(successfulSelection).not.toHaveProperty("legacyHedgeMaxInFlight"); + expect(successfulSelection).not.toHaveProperty("replayCacheTtlMinutes"); + expect(successfulSelection).not.toHaveProperty("cacheEffectivenessEnabled"); + expect(successfulSelection).toHaveProperty("replayEnabled"); + expect(successfulSelection).toHaveProperty("affinityIgnoreClientSessionId"); + expect(successfulSelection).toHaveProperty("streamGateMode"); + expect(successfulSelection).toHaveProperty("stickyTimeoutCooldownMs"); + expect(successfulSelection).toHaveProperty("racingTotalTimeoutMs"); + expect(successfulSelection).toHaveProperty("enableGeminiFunctionIdRectifier"); + expect(successfulSelection).toHaveProperty("enableThinkingEffortConflictRectifier"); vi.useRealTimers(); }); diff --git a/tests/unit/server-memory-governor.test.ts b/tests/unit/server-memory-governor.test.ts index fadf7d26e..da1184c03 100644 --- a/tests/unit/server-memory-governor.test.ts +++ b/tests/unit/server-memory-governor.test.ts @@ -27,7 +27,12 @@ describe("内存租约与本地准入", () => { }, }); coordinator.attach(worker); - const governor = new MemoryGovernor({ processRef: child, remote: true, monitor: false }); + const governor = new MemoryGovernor({ + processRef: child, + remote: true, + monitor: false, + enabled: true, + }); let admitted = false; const waiting = governor.acquire(128 * 1024).then((lease) => { admitted = true; @@ -60,7 +65,12 @@ describe("内存租约与本地准入", () => { }); it("20 秒拒绝,并移除等待者", async () => { vi.useFakeTimers(); - const governor = new MemoryGovernor({ limit: 1024, remote: false, monitor: false }); + const governor = new MemoryGovernor({ + limit: 1024, + remote: false, + monitor: false, + enabled: true, + }); const occupied = await governor.acquire(1024); const waiting = governor.acquire(1); const assertion = expect(waiting).rejects.toBeInstanceOf(LocalCapacityError); @@ -72,7 +82,12 @@ describe("内存租约与本地准入", () => { expect(governor.snapshot().usedBytes).toBe(0); }); it("扩容失败立即返回,不能持有部分工作集进入等待", async () => { - const governor = new MemoryGovernor({ limit: 1024, remote: false, monitor: false }); + const governor = new MemoryGovernor({ + limit: 1024, + remote: false, + monitor: false, + enabled: true, + }); const first = await governor.acquire(512); const second = await governor.acquire(512); expect(first.tryGrow(1024)).toBe(false); @@ -84,7 +99,7 @@ describe("内存租约与本地准入", () => { expect(first.tryGrow(1)).toBe(false); }); it("客户端断开立即移除准入等待", async () => { - const governor = new MemoryGovernor({ limit: 0, remote: false, monitor: false }); + const governor = new MemoryGovernor({ limit: 0, remote: false, monitor: false, enabled: true }); const abort = new AbortController(); const promise = governor.acquire(1, abort.signal); abort.abort(new Error("closed")); @@ -132,7 +147,12 @@ describe("内存租约与本地准入", () => { }); coordinator.resetBaseline(); coordinator.attach(worker); - const governor = new MemoryGovernor({ processRef: child, remote: true, monitor: false }); + const governor = new MemoryGovernor({ + processRef: child, + remote: true, + monitor: false, + enabled: true, + }); const acquiring = governor.acquire(128 * 1024); await vi.advanceTimersByTimeAsync(50); const lease = await acquiring; @@ -155,7 +175,12 @@ describe("内存租约与本地准入", () => { throw new Error("closed"); }), }); - const governor = new MemoryGovernor({ processRef: child, remote: true, monitor: false }); + const governor = new MemoryGovernor({ + processRef: child, + remote: true, + monitor: false, + enabled: true, + }); await governor.requestCredits(100); expect(governor.snapshot().limitBytes).toBe(0); child.send.mockImplementation((_message: unknown, callback: (error?: Error) => void) => @@ -200,7 +225,12 @@ describe("内存租约与本地准入", () => { }, }); coordinator.attach(worker); - const governor = new MemoryGovernor({ processRef: child, remote: true, monitor: false }); + const governor = new MemoryGovernor({ + processRef: child, + remote: true, + monitor: false, + enabled: true, + }); const acquiring = governor.acquire(capacity); await vi.advanceTimersByTimeAsync(50); expect(coordinator.snapshot().grantedBytes).toBe(capacity); @@ -232,6 +262,7 @@ describe("内存租约与本地准入", () => { const governor = new MemoryGovernor({ remote: false, monitor: false, + enabled: true, readSnapshot: () => ({ availableRamBytes: ram, availableSwapBytes: 0, @@ -280,6 +311,7 @@ describe("内存租约与本地准入", () => { const governor = new MemoryGovernor({ remote: false, monitor: false, + enabled: true, readSnapshot: () => ({ availableRamBytes: ram, availableSwapBytes: 0, memoryPressure: 0 }), }); const ceiling = governor.snapshot().limitBytes; @@ -314,7 +346,12 @@ describe("内存租约与本地准入", () => { }); it("租约台账按标签汇总在账量与最长持有时间,归还后移除", async () => { vi.setSystemTime(new Date("2026-01-01T00:00:00Z")); - const governor = new MemoryGovernor({ limit: 1000, remote: false, monitor: false }); + const governor = new MemoryGovernor({ + limit: 1000, + remote: false, + monitor: false, + enabled: true, + }); const body = governor.tryLease(300, "body_materialize")!; vi.setSystemTime(new Date("2026-01-01T00:01:00Z")); const gate = await governor.acquire(100, undefined, undefined, "gate"); diff --git a/tests/unit/server-memory-growth.test.ts b/tests/unit/server-memory-growth.test.ts index 1c8384fc9..eb45f467a 100644 --- a/tests/unit/server-memory-growth.test.ts +++ b/tests/unit/server-memory-growth.test.ts @@ -23,7 +23,12 @@ function remoteGovernor(capacity = 64 * 1024 ** 2) { }); coordinator.attach(worker); coordinator.resetBaseline(); - const governor = new MemoryGovernor({ processRef: child, remote: true, monitor: false }); + const governor = new MemoryGovernor({ + processRef: child, + remote: true, + monitor: false, + enabled: true, + }); return { governor, coordinator, @@ -77,7 +82,12 @@ describe("remote memory growth", () => { connected: true, send: vi.fn((_message: unknown, callback?: () => void) => callback?.()), }); - const governor = new MemoryGovernor({ processRef: child, remote: true, monitor: false }); + const governor = new MemoryGovernor({ + processRef: child, + remote: true, + monitor: false, + enabled: true, + }); governor.credits = 1024 ** 2; const lease = governor.tryLease(128 * 1024)!; const controller = new AbortController(); @@ -107,7 +117,12 @@ describe("remote memory growth", () => { ); it("does not queue on local exhaustion and honors cancellation before an available growth", async () => { - const governor = new MemoryGovernor({ limit: 1024, remote: false, monitor: false }); + const governor = new MemoryGovernor({ + limit: 1024, + remote: false, + monitor: false, + enabled: true, + }); const lease = governor.tryLease(512)!; await expect(lease.tryGrowAsync(2048)).resolves.toBe(false); await expect(lease.tryGrowAsync(-1)).rejects.toBeInstanceOf(RangeError); diff --git a/tests/unit/server-memory-resources.test.ts b/tests/unit/server-memory-resources.test.ts index c3b94800d..af6150292 100644 --- a/tests/unit/server-memory-resources.test.ts +++ b/tests/unit/server-memory-resources.test.ts @@ -106,6 +106,59 @@ describe("内存与 swap 的实际剩余容量", () => { }); }); + it("cgroup v2 用量扣除可回收的 inactive file cache,祖先组同样扣除", () => { + const snapshot = fixture({ + "/sys/fs/cgroup/service/worker/memory.max": String(GiB), + "/sys/fs/cgroup/service/worker/memory.current": String(1000 * MiB), + "/sys/fs/cgroup/service/worker/memory.stat": `anon ${20 * MiB}\nfile ${950 * MiB}\nactive_file ${50 * MiB}\ninactive_file ${900 * MiB}\n`, + "/sys/fs/cgroup/service/memory.max": String(2 * GiB), + "/sys/fs/cgroup/service/memory.current": String(1900 * MiB), + "/sys/fs/cgroup/service/memory.stat": `anon ${900 * MiB}\ninactive_file ${900 * MiB}\n`, + }); + // 只有 inactive 部分视为可回收,active file 仍计入占用。 + // worker:1024 - (1000 - 900) = 924 MiB;祖先:2048 - (1900 - 900) = 1048 MiB。 + expect(snapshot.availableRamBytes).toBe(924 * MiB); + expect(createMemoryPlan({ env: {}, snapshot }).hotBudgetBytes).toBe( + Math.floor(0.6 * (924 - 256) * MiB) + ); + }); + + it("cgroup v2 缺少 memory.stat 或字段时按 memory.current 计算", () => { + const files = { + "/sys/fs/cgroup/service/worker/memory.max": String(GiB), + "/sys/fs/cgroup/service/worker/memory.current": String(768 * MiB), + }; + expect(fixture(files).availableRamBytes).toBe(256 * MiB); + expect( + fixture({ ...files, "/sys/fs/cgroup/service/worker/memory.stat": "anon 1\nfile 2\n" }) + .availableRamBytes + ).toBe(256 * MiB); + }); + + it("inactive file 大于用量时实际占用不为负", () => { + expect( + fixture({ + "/sys/fs/cgroup/service/worker/memory.max": String(GiB), + "/sys/fs/cgroup/service/worker/memory.current": String(100 * MiB), + "/sys/fs/cgroup/service/worker/memory.stat": `inactive_file ${200 * MiB}\n`, + }).availableRamBytes + ).toBe(GiB); + }); + + it("cgroup v1 用量与 memsw 联合用量都扣除 total_inactive_file", () => { + expect( + fixture({ + "/proc/self/cgroup": "5:memory:/service", + "/proc/self/mountinfo": "29 23 0:26 / /sys/fs/cgroup/memory rw - cgroup cgroup rw,memory", + "/sys/fs/cgroup/memory/service/memory.limit_in_bytes": String(2 * GiB), + "/sys/fs/cgroup/memory/service/memory.usage_in_bytes": String(1.75 * GiB), + "/sys/fs/cgroup/memory/service/memory.stat": `inactive_file ${100 * MiB}\ntotal_inactive_file ${768 * MiB}\n`, + "/sys/fs/cgroup/memory/service/memory.memsw.limit_in_bytes": String(2.5 * GiB), + "/sys/fs/cgroup/memory/service/memory.memsw.usage_in_bytes": String(2 * GiB), + }) + ).toMatchObject({ availableRamBytes: GiB, availableSwapBytes: 256 * MiB }); + }); + it("cgroup namespace 中的根成员仍遵守挂载根限制", () => { expect( fixture({ diff --git a/tests/unit/settings/system-settings-form-memory-admission-toggle.test.tsx b/tests/unit/settings/system-settings-form-memory-admission-toggle.test.tsx new file mode 100644 index 000000000..081dc8297 --- /dev/null +++ b/tests/unit/settings/system-settings-form-memory-admission-toggle.test.tsx @@ -0,0 +1,190 @@ +import fs from "node:fs"; +import path from "node:path"; +import type { ReactNode } from "react"; +import { act } from "react"; +import { createRoot } from "react-dom/client"; +import { NextIntlClientProvider } from "next-intl"; +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { SystemSettingsForm } from "@/app/[locale]/settings/config/_components/system-settings-form"; +import type { SystemSettings } from "@/types/system-config"; + +vi.mock("next/navigation", () => ({ + useRouter: () => ({ refresh: vi.fn() }), +})); + +const systemConfigActionMocks = vi.hoisted(() => ({ + saveSystemSettings: vi.fn(async (): Promise => ({ ok: true })), +})); +vi.mock("@/actions/system-config", () => systemConfigActionMocks); + +const requestFiltersActionMocks = vi.hoisted(() => ({ + getDistinctProviderGroupsAction: vi.fn(async () => ({ ok: true, data: [] })), +})); +vi.mock("@/actions/request-filters", () => requestFiltersActionMocks); + +const sonnerMocks = vi.hoisted(() => ({ + toast: { + success: vi.fn(), + error: vi.fn(), + warning: vi.fn(), + }, +})); +vi.mock("sonner", () => sonnerMocks); + +const baseSettings = { + siteTitle: "CC Hub", + allowGlobalUsageView: true, + currencyDisplay: "USD", + billingModelSource: "original", + codexPriorityBillingSource: "requested", + timezone: "UTC", + verboseProviderError: false, + passThroughUpstreamErrorMessage: true, + enableHttp2: true, + enableHighConcurrencyMode: false, + enableMemoryAdmission: false, + interceptAnthropicWarmupRequests: false, + enableThinkingSignatureRectifier: true, + enableThinkingBudgetRectifier: true, + enableBillingHeaderRectifier: true, + enableResponseInputRectifier: true, + enableCodexSessionIdCompletion: true, + enableClaudeMetadataUserIdInjection: true, + enableResponseFixer: true, + allowNonConversationEndpointProviderFallback: true, + fakeStreamingWhitelist: [], + responseFixerConfig: { + fixEncoding: true, + fixSseFormat: true, + fixTruncatedJson: true, + }, + quotaDbRefreshIntervalSeconds: 10, + quotaLeasePercent5h: 0.05, + quotaLeasePercentDaily: 0.05, + quotaLeasePercentWeekly: 0.05, + quotaLeasePercentMonthly: 0.05, + quotaLeaseCapUsd: null, + ipGeoLookupEnabled: true, + ipExtractionConfig: null, + replayEnabled: null, + replayCacheTtlMinutes: 30, + cacheEffectivenessEnabled: null, +} satisfies Partial; + +function loadMessages(locale: string) { + const base = path.join(process.cwd(), `messages/${locale}/settings`); + const read = (name: string) => JSON.parse(fs.readFileSync(path.join(base, name), "utf8")); + + return { + settings: { + common: read("common.json"), + config: read("config.json"), + requestFilters: read("requestFilters.json"), + }, + }; +} + +function render(node: ReactNode) { + const container = document.createElement("div"); + document.body.appendChild(container); + const root = createRoot(container); + + act(() => { + root.render( + + {node} + + ); + }); + + return { + unmount: () => { + act(() => root.unmount()); + container.remove(); + }, + }; +} + +async function submitForm() { + const form = document.body.querySelector("form"); + if (!form) throw new Error("未找到系统设置表单"); + + await act(async () => { + form.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true })); + await Promise.resolve(); + await new Promise((resolve) => setTimeout(resolve, 0)); + }); +} + +function memoryAdmissionSwitch(): HTMLElement { + const label = loadMessages("en").settings.config.form.enableMemoryAdmission as string; + const element = document.body.querySelector(`[aria-label="${label}"]`); + if (!element) throw new Error("未找到内存准入开关"); + return element; +} + +describe("SystemSettingsForm 内存准入开关", () => { + beforeEach(() => { + document.body.innerHTML = ""; + vi.clearAllMocks(); + }); + + test("默认关闭,未切换时保存为 false", async () => { + const { unmount } = render( + + ); + + const toggle = memoryAdmissionSwitch(); + expect(toggle.id).toBe("enable-memory-admission"); + expect(toggle.getAttribute("aria-checked")).toBe("false"); + expect(document.body.textContent).toContain( + loadMessages("en").settings.config.form.enableMemoryAdmissionDesc + ); + + await submitForm(); + + expect(systemConfigActionMocks.saveSystemSettings).toHaveBeenCalledWith( + expect.objectContaining({ enableMemoryAdmission: false }) + ); + + unmount(); + }); + + test("开启后保存为 true,并按服务端返回值刷新开关", async () => { + systemConfigActionMocks.saveSystemSettings.mockResolvedValueOnce({ + ok: true, + data: { ...baseSettings, enableMemoryAdmission: false }, + }); + const { unmount } = render( + + ); + + await act(async () => { + memoryAdmissionSwitch().click(); + await Promise.resolve(); + }); + expect(memoryAdmissionSwitch().getAttribute("aria-checked")).toBe("true"); + + await submitForm(); + + expect(systemConfigActionMocks.saveSystemSettings).toHaveBeenCalledWith( + expect.objectContaining({ enableMemoryAdmission: true }) + ); + expect(memoryAdmissionSwitch().getAttribute("aria-checked")).toBe("false"); + + unmount(); + }); + + test("初始值为开启时显示为开启", () => { + const { unmount } = render( + + ); + + expect(memoryAdmissionSwitch().getAttribute("aria-checked")).toBe("true"); + + unmount(); + }); +});