diff --git a/.gitignore b/.gitignore index 0ad9d53f5..0908e34db 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ /coverage-quota /coverage-my-usage /coverage-proxy-guard-pipeline +/coverage-thinking-signature-rectifier # next.js /.next/ diff --git a/drizzle/0051_silent_maelstrom.sql b/drizzle/0051_silent_maelstrom.sql new file mode 100644 index 000000000..987702efd --- /dev/null +++ b/drizzle/0051_silent_maelstrom.sql @@ -0,0 +1 @@ +ALTER TABLE "system_settings" ADD COLUMN "enable_thinking_signature_rectifier" boolean DEFAULT true NOT NULL; \ No newline at end of file diff --git a/drizzle/meta/0051_snapshot.json b/drizzle/meta/0051_snapshot.json new file mode 100644 index 000000000..058e96d6c --- /dev/null +++ b/drizzle/meta/0051_snapshot.json @@ -0,0 +1,2359 @@ +{ + "id": "c7b01fc8-2ed8-4359-a233-9fa3a2f7e8ec", + "prevId": "708bddf0-5d35-4367-ab3f-a50a84d57c5d", + "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(50)", + "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 + }, + "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": {} + } + }, + "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_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(50)", + "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 e2a568734..2da6ac2a7 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -358,6 +358,13 @@ "when": 1767894561890, "tag": "0050_flippant_jack_flag", "breakpoints": true + }, + { + "idx": 51, + "version": "7", + "when": 1767976327237, + "tag": "0051_silent_maelstrom", + "breakpoints": true } ] } \ No newline at end of file diff --git a/messages/en/settings.json b/messages/en/settings.json index 388c99495..8c189ee55 100644 --- a/messages/en/settings.json +++ b/messages/en/settings.json @@ -104,6 +104,8 @@ "enableHttp2Desc": "When enabled, proxy requests will prefer HTTP/2 protocol. Automatically falls back to HTTP/1.1 on failure.", "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.", + "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).", "enableResponseFixer": "Enable Response Fixer", "enableResponseFixerDesc": "Automatically repairs common upstream response issues (encoding, SSE, truncated JSON). Enabled by default.", "responseFixerFixEncoding": "Fix encoding issues", diff --git a/messages/ja/settings.json b/messages/ja/settings.json index 571f8f063..ec99d47a8 100644 --- a/messages/ja/settings.json +++ b/messages/ja/settings.json @@ -102,6 +102,8 @@ "verboseProviderErrorDesc": "有効にすると、すべてのプロバイダーが利用不可の場合に詳細なエラーメッセージ(プロバイダー数、レート制限の理由など)を返します。無効の場合は簡潔なエラーコードのみを返します。", "interceptAnthropicWarmupRequests": "Warmup リクエストを遮断(Anthropic)", "interceptAnthropicWarmupRequestsDesc": "有効にすると、Claude Code の Warmup プローブ要求は CCH が直接短い応答を返し、上流プロバイダーへのリクエストを回避します。ログには残りますが、課金/レート制限/統計には含まれません。", + "enableThinkingSignatureRectifier": "thinking 署名整流を有効化", + "enableThinkingSignatureRectifierDesc": "Anthropic プロバイダーで thinking 署名の不整合や不正なリクエストエラーが発生した場合、thinking 関連ブロックを削除して同一プロバイダーへ1回だけ再試行します(既定で有効)。", "enableResponseFixer": "レスポンス整流を有効化", "enableResponseFixerDesc": "上流応答の一般的な形式問題(エンコーディング、SSE、途切れた JSON)を自動修復します(既定で有効)。", "responseFixerFixEncoding": "エンコーディングを修復", diff --git a/messages/ru/settings.json b/messages/ru/settings.json index efb1967f8..0ee5cb23e 100644 --- a/messages/ru/settings.json +++ b/messages/ru/settings.json @@ -102,6 +102,8 @@ "verboseProviderErrorDesc": "При включении возвращает подробные сообщения об ошибках при недоступности всех провайдеров (количество провайдеров, причины ограничений и т.д.); при отключении возвращает только простой код ошибки.", "interceptAnthropicWarmupRequests": "Перехватывать Warmup-запросы (Anthropic)", "interceptAnthropicWarmupRequestsDesc": "Если включено, Warmup-пробные запросы Claude Code будут отвечены самим CCH без обращения к провайдерам; запрос сохраняется в логах, но не тарифицируется, не учитывается в лимитах и исключается из статистики.", + "enableThinkingSignatureRectifier": "Включить исправление thinking-signature", + "enableThinkingSignatureRectifierDesc": "Если Anthropic-провайдер возвращает ошибку несовместимой подписи thinking или некорректного запроса, автоматически удаляет несовместимые thinking-блоки и повторяет запрос один раз к тому же провайдеру (включено по умолчанию).", "enableResponseFixer": "Включить исправление ответов", "enableResponseFixerDesc": "Автоматически исправляет распространённые проблемы ответа у провайдеров (кодировка, SSE, обрезанный JSON). Включено по умолчанию.", "responseFixerFixEncoding": "Исправлять кодировку", diff --git a/messages/zh-CN/settings.json b/messages/zh-CN/settings.json index 942619612..ebdc8f17c 100644 --- a/messages/zh-CN/settings.json +++ b/messages/zh-CN/settings.json @@ -85,6 +85,8 @@ "enableHttp2Desc": "启用后,代理请求将优先使用 HTTP/2 协议。如果 HTTP/2 失败,将自动降级到 HTTP/1.1。", "interceptAnthropicWarmupRequests": "拦截 Warmup 请求(Anthropic)", "interceptAnthropicWarmupRequestsDesc": "开启后,识别到 Claude Code 的 Warmup 探测请求将由 CCH 直接抢答短响应,避免访问上游供应商;该请求会记录在日志中,但不计费、不限流、不计入统计。", + "enableThinkingSignatureRectifier": "启用 thinking 签名整流器", + "enableThinkingSignatureRectifierDesc": "当 Anthropic 类型供应商返回 thinking 签名不兼容或非法请求等错误时,自动移除不兼容的 thinking 相关块并对同一供应商重试一次(默认开启)。", "enableResponseFixer": "启用响应整流", "enableResponseFixerDesc": "自动修复上游响应中常见的编码、SSE 与 JSON 格式问题(默认开启)。", "responseFixerFixEncoding": "修复编码问题", diff --git a/messages/zh-TW/settings.json b/messages/zh-TW/settings.json index 4ee371969..55624dac7 100644 --- a/messages/zh-TW/settings.json +++ b/messages/zh-TW/settings.json @@ -102,6 +102,8 @@ "verboseProviderErrorDesc": "開啟後,當所有供應商不可用時返回詳細錯誤資訊(包含供應商數量、限流原因等);關閉後僅返回簡潔錯誤碼。", "interceptAnthropicWarmupRequests": "攔截 Warmup 請求(Anthropic)", "interceptAnthropicWarmupRequestsDesc": "開啟後,識別到 Claude Code 的 Warmup 探測請求將由 CCH 直接搶答短回應,避免存取上游供應商;該請求會記錄在日誌中,但不計費、不限流、不計入統計。", + "enableThinkingSignatureRectifier": "啟用 thinking 簽名整流器", + "enableThinkingSignatureRectifierDesc": "當 Anthropic 類型供應商返回 thinking 簽名不相容或非法請求等錯誤時,自動移除不相容的 thinking 相關區塊並對同一供應商重試一次(預設開啟)。", "enableResponseFixer": "啟用回應整流", "enableResponseFixerDesc": "自動修復上游回應中常見的編碼、SSE 與 JSON 格式問題(預設開啟)。", "responseFixerFixEncoding": "修復編碼問題", diff --git a/package.json b/package.json index 8a8c639ae..b1b43852e 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: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", "test:coverage:proxy-guard-pipeline": "vitest run --config vitest.proxy-guard-pipeline.config.ts --coverage", diff --git a/src/actions/my-usage.ts b/src/actions/my-usage.ts index 17252e516..88b9741c7 100644 --- a/src/actions/my-usage.ts +++ b/src/actions/my-usage.ts @@ -541,12 +541,12 @@ export async function getMyStatsSummary( const settings = await getSystemSettings(); const currencyCode = settings.currencyDisplay; + // 日期字符串来自前端的 YYYY-MM-DD(目前使用 toISOString().split("T")[0] 生成),因此按 UTC 解析更一致。 + // 注意:new Date("YYYY-MM-DDT00:00:00") 会按本地时区解析,可能导致跨时区边界偏移。 const parsedStart = filters.startDate - ? new Date(`${filters.startDate}T00:00:00`).getTime() - : Number.NaN; - const parsedEnd = filters.endDate - ? new Date(`${filters.endDate}T00:00:00`).getTime() + ? Date.parse(`${filters.startDate}T00:00:00.000Z`) : Number.NaN; + const parsedEnd = filters.endDate ? Date.parse(`${filters.endDate}T00:00:00.000Z`) : Number.NaN; const startTime = Number.isFinite(parsedStart) ? parsedStart : undefined; // endTime 使用“次日零点”作为排他上界(created_at < endTime),避免 23:59:59.999 的边界问题 diff --git a/src/actions/system-config.ts b/src/actions/system-config.ts index 7ef36cdd2..eedf78985 100644 --- a/src/actions/system-config.ts +++ b/src/actions/system-config.ts @@ -38,6 +38,7 @@ export async function saveSystemSettings(formData: { verboseProviderError?: boolean; enableHttp2?: boolean; interceptAnthropicWarmupRequests?: boolean; + enableThinkingSignatureRectifier?: boolean; enableResponseFixer?: boolean; responseFixerConfig?: Partial; }): Promise> { @@ -61,6 +62,7 @@ export async function saveSystemSettings(formData: { verboseProviderError: validated.verboseProviderError, enableHttp2: validated.enableHttp2, interceptAnthropicWarmupRequests: validated.interceptAnthropicWarmupRequests, + enableThinkingSignatureRectifier: validated.enableThinkingSignatureRectifier, 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 719565c3a..ad21c2327 100644 --- a/src/app/[locale]/settings/config/_components/system-settings-form.tsx +++ b/src/app/[locale]/settings/config/_components/system-settings-form.tsx @@ -30,6 +30,7 @@ interface SystemSettingsFormProps { | "verboseProviderError" | "enableHttp2" | "interceptAnthropicWarmupRequests" + | "enableThinkingSignatureRectifier" | "enableResponseFixer" | "responseFixerConfig" >; @@ -56,6 +57,9 @@ export function SystemSettingsForm({ initialSettings }: SystemSettingsFormProps) const [interceptAnthropicWarmupRequests, setInterceptAnthropicWarmupRequests] = useState( initialSettings.interceptAnthropicWarmupRequests ); + const [enableThinkingSignatureRectifier, setEnableThinkingSignatureRectifier] = useState( + initialSettings.enableThinkingSignatureRectifier + ); const [enableResponseFixer, setEnableResponseFixer] = useState( initialSettings.enableResponseFixer ); @@ -81,6 +85,7 @@ export function SystemSettingsForm({ initialSettings }: SystemSettingsFormProps) verboseProviderError, enableHttp2, interceptAnthropicWarmupRequests, + enableThinkingSignatureRectifier, enableResponseFixer, responseFixerConfig, }); @@ -98,6 +103,7 @@ export function SystemSettingsForm({ initialSettings }: SystemSettingsFormProps) setVerboseProviderError(result.data.verboseProviderError); setEnableHttp2(result.data.enableHttp2); setInterceptAnthropicWarmupRequests(result.data.interceptAnthropicWarmupRequests); + setEnableThinkingSignatureRectifier(result.data.enableThinkingSignatureRectifier); setEnableResponseFixer(result.data.enableResponseFixer); setResponseFixerConfig(result.data.responseFixerConfig); } @@ -227,6 +233,23 @@ export function SystemSettingsForm({ initialSettings }: SystemSettingsFormProps) /> +
+
+ +

+ {t("enableThinkingSignatureRectifierDesc")} +

+
+ setEnableThinkingSignatureRectifier(checked)} + disabled={isPending} + /> +
+
diff --git a/src/app/[locale]/settings/config/page.tsx b/src/app/[locale]/settings/config/page.tsx index 9fee99375..6b00d4669 100644 --- a/src/app/[locale]/settings/config/page.tsx +++ b/src/app/[locale]/settings/config/page.tsx @@ -41,6 +41,7 @@ async function SettingsConfigContent() { verboseProviderError: settings.verboseProviderError, enableHttp2: settings.enableHttp2, interceptAnthropicWarmupRequests: settings.interceptAnthropicWarmupRequests, + enableThinkingSignatureRectifier: settings.enableThinkingSignatureRectifier, enableResponseFixer: settings.enableResponseFixer, responseFixerConfig: settings.responseFixerConfig, }} diff --git a/src/app/v1/_lib/proxy/forwarder.ts b/src/app/v1/_lib/proxy/forwarder.ts index ec604301a..fc18c9be8 100644 --- a/src/app/v1/_lib/proxy/forwarder.ts +++ b/src/app/v1/_lib/proxy/forwarder.ts @@ -10,7 +10,7 @@ import { recordSuccess, } from "@/lib/circuit-breaker"; import { applyCodexProviderOverridesWithAudit } from "@/lib/codex/provider-overrides"; -import { isHttp2Enabled } from "@/lib/config"; +import { getCachedSystemSettings, isHttp2Enabled } from "@/lib/config"; import { getEnvConfig } from "@/lib/config/env.schema"; import { PROVIDER_DEFAULTS, PROVIDER_LIMITS } from "@/lib/constants/provider.constants"; import { logger } from "@/lib/logger"; @@ -41,6 +41,10 @@ import { mapClientFormatToTransformer, mapProviderTypeToTransformer } from "./fo import { ModelRedirector } from "./model-redirector"; import { ProxyProviderResolver } from "./provider-selector"; import type { ProxySession } from "./session"; +import { + detectThinkingSignatureRectifierTrigger, + rectifyAnthropicRequestMessage, +} from "./thinking-signature-rectifier"; const STANDARD_ENDPOINTS = [ "/v1/messages", @@ -201,10 +205,11 @@ export class ProxyForwarder { totalProvidersAttempted++; let attemptCount = 0; // 当前供应商的尝试次数 - const maxAttemptsPerProvider = resolveMaxAttemptsForProvider( + let maxAttemptsPerProvider = resolveMaxAttemptsForProvider( currentProvider, envDefaultMaxAttempts ); + let thinkingSignatureRectifierRetried = false; logger.info("ProxyForwarder: Trying provider", { providerId: currentProvider.id, @@ -374,7 +379,7 @@ export class ProxyForwarder { // ⭐ 1. 分类错误(供应商错误 vs 系统错误 vs 客户端中断) // 使用异步版本确保错误规则已加载 - const errorCategory = await categorizeErrorAsync(lastError); + let errorCategory = await categorizeErrorAsync(lastError); const errorMessage = lastError instanceof ProxyError ? lastError.getDetailedErrorMessage() @@ -411,6 +416,148 @@ export class ProxyForwarder { throw lastError; } + // 2.5 Thinking signature 整流器:命中后对同供应商“整流 + 重试一次” + // 目标:解决 Anthropic 与非 Anthropic 渠道切换导致的 thinking 签名不兼容问题 + // 约束: + // - 仅对 Anthropic 类型供应商生效 + // - 不依赖 error rules 开关(用户可能关闭规则,但仍希望整流生效) + // - 不计入熔断器、不触发供应商切换 + const isAnthropicProvider = + currentProvider.providerType === "claude" || + currentProvider.providerType === "claude-auth"; + const rectifierTrigger = isAnthropicProvider + ? detectThinkingSignatureRectifierTrigger(errorMessage) + : null; + + if (rectifierTrigger) { + const settings = await getCachedSystemSettings(); + const enabled = settings.enableThinkingSignatureRectifier ?? true; + + if (enabled) { + // 已重试过仍失败:强制按“不可重试的客户端错误”处理,避免污染熔断器/触发供应商切换 + if (thinkingSignatureRectifierRetried) { + errorCategory = ErrorCategory.NON_RETRYABLE_CLIENT_ERROR; + } else { + const requestDetailsBeforeRectify = buildRequestDetails(session); + + // 整流请求体(原地修改 session.request.message) + const rectified = rectifyAnthropicRequestMessage( + session.request.message as Record + ); + + // 写入审计字段(specialSettings) + session.addSpecialSetting({ + type: "thinking_signature_rectifier", + scope: "request", + hit: rectified.applied, + providerId: currentProvider.id, + providerName: currentProvider.name, + trigger: rectifierTrigger, + attemptNumber: attemptCount, + retryAttemptNumber: attemptCount + 1, + removedThinkingBlocks: rectified.removedThinkingBlocks, + removedRedactedThinkingBlocks: rectified.removedRedactedThinkingBlocks, + removedSignatureFields: rectified.removedSignatureFields, + }); + + const specialSettings = session.getSpecialSettings(); + if (specialSettings && session.sessionId) { + try { + await SessionManager.storeSessionSpecialSettings( + session.sessionId, + specialSettings, + session.requestSequence + ); + } catch (persistError) { + logger.error("[ProxyForwarder] Failed to store special settings", { + error: persistError, + sessionId: session.sessionId, + }); + } + } + + if (specialSettings && session.messageContext?.id) { + try { + await updateMessageRequestDetails(session.messageContext.id, { + specialSettings, + }); + } catch (persistError) { + logger.error("[ProxyForwarder] Failed to persist special settings", { + error: persistError, + messageRequestId: session.messageContext.id, + }); + } + } + + // 无任何可整流内容:不做无意义重试,直接走既有“不可重试客户端错误”分支 + if (!rectified.applied) { + logger.info( + "ProxyForwarder: Thinking signature rectifier not applicable, skipping retry", + { + providerId: currentProvider.id, + providerName: currentProvider.name, + trigger: rectifierTrigger, + attemptNumber: attemptCount, + } + ); + errorCategory = ErrorCategory.NON_RETRYABLE_CLIENT_ERROR; + } else { + logger.info("ProxyForwarder: Thinking signature rectifier applied, retrying", { + providerId: currentProvider.id, + providerName: currentProvider.name, + trigger: rectifierTrigger, + attemptNumber: attemptCount, + willRetryAttemptNumber: attemptCount + 1, + }); + + thinkingSignatureRectifierRetried = true; + + // 记录失败的第一次请求(以 retry_failed 体现“发生过一次重试”) + if (lastError instanceof ProxyError) { + session.addProviderToChain(currentProvider, { + reason: "retry_failed", + circuitState: getCircuitState(currentProvider.id), + attemptNumber: attemptCount, + errorMessage, + statusCode: lastError.statusCode, + errorDetails: { + provider: { + id: currentProvider.id, + name: currentProvider.name, + statusCode: lastError.statusCode, + statusText: lastError.message, + upstreamBody: lastError.upstreamError?.body, + upstreamParsed: lastError.upstreamError?.parsed, + }, + request: requestDetailsBeforeRectify, + }, + }); + } else { + session.addProviderToChain(currentProvider, { + reason: "retry_failed", + circuitState: getCircuitState(currentProvider.id), + attemptNumber: attemptCount, + errorMessage, + errorDetails: { + system: { + errorType: lastError.constructor.name, + errorName: lastError.name, + errorMessage: lastError.message || lastError.name || "Unknown error", + errorStack: lastError.stack?.split("\n").slice(0, 3).join("\n"), + }, + request: requestDetailsBeforeRectify, + }, + }); + } + + // 确保即使 maxAttemptsPerProvider=1 也能完成一次额外重试 + maxAttemptsPerProvider = Math.max(maxAttemptsPerProvider, attemptCount + 1); + continue; + } + } + } + } + // ⭐ 3. 不可重试的客户端输入错误处理(不计入熔断器,不重试,立即返回) if (errorCategory === ErrorCategory.NON_RETRYABLE_CLIENT_ERROR) { const proxyError = lastError as ProxyError; diff --git a/src/app/v1/_lib/proxy/thinking-signature-rectifier.test.ts b/src/app/v1/_lib/proxy/thinking-signature-rectifier.test.ts new file mode 100644 index 000000000..ae25421e2 --- /dev/null +++ b/src/app/v1/_lib/proxy/thinking-signature-rectifier.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, test } from "vitest"; + +import { + detectThinkingSignatureRectifierTrigger, + rectifyAnthropicRequestMessage, +} from "./thinking-signature-rectifier"; + +describe("thinking-signature-rectifier", () => { + describe("detectThinkingSignatureRectifierTrigger", () => { + test("应命中:Invalid `signature` in `thinking` block(含反引号)", () => { + const trigger = detectThinkingSignatureRectifierTrigger( + "messages.1.content.0: Invalid `signature` in `thinking` block" + ); + expect(trigger).toBe("invalid_signature_in_thinking_block"); + }); + + test("应命中:Invalid signature in thinking block(无反引号/大小写混用)", () => { + const trigger = detectThinkingSignatureRectifierTrigger( + "Messages.1.Content.0: invalid signature in thinking block" + ); + expect(trigger).toBe("invalid_signature_in_thinking_block"); + }); + + test("应命中:非法请求/illegal request/invalid request", () => { + expect(detectThinkingSignatureRectifierTrigger("非法请求")).toBe("invalid_request"); + expect(detectThinkingSignatureRectifierTrigger("illegal request format")).toBe( + "invalid_request" + ); + expect(detectThinkingSignatureRectifierTrigger("invalid request: malformed JSON")).toBe( + "invalid_request" + ); + }); + + test("不应命中:无关错误", () => { + expect(detectThinkingSignatureRectifierTrigger("Request timeout")).toBeNull(); + }); + }); + + describe("rectifyAnthropicRequestMessage", () => { + test("应移除 thinking/redacted_thinking block,并移除非 thinking block 的 signature 字段", () => { + const message: Record = { + model: "claude-test", + messages: [ + { + role: "assistant", + content: [ + { type: "thinking", thinking: "t", signature: "sig_thinking" }, + { type: "text", text: "hello", signature: "sig_text_should_remove" }, + { + type: "tool_use", + id: "toolu_1", + name: "WebSearch", + input: { query: "q" }, + signature: "sig_tool_should_remove", + }, + { type: "redacted_thinking", data: "r", signature: "sig_redacted" }, + ], + }, + { + role: "user", + content: [{ type: "text", text: "hi" }], + }, + ], + }; + + const result = rectifyAnthropicRequestMessage(message); + expect(result.applied).toBe(true); + expect(result.removedThinkingBlocks).toBe(1); + expect(result.removedRedactedThinkingBlocks).toBe(1); + expect(result.removedSignatureFields).toBe(2); + + const messages = message.messages as any[]; + const content = messages[0].content as any[]; + expect(content.map((b) => b.type)).toEqual(["text", "tool_use"]); + expect(content[0].signature).toBeUndefined(); + expect(content[1].signature).toBeUndefined(); + }); + + test("无 messages 或 messages 不为数组时,应不修改", () => { + const message: Record = { model: "claude-test" }; + const result = rectifyAnthropicRequestMessage(message); + expect(result.applied).toBe(false); + expect(result.removedThinkingBlocks).toBe(0); + expect(result.removedRedactedThinkingBlocks).toBe(0); + expect(result.removedSignatureFields).toBe(0); + }); + }); +}); diff --git a/src/app/v1/_lib/proxy/thinking-signature-rectifier.ts b/src/app/v1/_lib/proxy/thinking-signature-rectifier.ts new file mode 100644 index 000000000..22b114978 --- /dev/null +++ b/src/app/v1/_lib/proxy/thinking-signature-rectifier.ts @@ -0,0 +1,123 @@ +export type ThinkingSignatureRectifierTrigger = + | "invalid_signature_in_thinking_block" + | "invalid_request"; + +export type ThinkingSignatureRectifierResult = { + applied: boolean; + removedThinkingBlocks: number; + removedRedactedThinkingBlocks: number; + removedSignatureFields: number; +}; + +/** + * 检测是否需要触发「thinking signature 整流器」 + * + * 注意:这里不依赖错误规则开关(error rules 可能被用户关闭),仅做字符串/正则判断。 + */ +export function detectThinkingSignatureRectifierTrigger( + errorMessage: string | null | undefined +): ThinkingSignatureRectifierTrigger | null { + if (!errorMessage) return null; + + const lower = errorMessage.toLowerCase(); + + // 兼容带/不带反引号、不同大小写的变体 + const looksLikeInvalidSignatureInThinkingBlock = + lower.includes("invalid") && + lower.includes("signature") && + lower.includes("thinking") && + lower.includes("block"); + + if (looksLikeInvalidSignatureInThinkingBlock) { + return "invalid_signature_in_thinking_block"; + } + + // 与默认错误规则保持一致(Issue #432 / Rule 6) + if (/非法请求|illegal request|invalid request/i.test(errorMessage)) { + return "invalid_request"; + } + + return null; +} + +/** + * 对 Anthropic 请求体做最小侵入整流: + * - 移除 messages[*].content 中的 thinking/redacted_thinking block(避免签名不兼容触发 400) + * - 移除非 thinking block 上遗留的 signature 字段(兼容跨渠道历史) + * + * 说明: + * - 仅在上游报错后、同供应商重试前调用,避免影响正常请求。 + * - 该函数会原地修改 message 对象(更适合代理链路的性能要求)。 + */ +export function rectifyAnthropicRequestMessage( + message: Record +): ThinkingSignatureRectifierResult { + const messages = message.messages; + if (!Array.isArray(messages)) { + return { + applied: false, + removedThinkingBlocks: 0, + removedRedactedThinkingBlocks: 0, + removedSignatureFields: 0, + }; + } + + let removedThinkingBlocks = 0; + let removedRedactedThinkingBlocks = 0; + let removedSignatureFields = 0; + let applied = false; + + for (const msg of messages) { + if (!msg || typeof msg !== "object") continue; + const msgObj = msg as Record; + const content = msgObj.content; + if (!Array.isArray(content)) continue; + + const newContent: unknown[] = []; + let contentWasModified = false; + + for (const block of content) { + if (!block || typeof block !== "object") { + newContent.push(block); + continue; + } + + const blockObj = block as Record; + const type = blockObj.type; + + if (type === "thinking") { + removedThinkingBlocks += 1; + contentWasModified = true; + continue; + } + + if (type === "redacted_thinking") { + removedRedactedThinkingBlocks += 1; + contentWasModified = true; + continue; + } + + if ("signature" in blockObj) { + const { signature: _signature, ...rest } = blockObj as any; + removedSignatureFields += 1; + contentWasModified = true; + newContent.push(rest); + continue; + } + + newContent.push(blockObj); + } + + if (contentWasModified) { + applied = true; + msgObj.content = newContent; + } + } + + return { + applied, + removedThinkingBlocks, + removedRedactedThinkingBlocks, + removedSignatureFields, + }; +} diff --git a/src/drizzle/schema.ts b/src/drizzle/schema.ts index e437f4bfc..f162fef90 100644 --- a/src/drizzle/schema.ts +++ b/src/drizzle/schema.ts @@ -485,6 +485,12 @@ export const systemSettings = pgTable('system_settings', { .notNull() .default(false), + // thinking signature 整流器(默认开启) + // 开启后:当 Anthropic 类型供应商出现 thinking 签名不兼容/非法请求等 400 错误时,自动整流并重试一次 + enableThinkingSignatureRectifier: boolean('enable_thinking_signature_rectifier') + .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 89fba2576..8c0071627 100644 --- a/src/lib/config/system-settings-cache.ts +++ b/src/lib/config/system-settings-cache.ts @@ -26,10 +26,15 @@ let cachedAt: number = 0; /** Default settings used when cache fetch fails */ const DEFAULT_SETTINGS: Pick< SystemSettings, - "enableHttp2" | "interceptAnthropicWarmupRequests" | "enableResponseFixer" | "responseFixerConfig" + | "enableHttp2" + | "interceptAnthropicWarmupRequests" + | "enableThinkingSignatureRectifier" + | "enableResponseFixer" + | "responseFixerConfig" > = { enableHttp2: false, interceptAnthropicWarmupRequests: false, + enableThinkingSignatureRectifier: true, enableResponseFixer: true, responseFixerConfig: { fixTruncatedJson: true, @@ -97,6 +102,7 @@ export async function getCachedSystemSettings(): Promise { enableClientVersionCheck: false, enableHttp2: DEFAULT_SETTINGS.enableHttp2, interceptAnthropicWarmupRequests: DEFAULT_SETTINGS.interceptAnthropicWarmupRequests, + enableThinkingSignatureRectifier: DEFAULT_SETTINGS.enableThinkingSignatureRectifier, 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 b66cb07a7..91047eb83 100644 --- a/src/lib/utils/special-settings.ts +++ b/src/lib/utils/special-settings.ts @@ -55,6 +55,18 @@ function buildSettingKey(setting: SpecialSetting): string { return JSON.stringify([setting.type, setting.ttl]); case "anthropic_context_1m_header_override": return JSON.stringify([setting.type, setting.header, setting.flag]); + case "thinking_signature_rectifier": + return JSON.stringify([ + setting.type, + setting.hit, + setting.providerId ?? null, + setting.trigger, + setting.attemptNumber, + setting.retryAttemptNumber, + setting.removedThinkingBlocks, + setting.removedRedactedThinkingBlocks, + setting.removedSignatureFields, + ]); default: { // 兜底:保证即使未来扩展类型也不会导致运行时崩溃 const _exhaustive: never = setting; diff --git a/src/lib/validation/schemas.ts b/src/lib/validation/schemas.ts index ef7b010b3..38f26ce54 100644 --- a/src/lib/validation/schemas.ts +++ b/src/lib/validation/schemas.ts @@ -741,6 +741,8 @@ export const UpdateSystemSettingsSchema = z.object({ enableHttp2: z.boolean().optional(), // 可选拦截 Anthropic Warmup 请求(可选) interceptAnthropicWarmupRequests: z.boolean().optional(), + // thinking signature 整流器(可选) + enableThinkingSignatureRectifier: z.boolean().optional(), // 响应整流(可选) enableResponseFixer: z.boolean().optional(), responseFixerConfig: z diff --git a/src/repository/_shared/transformers.ts b/src/repository/_shared/transformers.ts index e0f0f4b1d..1b144a705 100644 --- a/src/repository/_shared/transformers.ts +++ b/src/repository/_shared/transformers.ts @@ -170,6 +170,7 @@ export function toSystemSettings(dbSettings: any): SystemSettings { verboseProviderError: dbSettings?.verboseProviderError ?? false, enableHttp2: dbSettings?.enableHttp2 ?? false, interceptAnthropicWarmupRequests: dbSettings?.interceptAnthropicWarmupRequests ?? false, + enableThinkingSignatureRectifier: dbSettings?.enableThinkingSignatureRectifier ?? true, enableResponseFixer: dbSettings?.enableResponseFixer ?? true, responseFixerConfig: { ...defaultResponseFixerConfig, diff --git a/src/repository/system-config.ts b/src/repository/system-config.ts index f64a79a5a..b5f2ee944 100644 --- a/src/repository/system-config.ts +++ b/src/repository/system-config.ts @@ -148,6 +148,7 @@ function createFallbackSettings(): SystemSettings { verboseProviderError: false, enableHttp2: false, interceptAnthropicWarmupRequests: false, + enableThinkingSignatureRectifier: true, enableResponseFixer: true, responseFixerConfig: { fixTruncatedJson: true, @@ -180,6 +181,7 @@ export async function getSystemSettings(): Promise { verboseProviderError: systemSettings.verboseProviderError, enableHttp2: systemSettings.enableHttp2, interceptAnthropicWarmupRequests: systemSettings.interceptAnthropicWarmupRequests, + enableThinkingSignatureRectifier: systemSettings.enableThinkingSignatureRectifier, enableResponseFixer: systemSettings.enableResponseFixer, responseFixerConfig: systemSettings.responseFixerConfig, createdAt: systemSettings.createdAt, @@ -312,6 +314,11 @@ export async function updateSystemSettings( updates.interceptAnthropicWarmupRequests = payload.interceptAnthropicWarmupRequests; } + // thinking signature 整流器开关(如果提供) + if (payload.enableThinkingSignatureRectifier !== undefined) { + updates.enableThinkingSignatureRectifier = payload.enableThinkingSignatureRectifier; + } + // 响应整流开关(如果提供) if (payload.enableResponseFixer !== undefined) { updates.enableResponseFixer = payload.enableResponseFixer; @@ -342,6 +349,7 @@ export async function updateSystemSettings( verboseProviderError: systemSettings.verboseProviderError, enableHttp2: systemSettings.enableHttp2, interceptAnthropicWarmupRequests: systemSettings.interceptAnthropicWarmupRequests, + enableThinkingSignatureRectifier: systemSettings.enableThinkingSignatureRectifier, enableResponseFixer: systemSettings.enableResponseFixer, responseFixerConfig: systemSettings.responseFixerConfig, createdAt: systemSettings.createdAt, diff --git a/src/types/special-settings.ts b/src/types/special-settings.ts index 7ef0d2cca..a989e9bde 100644 --- a/src/types/special-settings.ts +++ b/src/types/special-settings.ts @@ -9,6 +9,7 @@ export type SpecialSetting = | ProviderParameterOverrideSpecialSetting | ResponseFixerSpecialSetting | GuardInterceptSpecialSetting + | ThinkingSignatureRectifierSpecialSetting | AnthropicCacheTtlHeaderOverrideSpecialSetting | AnthropicContext1mHeaderOverrideSpecialSetting; @@ -85,3 +86,24 @@ export type AnthropicContext1mHeaderOverrideSpecialSetting = { header: "anthropic-beta"; flag: string; }; + +/** + * Thinking signature 整流器审计 + * + * 用于记录:当 Anthropic 类型供应商遇到 thinking 签名不兼容/非法请求等 400 错误时, + * 代理对请求体进行最小整流(移除 thinking/redacted_thinking 与遗留 signature 字段) + * 并对同供应商自动重试一次的行为,便于在请求日志中审计与回溯。 + */ +export type ThinkingSignatureRectifierSpecialSetting = { + type: "thinking_signature_rectifier"; + scope: "request"; + hit: boolean; + providerId: number | null; + providerName: string | null; + trigger: "invalid_signature_in_thinking_block" | "invalid_request"; + attemptNumber: number; + retryAttemptNumber: number; + removedThinkingBlocks: number; + removedRedactedThinkingBlocks: number; + removedSignatureFields: number; +}; diff --git a/src/types/system-config.ts b/src/types/system-config.ts index d74466de4..02bf316b6 100644 --- a/src/types/system-config.ts +++ b/src/types/system-config.ts @@ -40,6 +40,10 @@ export interface SystemSettings { // 可选拦截 Anthropic Warmup 请求(默认关闭) interceptAnthropicWarmupRequests: boolean; + // thinking signature 整流器(默认开启) + // 目标:当 Anthropic 类型供应商出现 thinking 签名不兼容导致的 400 错误时,自动整流并重试一次 + enableThinkingSignatureRectifier: boolean; + // 响应整流(默认开启) enableResponseFixer: boolean; responseFixerConfig: ResponseFixerConfig; @@ -77,6 +81,9 @@ export interface UpdateSystemSettingsInput { // 可选拦截 Anthropic Warmup 请求(可选) interceptAnthropicWarmupRequests?: boolean; + // thinking signature 整流器(可选) + enableThinkingSignatureRectifier?: boolean; + // 响应整流(可选) enableResponseFixer?: boolean; responseFixerConfig?: Partial; diff --git a/tests/unit/proxy/proxy-forwarder-thinking-signature-rectifier.test.ts b/tests/unit/proxy/proxy-forwarder-thinking-signature-rectifier.test.ts new file mode 100644 index 000000000..2c8505ea6 --- /dev/null +++ b/tests/unit/proxy/proxy-forwarder-thinking-signature-rectifier.test.ts @@ -0,0 +1,269 @@ +import { beforeEach, describe, expect, test, vi } from "vitest"; + +const mocks = vi.hoisted(() => { + return { + getCachedSystemSettings: vi.fn(async () => ({ + enableThinkingSignatureRectifier: true, + })), + recordSuccess: vi.fn(), + recordFailure: vi.fn(async () => {}), + getCircuitState: vi.fn(() => "closed"), + getProviderHealthInfo: vi.fn(async () => ({ + health: { failureCount: 0 }, + config: { failureThreshold: 3 }, + })), + updateMessageRequestDetails: vi.fn(async () => {}), + }; +}); + +vi.mock("@/lib/config", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + isHttp2Enabled: vi.fn(async () => false), + getCachedSystemSettings: mocks.getCachedSystemSettings, + }; +}); + +vi.mock("@/lib/circuit-breaker", () => ({ + getCircuitState: mocks.getCircuitState, + getProviderHealthInfo: mocks.getProviderHealthInfo, + recordFailure: mocks.recordFailure, + recordSuccess: mocks.recordSuccess, +})); + +vi.mock("@/repository/message", () => ({ + updateMessageRequestDetails: mocks.updateMessageRequestDetails, +})); + +import { ProxyForwarder } from "@/app/v1/_lib/proxy/forwarder"; +import { ProxyError } from "@/app/v1/_lib/proxy/errors"; +import { ProxySession } from "@/app/v1/_lib/proxy/session"; +import type { Provider } from "@/types/provider"; + +function createSession(): ProxySession { + const headers = new Headers(); + const session = Object.create(ProxySession.prototype); + + Object.assign(session, { + startTime: Date.now(), + method: "POST", + requestUrl: new URL("https://example.com/v1/messages"), + headers, + originalHeaders: new Headers(headers), + headerLog: JSON.stringify(Object.fromEntries(headers.entries())), + request: { + model: "claude-test", + log: "", + message: { + model: "claude-test", + messages: [ + { + role: "assistant", + content: [ + { type: "thinking", thinking: "t", signature: "sig_thinking" }, + { type: "text", text: "hello", signature: "sig_text_should_remove" }, + { type: "redacted_thinking", data: "r", signature: "sig_redacted" }, + ], + }, + ], + }, + }, + userAgent: null, + context: null, + clientAbortSignal: null, + userName: "test-user", + authState: { success: true, user: null, key: null, apiKey: null }, + provider: null, + messageContext: { id: 123, createdAt: new Date(), user: { id: 1 }, key: {}, apiKey: "k" }, + sessionId: null, + requestSequence: 1, + originalFormat: "claude", + providerType: null, + originalModelName: null, + originalUrlPathname: null, + providerChain: [], + cacheTtlResolved: null, + context1mApplied: false, + specialSettings: [], + cachedPriceData: undefined, + cachedBillingModelSource: undefined, + isHeaderModified: () => false, + }); + + return session as any; +} + +function createAnthropicProvider(): Provider { + return { + id: 1, + name: "anthropic-1", + providerType: "claude", + url: "https://example.com/v1/messages", + key: "k", + preserveClientIp: false, + priority: 0, + } as unknown as Provider; +} + +describe("ProxyForwarder - thinking signature rectifier", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + test("首次命中特定 400 错误时应整流并对同供应商重试一次(成功后不抛错)", async () => { + const session = createSession(); + session.setProvider(createAnthropicProvider()); + + const doForward = vi.spyOn(ProxyForwarder as any, "doForward"); + + doForward.mockImplementationOnce(async () => { + throw new ProxyError("Invalid `signature` in `thinking` block", 400, { + body: "", + providerId: 1, + providerName: "anthropic-1", + }); + }); + + doForward.mockImplementationOnce(async (s: ProxySession) => { + const msg = s.request.message as any; + const blocks = msg.messages[0].content as any[]; + expect(blocks.some((b) => b.type === "thinking")).toBe(false); + expect(blocks.some((b) => b.type === "redacted_thinking")).toBe(false); + expect(blocks.some((b) => "signature" in b)).toBe(false); + + const body = JSON.stringify({ + type: "message", + content: [{ type: "text", text: "ok" }], + }); + + return new Response(body, { + status: 200, + headers: { + "content-type": "application/json", + "content-length": String(body.length), + }, + }); + }); + + const response = await ProxyForwarder.send(session); + + expect(response.status).toBe(200); + expect(doForward).toHaveBeenCalledTimes(2); + expect(session.getProviderChain()?.length).toBeGreaterThanOrEqual(2); + + const special = session.getSpecialSettings(); + expect(special).not.toBeNull(); + expect(JSON.stringify(special)).toContain("thinking_signature_rectifier"); + expect(mocks.updateMessageRequestDetails).toHaveBeenCalledTimes(1); + }); + + test("命中 invalid request 相关 400 错误时也应整流并对同供应商重试一次", async () => { + const session = createSession(); + session.setProvider(createAnthropicProvider()); + + const doForward = vi.spyOn(ProxyForwarder as any, "doForward"); + + doForward.mockImplementationOnce(async () => { + throw new ProxyError("invalid request: malformed content", 400, { + body: "", + providerId: 1, + providerName: "anthropic-1", + }); + }); + + doForward.mockImplementationOnce(async (s: ProxySession) => { + const msg = s.request.message as any; + const blocks = msg.messages[0].content as any[]; + expect(blocks.some((b) => b.type === "thinking")).toBe(false); + expect(blocks.some((b) => b.type === "redacted_thinking")).toBe(false); + expect(blocks.some((b) => "signature" in b)).toBe(false); + + const body = JSON.stringify({ + type: "message", + content: [{ type: "text", text: "ok" }], + }); + + return new Response(body, { + status: 200, + headers: { + "content-type": "application/json", + "content-length": String(body.length), + }, + }); + }); + + const response = await ProxyForwarder.send(session); + + expect(response.status).toBe(200); + expect(doForward).toHaveBeenCalledTimes(2); + expect(session.getProviderChain()?.length).toBeGreaterThanOrEqual(2); + + const special = session.getSpecialSettings(); + expect(special).not.toBeNull(); + expect(JSON.stringify(special)).toContain("thinking_signature_rectifier"); + expect(mocks.updateMessageRequestDetails).toHaveBeenCalledTimes(1); + }); + + test("匹配触发但无可整流内容时不应做无意义重试", async () => { + const session = createSession(); + session.setProvider(createAnthropicProvider()); + + const msg = session.request.message as any; + msg.messages[0].content = [{ type: "text", text: "hello" }]; + + const doForward = vi.spyOn(ProxyForwarder as any, "doForward"); + + doForward.mockImplementationOnce(async () => { + throw new ProxyError("Invalid `signature` in `thinking` block", 400, { + body: "", + providerId: 1, + providerName: "anthropic-1", + }); + }); + + await expect(ProxyForwarder.send(session)).rejects.toBeInstanceOf(ProxyError); + expect(doForward).toHaveBeenCalledTimes(1); + + // 仍应写入一次审计字段,但不应触发第二次 doForward 调用 + expect(mocks.updateMessageRequestDetails).toHaveBeenCalledTimes(1); + + const special = (session.getSpecialSettings() ?? []) as any[]; + const rectifier = special.find((s) => s.type === "thinking_signature_rectifier"); + expect(rectifier).toBeTruthy(); + expect(rectifier.hit).toBe(false); + }); + + test("重试后仍失败时应停止继续重试/切换,并按最终错误抛出", async () => { + const session = createSession(); + session.setProvider(createAnthropicProvider()); + + const doForward = vi.spyOn(ProxyForwarder as any, "doForward"); + + doForward.mockImplementationOnce(async () => { + throw new ProxyError("Invalid `signature` in `thinking` block", 400, { + body: "", + providerId: 1, + providerName: "anthropic-1", + }); + }); + + doForward.mockImplementationOnce(async () => { + throw new ProxyError("Invalid `signature` in `thinking` block", 400, { + body: "", + providerId: 1, + providerName: "anthropic-1", + }); + }); + + await expect(ProxyForwarder.send(session)).rejects.toBeInstanceOf(ProxyError); + expect(doForward).toHaveBeenCalledTimes(2); + + // 第一次失败会写入审计字段,且只需要写一次(同一条 message_request 记录) + expect(mocks.updateMessageRequestDetails).toHaveBeenCalledTimes(1); + + const special = session.getSpecialSettings(); + expect(special).not.toBeNull(); + expect(JSON.stringify(special)).toContain("thinking_signature_rectifier"); + }); +}); diff --git a/vitest.thinking-signature-rectifier.config.ts b/vitest.thinking-signature-rectifier.config.ts new file mode 100644 index 000000000..63f7d0079 --- /dev/null +++ b/vitest.thinking-signature-rectifier.config.ts @@ -0,0 +1,52 @@ +import path from "node:path"; +import { defineConfig } from "vitest/config"; + +/** + * thinking signature 整流器专项覆盖率配置 + * + * 目的: + * - 仅统计本次新增的整流器模块,避免把 Next/DB/Redis 等重模块纳入阈值 + * - 对“错误整流 + 重试一次”这类稳定性修复设置覆盖率门槛(>= 80%) + */ +export default defineConfig({ + test: { + globals: true, + environment: "node", + setupFiles: ["./tests/setup.ts"], + + include: [ + "src/app/v1/_lib/proxy/thinking-signature-rectifier.test.ts", + "tests/unit/proxy/proxy-forwarder-thinking-signature-rectifier.test.ts", + ], + exclude: ["node_modules", ".next", "dist", "build", "coverage", "tests/integration/**"], + + coverage: { + provider: "v8", + reporter: ["text", "html", "json"], + reportsDirectory: "./coverage-thinking-signature-rectifier", + + include: ["src/app/v1/_lib/proxy/thinking-signature-rectifier.ts"], + exclude: ["node_modules/", "tests/", "**/*.d.ts", ".next/"], + + thresholds: { + lines: 80, + functions: 80, + branches: 70, + 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"), + }, + }, +});