diff --git a/drizzle/0054_tidy_winter_soldier.sql b/drizzle/0054_tidy_winter_soldier.sql new file mode 100644 index 000000000..afa8a9f03 --- /dev/null +++ b/drizzle/0054_tidy_winter_soldier.sql @@ -0,0 +1 @@ +ALTER TABLE "system_settings" ADD COLUMN "enable_codex_session_id_completion" boolean DEFAULT true NOT NULL; \ No newline at end of file diff --git a/drizzle/meta/0054_snapshot.json b/drizzle/meta/0054_snapshot.json new file mode 100644 index 000000000..2ea76fc77 --- /dev/null +++ b/drizzle/meta/0054_snapshot.json @@ -0,0 +1,2388 @@ +{ + "id": "36887729-08df-4af3-98fe-d4fa87c7c5c7", + "prevId": "3d8f6ad1-ff20-411e-87a0-78476ee22dd3", + "version": "7", + "dialect": "postgresql", + "tables": { + "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", + "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_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 + }, + "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_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 + }, + "session_id": { + "name": "session_id", + "type": "varchar(64)", + "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 + }, + "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 + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ttfb_ms": { + "name": "ttfb_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cache_read_input_tokens": { + "name": "cache_read_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cache_creation_5m_input_tokens": { + "name": "cache_creation_5m_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cache_creation_1h_input_tokens": { + "name": "cache_creation_1h_input_tokens", + "type": "integer", + "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 + }, + "special_settings": { + "name": "special_settings", + "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 + }, + "messages_count": { + "name": "messages_count", + "type": "integer", + "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_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_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_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_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_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": {} + } + }, + "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": "'litellm'" + }, + "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": {} + } + }, + "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 + }, + "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, + "default": "'Asia/Shanghai'" + }, + "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.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 + }, + "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 + }, + "cost_multiplier": { + "name": "cost_multiplier", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": false, + "default": "'1.0'" + }, + "group_tag": { + "name": "group_tag", + "type": "varchar(50)", + "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 + }, + "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" + }, + "join_claude_pool": { + "name": "join_claude_pool", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": 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_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 + }, + "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 + }, + "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 + }, + "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_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": {} + } + }, + "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 + }, + "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": {} + } + }, + "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": "'Claude Code 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'" + }, + "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 + }, + "enable_http2": { + "name": "enable_http2", + "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_codex_session_id_completion": { + "name": "enable_codex_session_id_completion", + "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" + }, + "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.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_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 + }, + "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" + }, + "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_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" + ] + }, + "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 293f0bdd8..106e43116 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -379,6 +379,13 @@ "when": 1768143975091, "tag": "0053_watery_madame_hydra", "breakpoints": true + }, + { + "idx": 54, + "version": "7", + "when": 1768240715707, + "tag": "0054_tidy_winter_soldier", + "breakpoints": true } ] } \ No newline at end of file diff --git a/messages/en/settings/config.json b/messages/en/settings/config.json index 7b995c2b7..e1e542082 100644 --- a/messages/en/settings/config.json +++ b/messages/en/settings/config.json @@ -51,6 +51,8 @@ "enableResponseFixerDesc": "Automatically repairs common upstream response issues (encoding, SSE, truncated JSON). Enabled by default.", "enableThinkingSignatureRectifier": "Enable Thinking Signature Rectifier", "enableThinkingSignatureRectifierDesc": "When Anthropic providers return thinking signature incompatibility or invalid request errors, automatically removes incompatible thinking blocks and retries once against the same provider (enabled by default).", + "enableCodexSessionIdCompletion": "Enable Codex Session ID Completion", + "enableCodexSessionIdCompletionDesc": "When Codex requests provide only one of session_id (header) or prompt_cache_key (body), automatically completes the other. If both are missing, generates a UUID v7 session id and reuses it stably within the same conversation.", "interceptAnthropicWarmupRequests": "Intercept Warmup Requests (Anthropic)", "interceptAnthropicWarmupRequestsDesc": "When enabled, Claude Code warmup probe requests will be answered by CCH directly to avoid upstream provider calls; the request is logged for audit but is not billed, not rate-limited, and excluded from statistics.", "keepDays": "Retention Days", diff --git a/messages/ja/settings/config.json b/messages/ja/settings/config.json index 88d3e69b2..f5faa7c68 100644 --- a/messages/ja/settings/config.json +++ b/messages/ja/settings/config.json @@ -51,6 +51,8 @@ "enableResponseFixerDesc": "上流応答の一般的な形式問題(エンコーディング、SSE、途切れた JSON)を自動修復します(既定で有効)。", "enableThinkingSignatureRectifier": "thinking 署名整流を有効化", "enableThinkingSignatureRectifierDesc": "Anthropic プロバイダーで thinking 署名の不整合や不正なリクエストエラーが発生した場合、thinking 関連ブロックを削除して同一プロバイダーへ1回だけ再試行します(既定で有効)。", + "enableCodexSessionIdCompletion": "Codex セッションID補完を有効化", + "enableCodexSessionIdCompletionDesc": "Codex リクエストで session_id(ヘッダー)または prompt_cache_key(ボディ)のどちらか一方しか提供されない場合に、欠けている方を自動補完します。両方ない場合は UUID v7 のセッションIDを生成し、同一対話内で安定して再利用します。", "interceptAnthropicWarmupRequests": "Warmup リクエストを遮断(Anthropic)", "interceptAnthropicWarmupRequestsDesc": "有効にすると、Claude Code の Warmup プローブ要求は CCH が直接短い応答を返し、上流プロバイダーへのリクエストを回避します。ログには残りますが、課金/レート制限/統計には含まれません。", "keepDays": "保持日数", diff --git a/messages/ru/settings/config.json b/messages/ru/settings/config.json index 8e39df2e2..5fdd3fef9 100644 --- a/messages/ru/settings/config.json +++ b/messages/ru/settings/config.json @@ -51,6 +51,8 @@ "enableResponseFixerDesc": "Автоматически исправляет распространённые проблемы ответа у провайдеров (кодировка, SSE, обрезанный JSON). Включено по умолчанию.", "enableThinkingSignatureRectifier": "Включить исправление thinking-signature", "enableThinkingSignatureRectifierDesc": "Если Anthropic-провайдер возвращает ошибку несовместимой подписи thinking или некорректного запроса, автоматически удаляет несовместимые thinking-блоки и повторяет запрос один раз к тому же провайдеру (включено по умолчанию).", + "enableCodexSessionIdCompletion": "Включить дополнение Session ID для Codex", + "enableCodexSessionIdCompletionDesc": "Если в Codex-запросе присутствует только session_id (в заголовках) или prompt_cache_key (в теле), автоматически дополняет отсутствующее поле. Если оба отсутствуют, генерирует UUID v7 и стабильно переиспользует его в рамках одного диалога.", "interceptAnthropicWarmupRequests": "Перехватывать Warmup-запросы (Anthropic)", "interceptAnthropicWarmupRequestsDesc": "Если включено, Warmup-пробные запросы Claude Code будут отвечены самим CCH без обращения к провайдерам; запрос сохраняется в логах, но не тарифицируется, не учитывается в лимитах и исключается из статистики.", "keepDays": "Хранить дней", diff --git a/messages/zh-CN/settings/config.json b/messages/zh-CN/settings/config.json index f5605ed00..06ed7b655 100644 --- a/messages/zh-CN/settings/config.json +++ b/messages/zh-CN/settings/config.json @@ -40,6 +40,8 @@ "interceptAnthropicWarmupRequestsDesc": "开启后,识别到 Claude Code 的 Warmup 探测请求将由 CCH 直接抢答短响应,避免访问上游供应商;该请求会记录在日志中,但不计费、不限流、不计入统计。", "enableThinkingSignatureRectifier": "启用 thinking 签名整流器", "enableThinkingSignatureRectifierDesc": "当 Anthropic 类型供应商返回 thinking 签名不兼容或非法请求等错误时,自动移除不兼容的 thinking 相关块并对同一供应商重试一次(默认开启)。", + "enableCodexSessionIdCompletion": "启用 Codex Session ID 补全", + "enableCodexSessionIdCompletionDesc": "当 Codex 请求仅提供 session_id(请求头)或 prompt_cache_key(请求体)之一时,自动补全另一个;若两者均缺失,则生成 UUID v7 会话 ID,并在同一对话内稳定复用。", "enableResponseFixer": "启用响应整流", "enableResponseFixerDesc": "自动修复上游响应中常见的编码、SSE 与 JSON 格式问题(默认开启)。", "responseFixerFixEncoding": "修复编码问题", diff --git a/messages/zh-TW/settings/config.json b/messages/zh-TW/settings/config.json index 83f70a5fa..bad284b0c 100644 --- a/messages/zh-TW/settings/config.json +++ b/messages/zh-TW/settings/config.json @@ -51,6 +51,8 @@ "enableResponseFixerDesc": "自動修復上游回應中常見的編碼、SSE 與 JSON 格式問題(預設開啟)。", "enableThinkingSignatureRectifier": "啟用 thinking 簽名整流器", "enableThinkingSignatureRectifierDesc": "當 Anthropic 類型供應商返回 thinking 簽名不相容或非法請求等錯誤時,自動移除不相容的 thinking 相關區塊並對同一供應商重試一次(預設開啟)。", + "enableCodexSessionIdCompletion": "啟用 Codex Session ID 補全", + "enableCodexSessionIdCompletionDesc": "當 Codex 請求僅提供 session_id(請求頭)或 prompt_cache_key(請求體)之一時,自動補全另一個;若兩者皆缺失,則產生 UUID v7 會話 ID,並在同一對話內穩定複用。", "interceptAnthropicWarmupRequests": "攔截 Warmup 請求(Anthropic)", "interceptAnthropicWarmupRequestsDesc": "開啟後,識別到 Claude Code 的 Warmup 探測請求將由 CCH 直接搶答短回應,避免存取上游供應商;該請求會記錄在日誌中,但不計費、不限流、不計入統計。", "keepDays": "保留天數", diff --git a/package.json b/package.json index 14d6e219e..8e6f5aacb 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "test:e2e": "vitest run --config vitest.e2e.config.ts --reporter=verbose", "test:integration": "vitest run --config vitest.integration.config.ts --reporter=verbose", "test:coverage": "vitest run --coverage", + "test:coverage:codex-session-id-completer": "vitest run --config vitest.codex-session-id-completer.config.ts --coverage", "test:coverage:thinking-signature-rectifier": "vitest run --config vitest.thinking-signature-rectifier.config.ts --coverage", "test:coverage:quota": "vitest run --config vitest.quota.config.ts --coverage", "test:coverage:my-usage": "vitest run --config vitest.my-usage.config.ts --coverage", diff --git a/src/actions/system-config.ts b/src/actions/system-config.ts index eedf78985..02545989f 100644 --- a/src/actions/system-config.ts +++ b/src/actions/system-config.ts @@ -39,6 +39,7 @@ export async function saveSystemSettings(formData: { enableHttp2?: boolean; interceptAnthropicWarmupRequests?: boolean; enableThinkingSignatureRectifier?: boolean; + enableCodexSessionIdCompletion?: boolean; enableResponseFixer?: boolean; responseFixerConfig?: Partial; }): Promise> { @@ -63,6 +64,7 @@ export async function saveSystemSettings(formData: { enableHttp2: validated.enableHttp2, interceptAnthropicWarmupRequests: validated.interceptAnthropicWarmupRequests, enableThinkingSignatureRectifier: validated.enableThinkingSignatureRectifier, + enableCodexSessionIdCompletion: validated.enableCodexSessionIdCompletion, enableResponseFixer: validated.enableResponseFixer, responseFixerConfig: validated.responseFixerConfig, }); 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 ad21c2327..b0c6e1164 100644 --- a/src/app/[locale]/settings/config/_components/system-settings-form.tsx +++ b/src/app/[locale]/settings/config/_components/system-settings-form.tsx @@ -31,6 +31,7 @@ interface SystemSettingsFormProps { | "enableHttp2" | "interceptAnthropicWarmupRequests" | "enableThinkingSignatureRectifier" + | "enableCodexSessionIdCompletion" | "enableResponseFixer" | "responseFixerConfig" >; @@ -60,6 +61,9 @@ export function SystemSettingsForm({ initialSettings }: SystemSettingsFormProps) const [enableThinkingSignatureRectifier, setEnableThinkingSignatureRectifier] = useState( initialSettings.enableThinkingSignatureRectifier ); + const [enableCodexSessionIdCompletion, setEnableCodexSessionIdCompletion] = useState( + initialSettings.enableCodexSessionIdCompletion + ); const [enableResponseFixer, setEnableResponseFixer] = useState( initialSettings.enableResponseFixer ); @@ -86,6 +90,7 @@ export function SystemSettingsForm({ initialSettings }: SystemSettingsFormProps) enableHttp2, interceptAnthropicWarmupRequests, enableThinkingSignatureRectifier, + enableCodexSessionIdCompletion, enableResponseFixer, responseFixerConfig, }); @@ -104,6 +109,7 @@ export function SystemSettingsForm({ initialSettings }: SystemSettingsFormProps) setEnableHttp2(result.data.enableHttp2); setInterceptAnthropicWarmupRequests(result.data.interceptAnthropicWarmupRequests); setEnableThinkingSignatureRectifier(result.data.enableThinkingSignatureRectifier); + setEnableCodexSessionIdCompletion(result.data.enableCodexSessionIdCompletion); setEnableResponseFixer(result.data.enableResponseFixer); setResponseFixerConfig(result.data.responseFixerConfig); } @@ -250,6 +256,23 @@ export function SystemSettingsForm({ initialSettings }: SystemSettingsFormProps) /> +
+
+ +

+ {t("enableCodexSessionIdCompletionDesc")} +

+
+ setEnableCodexSessionIdCompletion(checked)} + disabled={isPending} + /> +
+
diff --git a/src/app/[locale]/settings/config/page.tsx b/src/app/[locale]/settings/config/page.tsx index 6b00d4669..ae0b90dc9 100644 --- a/src/app/[locale]/settings/config/page.tsx +++ b/src/app/[locale]/settings/config/page.tsx @@ -42,6 +42,7 @@ async function SettingsConfigContent() { enableHttp2: settings.enableHttp2, interceptAnthropicWarmupRequests: settings.interceptAnthropicWarmupRequests, enableThinkingSignatureRectifier: settings.enableThinkingSignatureRectifier, + enableCodexSessionIdCompletion: settings.enableCodexSessionIdCompletion, enableResponseFixer: settings.enableResponseFixer, responseFixerConfig: settings.responseFixerConfig, }} diff --git a/src/app/v1/_lib/codex/session-completer.ts b/src/app/v1/_lib/codex/session-completer.ts new file mode 100644 index 000000000..c146a9b8f --- /dev/null +++ b/src/app/v1/_lib/codex/session-completer.ts @@ -0,0 +1,330 @@ +import "server-only"; + +import crypto from "node:crypto"; +import { logger } from "@/lib/logger"; +import { getRedisClient } from "@/lib/redis"; +import { normalizeCodexSessionId } from "./session-extractor"; + +export type CodexSessionCompletionAction = + | "none" + | "completed_missing_fields" + | "generated_uuid_v7" + | "reused_fingerprint_cache"; + +export type CodexSessionCompletionSource = + | "header_session_id" + | "header_x_session_id" + | "body_prompt_cache_key" + | "body_metadata_session_id" + | "fingerprint_cache" + | "generated_uuid_v7"; + +export type CodexSessionCompletionResult = { + applied: boolean; + action: CodexSessionCompletionAction; + sessionId: string; + source: CodexSessionCompletionSource; +}; + +type CompleteArgs = { + keyId: number; + headers: Headers; + requestBody: Record; + userAgent: string | null; +}; + +type CodexMetadata = Record; + +function parseMetadata(requestBody: Record): CodexMetadata | null { + const metadata = requestBody.metadata; + if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) return null; + return metadata as CodexMetadata; +} + +function getSessionTtlSeconds(): number { + const raw = process.env.SESSION_TTL; + const parsed = raw ? Number.parseInt(raw, 10) : Number.NaN; + if (!Number.isFinite(parsed) || parsed <= 0) { + return 300; + } + return parsed; +} + +function extractClientIp(headers: Headers): string | null { + const forwardedFor = headers.get("x-forwarded-for"); + if (forwardedFor) { + const first = forwardedFor + .split(",") + .map((ip) => ip.trim()) + .filter(Boolean)[0]; + if (first) return first; + } + + const realIp = headers.get("x-real-ip"); + return realIp ? realIp.trim() : null; +} + +function extractInitialMessageTextHash(requestBody: Record): string | null { + const input = requestBody.input; + if (!Array.isArray(input) || input.length === 0) { + return null; + } + + const texts: string[] = []; + + for (const item of input) { + if (!item || typeof item !== "object") continue; + const obj = item as Record; + + // Only consider "message" items for conversation fingerprinting. + const itemType = typeof obj.type === "string" ? obj.type : null; + if (itemType && itemType !== "message") continue; + + const content = obj.content; + + if (typeof content === "string") { + if (content.trim()) texts.push(content); + } else if (Array.isArray(content)) { + const parts: string[] = []; + for (const part of content) { + if (!part || typeof part !== "object") continue; + const partObj = part as Record; + const text = typeof partObj.text === "string" ? partObj.text : null; + if (!text) continue; + parts.push(text); + } + const joined = parts.join(""); + if (joined.trim()) texts.push(joined); + } + + if (texts.length >= 3) { + break; + } + } + + if (texts.length === 0) return null; + + const combined = texts.join("|"); + const hash = crypto.createHash("sha256").update(combined, "utf8").digest("hex"); + return hash.substring(0, 16); +} + +export function generateUuidV7(): string { + const timestampMs = Date.now(); + const bytes = crypto.randomBytes(16); + + // 48-bit big-endian Unix timestamp in milliseconds + // Note: avoid BigInt to support TS targets < ES2020. + let ts = timestampMs; + bytes[5] = ts % 256; + ts = Math.floor(ts / 256); + bytes[4] = ts % 256; + ts = Math.floor(ts / 256); + bytes[3] = ts % 256; + ts = Math.floor(ts / 256); + bytes[2] = ts % 256; + ts = Math.floor(ts / 256); + bytes[1] = ts % 256; + ts = Math.floor(ts / 256); + bytes[0] = ts % 256; + + // Version (7): high nibble of byte 6 + bytes[6] = (bytes[6] & 0x0f) | 0x70; + + // Variant (RFC 4122): 10xx xxxx in byte 8 + bytes[8] = (bytes[8] & 0x3f) | 0x80; + + const hex = bytes.toString("hex"); + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice( + 16, + 20 + )}-${hex.slice(20)}`; +} + +function calculateFingerprintHash(args: CompleteArgs): string | null { + const ip = extractClientIp(args.headers) ?? "unknown"; + const ua = args.userAgent ?? args.headers.get("user-agent") ?? "unknown"; + const messageHash = extractInitialMessageTextHash(args.requestBody) ?? "unknown"; + + const fingerprint = `v1|key:${args.keyId}|ip:${ip}|ua:${ua}|m:${messageHash}`; + return crypto.createHash("sha256").update(fingerprint, "utf8").digest("hex"); +} + +type FingerprintSessionIdResult = { + sessionId: string; + source: "fingerprint_cache" | "generated_uuid_v7"; + action: "reused_fingerprint_cache" | "generated_uuid_v7"; +}; + +async function getOrCreateSessionIdFromFingerprint( + args: CompleteArgs +): Promise { + const redis = getRedisClient(); + const ttlSeconds = getSessionTtlSeconds(); + const fingerprintHash = calculateFingerprintHash(args); + + if (!redis || redis.status !== "ready" || !fingerprintHash) { + return { + sessionId: generateUuidV7(), + source: "generated_uuid_v7", + action: "generated_uuid_v7", + }; + } + + const redisKey = `codex:fingerprint:${fingerprintHash}:session_id`; + + try { + const existing = normalizeCodexSessionId(await redis.get(redisKey)); + if (existing) { + return { + sessionId: existing, + source: "fingerprint_cache", + action: "reused_fingerprint_cache", + }; + } + + const candidate = generateUuidV7(); + + const setResult = await redis.set(redisKey, candidate, "EX", ttlSeconds, "NX"); + if (setResult === "OK") { + return { sessionId: candidate, source: "generated_uuid_v7", action: "generated_uuid_v7" }; + } + + const existingAfter = normalizeCodexSessionId(await redis.get(redisKey)); + if (existingAfter) { + return { + sessionId: existingAfter, + source: "fingerprint_cache", + action: "reused_fingerprint_cache", + }; + } + + await redis.set(redisKey, candidate, "EX", ttlSeconds); + return { sessionId: candidate, source: "generated_uuid_v7", action: "generated_uuid_v7" }; + } catch (error) { + logger.warn("[CodexSessionCompleter] Redis unavailable, falling back to UUID v7", { + error: error instanceof Error ? error.message : String(error), + }); + return { + sessionId: generateUuidV7(), + source: "generated_uuid_v7", + action: "generated_uuid_v7", + }; + } +} + +function ensureMetadataSessionId(requestBody: Record, sessionId: string): void { + const metadata = parseMetadata(requestBody); + if (metadata) { + metadata.session_id = sessionId; + requestBody.metadata = metadata; + return; + } + + // Only create metadata when the current value is missing. + if (requestBody.metadata === undefined) { + requestBody.metadata = { session_id: sessionId } satisfies CodexMetadata; + } +} + +/** + * Ensure Codex session identifiers exist in both: + * - Header: `session_id` (+ `x-session-id` for compatibility) + * - Body: `prompt_cache_key` + * + * Completion rules: + * - If either side provides a valid identifier, copy to the missing side + * - If both are missing/invalid, generate a UUID v7 and keep stable via fingerprint cache + */ +export async function completeCodexSessionIdentifiers( + args: CompleteArgs +): Promise { + const headerSessionId = normalizeCodexSessionId(args.headers.get("session_id")); + const headerXSessionId = normalizeCodexSessionId(args.headers.get("x-session-id")); + const bodyPromptCacheKey = normalizeCodexSessionId(args.requestBody.prompt_cache_key); + const metadata = parseMetadata(args.requestBody); + const bodyMetadataSessionId = metadata ? normalizeCodexSessionId(metadata.session_id) : null; + + const missingHeader = !headerSessionId && !headerXSessionId; + const missingBody = !bodyPromptCacheKey; + + const existing: { sessionId: string; source: CodexSessionCompletionSource } | null = + headerSessionId + ? { sessionId: headerSessionId, source: "header_session_id" } + : headerXSessionId + ? { sessionId: headerXSessionId, source: "header_x_session_id" } + : bodyPromptCacheKey + ? { sessionId: bodyPromptCacheKey, source: "body_prompt_cache_key" } + : bodyMetadataSessionId + ? { sessionId: bodyMetadataSessionId, source: "body_metadata_session_id" } + : null; + + // Both required fields present: keep as-is (idempotent) + // Note: x-session-id is treated as a compatibility header and does not satisfy the requirement + // of having `session_id` present. + if (headerSessionId && bodyPromptCacheKey && existing) { + return { + applied: false, + action: "none", + sessionId: existing.sessionId, + source: existing.source, + }; + } + + let sessionId: string; + let source: CodexSessionCompletionSource; + let action: CodexSessionCompletionAction; + + if (existing) { + sessionId = existing.sessionId; + source = existing.source; + action = "none"; + } else { + const fingerprintResolved = await getOrCreateSessionIdFromFingerprint(args); + sessionId = fingerprintResolved.sessionId; + source = fingerprintResolved.source; + action = fingerprintResolved.action; + } + + let applied = false; + let changedHeaderOrBody = false; + + if (missingHeader) { + args.headers.set("session_id", sessionId); + args.headers.set("x-session-id", sessionId); + applied = true; + changedHeaderOrBody = true; + } else if (!headerSessionId && headerXSessionId) { + // Keep both header keys present for downstream compatibility. + args.headers.set("session_id", headerXSessionId); + applied = true; + changedHeaderOrBody = true; + } else if (headerSessionId && !headerXSessionId) { + args.headers.set("x-session-id", headerSessionId); + applied = true; + changedHeaderOrBody = true; + } + + if (missingBody) { + args.requestBody.prompt_cache_key = sessionId; + applied = true; + changedHeaderOrBody = true; + } + + // Only touch metadata when we have applied completion/generation. + if (applied && (!bodyMetadataSessionId || bodyMetadataSessionId !== sessionId)) { + ensureMetadataSessionId(args.requestBody, sessionId); + applied = true; + } + + if (existing) { + action = changedHeaderOrBody ? "completed_missing_fields" : "none"; + } + + return { + applied, + action, + sessionId, + source, + }; +} diff --git a/src/app/v1/_lib/proxy/message-service.ts b/src/app/v1/_lib/proxy/message-service.ts index 92e0c420d..1c67cacb1 100644 --- a/src/app/v1/_lib/proxy/message-service.ts +++ b/src/app/v1/_lib/proxy/message-service.ts @@ -43,6 +43,7 @@ export class ProxyMessageService { original_model: session.getOriginalModel() ?? undefined, // 传入原始模型(用户请求的模型) messages_count: session.getMessagesLength(), // 传入 messages 数量 endpoint, // 传入请求端点(可能为 undefined) + special_settings: session.getSpecialSettings(), // 特殊设置(审计/展示) }); session.setMessageContext({ diff --git a/src/app/v1/_lib/proxy/session-guard.ts b/src/app/v1/_lib/proxy/session-guard.ts index b528c1f75..d3857af82 100644 --- a/src/app/v1/_lib/proxy/session-guard.ts +++ b/src/app/v1/_lib/proxy/session-guard.ts @@ -2,6 +2,7 @@ import { getCachedSystemSettings } from "@/lib/config"; import { logger } from "@/lib/logger"; import { SessionManager } from "@/lib/session-manager"; import { SessionTracker } from "@/lib/session-tracker"; +import { completeCodexSessionIdentifiers } from "../codex/session-completer"; import type { ProxySession } from "./session"; /** @@ -47,13 +48,40 @@ export class ProxySessionGuard { } try { + const systemSettings = await getCachedSystemSettings(); + + // Codex Session ID 补全:在提取 clientSessionId 之前触发,避免落入不稳定的降级方案 + const codexCompletionEnabled = systemSettings.enableCodexSessionIdCompletion ?? true; + const requestMessage = session.request.message as Record; + const isCodexRequest = Array.isArray(requestMessage.input); + + if (codexCompletionEnabled && isCodexRequest) { + const completion = await completeCodexSessionIdentifiers({ + keyId, + headers: session.headers, + requestBody: requestMessage, + userAgent: session.userAgent, + }); + + if (completion.applied && completion.action !== "none") { + session.addSpecialSetting({ + type: "codex_session_id_completion", + scope: "request", + hit: true, + action: completion.action, + source: completion.source, + sessionId: completion.sessionId, + }); + } + } + const warmupMaybeIntercepted = session.isWarmupRequest() && !!session.authState?.success && !!session.authState.user && !!session.authState.key && !!session.authState.apiKey && - (await getCachedSystemSettings()).interceptAnthropicWarmupRequests; + systemSettings.interceptAnthropicWarmupRequests; // 1. 尝试从客户端提取 session_id(metadata.session_id) const clientSessionId = diff --git a/src/drizzle/schema.ts b/src/drizzle/schema.ts index 7edb93756..aecad1909 100644 --- a/src/drizzle/schema.ts +++ b/src/drizzle/schema.ts @@ -495,6 +495,12 @@ export const systemSettings = pgTable('system_settings', { .notNull() .default(true), + // Codex Session ID 补全(默认开启) + // 开启后:当 Codex 请求缺少 session_id / prompt_cache_key 时,自动补全或生成稳定的会话标识 + enableCodexSessionIdCompletion: boolean('enable_codex_session_id_completion') + .notNull() + .default(true), + // 响应整流(默认开启) enableResponseFixer: boolean('enable_response_fixer').notNull().default(true), responseFixerConfig: jsonb('response_fixer_config') diff --git a/src/lib/config/system-settings-cache.ts b/src/lib/config/system-settings-cache.ts index 8c0071627..4fa19dfc8 100644 --- a/src/lib/config/system-settings-cache.ts +++ b/src/lib/config/system-settings-cache.ts @@ -29,12 +29,14 @@ const DEFAULT_SETTINGS: Pick< | "enableHttp2" | "interceptAnthropicWarmupRequests" | "enableThinkingSignatureRectifier" + | "enableCodexSessionIdCompletion" | "enableResponseFixer" | "responseFixerConfig" > = { enableHttp2: false, interceptAnthropicWarmupRequests: false, enableThinkingSignatureRectifier: true, + enableCodexSessionIdCompletion: true, enableResponseFixer: true, responseFixerConfig: { fixTruncatedJson: true, @@ -103,6 +105,7 @@ export async function getCachedSystemSettings(): Promise { enableHttp2: DEFAULT_SETTINGS.enableHttp2, interceptAnthropicWarmupRequests: DEFAULT_SETTINGS.interceptAnthropicWarmupRequests, enableThinkingSignatureRectifier: DEFAULT_SETTINGS.enableThinkingSignatureRectifier, + enableCodexSessionIdCompletion: DEFAULT_SETTINGS.enableCodexSessionIdCompletion, enableResponseFixer: DEFAULT_SETTINGS.enableResponseFixer, responseFixerConfig: DEFAULT_SETTINGS.responseFixerConfig, createdAt: new Date(), diff --git a/src/lib/utils/special-settings.ts b/src/lib/utils/special-settings.ts index 91047eb83..74803da9a 100644 --- a/src/lib/utils/special-settings.ts +++ b/src/lib/utils/special-settings.ts @@ -67,6 +67,14 @@ function buildSettingKey(setting: SpecialSetting): string { setting.removedRedactedThinkingBlocks, setting.removedSignatureFields, ]); + case "codex_session_id_completion": + return JSON.stringify([ + setting.type, + setting.hit, + setting.action, + setting.source, + setting.sessionId, + ]); default: { // 兜底:保证即使未来扩展类型也不会导致运行时崩溃 const _exhaustive: never = setting; diff --git a/src/lib/validation/schemas.ts b/src/lib/validation/schemas.ts index b952a8bb2..b92501375 100644 --- a/src/lib/validation/schemas.ts +++ b/src/lib/validation/schemas.ts @@ -753,6 +753,8 @@ export const UpdateSystemSettingsSchema = z.object({ interceptAnthropicWarmupRequests: z.boolean().optional(), // thinking signature 整流器(可选) enableThinkingSignatureRectifier: z.boolean().optional(), + // Codex Session ID 补全(可选) + enableCodexSessionIdCompletion: z.boolean().optional(), // 响应整流(可选) enableResponseFixer: z.boolean().optional(), responseFixerConfig: z diff --git a/src/repository/_shared/transformers.ts b/src/repository/_shared/transformers.ts index 218048ceb..00eeee4c8 100644 --- a/src/repository/_shared/transformers.ts +++ b/src/repository/_shared/transformers.ts @@ -172,6 +172,7 @@ export function toSystemSettings(dbSettings: any): SystemSettings { enableHttp2: dbSettings?.enableHttp2 ?? false, interceptAnthropicWarmupRequests: dbSettings?.interceptAnthropicWarmupRequests ?? false, enableThinkingSignatureRectifier: dbSettings?.enableThinkingSignatureRectifier ?? true, + enableCodexSessionIdCompletion: dbSettings?.enableCodexSessionIdCompletion ?? true, enableResponseFixer: dbSettings?.enableResponseFixer ?? true, responseFixerConfig: { ...defaultResponseFixerConfig, diff --git a/src/repository/message.ts b/src/repository/message.ts index 50255c685..a90d0e0ec 100644 --- a/src/repository/message.ts +++ b/src/repository/message.ts @@ -32,6 +32,7 @@ export async function createMessageRequest( userAgent: data.user_agent, // User-Agent endpoint: data.endpoint, // 请求端点(可为空) messagesCount: data.messages_count, // Messages 数量 + specialSettings: data.special_settings ?? undefined, // 特殊设置(审计/展示) cacheTtlApplied: data.cache_ttl_applied, cacheCreationInputTokens: data.cache_creation_input_tokens, cacheCreation5mInputTokens: data.cache_creation_5m_input_tokens, @@ -59,6 +60,7 @@ export async function createMessageRequest( cacheCreation5mInputTokens: messageRequest.cacheCreation5mInputTokens, cacheCreation1hInputTokens: messageRequest.cacheCreation1hInputTokens, cacheReadInputTokens: messageRequest.cacheReadInputTokens, + specialSettings: messageRequest.specialSettings, createdAt: messageRequest.createdAt, updatedAt: messageRequest.updatedAt, deletedAt: messageRequest.deletedAt, diff --git a/src/repository/system-config.ts b/src/repository/system-config.ts index b5f2ee944..0f764b225 100644 --- a/src/repository/system-config.ts +++ b/src/repository/system-config.ts @@ -149,6 +149,7 @@ function createFallbackSettings(): SystemSettings { enableHttp2: false, interceptAnthropicWarmupRequests: false, enableThinkingSignatureRectifier: true, + enableCodexSessionIdCompletion: true, enableResponseFixer: true, responseFixerConfig: { fixTruncatedJson: true, @@ -182,6 +183,7 @@ export async function getSystemSettings(): Promise { enableHttp2: systemSettings.enableHttp2, interceptAnthropicWarmupRequests: systemSettings.interceptAnthropicWarmupRequests, enableThinkingSignatureRectifier: systemSettings.enableThinkingSignatureRectifier, + enableCodexSessionIdCompletion: systemSettings.enableCodexSessionIdCompletion, enableResponseFixer: systemSettings.enableResponseFixer, responseFixerConfig: systemSettings.responseFixerConfig, createdAt: systemSettings.createdAt, @@ -319,6 +321,11 @@ export async function updateSystemSettings( updates.enableThinkingSignatureRectifier = payload.enableThinkingSignatureRectifier; } + // Codex Session ID 补全开关(如果提供) + if (payload.enableCodexSessionIdCompletion !== undefined) { + updates.enableCodexSessionIdCompletion = payload.enableCodexSessionIdCompletion; + } + // 响应整流开关(如果提供) if (payload.enableResponseFixer !== undefined) { updates.enableResponseFixer = payload.enableResponseFixer; @@ -350,6 +357,7 @@ export async function updateSystemSettings( enableHttp2: systemSettings.enableHttp2, interceptAnthropicWarmupRequests: systemSettings.interceptAnthropicWarmupRequests, enableThinkingSignatureRectifier: systemSettings.enableThinkingSignatureRectifier, + enableCodexSessionIdCompletion: systemSettings.enableCodexSessionIdCompletion, enableResponseFixer: systemSettings.enableResponseFixer, responseFixerConfig: systemSettings.responseFixerConfig, createdAt: systemSettings.createdAt, diff --git a/src/types/special-settings.ts b/src/types/special-settings.ts index fdbcac1a6..ae9c622e9 100644 --- a/src/types/special-settings.ts +++ b/src/types/special-settings.ts @@ -10,6 +10,7 @@ export type SpecialSetting = | ResponseFixerSpecialSetting | GuardInterceptSpecialSetting | ThinkingSignatureRectifierSpecialSetting + | CodexSessionIdCompletionSpecialSetting | AnthropicCacheTtlHeaderOverrideSpecialSetting | AnthropicContext1mHeaderOverrideSpecialSetting; @@ -110,3 +111,24 @@ export type ThinkingSignatureRectifierSpecialSetting = { removedRedactedThinkingBlocks: number; removedSignatureFields: number; }; + +/** + * Codex Session ID 补全审计 + * + * 用于记录:当 Codex 请求缺少 session_id / prompt_cache_key 时, + * 系统自动补全或生成会话标识,提升供应商复用与会话粘性稳定性。 + */ +export type CodexSessionIdCompletionSpecialSetting = { + type: "codex_session_id_completion"; + scope: "request"; + hit: boolean; + action: "completed_missing_fields" | "generated_uuid_v7" | "reused_fingerprint_cache"; + source: + | "header_session_id" + | "header_x_session_id" + | "body_prompt_cache_key" + | "body_metadata_session_id" + | "fingerprint_cache" + | "generated_uuid_v7"; + sessionId: string; +}; diff --git a/src/types/system-config.ts b/src/types/system-config.ts index 02bf316b6..36e907e89 100644 --- a/src/types/system-config.ts +++ b/src/types/system-config.ts @@ -44,6 +44,10 @@ export interface SystemSettings { // 目标:当 Anthropic 类型供应商出现 thinking 签名不兼容导致的 400 错误时,自动整流并重试一次 enableThinkingSignatureRectifier: boolean; + // Codex Session ID 补全(默认开启) + // 目标:当 Codex 请求缺少 session_id / prompt_cache_key 时,自动补全或生成稳定的会话标识 + enableCodexSessionIdCompletion: boolean; + // 响应整流(默认开启) enableResponseFixer: boolean; responseFixerConfig: ResponseFixerConfig; @@ -84,6 +88,9 @@ export interface UpdateSystemSettingsInput { // thinking signature 整流器(可选) enableThinkingSignatureRectifier?: boolean; + // Codex Session ID 补全(可选) + enableCodexSessionIdCompletion?: boolean; + // 响应整流(可选) enableResponseFixer?: boolean; responseFixerConfig?: Partial; diff --git a/tests/unit/codex/session-completer.test.ts b/tests/unit/codex/session-completer.test.ts new file mode 100644 index 000000000..35bc9d66b --- /dev/null +++ b/tests/unit/codex/session-completer.test.ts @@ -0,0 +1,414 @@ +import { beforeEach, describe, expect, test, vi } from "vitest"; + +let redisClientRef: any = null; + +const UUID_V7_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; + +const ORIGINAL_SESSION_TTL = process.env.SESSION_TTL; + +vi.mock("@/lib/logger", () => ({ + logger: { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + trace: vi.fn(), + }, +})); + +vi.mock("@/lib/redis", () => ({ + getRedisClient: () => redisClientRef, +})); + +function makeCodexRequestBody(overrides?: Record): Record { + return { + model: "gpt-5-codex", + input: [ + { + type: "message", + role: "user", + content: [{ type: "input_text", text: "hello" }], + }, + ], + ...(overrides ?? {}), + }; +} + +function makeFakeRedis() { + const store = new Map(); + + const client = { + status: "ready", + get: vi.fn(async (key: string) => store.get(key) ?? null), + set: vi.fn( + async (key: string, value: string, mode?: string, ttlSeconds?: number, nx?: string) => { + if (mode !== "EX" || typeof ttlSeconds !== "number") { + throw new Error("FakeRedis only supports SET key value EX ttl [NX]"); + } + + if (nx === "NX" && store.has(key)) { + return null; + } + + store.set(key, value); + return "OK"; + } + ), + }; + + return { client, store }; +} + +describe("Codex session completer", () => { + beforeEach(() => { + redisClientRef = null; + if (ORIGINAL_SESSION_TTL === undefined) { + delete process.env.SESSION_TTL; + } else { + process.env.SESSION_TTL = ORIGINAL_SESSION_TTL; + } + }); + + test("completes body.prompt_cache_key from header session_id", async () => { + const { completeCodexSessionIdentifiers } = await import( + "@/app/v1/_lib/codex/session-completer" + ); + + const sessionId = "sess_123456789012345678901"; + const headers = new Headers({ session_id: sessionId }); + const body = makeCodexRequestBody(); + + const result = await completeCodexSessionIdentifiers({ + keyId: 1, + headers, + requestBody: body, + userAgent: "codex_cli_rs/0.50.0", + }); + + expect(result.applied).toBe(true); + expect(result.sessionId).toBe(sessionId); + expect(body.prompt_cache_key).toBe(sessionId); + expect(headers.get("session_id")).toBe(sessionId); + }); + + test("completes header session_id from body.prompt_cache_key", async () => { + const { completeCodexSessionIdentifiers } = await import( + "@/app/v1/_lib/codex/session-completer" + ); + + const promptCacheKey = "019b82ff-08ff-75a3-a203-7e10274fdbd8"; + const headers = new Headers(); + const body = makeCodexRequestBody({ prompt_cache_key: promptCacheKey }); + + const result = await completeCodexSessionIdentifiers({ + keyId: 1, + headers, + requestBody: body, + userAgent: "codex_cli_rs/0.50.0", + }); + + expect(result.applied).toBe(true); + expect(result.sessionId).toBe(promptCacheKey); + expect(headers.get("session_id")).toBe(promptCacheKey); + expect(body.prompt_cache_key).toBe(promptCacheKey); + }); + + test("no-op when both session_id and prompt_cache_key already exist", async () => { + const { completeCodexSessionIdentifiers } = await import( + "@/app/v1/_lib/codex/session-completer" + ); + + const sessionId = "sess_123456789012345678901"; + const headers = new Headers({ session_id: sessionId }); + const body = makeCodexRequestBody({ prompt_cache_key: sessionId }); + + const result = await completeCodexSessionIdentifiers({ + keyId: 1, + headers, + requestBody: body, + userAgent: "codex_cli_rs/0.50.0", + }); + + expect(result.applied).toBe(false); + expect(result.sessionId).toBe(sessionId); + expect(headers.get("session_id")).toBe(sessionId); + expect(body.prompt_cache_key).toBe(sessionId); + }); + + test("generates a UUID v7 when both identifiers are missing and Redis is unavailable", async () => { + const { completeCodexSessionIdentifiers } = await import( + "@/app/v1/_lib/codex/session-completer" + ); + + const headers = new Headers(); + const body = makeCodexRequestBody(); + + const result = await completeCodexSessionIdentifiers({ + keyId: 1, + headers, + requestBody: body, + userAgent: "codex_cli_rs/0.50.0", + }); + + expect(result.applied).toBe(true); + expect(result.action).toBe("generated_uuid_v7"); + expect(result.sessionId).toMatch(UUID_V7_PATTERN); + expect(headers.get("session_id")).toBe(result.sessionId); + expect(body.prompt_cache_key).toBe(result.sessionId); + }); + + test("reuses the same generated session id for the same fingerprint when Redis is available", async () => { + const { completeCodexSessionIdentifiers } = await import( + "@/app/v1/_lib/codex/session-completer" + ); + + const { client: fakeRedis } = makeFakeRedis(); + redisClientRef = fakeRedis; + + const baseHeaders = new Headers({ + "x-forwarded-for": "203.0.113.10", + "user-agent": "codex_cli_rs/0.50.0", + }); + + const first = await completeCodexSessionIdentifiers({ + keyId: 123, + headers: new Headers(baseHeaders), + requestBody: makeCodexRequestBody(), + userAgent: "codex_cli_rs/0.50.0", + }); + + const second = await completeCodexSessionIdentifiers({ + keyId: 123, + headers: new Headers(baseHeaders), + requestBody: makeCodexRequestBody(), + userAgent: "codex_cli_rs/0.50.0", + }); + + expect(first.action).toBe("generated_uuid_v7"); + expect(second.action).toBe("reused_fingerprint_cache"); + expect(first.sessionId).toBe(second.sessionId); + expect(first.sessionId).toMatch(UUID_V7_PATTERN); + }); + + test("completes header session_id when only x-session-id is provided", async () => { + const { completeCodexSessionIdentifiers } = await import( + "@/app/v1/_lib/codex/session-completer" + ); + + const xSessionId = "sess_123456789012345678902"; + const headers = new Headers({ "x-session-id": xSessionId }); + const body = makeCodexRequestBody(); + + const result = await completeCodexSessionIdentifiers({ + keyId: 1, + headers, + requestBody: body, + userAgent: "codex_cli_rs/0.50.0", + }); + + expect(result.applied).toBe(true); + expect(result.sessionId).toBe(xSessionId); + expect(headers.get("session_id")).toBe(xSessionId); + expect(headers.get("x-session-id")).toBe(xSessionId); + expect(body.prompt_cache_key).toBe(xSessionId); + }); + + test("completes canonical session_id when x-session-id and prompt_cache_key are provided", async () => { + const { completeCodexSessionIdentifiers } = await import( + "@/app/v1/_lib/codex/session-completer" + ); + + const xSessionId = "sess_123456789012345678904"; + const headers = new Headers({ "x-session-id": xSessionId }); + const body = makeCodexRequestBody({ prompt_cache_key: xSessionId }); + + const result = await completeCodexSessionIdentifiers({ + keyId: 1, + headers, + requestBody: body, + userAgent: "codex_cli_rs/0.50.0", + }); + + expect(result.applied).toBe(true); + expect(result.action).toBe("completed_missing_fields"); + expect(result.sessionId).toBe(xSessionId); + expect(headers.get("session_id")).toBe(xSessionId); + expect(headers.get("x-session-id")).toBe(xSessionId); + expect(body.prompt_cache_key).toBe(xSessionId); + }); + + test("updates metadata.session_id in-place when metadata exists", async () => { + const { completeCodexSessionIdentifiers } = await import( + "@/app/v1/_lib/codex/session-completer" + ); + + const sessionId = "sess_123456789012345678903"; + const headers = new Headers({ session_id: sessionId }); + const body = makeCodexRequestBody({ + metadata: { session_id: "sess_aaaaaaaaaaaaaaaaaaaaa", other: "value" }, + }); + + const result = await completeCodexSessionIdentifiers({ + keyId: 1, + headers, + requestBody: body, + userAgent: "codex_cli_rs/0.50.0", + }); + + expect(result.applied).toBe(true); + expect(result.action).toBe("completed_missing_fields"); + expect((body.metadata as any).session_id).toBe(sessionId); + expect((body.metadata as any).other).toBe("value"); + }); + + test("uses x-real-ip when x-forwarded-for is absent (fingerprint stability)", async () => { + const { completeCodexSessionIdentifiers } = await import( + "@/app/v1/_lib/codex/session-completer" + ); + + const { client: fakeRedis } = makeFakeRedis(); + redisClientRef = fakeRedis; + + const headers = new Headers({ + "x-real-ip": "198.51.100.7", + "user-agent": "codex_cli_rs/0.50.0", + }); + + const first = await completeCodexSessionIdentifiers({ + keyId: 999, + headers: new Headers(headers), + requestBody: makeCodexRequestBody(), + userAgent: "codex_cli_rs/0.50.0", + }); + + const second = await completeCodexSessionIdentifiers({ + keyId: 999, + headers: new Headers(headers), + requestBody: makeCodexRequestBody(), + userAgent: "codex_cli_rs/0.50.0", + }); + + expect(first.action).toBe("generated_uuid_v7"); + expect(second.action).toBe("reused_fingerprint_cache"); + expect(first.sessionId).toBe(second.sessionId); + }); + + test("fingerprint skips non-message items and supports string content", async () => { + const { completeCodexSessionIdentifiers } = await import( + "@/app/v1/_lib/codex/session-completer" + ); + + const { client: fakeRedis } = makeFakeRedis(); + redisClientRef = fakeRedis; + + const body = makeCodexRequestBody({ + input: [ + { type: "function_call", call_id: "call_123", name: "tool", arguments: "{}" }, + { type: "message", role: "user", content: "hello from string content" }, + ], + }); + + const result = await completeCodexSessionIdentifiers({ + keyId: 321, + headers: new Headers({ "x-forwarded-for": "203.0.113.99" }), + requestBody: body, + userAgent: "codex_cli_rs/0.50.0", + }); + + expect(result.action).toBe("generated_uuid_v7"); + expect(result.sessionId).toMatch(UUID_V7_PATTERN); + }); + + test("does not overwrite non-object metadata", async () => { + const { completeCodexSessionIdentifiers } = await import( + "@/app/v1/_lib/codex/session-completer" + ); + + const body = makeCodexRequestBody({ metadata: "not-an-object" }); + const result = await completeCodexSessionIdentifiers({ + keyId: 1, + headers: new Headers(), + requestBody: body, + userAgent: "codex_cli_rs/0.50.0", + }); + + expect(result.sessionId).toMatch(UUID_V7_PATTERN); + expect(body.metadata).toBe("not-an-object"); + }); + + test("handles Redis NX race by re-reading existing value", async () => { + const { completeCodexSessionIdentifiers } = await import( + "@/app/v1/_lib/codex/session-completer" + ); + + const existing = "019b82ff-08ff-75a3-a203-7e10274fdbd8"; + let sawFirstGet = false; + + redisClientRef = { + status: "ready", + get: vi.fn(async () => { + if (!sawFirstGet) { + sawFirstGet = true; + return null; + } + return existing; + }), + set: vi.fn(async (_key: string, _value: string, _ex: string, _ttl: number, nx?: string) => { + // Simulate another request writing between GET and SET NX + if (nx === "NX") return null; + return "OK"; + }), + }; + + const result = await completeCodexSessionIdentifiers({ + keyId: 1, + headers: new Headers({ "x-forwarded-for": "203.0.113.10" }), + requestBody: makeCodexRequestBody(), + userAgent: "codex_cli_rs/0.50.0", + }); + + expect(result.action).toBe("reused_fingerprint_cache"); + expect(result.sessionId).toBe(existing); + }); + + test("uses SESSION_TTL when it is a valid integer", async () => { + const { completeCodexSessionIdentifiers } = await import( + "@/app/v1/_lib/codex/session-completer" + ); + + process.env.SESSION_TTL = "600"; + + const { client: fakeRedis } = makeFakeRedis(); + redisClientRef = fakeRedis; + + const result = await completeCodexSessionIdentifiers({ + keyId: 1, + headers: new Headers({ "x-forwarded-for": "203.0.113.10" }), + requestBody: makeCodexRequestBody(), + userAgent: "codex_cli_rs/0.50.0", + }); + + expect(result.sessionId).toMatch(UUID_V7_PATTERN); + }); + + test("treats invalid session_id as missing and generates a new one", async () => { + const { completeCodexSessionIdentifiers } = await import( + "@/app/v1/_lib/codex/session-completer" + ); + + const headers = new Headers({ session_id: "short_id_12345" }); + const body = makeCodexRequestBody(); + + const result = await completeCodexSessionIdentifiers({ + keyId: 1, + headers, + requestBody: body, + userAgent: "codex_cli_rs/0.50.0", + }); + + expect(result.sessionId).not.toBe("short_id_12345"); + expect(result.sessionId).toMatch(UUID_V7_PATTERN); + expect(headers.get("session_id")).toBe(result.sessionId); + expect(body.prompt_cache_key).toBe(result.sessionId); + }); +}); diff --git a/tests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsx b/tests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsx index 073858fd5..dc19c6357 100644 --- a/tests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsx +++ b/tests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsx @@ -2,14 +2,13 @@ * @vitest-environment happy-dom */ -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 { ModelMultiSelect } from "@/app/[locale]/settings/providers/_components/model-multi-select"; +import { loadMessages as loadTestMessages } from "../prices/test-messages"; const modelPricesActionMocks = vi.hoisted(() => ({ getAvailableModelsByProviderType: vi.fn(async () => ["remote-model-1"]), @@ -22,19 +21,6 @@ const providersActionMocks = vi.hoisted(() => ({ })); vi.mock("@/actions/providers", () => providersActionMocks); -function loadMessages() { - const base = path.join(process.cwd(), "messages/en"); - const read = (name: string) => JSON.parse(fs.readFileSync(path.join(base, name), "utf8")); - - return { - common: read("common.json"), - errors: read("errors.json"), - ui: read("ui.json"), - forms: read("forms.json"), - settings: read("settings.json"), - }; -} - function render(node: ReactNode) { const container = document.createElement("div"); document.body.appendChild(container); @@ -66,7 +52,7 @@ describe("ModelMultiSelect: 自定义白名单模型应可在列表中取消选 }); test("已选中但不在 availableModels 的模型应出现在列表中,并可取消选中删除", async () => { - const messages = loadMessages(); + const messages = loadTestMessages("en"); const onChange = vi.fn(); const { unmount } = render( diff --git a/tests/unit/usage-doc/opencode-usage-doc.test.tsx b/tests/unit/usage-doc/opencode-usage-doc.test.tsx index f14560bf4..d3239ca84 100644 --- a/tests/unit/usage-doc/opencode-usage-doc.test.tsx +++ b/tests/unit/usage-doc/opencode-usage-doc.test.tsx @@ -70,7 +70,7 @@ describe("UsageDoc - OpenCode 配置教程", () => { expect(text).toContain('"npm": "@ai-sdk/anthropic"'); expect(text).toContain('"npm": "@ai-sdk/google"'); - expect(text).not.toContain('"npm": "@ai-sdk/openai"'); + expect(text).toContain('"npm": "@ai-sdk/openai"'); expect(text).not.toContain("@ai-sdk/openai-compatible"); expect(text).toContain("claude-haiku-4-5-20251001"); diff --git a/vitest.codex-session-id-completer.config.ts b/vitest.codex-session-id-completer.config.ts new file mode 100644 index 000000000..e83853c58 --- /dev/null +++ b/vitest.codex-session-id-completer.config.ts @@ -0,0 +1,49 @@ +import path from "node:path"; +import { defineConfig } from "vitest/config"; + +/** + * Codex Session ID 补全专项覆盖率配置 + * + * 目的: + * - 仅统计 Codex Session ID 补全模块,避免把 Next/DB/Redis 等重模块纳入阈值 + * - 为“会话粘性/供应商复用稳定性”类功能设置覆盖率门槛(>= 80%) + */ +export default defineConfig({ + test: { + globals: true, + environment: "happy-dom", + setupFiles: ["./tests/setup.ts"], + + include: ["tests/unit/codex/session-completer.test.ts"], + exclude: ["node_modules", ".next", "dist", "build", "coverage", "tests/integration/**"], + + coverage: { + provider: "v8", + reporter: ["text", "html", "json"], + reportsDirectory: "./coverage-codex-session-id-completer", + + include: ["src/app/v1/_lib/codex/session-completer.ts"], + exclude: ["node_modules/", "tests/", "**/*.d.ts", ".next/"], + + thresholds: { + lines: 80, + functions: 80, + branches: 80, + statements: 80, + }, + }, + + reporters: ["verbose"], + isolate: true, + mockReset: true, + restoreMocks: true, + clearMocks: true, + }, + + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + "server-only": path.resolve(__dirname, "./tests/server-only.mock.ts"), + }, + }, +});