diff --git a/drizzle/0113_reflective_centennial.sql b/drizzle/0113_reflective_centennial.sql new file mode 100644 index 000000000..37b058143 --- /dev/null +++ b/drizzle/0113_reflective_centennial.sql @@ -0,0 +1,2 @@ +ALTER TABLE "system_settings" ADD COLUMN IF NOT EXISTS "replay_enabled" boolean;--> statement-breakpoint +ALTER TABLE "system_settings" ADD COLUMN IF NOT EXISTS "cache_effectiveness_enabled" boolean; \ No newline at end of file diff --git a/drizzle/meta/0113_snapshot.json b/drizzle/meta/0113_snapshot.json new file mode 100644 index 000000000..3d384a7ce --- /dev/null +++ b/drizzle/meta/0113_snapshot.json @@ -0,0 +1,5163 @@ +{ + "id": "87e53bfd-94b5-42d6-b589-0ec54a331157", + "prevId": "5fd1703b-6bd3-4594-9c92-9f9c889dcd24", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "action_category": { + "name": "action_category", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "action_type": { + "name": "action_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "target_id": { + "name": "target_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "target_name": { + "name": "target_name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "before_value": { + "name": "before_value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "after_value": { + "name": "after_value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "operator_user_id": { + "name": "operator_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "operator_user_name": { + "name": "operator_user_name", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "operator_key_id": { + "name": "operator_key_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "operator_key_name": { + "name": "operator_key_name", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "operator_ip": { + "name": "operator_ip", + "type": "varchar(45)", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_audit_log_category_created_at": { + "name": "idx_audit_log_category_created_at", + "columns": [ + { + "expression": "action_category", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_log_operator_user_created_at": { + "name": "idx_audit_log_operator_user_created_at", + "columns": [ + { + "expression": "operator_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"audit_log\".\"operator_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_log_operator_ip_created_at": { + "name": "idx_audit_log_operator_ip_created_at", + "columns": [ + { + "expression": "operator_ip", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"audit_log\".\"operator_ip\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_log_target": { + "name": "idx_audit_log_target", + "columns": [ + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"audit_log\".\"target_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_log_created_at_id": { + "name": "idx_audit_log_created_at_id", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cloud_pricing_catalog": { + "name": "cloud_pricing_catalog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "version": { + "name": "version", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "refreshed_at": { + "name": "refreshed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "providers": { + "name": "providers", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "vendors": { + "name": "vendors", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "model_count": { + "name": "model_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "synced_at": { + "name": "synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.error_rules": { + "name": "error_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "pattern": { + "name": "pattern", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "match_type": { + "name": "match_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'regex'" + }, + "category": { + "name": "category", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "override_response": { + "name": "override_response", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "override_status_code": { + "name": "override_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_error_rules_enabled": { + "name": "idx_error_rules_enabled", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "unique_pattern": { + "name": "unique_pattern", + "columns": [ + { + "expression": "pattern", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_category": { + "name": "idx_category", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_match_type": { + "name": "idx_match_type", + "columns": [ + { + "expression": "match_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.keys": { + "name": "keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "can_login_web_ui": { + "name": "can_login_web_ui", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "limit_5h_usd": { + "name": "limit_5h_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_5h_reset_mode": { + "name": "limit_5h_reset_mode", + "type": "daily_reset_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'rolling'" + }, + "limit_daily_usd": { + "name": "limit_daily_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "daily_reset_mode": { + "name": "daily_reset_mode", + "type": "daily_reset_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'fixed'" + }, + "daily_reset_time": { + "name": "daily_reset_time", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'00:00'" + }, + "limit_weekly_usd": { + "name": "limit_weekly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_monthly_usd": { + "name": "limit_monthly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_total_usd": { + "name": "limit_total_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "cost_reset_at": { + "name": "cost_reset_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "limit_concurrent_sessions": { + "name": "limit_concurrent_sessions", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "provider_group": { + "name": "provider_group", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false, + "default": "'default'" + }, + "cache_ttl_preference": { + "name": "cache_ttl_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_keys_user_id": { + "name": "idx_keys_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_keys_key": { + "name": "idx_keys_key", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_keys_created_at": { + "name": "idx_keys_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_keys_deleted_at": { + "name": "idx_keys_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.message_request": { + "name": "message_request", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cost_usd": { + "name": "cost_usd", + "type": "numeric(21, 15)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "cost_multiplier": { + "name": "cost_multiplier", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": false + }, + "group_cost_multiplier": { + "name": "group_cost_multiplier", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": false + }, + "cost_breakdown": { + "name": "cost_breakdown", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "request_sequence": { + "name": "request_sequence", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "provider_chain": { + "name": "provider_chain", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "routing_trace": { + "name": "routing_trace", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "api_type": { + "name": "api_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "endpoint": { + "name": "endpoint", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "original_model": { + "name": "original_model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "actual_response_model": { + "name": "actual_response_model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "ttfb_ms": { + "name": "ttfb_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_read_input_tokens": { + "name": "cache_read_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_creation_5m_input_tokens": { + "name": "cache_creation_5m_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_creation_1h_input_tokens": { + "name": "cache_creation_1h_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_ttl_applied": { + "name": "cache_ttl_applied", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "context_1m_applied": { + "name": "context_1m_applied", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "swap_cache_ttl_applied": { + "name": "swap_cache_ttl_applied", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "special_settings": { + "name": "special_settings", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "hedge_losers": { + "name": "hedge_losers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_stack": { + "name": "error_stack", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_cause": { + "name": "error_cause", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "blocked_by": { + "name": "blocked_by", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "blocked_reason": { + "name": "blocked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "client_ip": { + "name": "client_ip", + "type": "varchar(45)", + "primaryKey": false, + "notNull": false + }, + "messages_count": { + "name": "messages_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cache_compatibility_key": { + "name": "cache_compatibility_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "cache_score_eligible": { + "name": "cache_score_eligible", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cache_score_excluded_reason": { + "name": "cache_score_excluded_reason", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "theoretical_cache_tokens": { + "name": "theoretical_cache_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_ttl_bucket": { + "name": "cache_ttl_bucket", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_message_request_user_date_cost": { + "name": "idx_message_request_user_date_cost", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_user_created_at_cost_stats": { + "name": "idx_message_request_user_created_at_cost_stats", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_user_query": { + "name": "idx_message_request_user_query", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_provider_created_at_active": { + "name": "idx_message_request_provider_created_at_active", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_provider_created_at_finalized_active": { + "name": "idx_message_request_provider_created_at_finalized_active", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"status_code\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_session_id": { + "name": "idx_message_request_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_session_id_prefix": { + "name": "idx_message_request_session_id_prefix", + "columns": [ + { + "expression": "\"session_id\" varchar_pattern_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_session_seq": { + "name": "idx_message_request_session_seq", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_endpoint": { + "name": "idx_message_request_endpoint", + "columns": [ + { + "expression": "endpoint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_blocked_by": { + "name": "idx_message_request_blocked_by", + "columns": [ + { + "expression": "blocked_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_provider_id": { + "name": "idx_message_request_provider_id", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_user_id": { + "name": "idx_message_request_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key": { + "name": "idx_message_request_key", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key_created_at_id": { + "name": "idx_message_request_key_created_at_id", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key_model_active": { + "name": "idx_message_request_key_model_active", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"model\" IS NOT NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key_endpoint_active": { + "name": "idx_message_request_key_endpoint_active", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "endpoint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"endpoint\" IS NOT NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_created_at_id_active": { + "name": "idx_message_request_created_at_id_active", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_model_active": { + "name": "idx_message_request_model_active", + "columns": [ + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"model\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_status_code_active": { + "name": "idx_message_request_status_code_active", + "columns": [ + { + "expression": "status_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"status_code\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_created_at": { + "name": "idx_message_request_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_deleted_at": { + "name": "idx_message_request_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key_last_active": { + "name": "idx_message_request_key_last_active", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key_cost_active": { + "name": "idx_message_request_key_cost_active", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_session_user_info": { + "name": "idx_message_request_session_user_info", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_client_ip_created_at": { + "name": "idx_message_request_client_ip_created_at", + "columns": [ + { + "expression": "client_ip", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"client_ip\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_prices": { + "name": "model_prices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "model_name": { + "name": "model_name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "price_data": { + "name": "price_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'cloud'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_model_prices_latest": { + "name": "idx_model_prices_latest", + "columns": [ + { + "expression": "model_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_model_prices_model_name": { + "name": "idx_model_prices_model_name", + "columns": [ + { + "expression": "model_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_model_prices_created_at": { + "name": "idx_model_prices_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_model_prices_source": { + "name": "idx_model_prices_source", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_model_prices_vendor": { + "name": "idx_model_prices_vendor", + "columns": [ + { + "expression": "((\"price_data\" ->> 'vendor'))", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_model_prices_aliases": { + "name": "idx_model_prices_aliases", + "columns": [ + { + "expression": "((\"price_data\" -> 'aliases'))", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_settings": { + "name": "notification_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "use_legacy_mode": { + "name": "use_legacy_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "circuit_breaker_enabled": { + "name": "circuit_breaker_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "circuit_breaker_webhook": { + "name": "circuit_breaker_webhook", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "daily_leaderboard_enabled": { + "name": "daily_leaderboard_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "daily_leaderboard_webhook": { + "name": "daily_leaderboard_webhook", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "daily_leaderboard_time": { + "name": "daily_leaderboard_time", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false, + "default": "'09:00'" + }, + "daily_leaderboard_top_n": { + "name": "daily_leaderboard_top_n", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5 + }, + "cost_alert_enabled": { + "name": "cost_alert_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cost_alert_webhook": { + "name": "cost_alert_webhook", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "cost_alert_threshold": { + "name": "cost_alert_threshold", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false, + "default": "'0.80'" + }, + "cost_alert_check_interval": { + "name": "cost_alert_check_interval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 60 + }, + "cache_hit_rate_alert_enabled": { + "name": "cache_hit_rate_alert_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cache_hit_rate_alert_webhook": { + "name": "cache_hit_rate_alert_webhook", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "cache_hit_rate_alert_window_mode": { + "name": "cache_hit_rate_alert_window_mode", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false, + "default": "'auto'" + }, + "cache_hit_rate_alert_check_interval": { + "name": "cache_hit_rate_alert_check_interval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5 + }, + "cache_hit_rate_alert_historical_lookback_days": { + "name": "cache_hit_rate_alert_historical_lookback_days", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 7 + }, + "cache_hit_rate_alert_min_eligible_requests": { + "name": "cache_hit_rate_alert_min_eligible_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 20 + }, + "cache_hit_rate_alert_min_eligible_tokens": { + "name": "cache_hit_rate_alert_min_eligible_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "cache_hit_rate_alert_abs_min": { + "name": "cache_hit_rate_alert_abs_min", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "cache_hit_rate_alert_drop_rel": { + "name": "cache_hit_rate_alert_drop_rel", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.3'" + }, + "cache_hit_rate_alert_drop_abs": { + "name": "cache_hit_rate_alert_drop_abs", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.1'" + }, + "cache_hit_rate_alert_cooldown_minutes": { + "name": "cache_hit_rate_alert_cooldown_minutes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30 + }, + "cache_hit_rate_alert_top_n": { + "name": "cache_hit_rate_alert_top_n", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 10 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_target_bindings": { + "name": "notification_target_bindings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "notification_type": { + "name": "notification_type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "schedule_cron": { + "name": "schedule_cron", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "schedule_timezone": { + "name": "schedule_timezone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "template_override": { + "name": "template_override", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "unique_notification_target_binding": { + "name": "unique_notification_target_binding", + "columns": [ + { + "expression": "notification_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_notification_bindings_type": { + "name": "idx_notification_bindings_type", + "columns": [ + { + "expression": "notification_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_notification_bindings_target": { + "name": "idx_notification_bindings_target", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notification_target_bindings_target_id_webhook_targets_id_fk": { + "name": "notification_target_bindings_target_id_webhook_targets_id_fk", + "tableFrom": "notification_target_bindings", + "tableTo": "webhook_targets", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_batch_apply_operations": { + "name": "provider_batch_apply_operations", + "schema": "", + "columns": { + "claim_key": { + "name": "claim_key", + "type": "varchar(256)", + "primaryKey": true, + "notNull": true + }, + "preview_token": { + "name": "preview_token", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "payload_fingerprint": { + "name": "payload_fingerprint", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "operation_id": { + "name": "operation_id", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "undo_token": { + "name": "undo_token", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "undo_expires_at": { + "name": "undo_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "undo_consumed_at": { + "name": "undo_consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uniq_provider_batch_apply_operations_preview_token": { + "name": "uniq_provider_batch_apply_operations_preview_token", + "columns": [ + { + "expression": "preview_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "uniq_provider_batch_apply_operations_operation_id": { + "name": "uniq_provider_batch_apply_operations_operation_id", + "columns": [ + { + "expression": "operation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "uniq_provider_batch_apply_operations_undo_token": { + "name": "uniq_provider_batch_apply_operations_undo_token", + "columns": [ + { + "expression": "undo_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_batch_apply_operations_expires_at": { + "name": "idx_provider_batch_apply_operations_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_cache_effectiveness": { + "name": "provider_cache_effectiveness", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "cache_ttl_bucket": { + "name": "cache_ttl_bucket", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true + }, + "window_start": { + "name": "window_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "window_end": { + "name": "window_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "sample_count": { + "name": "sample_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "eligible_count": { + "name": "eligible_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "theoretical_cache_tokens": { + "name": "theoretical_cache_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "observed_cache_read_tokens": { + "name": "observed_cache_read_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "raw_effectiveness_bp": { + "name": "raw_effectiveness_bp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "confidence_bp": { + "name": "confidence_bp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "effectiveness_bp": { + "name": "effectiveness_bp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_provider_cache_effectiveness_window": { + "name": "idx_provider_cache_effectiveness_window", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_start", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_endpoint_probe_logs": { + "name": "provider_endpoint_probe_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "endpoint_id": { + "name": "endpoint_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'scheduled'" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error_type": { + "name": "error_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_provider_endpoint_probe_logs_endpoint_created_at": { + "name": "idx_provider_endpoint_probe_logs_endpoint_created_at", + "columns": [ + { + "expression": "endpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoint_probe_logs_created_at": { + "name": "idx_provider_endpoint_probe_logs_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "provider_endpoint_probe_logs_endpoint_id_provider_endpoints_id_fk": { + "name": "provider_endpoint_probe_logs_endpoint_id_provider_endpoints_id_fk", + "tableFrom": "provider_endpoint_probe_logs", + "tableTo": "provider_endpoints", + "columnsFrom": [ + "endpoint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_endpoints": { + "name": "provider_endpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provider_type": { + "name": "provider_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'claude'" + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_probed_at": { + "name": "last_probed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_probe_ok": { + "name": "last_probe_ok", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "last_probe_status_code": { + "name": "last_probe_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_probe_latency_ms": { + "name": "last_probe_latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_probe_error_type": { + "name": "last_probe_error_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "last_probe_error_message": { + "name": "last_probe_error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "uniq_provider_endpoints_vendor_type_url": { + "name": "uniq_provider_endpoints_vendor_type_url", + "columns": [ + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"provider_endpoints\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_vendor_type": { + "name": "idx_provider_endpoints_vendor_type", + "columns": [ + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"provider_endpoints\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_enabled": { + "name": "idx_provider_endpoints_enabled", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"provider_endpoints\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_pick_enabled": { + "name": "idx_provider_endpoints_pick_enabled", + "columns": [ + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"provider_endpoints\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_created_at": { + "name": "idx_provider_endpoints_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_deleted_at": { + "name": "idx_provider_endpoints_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "provider_endpoints_vendor_id_provider_vendors_id_fk": { + "name": "provider_endpoints_vendor_id_provider_vendors_id_fk", + "tableFrom": "provider_endpoints", + "tableTo": "provider_vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_groups": { + "name": "provider_groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "cost_multiplier": { + "name": "cost_multiplier", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": true, + "default": "'1.0'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "provider_groups_name_unique": { + "name": "provider_groups_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_vendors": { + "name": "provider_vendors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "website_domain": { + "name": "website_domain", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "website_url": { + "name": "website_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "favicon_url": { + "name": "favicon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "uniq_provider_vendors_website_domain": { + "name": "uniq_provider_vendors_website_domain", + "columns": [ + { + "expression": "website_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_vendors_created_at": { + "name": "idx_provider_vendors_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.providers": { + "name": "providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "provider_vendor_id": { + "name": "provider_vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "weight": { + "name": "weight", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "group_priorities": { + "name": "group_priorities", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'null'::jsonb" + }, + "cost_multiplier": { + "name": "cost_multiplier", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": false, + "default": "'1.0'" + }, + "group_tag": { + "name": "group_tag", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "provider_type": { + "name": "provider_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'claude'" + }, + "preserve_client_ip": { + "name": "preserve_client_ip", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "disable_session_reuse": { + "name": "disable_session_reuse", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "model_redirects": { + "name": "model_redirects", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "allowed_models": { + "name": "allowed_models", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'null'::jsonb" + }, + "allowed_clients": { + "name": "allowed_clients", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "blocked_clients": { + "name": "blocked_clients", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "active_time_start": { + "name": "active_time_start", + "type": "varchar(5)", + "primaryKey": false, + "notNull": false + }, + "active_time_end": { + "name": "active_time_end", + "type": "varchar(5)", + "primaryKey": false, + "notNull": false + }, + "codex_instructions_strategy": { + "name": "codex_instructions_strategy", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "default": "'auto'" + }, + "mcp_passthrough_type": { + "name": "mcp_passthrough_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "mcp_passthrough_url": { + "name": "mcp_passthrough_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "limit_5h_usd": { + "name": "limit_5h_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_5h_reset_mode": { + "name": "limit_5h_reset_mode", + "type": "daily_reset_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'rolling'" + }, + "limit_daily_usd": { + "name": "limit_daily_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "daily_reset_mode": { + "name": "daily_reset_mode", + "type": "daily_reset_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'fixed'" + }, + "daily_reset_time": { + "name": "daily_reset_time", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'00:00'" + }, + "limit_weekly_usd": { + "name": "limit_weekly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_monthly_usd": { + "name": "limit_monthly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_total_usd": { + "name": "limit_total_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "total_cost_reset_at": { + "name": "total_cost_reset_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "limit_concurrent_sessions": { + "name": "limit_concurrent_sessions", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "max_retry_attempts": { + "name": "max_retry_attempts", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "circuit_breaker_failure_threshold": { + "name": "circuit_breaker_failure_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5 + }, + "circuit_breaker_open_duration": { + "name": "circuit_breaker_open_duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1800000 + }, + "circuit_breaker_half_open_success_threshold": { + "name": "circuit_breaker_half_open_success_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 2 + }, + "proxy_url": { + "name": "proxy_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "proxy_fallback_to_direct": { + "name": "proxy_fallback_to_direct", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "custom_headers": { + "name": "custom_headers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "first_byte_timeout_streaming_ms": { + "name": "first_byte_timeout_streaming_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "streaming_idle_timeout_ms": { + "name": "streaming_idle_timeout_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "request_timeout_non_streaming_ms": { + "name": "request_timeout_non_streaming_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "website_url": { + "name": "website_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "favicon_url": { + "name": "favicon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cache_ttl_preference": { + "name": "cache_ttl_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "swap_cache_ttl_billing": { + "name": "swap_cache_ttl_billing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "context_1m_preference": { + "name": "context_1m_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "codex_reasoning_effort_preference": { + "name": "codex_reasoning_effort_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "codex_reasoning_summary_preference": { + "name": "codex_reasoning_summary_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "codex_text_verbosity_preference": { + "name": "codex_text_verbosity_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "codex_parallel_tool_calls_preference": { + "name": "codex_parallel_tool_calls_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "codex_image_generation_preference": { + "name": "codex_image_generation_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "codex_service_tier_preference": { + "name": "codex_service_tier_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "anthropic_max_tokens_preference": { + "name": "anthropic_max_tokens_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "anthropic_thinking_budget_preference": { + "name": "anthropic_thinking_budget_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "anthropic_adaptive_thinking": { + "name": "anthropic_adaptive_thinking", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'null'::jsonb" + }, + "gemini_google_search_preference": { + "name": "gemini_google_search_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "tpm": { + "name": "tpm", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "rpm": { + "name": "rpm", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "rpd": { + "name": "rpd", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "cc": { + "name": "cc", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_providers_enabled_priority": { + "name": "idx_providers_enabled_priority", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "weight", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"providers\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_group": { + "name": "idx_providers_group", + "columns": [ + { + "expression": "group_tag", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"providers\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_vendor_type_url_active": { + "name": "idx_providers_vendor_type_url_active", + "columns": [ + { + "expression": "provider_vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"providers\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_created_at": { + "name": "idx_providers_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_deleted_at": { + "name": "idx_providers_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_vendor_type": { + "name": "idx_providers_vendor_type", + "columns": [ + { + "expression": "provider_vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"providers\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_enabled_vendor_type": { + "name": "idx_providers_enabled_vendor_type", + "columns": [ + { + "expression": "provider_vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"providers\".\"deleted_at\" IS NULL AND \"providers\".\"is_enabled\" = true AND \"providers\".\"provider_vendor_id\" IS NOT NULL AND \"providers\".\"provider_vendor_id\" > 0", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "providers_provider_vendor_id_provider_vendors_id_fk": { + "name": "providers_provider_vendor_id_provider_vendors_id_fk", + "tableFrom": "providers", + "tableTo": "provider_vendors", + "columnsFrom": [ + "provider_vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.replay_payloads": { + "name": "replay_payloads", + "schema": "", + "columns": { + "replay_id": { + "name": "replay_id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "verifier": { + "name": "verifier", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "scope_tag": { + "name": "scope_tag", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "format": { + "name": "format", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "headers_json": { + "name": "headers_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "source_message_request_id": { + "name": "source_message_request_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_replay_payloads_key_id": { + "name": "idx_replay_payloads_key_id", + "columns": [ + { + "expression": "key_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_replay_payloads_expires_at": { + "name": "idx_replay_payloads_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.request_filters": { + "name": "request_filters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true + }, + "match_type": { + "name": "match_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "target": { + "name": "target", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "replacement": { + "name": "replacement", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "binding_type": { + "name": "binding_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'global'" + }, + "provider_ids": { + "name": "provider_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "group_tags": { + "name": "group_tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rule_mode": { + "name": "rule_mode", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'simple'" + }, + "execution_phase": { + "name": "execution_phase", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'guard'" + }, + "operations": { + "name": "operations", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_request_filters_enabled": { + "name": "idx_request_filters_enabled", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_request_filters_scope": { + "name": "idx_request_filters_scope", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_request_filters_action": { + "name": "idx_request_filters_action", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_request_filters_binding": { + "name": "idx_request_filters_binding", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "binding_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_request_filters_phase": { + "name": "idx_request_filters_phase", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_phase", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sensitive_words": { + "name": "sensitive_words", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "word": { + "name": "word", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "match_type": { + "name": "match_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'contains'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_sensitive_words_enabled": { + "name": "idx_sensitive_words_enabled", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "match_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sensitive_words_created_at": { + "name": "idx_sensitive_words_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.system_settings": { + "name": "system_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "site_title": { + "name": "site_title", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "default": "'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'" + }, + "codex_priority_billing_source": { + "name": "codex_priority_billing_source", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'requested'" + }, + "bill_non_successful_requests": { + "name": "bill_non_successful_requests", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "bill_hedge_losers": { + "name": "bill_hedge_losers", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "discovery_enabled": { + "name": "discovery_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "discovery_concurrency": { + "name": "discovery_concurrency", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 2 + }, + "max_discovery_rounds": { + "name": "max_discovery_rounds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 2 + }, + "discovery_sla_ms": { + "name": "discovery_sla_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10000 + }, + "sticky_sla_ms": { + "name": "sticky_sla_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 20000 + }, + "racing_total_timeout_ms": { + "name": "racing_total_timeout_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 60000 + }, + "sticky_timeout_cooldown_ms": { + "name": "sticky_timeout_cooldown_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 300000 + }, + "timezone": { + "name": "timezone", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "enable_auto_cleanup": { + "name": "enable_auto_cleanup", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "cleanup_retention_days": { + "name": "cleanup_retention_days", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30 + }, + "cleanup_schedule": { + "name": "cleanup_schedule", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "default": "'0 2 * * *'" + }, + "cleanup_batch_size": { + "name": "cleanup_batch_size", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 10000 + }, + "enable_client_version_check": { + "name": "enable_client_version_check", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "verbose_provider_error": { + "name": "verbose_provider_error", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pass_through_upstream_error_message": { + "name": "pass_through_upstream_error_message", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_http2": { + "name": "enable_http2", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_openai_responses_websocket": { + "name": "enable_openai_responses_websocket", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_high_concurrency_mode": { + "name": "enable_high_concurrency_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "intercept_anthropic_warmup_requests": { + "name": "intercept_anthropic_warmup_requests", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_thinking_signature_rectifier": { + "name": "enable_thinking_signature_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_thinking_budget_rectifier": { + "name": "enable_thinking_budget_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_thinking_effort_conflict_rectifier": { + "name": "enable_thinking_effort_conflict_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_gemini_function_id_rectifier": { + "name": "enable_gemini_function_id_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_billing_header_rectifier": { + "name": "enable_billing_header_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_response_input_rectifier": { + "name": "enable_response_input_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_non_conversation_endpoint_provider_fallback": { + "name": "allow_non_conversation_endpoint_provider_fallback", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "fake_streaming_whitelist": { + "name": "fake_streaming_whitelist", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "enable_codex_session_id_completion": { + "name": "enable_codex_session_id_completion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_claude_metadata_user_id_injection": { + "name": "enable_claude_metadata_user_id_injection", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_response_fixer": { + "name": "enable_response_fixer", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "response_fixer_config": { + "name": "response_fixer_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{\"fixTruncatedJson\":true,\"fixSseFormat\":true,\"fixEncoding\":true,\"maxJsonDepth\":200,\"maxFixSize\":1048576}'::jsonb" + }, + "quota_db_refresh_interval_seconds": { + "name": "quota_db_refresh_interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 10 + }, + "quota_lease_percent_5h": { + "name": "quota_lease_percent_5h", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "quota_lease_percent_daily": { + "name": "quota_lease_percent_daily", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "quota_lease_percent_weekly": { + "name": "quota_lease_percent_weekly", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "quota_lease_percent_monthly": { + "name": "quota_lease_percent_monthly", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "quota_lease_cap_usd": { + "name": "quota_lease_cap_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "ip_extraction_config": { + "name": "ip_extraction_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ip_geo_lookup_enabled": { + "name": "ip_geo_lookup_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "public_status_window_hours": { + "name": "public_status_window_hours", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 24 + }, + "public_status_aggregation_interval_minutes": { + "name": "public_status_aggregation_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "stream_gate_mode": { + "name": "stream_gate_mode", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true, + "default": "'enforce'" + }, + "affinity_ignore_client_session_id": { + "name": "affinity_ignore_client_session_id", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "replay_enabled": { + "name": "replay_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cache_effectiveness_enabled": { + "name": "cache_effectiveness_enabled", + "type": "boolean", + "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 + }, + "public.usage_ledger": { + "name": "usage_ledger", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "final_provider_id": { + "name": "final_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "original_model": { + "name": "original_model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "actual_response_model": { + "name": "actual_response_model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "endpoint": { + "name": "endpoint", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "api_type": { + "name": "api_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_success": { + "name": "is_success", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "success_rate_outcome": { + "name": "success_rate_outcome", + "type": "varchar(16)", + "primaryKey": false, + "notNull": false + }, + "blocked_by": { + "name": "blocked_by", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "cost_usd": { + "name": "cost_usd", + "type": "numeric(21, 15)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "cost_multiplier": { + "name": "cost_multiplier", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": false + }, + "group_cost_multiplier": { + "name": "group_cost_multiplier", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_read_input_tokens": { + "name": "cache_read_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_creation_5m_input_tokens": { + "name": "cache_creation_5m_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_creation_1h_input_tokens": { + "name": "cache_creation_1h_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_ttl_applied": { + "name": "cache_ttl_applied", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "context_1m_applied": { + "name": "context_1m_applied", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "swap_cache_ttl_applied": { + "name": "swap_cache_ttl_applied", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ttfb_ms": { + "name": "ttfb_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_ip": { + "name": "client_ip", + "type": "varchar(45)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_usage_ledger_request_id": { + "name": "idx_usage_ledger_request_id", + "columns": [ + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_user_created_at": { + "name": "idx_usage_ledger_user_created_at", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_key_created_at": { + "name": "idx_usage_ledger_key_created_at", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_provider_created_at": { + "name": "idx_usage_ledger_provider_created_at", + "columns": [ + { + "expression": "final_provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_created_at_minute": { + "name": "idx_usage_ledger_created_at_minute", + "columns": [ + { + "expression": "date_trunc('minute', \"created_at\" AT TIME ZONE 'UTC')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_created_at_desc_id": { + "name": "idx_usage_ledger_created_at_desc_id", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_session_id": { + "name": "idx_usage_ledger_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"session_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_model": { + "name": "idx_usage_ledger_model", + "columns": [ + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"model\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_key_cost": { + "name": "idx_usage_ledger_key_cost", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "endpoint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_user_cost_cover": { + "name": "idx_usage_ledger_user_cost_cover", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "endpoint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_provider_cost_cover": { + "name": "idx_usage_ledger_provider_cost_cover", + "columns": [ + { + "expression": "final_provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "endpoint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_key_created_at_desc_cover": { + "name": "idx_usage_ledger_key_created_at_desc_cover", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"created_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "final_provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "varchar", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "rpm_limit": { + "name": "rpm_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "daily_limit_usd": { + "name": "daily_limit_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "provider_group": { + "name": "provider_group", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false, + "default": "'default'" + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "limit_5h_usd": { + "name": "limit_5h_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_5h_reset_mode": { + "name": "limit_5h_reset_mode", + "type": "daily_reset_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'rolling'" + }, + "limit_weekly_usd": { + "name": "limit_weekly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_monthly_usd": { + "name": "limit_monthly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_total_usd": { + "name": "limit_total_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "cost_reset_at": { + "name": "cost_reset_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "limit_5h_cost_reset_at": { + "name": "limit_5h_cost_reset_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "limit_concurrent_sessions": { + "name": "limit_concurrent_sessions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "daily_reset_mode": { + "name": "daily_reset_mode", + "type": "daily_reset_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'fixed'" + }, + "daily_reset_time": { + "name": "daily_reset_time", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'00:00'" + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "allowed_clients": { + "name": "allowed_clients", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "allowed_models": { + "name": "allowed_models", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "blocked_clients": { + "name": "blocked_clients", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_users_active_role_sort": { + "name": "idx_users_active_role_sort", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"users\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_users_enabled_expires_at": { + "name": "idx_users_enabled_expires_at", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"users\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_users_tags_gin": { + "name": "idx_users_tags_gin", + "columns": [ + { + "expression": "tags", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"users\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_users_created_at": { + "name": "idx_users_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_users_deleted_at": { + "name": "idx_users_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_targets": { + "name": "webhook_targets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "provider_type": { + "name": "provider_type", + "type": "webhook_provider_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "webhook_url": { + "name": "webhook_url", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": false + }, + "telegram_bot_token": { + "name": "telegram_bot_token", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "telegram_chat_id": { + "name": "telegram_chat_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "dingtalk_secret": { + "name": "dingtalk_secret", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "custom_template": { + "name": "custom_template", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "custom_headers": { + "name": "custom_headers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "proxy_url": { + "name": "proxy_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "proxy_fallback_to_direct": { + "name": "proxy_fallback_to_direct", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_test_at": { + "name": "last_test_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_test_result": { + "name": "last_test_result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.daily_reset_mode": { + "name": "daily_reset_mode", + "schema": "public", + "values": [ + "fixed", + "rolling" + ] + }, + "public.notification_type": { + "name": "notification_type", + "schema": "public", + "values": [ + "circuit_breaker", + "daily_leaderboard", + "cost_alert", + "cache_hit_rate_alert" + ] + }, + "public.webhook_provider_type": { + "name": "webhook_provider_type", + "schema": "public", + "values": [ + "wechat", + "feishu", + "dingtalk", + "telegram", + "custom" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 91847a65c..d5f102ac7 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -792,6 +792,13 @@ "when": 1784800591867, "tag": "0112_complex_sabra", "breakpoints": true + }, + { + "idx": 113, + "version": "7", + "when": 1784833275913, + "tag": "0113_reflective_centennial", + "breakpoints": true } ] } \ No newline at end of file diff --git a/messages/en/dashboard.json b/messages/en/dashboard.json index 6256fb9cf..96ff6a8c6 100644 --- a/messages/en/dashboard.json +++ b/messages/en/dashboard.json @@ -132,6 +132,7 @@ "provider": "Provider", "model": "Billing Model", "reasoningEffort": "Reasoning Effort", + "reasoningEffortTooltip": "Thinking effort of the model request", "endpoint": "Endpoint", "inputTokens": "Input", "outputTokens": "Output", @@ -258,7 +259,7 @@ "responseModelLabel": "Actual Response Model", "mismatchTooltip": "The upstream provider returned a different model than the one requested. Billing is still based on the requested model.", "secondaryLineAriaLabel": "Actual response model: {model}", - "arrowPrefix": "\u21b3", + "arrowPrefix": "↳", "noSignatureBadge": "No thinking signature", "noSignatureTooltip": "Thinking was enabled but no thinking signature was returned in the stream; the actual response model falls back to the plain model field from message_start." }, @@ -549,6 +550,11 @@ "originDecisionLoading": "Loading original decision...", "originDecisionUnavailable": "Original decision record unavailable", "originDecisionExpand": "View original selection" + }, + "replayServe": { + "title": "Served from Replay Cache", + "desc": "This request was served from the replay cache (identical request already in flight or completed). No upstream provider call was made and no cost was incurred.", + "replayId": "Replay ID" } }, "providerChain": { diff --git a/messages/en/provider-chain.json b/messages/en/provider-chain.json index 3b596c093..90b57631f 100644 --- a/messages/en/provider-chain.json +++ b/messages/en/provider-chain.json @@ -60,6 +60,7 @@ "concurrent_limit_failed": "Concurrent Limit", "http2_fallback": "HTTP/2 Fallback", "session_reuse": "Session Reuse", + "affinity_hit": "Cache Reuse (Prefix Affinity)", "initial_selection": "Initial Selection", "endpoint_pool_exhausted": "Endpoint Pool Exhausted", "vendor_type_all_timeout": "Vendor-Type All Endpoints Timeout", @@ -249,6 +250,27 @@ "session_reuse": "Session Reuse", "weighted_random": "Weighted Random", "group_filtered": "Group Filtered", - "fail_open_fallback": "Fail-Open Fallback" + "fail_open_fallback": "Fail-Open Fallback", + "prefix_affinity": "Longest-Prefix Affinity" + }, + "affinity": { + "matchedDepth": "Matched depth", + "matchedPrefixBytes": "Matched prefix bytes", + "matchedFp": "Matched fingerprint" + }, + "streamGate": { + "title": "Stream Gate Commit", + "frameIndex": "Trigger frame", + "chunkIndex": "Trigger chunk", + "eventName": "Trigger event", + "bufferedBytes": "Buffered bytes", + "echoExcludedBytes": "Echo-excluded bytes", + "gateWaitMs": "Gate wait" + }, + "replayServe": { + "sources": { + "completed": "Completed replay", + "attached_live": "Attached to live stream" + } } } diff --git a/messages/en/settings/config.json b/messages/en/settings/config.json index 9c50f60bc..77fa27be1 100644 --- a/messages/en/settings/config.json +++ b/messages/en/settings/config.json @@ -169,7 +169,11 @@ "off": "Off", "shadow": "Shadow mode", "enforce": "Enabled" - } + }, + "replayEnabled": "Request Replay", + "replayEnabledDesc": "Caches upstream responses and reuses upstream connections: identical concurrent or reconnecting requests attach to the in-flight stream instead of re-hitting the provider. Follows the ENABLE_REQUEST_REPLAY environment variable until saved here. Default off.", + "cacheEffectivenessEnabled": "Prefix Cache Simulation", + "cacheEffectivenessEnabledDesc": "Simulates longest-prefix cache hit rates (theoretical vs actual) for observability only; never affects routing. Follows the ENABLE_CACHE_EFFECTIVENESS environment variable until saved here. Default on." }, "ipLogging": { "title": "IP logging & extraction", diff --git a/messages/ja/dashboard.json b/messages/ja/dashboard.json index 01121a9fc..617593896 100644 --- a/messages/ja/dashboard.json +++ b/messages/ja/dashboard.json @@ -132,6 +132,7 @@ "provider": "プロバイダー", "model": "課金モデル", "reasoningEffort": "推論強度", + "reasoningEffortTooltip": "このモデルリクエストの推論強度", "endpoint": "エンドポイント", "inputTokens": "入力", "outputTokens": "出力", @@ -549,6 +550,11 @@ "originDecisionLoading": "元の決定を読み込み中...", "originDecisionUnavailable": "元の決定記録は利用できません", "originDecisionExpand": "元の選択を表示" + }, + "replayServe": { + "title": "Replay キャッシュから応答", + "desc": "このリクエストは Replay キャッシュから直接応答されました(同一リクエストが進行中または完了済み)。上流プロバイダーへの呼び出しは行われず、費用は発生しません。", + "replayId": "Replay ID" } }, "providerChain": { diff --git a/messages/ja/provider-chain.json b/messages/ja/provider-chain.json index 58dcb8e4d..3106f1a13 100644 --- a/messages/ja/provider-chain.json +++ b/messages/ja/provider-chain.json @@ -60,6 +60,7 @@ "concurrent_limit_failed": "同時実行制限", "http2_fallback": "HTTP/2 フォールバック", "session_reuse": "セッション再利用", + "affinity_hit": "キャッシュ再利用(プレフィックス親和)", "initial_selection": "初期選択", "endpoint_pool_exhausted": "エンドポイントプール枯渇", "vendor_type_all_timeout": "ベンダータイプ全エンドポイントタイムアウト", @@ -249,6 +250,27 @@ "session_reuse": "セッション再利用", "weighted_random": "重み付きランダム", "group_filtered": "グループフィルタ", - "fail_open_fallback": "フェイルオープンフォールバック" + "fail_open_fallback": "フェイルオープンフォールバック", + "prefix_affinity": "最長プレフィックス親和" + }, + "affinity": { + "matchedDepth": "一致深度", + "matchedPrefixBytes": "一致プレフィックスバイト数", + "matchedFp": "一致フィンガープリント" + }, + "streamGate": { + "title": "ストリームゲートコミット", + "frameIndex": "トリガーフレーム番号", + "chunkIndex": "トリガーチャンク番号", + "eventName": "トリガーイベント", + "bufferedBytes": "バッファ済みバイト数", + "echoExcludedBytes": "エコー除外バイト数", + "gateWaitMs": "ゲート待機" + }, + "replayServe": { + "sources": { + "completed": "完了済みリプレイ", + "attached_live": "ライブストリームに追随" + } } } diff --git a/messages/ja/settings/config.json b/messages/ja/settings/config.json index 7cdea6512..ddb081fb3 100644 --- a/messages/ja/settings/config.json +++ b/messages/ja/settings/config.json @@ -169,7 +169,11 @@ "off": "オフ", "shadow": "シャドウモード", "enforce": "有効" - } + }, + "replayEnabled": "リクエスト Replay", + "replayEnabledDesc": "上流レスポンスをキャッシュし上流接続を再利用します。同一リクエストの並行実行や再接続は進行中のストリームに追随し、プロバイダーへ再送しません。保存するまでは環境変数 ENABLE_REQUEST_REPLAY に従います。デフォルトはオフ。", + "cacheEffectivenessEnabled": "プレフィックスキャッシュシミュレーション", + "cacheEffectivenessEnabledDesc": "最長プレフィックス一致のキャッシュヒット率(理論値 vs 実測値)を観測目的でシミュレートします。ルーティングには影響しません。保存するまでは環境変数 ENABLE_CACHE_EFFECTIVENESS に従います。デフォルトはオン。" }, "ipLogging": { "title": "IP ログと抽出", diff --git a/messages/ru/dashboard.json b/messages/ru/dashboard.json index 9010fe27d..1a5bf7db0 100644 --- a/messages/ru/dashboard.json +++ b/messages/ru/dashboard.json @@ -132,6 +132,7 @@ "provider": "Поставщик", "model": "Модель тарификации", "reasoningEffort": "Интенсивность рассуждений", + "reasoningEffortTooltip": "Интенсивность рассуждений для запроса этой модели", "endpoint": "Эндпоинт", "inputTokens": "Вход", "outputTokens": "Выход", @@ -549,6 +550,11 @@ "originDecisionLoading": "Загрузка исходного решения...", "originDecisionUnavailable": "Запись исходного решения недоступна", "originDecisionExpand": "Просмотр исходного выбора" + }, + "replayServe": { + "title": "Обслужено из Replay-кэша", + "desc": "Запрос обслужен из Replay-кэша (идентичный запрос уже выполняется или завершён). Обращение к провайдеру не выполнялось, затрат нет.", + "replayId": "Replay ID" } }, "providerChain": { diff --git a/messages/ru/provider-chain.json b/messages/ru/provider-chain.json index bbc34ad9d..e6aef87af 100644 --- a/messages/ru/provider-chain.json +++ b/messages/ru/provider-chain.json @@ -60,6 +60,7 @@ "concurrent_limit_failed": "Лимит параллельных запросов", "http2_fallback": "Откат HTTP/2", "session_reuse": "Повторное использование сессии", + "affinity_hit": "Переиспользование кэша (префиксная аффинность)", "initial_selection": "Первоначальный выбор", "endpoint_pool_exhausted": "Пул конечных точек исчерпан", "vendor_type_all_timeout": "Тайм-аут всех конечных точек типа поставщика", @@ -249,6 +250,27 @@ "session_reuse": "Повторное использование сессии", "weighted_random": "Взвешенный случайный", "group_filtered": "Фильтрация по группе", - "fail_open_fallback": "Резервный вариант при сбое" + "fail_open_fallback": "Резервный вариант при сбое", + "prefix_affinity": "Аффинность по наибольшему префиксу" + }, + "affinity": { + "matchedDepth": "Глубина совпадения", + "matchedPrefixBytes": "Байты совпавшего префикса", + "matchedFp": "Совпавший отпечаток" + }, + "streamGate": { + "title": "Коммит стрим-гейта", + "frameIndex": "Триггерный кадр", + "chunkIndex": "Триггерный чанк", + "eventName": "Триггерное событие", + "bufferedBytes": "Буферизовано байт", + "echoExcludedBytes": "Исключено байт эха", + "gateWaitMs": "Ожидание гейта" + }, + "replayServe": { + "sources": { + "completed": "Завершённый повтор", + "attached_live": "Присоединение к живому потоку" + } } } diff --git a/messages/ru/settings/config.json b/messages/ru/settings/config.json index 9fd5abca0..5781aab60 100644 --- a/messages/ru/settings/config.json +++ b/messages/ru/settings/config.json @@ -169,7 +169,11 @@ "off": "Выключено", "shadow": "Теневой режим", "enforce": "Включено" - } + }, + "replayEnabled": "Replay запросов", + "replayEnabledDesc": "Кэширует ответы провайдера и переиспользует соединения: одинаковые параллельные или переподключающиеся запросы присоединяются к текущему потоку вместо повторного обращения к провайдеру. До сохранения следует переменной окружения ENABLE_REQUEST_REPLAY. По умолчанию выключено.", + "cacheEffectivenessEnabled": "Симуляция префиксного кэша", + "cacheEffectivenessEnabledDesc": "Симулирует хит-рейт кэша по наибольшему префиксу (теория и факт) только для наблюдаемости; не влияет на маршрутизацию. До сохранения следует переменной окружения ENABLE_CACHE_EFFECTIVENESS. По умолчанию включено." }, "ipLogging": { "title": "Журналирование и извлечение IP", diff --git a/messages/zh-CN/dashboard.json b/messages/zh-CN/dashboard.json index ad2ce3cfe..c3b8dbc16 100644 --- a/messages/zh-CN/dashboard.json +++ b/messages/zh-CN/dashboard.json @@ -132,6 +132,7 @@ "provider": "供应商", "model": "计费模型", "reasoningEffort": "思考强度", + "reasoningEffortTooltip": "该模型请求的思考强度", "endpoint": "端点", "inputTokens": "输入", "outputTokens": "输出", @@ -549,6 +550,11 @@ "originDecisionLoading": "正在加载原始决策...", "originDecisionUnavailable": "原始决策记录不可用", "originDecisionExpand": "查看原始选择" + }, + "replayServe": { + "title": "由 Replay 缓存服务", + "desc": "该请求由 Replay 缓存直接服务(相同请求正在进行或已完成),未发起上游供应商调用,不产生费用。", + "replayId": "Replay ID" } }, "providerChain": { diff --git a/messages/zh-CN/provider-chain.json b/messages/zh-CN/provider-chain.json index d136eed35..ca6f974b7 100644 --- a/messages/zh-CN/provider-chain.json +++ b/messages/zh-CN/provider-chain.json @@ -60,6 +60,7 @@ "concurrent_limit_failed": "并发限制", "http2_fallback": "HTTP/2 回退", "session_reuse": "会话复用", + "affinity_hit": "缓存复用(前缀亲和)", "initial_selection": "首次选择", "endpoint_pool_exhausted": "端点池耗尽", "vendor_type_all_timeout": "供应商类型全端点超时", @@ -249,6 +250,27 @@ "session_reuse": "会话复用", "weighted_random": "加权随机", "group_filtered": "分组过滤", - "fail_open_fallback": "故障开放回退" + "fail_open_fallback": "故障开放回退", + "prefix_affinity": "最长前缀亲和" + }, + "affinity": { + "matchedDepth": "命中深度", + "matchedPrefixBytes": "命中前缀字节", + "matchedFp": "命中指纹" + }, + "streamGate": { + "title": "流式门控提交", + "frameIndex": "触发帧序号", + "chunkIndex": "触发 Chunk 序号", + "eventName": "触发事件", + "bufferedBytes": "已缓冲字节", + "echoExcludedBytes": "回显排除字节", + "gateWaitMs": "门控等待" + }, + "replayServe": { + "sources": { + "completed": "已完成重放", + "attached_live": "跟尾在途流" + } } } diff --git a/messages/zh-CN/settings/config.json b/messages/zh-CN/settings/config.json index f3497f20c..ae3d6ff7c 100644 --- a/messages/zh-CN/settings/config.json +++ b/messages/zh-CN/settings/config.json @@ -94,6 +94,10 @@ "shadow": "影子模式", "enforce": "启用" }, + "replayEnabled": "请求 Replay", + "replayEnabledDesc": "缓存上游响应并复用上游连接:并发或断线重连的相同请求直接跟尾在途流,不再重复请求供应商。保存前跟随环境变量 ENABLE_REQUEST_REPLAY,默认关闭。", + "cacheEffectivenessEnabled": "前缀缓存模拟", + "cacheEffectivenessEnabledDesc": "模拟最长前缀匹配的缓存命中率(理论 vs 实际),仅用于观测,不影响路由。保存前跟随环境变量 ENABLE_CACHE_EFFECTIVENESS,默认开启。", "affinityIgnoreClientSessionId": "忽略客户端 Session ID", "affinityIgnoreClientSessionIdDesc": "开启后,可指纹化的请求强制使用最长前缀亲和做供应商粘性(跳过客户端 Session ID 绑定);不可指纹化的请求仍走会话复用。默认开启。", "fakeStreaming": { diff --git a/messages/zh-TW/dashboard.json b/messages/zh-TW/dashboard.json index bef309e78..f2cbb699f 100644 --- a/messages/zh-TW/dashboard.json +++ b/messages/zh-TW/dashboard.json @@ -132,6 +132,7 @@ "provider": "供應商", "model": "計費模型", "reasoningEffort": "思考強度", + "reasoningEffortTooltip": "該模型請求的思考強度", "endpoint": "端點", "inputTokens": "輸入", "outputTokens": "輸出", @@ -549,6 +550,11 @@ "originDecisionLoading": "正在載入原始決策...", "originDecisionUnavailable": "原始決策記錄不可用", "originDecisionExpand": "查看原始選擇" + }, + "replayServe": { + "title": "由 Replay 快取服務", + "desc": "該請求由 Replay 快取直接服務(相同請求正在進行或已完成),未發起上游供應商呼叫,不產生費用。", + "replayId": "Replay ID" } }, "providerChain": { diff --git a/messages/zh-TW/provider-chain.json b/messages/zh-TW/provider-chain.json index de200a133..1b91777d9 100644 --- a/messages/zh-TW/provider-chain.json +++ b/messages/zh-TW/provider-chain.json @@ -60,6 +60,7 @@ "concurrent_limit_failed": "並發限制", "http2_fallback": "HTTP/2 回退", "session_reuse": "會話複用", + "affinity_hit": "快取複用(前綴親和)", "initial_selection": "首次選擇", "endpoint_pool_exhausted": "端點池耗盡", "vendor_type_all_timeout": "供應商類型全端點逾時", @@ -249,6 +250,27 @@ "session_reuse": "會話複用", "weighted_random": "加權隨機", "group_filtered": "分組過濾", - "fail_open_fallback": "故障開放回退" + "fail_open_fallback": "故障開放回退", + "prefix_affinity": "最長前綴親和" + }, + "affinity": { + "matchedDepth": "命中深度", + "matchedPrefixBytes": "命中前綴位元組", + "matchedFp": "命中指紋" + }, + "streamGate": { + "title": "串流門控提交", + "frameIndex": "觸發幀序號", + "chunkIndex": "觸發 Chunk 序號", + "eventName": "觸發事件", + "bufferedBytes": "已緩衝位元組", + "echoExcludedBytes": "回顯排除位元組", + "gateWaitMs": "門控等待" + }, + "replayServe": { + "sources": { + "completed": "已完成重放", + "attached_live": "跟尾在途串流" + } } } diff --git a/messages/zh-TW/settings/config.json b/messages/zh-TW/settings/config.json index 3b6411888..991b84948 100644 --- a/messages/zh-TW/settings/config.json +++ b/messages/zh-TW/settings/config.json @@ -169,7 +169,11 @@ "off": "關閉", "shadow": "影子模式", "enforce": "啟用" - } + }, + "replayEnabled": "請求 Replay", + "replayEnabledDesc": "快取上游回應並重用上游連線:並發或斷線重連的相同請求直接跟尾在途串流,不再重複請求供應商。儲存前跟隨環境變數 ENABLE_REQUEST_REPLAY,預設關閉。", + "cacheEffectivenessEnabled": "前綴快取模擬", + "cacheEffectivenessEnabledDesc": "模擬最長前綴匹配的快取命中率(理論 vs 實際),僅用於觀測,不影響路由。儲存前跟隨環境變數 ENABLE_CACHE_EFFECTIVENESS,預設開啟。" }, "ipLogging": { "title": "IP 記錄與提取", diff --git a/src/actions/system-config.ts b/src/actions/system-config.ts index 3dac0a71d..639f6dbe0 100644 --- a/src/actions/system-config.ts +++ b/src/actions/system-config.ts @@ -105,6 +105,8 @@ export async function saveSystemSettings(formData: { fakeStreamingWhitelist?: FakeStreamingWhitelistEntry[]; streamGateMode?: StreamGateSettingMode; affinityIgnoreClientSessionId?: boolean; + replayEnabled?: boolean | null; + cacheEffectivenessEnabled?: boolean | null; enableCodexSessionIdCompletion?: boolean; enableClaudeMetadataUserIdInjection?: boolean; enableResponseFixer?: boolean; @@ -193,6 +195,8 @@ export async function saveSystemSettings(formData: { fakeStreamingWhitelist: validated.fakeStreamingWhitelist, streamGateMode: validated.streamGateMode, affinityIgnoreClientSessionId: validated.affinityIgnoreClientSessionId, + replayEnabled: validated.replayEnabled, + cacheEffectivenessEnabled: validated.cacheEffectivenessEnabled, enableCodexSessionIdCompletion: validated.enableCodexSessionIdCompletion, enableClaudeMetadataUserIdInjection: validated.enableClaudeMetadataUserIdInjection, enableResponseFixer: validated.enableResponseFixer, diff --git a/src/app/[locale]/dashboard/logs/_components/codex-reasoning-effort-display.test.tsx b/src/app/[locale]/dashboard/logs/_components/codex-reasoning-effort-display.test.tsx deleted file mode 100644 index 9fc307ca5..000000000 --- a/src/app/[locale]/dashboard/logs/_components/codex-reasoning-effort-display.test.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import type { ReactNode } from "react"; -import { renderToStaticMarkup } from "react-dom/server"; -import { describe, expect, test, vi } from "vitest"; -import { CodexReasoningEffortDisplay } from "./codex-reasoning-effort-display"; - -vi.mock("next-intl", () => ({ - useTranslations: () => (key: string) => key, -})); - -vi.mock("@/components/ui/tooltip", () => ({ - TooltipProvider: ({ children }: { children?: ReactNode }) =>
{children}
, - Tooltip: ({ children }: { children?: ReactNode }) =>
{children}
, - TooltipTrigger: ({ children }: { children?: ReactNode }) =>
{children}
, - TooltipContent: ({ children }: { children?: ReactNode }) =>
{children}
, -})); - -describe("CodexReasoningEffortDisplay", () => { - test("未记录 Codex 思考强度时显示占位符", () => { - const html = renderToStaticMarkup(); - - expect(html).toContain(">-"); - expect(html).not.toContain('data-slot="codex-reasoning-effort"'); - }); - - test("显示 Codex 请求中的思考强度", () => { - const html = renderToStaticMarkup( - - ); - - expect(html).toContain('data-slot="codex-reasoning-effort"'); - expect(html).toContain("high"); - expect(html).toContain("tooltip"); - expect(html).not.toContain("overridden"); - }); - - test("供应商覆写时显示请求值和实际值", () => { - const html = renderToStaticMarkup( - - ); - - expect(html).toContain("low"); - expect(html).toContain("max"); - expect(html).toContain("overridden"); - expect(html).toContain("lucide-arrow-right"); - }); -}); diff --git a/src/app/[locale]/dashboard/logs/_components/column-visibility-dropdown.tsx b/src/app/[locale]/dashboard/logs/_components/column-visibility-dropdown.tsx index 651fdb298..9d6e59b64 100644 --- a/src/app/[locale]/dashboard/logs/_components/column-visibility-dropdown.tsx +++ b/src/app/[locale]/dashboard/logs/_components/column-visibility-dropdown.tsx @@ -33,6 +33,7 @@ const COLUMN_LABEL_KEYS: Record = { sessionId: "logs.columns.sessionId", ip: "logs.columns.ip", provider: "logs.columns.provider", + reasoningEffort: "logs.columns.reasoningEffort", tokens: "logs.columns.tokens", cost: "logs.columns.cost", cache: "logs.columns.cache", diff --git a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/LogicTraceTab.tsx b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/LogicTraceTab.tsx index c03f50fe5..0a143e9ef 100644 --- a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/LogicTraceTab.tsx +++ b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/LogicTraceTab.tsx @@ -8,6 +8,7 @@ import { Clock, Copy, Database, + DatabaseZap, Filter, GitBranch, Globe, @@ -39,6 +40,10 @@ function getRequestStatus(item: ProviderChainItem): StepStatus { if (item.reason === "session_reuse" || item.selectionMethod === "session_reuse") { return "session_reuse"; } + // Affinity hit is a reuse-style nomination step: same visual family as session reuse + if (item.reason === "affinity_hit" || item.selectionMethod === "prefix_affinity") { + return "session_reuse"; + } if ( item.reason === "request_success" || item.reason === "retry_success" || @@ -174,8 +179,15 @@ export function LogicTraceTab({ const sessionReuseContext = isSessionReuseFlow ? providerChain?.[0]?.decisionContext : undefined; const sessionReuseProvider = isSessionReuseFlow ? providerChain?.[0] : undefined; - // Extract decision context from first chain item (not used for session reuse) - const decisionContext = isSessionReuseFlow ? undefined : providerChain?.[0]?.decisionContext; + // F3a: prefix affinity hit flow (cache-reuse nomination replaces initial selection) + const isAffinityHitFlow = + providerChain?.[0]?.reason === "affinity_hit" || + providerChain?.[0]?.selectionMethod === "prefix_affinity"; + + // Extract decision context from first chain item (not used for reuse-style flows, + // whose first item carries an empty placeholder context) + const decisionContext = + isSessionReuseFlow || isAffinityHitFlow ? undefined : providerChain?.[0]?.decisionContext; // Extract filtered providers from all chain items (not applicable for session reuse) const filteredProviders = isSessionReuseFlow @@ -234,8 +246,36 @@ export function LogicTraceTab({ )} + {/* F2 Replay Serve Info (cache hit served without upstream call) */} + {blockedBy === "replay_serve" && ( +
+
+ + + {t("replayServe.title")} + + {parsedBlockedReason?.source && ( + + {tChain.has(`replayServe.sources.${parsedBlockedReason.source}`) + ? tChain(`replayServe.sources.${parsedBlockedReason.source}`) + : parsedBlockedReason.source} + + )} +
+

{t("replayServe.desc")}

+ {parsedBlockedReason?.replayId && ( +
+ {t("replayServe.replayId")}: + + {parsedBlockedReason.replayId} + +
+ )} +
+ )} + {/* Block Info */} - {isBlocked && blockedBy && ( + {isBlocked && blockedBy && blockedBy !== "replay_serve" && (
@@ -282,6 +322,8 @@ export function LogicTraceTab({ ) : isSessionReuseFlow ? ( + ) : isAffinityHitFlow ? ( + ) : ( )} @@ -297,6 +339,13 @@ export function LogicTraceTab({ > {t("logicTrace.sessionReuse")} + ) : isAffinityHitFlow ? ( + + {tChain("reasons.affinity_hit")} + ) : ( <> @@ -820,6 +869,8 @@ export function LogicTraceTab({ const isRetry = item.attemptNumber && item.attemptNumber > 1; const isSessionReuse = item.reason === "session_reuse" || item.selectionMethod === "session_reuse"; + const isAffinityHit = + item.reason === "affinity_hit" || item.selectionMethod === "prefix_affinity"; // Determine icon based on type const isHedgeTriggered = item.reason === "hedge_triggered"; @@ -835,35 +886,39 @@ export function LogicTraceTab({ : null; const stepIcon = isSessionReuse ? Link2 - : isHedgeTriggered - ? GitBranch - : isHedgeLoser || isHedgeLoserBilled || isClientAbort - ? XCircle - : isRetry - ? RefreshCw - : status === "success" - ? CheckCircle - : status === "failure" - ? XCircle - : Server; + : isAffinityHit + ? DatabaseZap + : isHedgeTriggered + ? GitBranch + : isHedgeLoser || isHedgeLoserBilled || isClientAbort + ? XCircle + : isRetry + ? RefreshCw + : status === "success" + ? CheckCircle + : status === "failure" + ? XCircle + : Server; // Determine title based on type // For session reuse flow, show simplified "Execute Request" title for the first item const stepTitle = isSessionReuse ? t("logicTrace.executeRequest") - : isHedgeTriggered - ? tChain("timeline.hedgeTriggered") - : isHedgeLoser - ? tChain("timeline.hedgeLoserCancelled") - : isHedgeLoserBilled - ? tChain("timeline.hedgeLoserBilled") - : isClientAbort - ? tChain("timeline.clientAbort") - : isRetry - ? t("logicTrace.retryAttempt", { number: item.attemptNumber ?? 1 }) - : item.reason === "hedge_winner" - ? tChain("timeline.hedgeWinner") - : t("logicTrace.attemptProvider", { provider: item.name }); + : isAffinityHit + ? tChain("reasons.affinity_hit") + : isHedgeTriggered + ? tChain("timeline.hedgeTriggered") + : isHedgeLoser + ? tChain("timeline.hedgeLoserCancelled") + : isHedgeLoserBilled + ? tChain("timeline.hedgeLoserBilled") + : isClientAbort + ? tChain("timeline.clientAbort") + : isRetry + ? t("logicTrace.retryAttempt", { number: item.attemptNumber ?? 1 }) + : item.reason === "hedge_winner" + ? tChain("timeline.hedgeWinner") + : t("logicTrace.attemptProvider", { provider: item.name }); return ( )} + {/* F3a Affinity Hit Info */} + {isAffinityHit && ( +
+
+ + {tChain("reasons.affinity_hit")} +
+
+ {item.affinity?.matchedDepth != null && ( +
+ + {tChain("affinity.matchedDepth")}: + {" "} + {item.affinity.matchedDepth} +
+ )} + {item.affinity?.matchedPrefixBytes != null && ( +
+ + {tChain("affinity.matchedPrefixBytes")}: + {" "} + {item.affinity.matchedPrefixBytes} +
+ )} + {item.affinity?.matchedFp && ( +
+ + {tChain("affinity.matchedFp")}: + {" "} + + {item.affinity.matchedFp} + +
+ )} +
+
+ )} + + {/* F1 Stream Gate Commit Marker */} + {item.streamGate && ( +
+
+ + {tChain("streamGate.title")} +
+
+
+ + {tChain("streamGate.frameIndex")}: + {" "} + #{item.streamGate.frameIndex} +
+
+ + {tChain("streamGate.chunkIndex")}: + {" "} + #{item.streamGate.chunkIndex} +
+ {item.streamGate.eventName && ( +
+ + {tChain("streamGate.eventName")}: + {" "} + + {item.streamGate.eventName} + +
+ )} +
+ + {tChain("streamGate.bufferedBytes")}: + {" "} + {item.streamGate.bufferedBytes} +
+ {item.streamGate.echoExcludedBytes > 0 && ( +
+ + {tChain("streamGate.echoExcludedBytes")}: + {" "} + {item.streamGate.echoExcludedBytes} +
+ )} +
+ + {tChain("streamGate.gateWaitMs")}: + {" "} + {item.streamGate.gateWaitMs}ms +
+
+
+ )} + {/* Basic Info */}
diff --git a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx index 9fce9036a..7e70d4679 100644 --- a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx +++ b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx @@ -26,8 +26,6 @@ import { Button } from "@/components/ui/button"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { Link } from "@/i18n/routing"; import { cn, formatTokenAmount } from "@/lib/utils"; -import { extractAnthropicEffortInfo } from "@/lib/utils/anthropic-effort"; -import { extractCodexReasoningEffortInfo } from "@/lib/utils/codex-reasoning-effort"; import { formatCurrency } from "@/lib/utils/currency"; import { buildHedgeBillingTable } from "@/lib/utils/hedge-billing"; import { resolveModelAuditDisplay } from "@/lib/utils/model-audit-display"; @@ -36,6 +34,7 @@ import { getThinkingSignatureModelDetectionSpecialSetting, hasPriorityServiceTierSpecialSetting, } from "@/lib/utils/special-settings"; +import { extractThinkingEffortInfo } from "@/lib/utils/thinking-effort"; import { getFake200ReasonKey } from "../../fake200-reason"; import { Fake200RetryTooltip } from "../../fake200-retry-tooltip"; import { @@ -109,27 +108,18 @@ export function SummaryTab({ getThinkingSignatureModelDetectionSpecialSetting(specialSettings); const showNoSignatureBadge = thinkingSignatureDetection?.source === "fallback_no_signature_with_thinking"; - const anthropicEffortInfo = extractAnthropicEffortInfo(specialSettings); - const codexReasoningEffortInfo = extractCodexReasoningEffortInfo(specialSettings); - const effortDisplay = codexReasoningEffortInfo + const thinkingEffortInfo = extractThinkingEffortInfo(specialSettings); + const effortMessageKey = thinkingEffortInfo?.source === "codex" ? "reasoningEffort" : "effort"; + const effortDisplay = thinkingEffortInfo ? { - requestedEffort: codexReasoningEffortInfo.requestedEffort, - effectiveEffort: codexReasoningEffortInfo.effectiveEffort, - isOverridden: codexReasoningEffortInfo.isOverridden, - label: t("reasoningEffort.label"), - tooltip: t("reasoningEffort.tooltip"), - overridden: t("reasoningEffort.overridden"), + requestedEffort: thinkingEffortInfo.requestedEffort, + effectiveEffort: thinkingEffortInfo.effectiveEffort, + isOverridden: thinkingEffortInfo.isOverridden, + label: t(`${effortMessageKey}.label`), + tooltip: t(`${effortMessageKey}.tooltip`), + overridden: t(`${effortMessageKey}.overridden`), } - : anthropicEffortInfo - ? { - requestedEffort: anthropicEffortInfo.originalEffort, - effectiveEffort: anthropicEffortInfo.overriddenEffort, - isOverridden: anthropicEffortInfo.isOverridden, - label: t("effort.label"), - tooltip: t("effort.tooltip"), - overridden: t("effort.overridden"), - } - : null; + : null; const isFake200PostStreamFailure = typeof errorMessage === "string" && errorMessage.startsWith("FAKE_200_"); const fake200Code = diff --git a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/types.ts b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/types.ts index dc1ea1247..d68695b61 100644 --- a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/types.ts +++ b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/types.ts @@ -114,9 +114,14 @@ export interface MetadataTabProps extends TabSharedProps { /** * Parse blocked reason JSON string */ -export function parseBlockedReason( - blockedReason: string | null | undefined -): { word?: string; matchType?: string; matchedText?: string } | null { +export function parseBlockedReason(blockedReason: string | null | undefined): { + word?: string; + matchType?: string; + matchedText?: string; + // F2 replay_serve audit payload + source?: string; + replayId?: string; +} | null { if (!blockedReason) return null; try { return JSON.parse(blockedReason); diff --git a/src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx b/src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx index 3b5e5b6f2..a3d519c75 100644 --- a/src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx +++ b/src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx @@ -5,6 +5,7 @@ import { CheckCircle, ChevronRight, Clock3, + DatabaseZap, GitBranch, InfoIcon, Link2, @@ -286,6 +287,13 @@ function getItemStatus(item: ProviderChainItem): { bgColor: "bg-amber-50 dark:bg-amber-950/30", }; } + if (item.reason === "affinity_hit") { + return { + icon: DatabaseZap, + color: "text-teal-600", + bgColor: "bg-teal-50 dark:bg-teal-950/30", + }; + } return { icon: RefreshCw, color: "text-slate-500", @@ -512,6 +520,14 @@ export function ProviderChainPopover({ const isSessionReuse = chain[0]?.reason === "session_reuse" || chain[0]?.selectionMethod === "session_reuse"; + // F3a: prefix affinity hit (cache reuse nomination); chain[0]-based like + // session reuse so mid-chain retries are not misread as cache reuse + const affinityHitItem = + chain[0]?.reason === "affinity_hit" || chain[0]?.selectionMethod === "prefix_affinity" + ? chain[0] + : undefined; + const isAffinityHit = Boolean(affinityHitItem); + // Get initial selection context for tooltip const initialSelection = chain.find((item) => item.reason === "initial_selection"); const selectionContext = initialSelection?.decisionContext; @@ -538,6 +554,10 @@ export function ProviderChainPopover({ {isSessionReuse && !isLeaseConflictProtection && ( )} + {/* Affinity hit (cache reuse) indicator */} + {isAffinityHit && !isSessionReuse && !isLeaseConflictProtection && ( + + )} {/* Initial selection: show compact priority badge before name */} {!isSessionReuse && selectionContext && ( @@ -668,6 +688,48 @@ export function ProviderChainPopover({
)} + {/* Affinity hit (cache reuse) detailed info */} + {isAffinityHit && !isSessionReuse && ( +
+
+ + {tChain("reasons.affinity_hit")} +
+
+ {affinityHitItem?.affinity?.matchedDepth != null && ( +
+ + {tChain("affinity.matchedDepth")}: + {" "} + + {affinityHitItem.affinity.matchedDepth} + +
+ )} + {affinityHitItem?.affinity?.matchedPrefixBytes != null && ( +
+ + {tChain("affinity.matchedPrefixBytes")}: + {" "} + + {affinityHitItem.affinity.matchedPrefixBytes} + +
+ )} + {affinityHitItem?.affinity?.matchedFp && ( +
+ + {tChain("affinity.matchedFp")}: + {" "} + + {affinityHitItem.affinity.matchedFp.slice(0, 16)} + +
+ )} +
+
+ )} + {/* Initial selection detailed info */} {!isSessionReuse && selectionContext && (
diff --git a/src/app/[locale]/dashboard/logs/_components/thinking-effort-display.test.tsx b/src/app/[locale]/dashboard/logs/_components/thinking-effort-display.test.tsx new file mode 100644 index 000000000..d9f75b192 --- /dev/null +++ b/src/app/[locale]/dashboard/logs/_components/thinking-effort-display.test.tsx @@ -0,0 +1,182 @@ +import type { ReactNode } from "react"; +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, test, vi } from "vitest"; +import { ThinkingEffortDisplay } from "./thinking-effort-display"; + +vi.mock("next-intl", () => ({ + useTranslations: () => (key: string) => key, +})); + +vi.mock("@/components/ui/tooltip", () => ({ + TooltipProvider: ({ children }: { children?: ReactNode }) =>
{children}
, + Tooltip: ({ children }: { children?: ReactNode }) =>
{children}
, + TooltipTrigger: ({ children }: { children?: ReactNode }) =>
{children}
, + TooltipContent: ({ children }: { children?: ReactNode }) =>
{children}
, +})); + +describe("ThinkingEffortDisplay", () => { + test("未记录思考强度时显示占位符", () => { + const html = renderToStaticMarkup(); + + expect(html).toContain(">-"); + expect(html).not.toContain('data-slot="thinking-effort"'); + }); + + test("显示 Codex 请求中的思考强度", () => { + const html = renderToStaticMarkup( + + ); + + expect(html).toContain('data-slot="thinking-effort"'); + expect(html).toContain("high"); + expect(html).toContain("reasoningEffort.tooltip"); + expect(html).not.toContain("overridden"); + }); + + test("供应商覆写 Codex 强度时显示请求值和实际值", () => { + const html = renderToStaticMarkup( + + ); + + expect(html).toContain("low"); + expect(html).toContain("max"); + expect(html).toContain("reasoningEffort.overridden"); + expect(html).toContain("lucide-arrow-right"); + }); + + test("显示 Anthropic 请求中的思考强度", () => { + const html = renderToStaticMarkup( + + ); + + expect(html).toContain('data-slot="thinking-effort"'); + expect(html).toContain("medium"); + expect(html).toContain("effort.tooltip"); + expect(html).not.toContain("overridden"); + }); + + test("供应商覆写 Anthropic 强度时显示请求值和实际值", () => { + const html = renderToStaticMarkup( + + ); + + expect(html).toContain("medium"); + expect(html).toContain("high"); + expect(html).toContain("effort.overridden"); + expect(html).toContain("lucide-arrow-right"); + }); + + test("供应商剥离 Anthropic 强度时仅显示请求值与覆写说明", () => { + const html = renderToStaticMarkup( + + ); + + expect(html).toContain("medium"); + expect(html).toContain("effort.overridden"); + expect(html).not.toContain("lucide-arrow-right"); + }); + + test("同时存在两种审计时优先展示 Codex 强度", () => { + const html = renderToStaticMarkup( + + ); + + expect(html).toContain("xhigh"); + expect(html).toContain("reasoningEffort.tooltip"); + expect(html).not.toContain(">medium<"); + }); +}); diff --git a/src/app/[locale]/dashboard/logs/_components/codex-reasoning-effort-display.tsx b/src/app/[locale]/dashboard/logs/_components/thinking-effort-display.tsx similarity index 58% rename from src/app/[locale]/dashboard/logs/_components/codex-reasoning-effort-display.tsx rename to src/app/[locale]/dashboard/logs/_components/thinking-effort-display.tsx index eb7098da2..48ed68ba3 100644 --- a/src/app/[locale]/dashboard/logs/_components/codex-reasoning-effort-display.tsx +++ b/src/app/[locale]/dashboard/logs/_components/thinking-effort-display.tsx @@ -4,35 +4,38 @@ import { ArrowRight } from "lucide-react"; import { useTranslations } from "next-intl"; import { ThinkingEffortBadge } from "@/components/customs/thinking-effort-badge"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; -import { extractCodexReasoningEffortInfo } from "@/lib/utils/codex-reasoning-effort"; +import { extractThinkingEffortInfo } from "@/lib/utils/thinking-effort"; import type { SpecialSetting } from "@/types/special-settings"; -/** Codex 思考强度展示属性。 */ -interface CodexReasoningEffortDisplayProps { +/** 思考强度展示属性。 */ +interface ThinkingEffortDisplayProps { /** 使用记录中的请求参数与供应商覆写审计。 */ specialSettings: SpecialSetting[] | null | undefined; } /** - * 在使用记录中展示 Codex reasoning.effort。 + * 在使用记录中展示任意模型的思考强度(Codex reasoning.effort 或 Anthropic effort)。 * * 供应商改变强度时同时展示请求值和实际转发值,避免只看到客户端参数而误判上游行为。 */ -export function CodexReasoningEffortDisplay({ specialSettings }: CodexReasoningEffortDisplayProps) { - const t = useTranslations("dashboard.logs.details.reasoningEffort"); - const effortInfo = extractCodexReasoningEffortInfo(specialSettings); +export function ThinkingEffortDisplay({ specialSettings }: ThinkingEffortDisplayProps) { + const t = useTranslations("dashboard.logs.details"); + const effortInfo = extractThinkingEffortInfo(specialSettings); if (!effortInfo) { return -; } + const messageNamespace = effortInfo.source === "codex" ? "reasoningEffort" : "effort"; + const showEffectiveBadge = effortInfo.isOverridden && effortInfo.effectiveEffort != null; + return ( {effortInfo.requestedEffort && ( )} - {effortInfo.isOverridden && effortInfo.requestedEffort && ( + {showEffectiveBadge && effortInfo.requestedEffort && ( -

{t("tooltip")}

+

{t(`${messageNamespace}.tooltip`)}

{effortInfo.isOverridden && ( -

{t("overridden")}

+

{t(`${messageNamespace}.overridden`)}

)}
diff --git a/src/app/[locale]/dashboard/logs/_components/usage-logs-table.test.tsx b/src/app/[locale]/dashboard/logs/_components/usage-logs-table.test.tsx index d57182e3d..7b9dd0f0a 100644 --- a/src/app/[locale]/dashboard/logs/_components/usage-logs-table.test.tsx +++ b/src/app/[locale]/dashboard/logs/_components/usage-logs-table.test.tsx @@ -104,7 +104,7 @@ const discoveryTrace: RoutingTraceV1 = { events: [], }; -describe("usage-logs-table Codex reasoning effort", () => { +describe("usage-logs-table thinking effort", () => { test("在计费模型右侧显示思考强度列", () => { const html = renderToStaticMarkup( { expect(cells[7]?.textContent).toContain("max"); expect(cells[7]?.className).toContain("overflow-hidden"); }); + + test("显示 Anthropic 请求的思考强度", () => { + const html = renderToStaticMarkup( + {}} + isPending={false} + /> + ); + const container = document.createElement("div"); + container.innerHTML = html; + + const cells = [...container.querySelectorAll("tbody tr:first-child td")]; + expect(cells[7]?.textContent).toContain("medium"); + }); + + test("hiddenColumns 含 reasoningEffort 时隐藏思考强度列", () => { + const html = renderToStaticMarkup( + {}} + isPending={false} + hiddenColumns={["reasoningEffort"]} + /> + ); + const container = document.createElement("div"); + container.innerHTML = html; + + const headers = [...container.querySelectorAll("thead th")].map((node) => node.textContent); + expect(headers).not.toContain("logs.columns.reasoningEffort"); + expect(headers).toHaveLength(12); + expect(container.querySelectorAll("tbody tr:first-child td")).toHaveLength(12); + expect(html).not.toContain('data-slot="thinking-effort"'); + }); + + test("隐藏思考强度列后空态占满全部可见列", () => { + const html = renderToStaticMarkup( + {}} + isPending={false} + hiddenColumns={["reasoningEffort"]} + /> + ); + const container = document.createElement("div"); + container.innerHTML = html; + + const emptyCell = container.querySelector("tbody td"); + expect(emptyCell?.getAttribute("colspan")).toBe("12"); + }); }); describe("usage-logs-table multiplier badge", () => { diff --git a/src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx b/src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx index a60d16849..880eae98a 100644 --- a/src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx +++ b/src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx @@ -17,6 +17,7 @@ import { TableRow, } from "@/components/ui/table"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import type { LogsTableColumn } from "@/lib/column-visibility"; import { cn, formatTokenAmount } from "@/lib/utils"; import { copyTextToClipboard } from "@/lib/utils/clipboard"; import type { CurrencyCode } from "@/lib/utils/currency"; @@ -36,10 +37,10 @@ import { } from "@/lib/utils/special-settings"; import type { UsageLogRow } from "@/repository/usage-logs"; import type { BillingModelSource } from "@/types/system-config"; -import { CodexReasoningEffortDisplay } from "./codex-reasoning-effort-display"; import { ErrorDetailsDialog } from "./error-details-dialog"; import { ModelDisplayWithRedirect } from "./model-display-with-redirect"; import { ProviderChainPopover } from "./provider-chain-popover"; +import { ThinkingEffortDisplay } from "./thinking-effort-display"; interface UsageLogsTableProps { logs: UsageLogRow[]; @@ -51,6 +52,7 @@ interface UsageLogsTableProps { newLogIds?: Set; // 新增记录 ID 集合(用于动画高亮) currencyCode?: CurrencyCode; billingModelSource?: BillingModelSource; + hiddenColumns?: LogsTableColumn[]; } export function UsageLogsTable({ @@ -63,10 +65,13 @@ export function UsageLogsTable({ newLogIds, currencyCode = "USD", billingModelSource = "original", + hiddenColumns, }: UsageLogsTableProps) { const t = useTranslations("dashboard"); const tChain = useTranslations("provider-chain"); const totalPages = Math.ceil(total / pageSize); + const hideReasoningEffortColumn = hiddenColumns?.includes("reasoningEffort") ?? false; + const visibleColumnCount = 13 - (hideReasoningEffortColumn ? 1 : 0); const getPricingSourceLabel = (source: string) => t(`logs.billingDetails.pricingSource.${source}`); @@ -106,7 +111,11 @@ export function UsageLogsTable({ {t("logs.columns.ip")} {t("logs.columns.provider")} {t("logs.columns.model")} - {t("logs.columns.reasoningEffort")} + {hideReasoningEffortColumn ? null : ( + + {t("logs.columns.reasoningEffort")} + + )} {t("logs.columns.tokens")} {t("logs.columns.cache")} {t("logs.columns.cost")} @@ -117,7 +126,10 @@ export function UsageLogsTable({ {logs.length === 0 ? ( - + {t("logs.table.noData")} @@ -304,9 +316,11 @@ export function UsageLogsTable({
- - - + {hideReasoningEffortColumn ? null : ( + + + + )} diff --git a/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.test.tsx b/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.test.tsx index 27136c7a1..a16e6c5e4 100644 --- a/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.test.tsx +++ b/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.test.tsx @@ -195,7 +195,7 @@ function renderCostTooltipWithLog(overrides: Partial) { return tooltip; } -describe("virtualized-logs-table Codex reasoning effort", () => { +describe("virtualized-logs-table thinking effort", () => { test("在计费模型右侧显示思考强度列", () => { const html = renderTableWithLog({ model: "gpt-5.4", @@ -229,11 +229,57 @@ describe("virtualized-logs-table Codex reasoning effort", () => { expect(effortIndex).toBeGreaterThan(modelIndex); expect(tokensIndex).toBeGreaterThan(effortIndex); - const effortDisplay = container.querySelector('[data-slot="codex-reasoning-effort"]'); + const effortDisplay = container.querySelector('[data-slot="thinking-effort"]'); expect(effortDisplay?.textContent).toContain("low"); expect(effortDisplay?.textContent).toContain("max"); expect(effortDisplay?.closest(".overflow-hidden")).not.toBeNull(); }); + + test("显示 Anthropic 请求的思考强度", () => { + const html = renderTableWithLog({ + model: "claude-opus-4-5", + specialSettings: [ + { + type: "anthropic_effort", + scope: "request", + hit: true, + effort: "medium", + }, + ], + }); + const container = document.createElement("div"); + container.innerHTML = html; + + const effortDisplay = container.querySelector('[data-slot="thinking-effort"]'); + expect(effortDisplay?.textContent).toContain("medium"); + }); + + test("hides reasoning effort column when hiddenColumns includes reasoningEffort", () => { + mockIsLoading = false; + mockIsError = false; + mockError = null; + mockHasNextPage = false; + mockIsFetchingNextPage = false; + + mockLogs = [ + makeLog({ + id: 1, + specialSettings: [ + { type: "codex_reasoning_effort", scope: "request", hit: true, effort: "high" }, + ], + }), + ]; + + const htmlHidden = renderToStaticMarkup( + + ); + expect(htmlHidden).not.toContain("logs.columns.reasoningEffort"); + expect(htmlHidden).not.toContain('data-slot="thinking-effort"'); + }); }); describe("virtualized-logs-table multiplier badge", () => { diff --git a/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx b/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx index 38f497eb9..a8235282f 100644 --- a/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx +++ b/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx @@ -42,10 +42,10 @@ import { isProviderFinalized } from "@/lib/utils/provider-display"; import { hasPriorityServiceTierSpecialSetting } from "@/lib/utils/special-settings"; import type { UsageLogRow, UsageLogsBatchResult } from "@/repository/usage-logs"; import type { BillingModelSource } from "@/types/system-config"; -import { CodexReasoningEffortDisplay } from "./codex-reasoning-effort-display"; import { ErrorDetailsDialog } from "./error-details-dialog"; import { ModelDisplayWithRedirect } from "./model-display-with-redirect"; import { ProviderChainPopover } from "./provider-chain-popover"; +import { ThinkingEffortDisplay } from "./thinking-effort-display"; const BATCH_SIZE = 50; const ROW_HEIGHT = 52; // Estimated row height in pixels @@ -139,6 +139,7 @@ export function VirtualizedLogsTable({ const shouldPoll = autoRefreshEnabled && !isHistoryBrowsing; const hideProviderColumn = hiddenColumns?.includes("provider") ?? false; + const hideReasoningEffortColumn = hiddenColumns?.includes("reasoningEffort") ?? false; const hideUserColumn = hiddenColumns?.includes("user") ?? false; const hideKeyColumn = hiddenColumns?.includes("key") ?? false; const hideSessionIdColumn = hiddenColumns?.includes("sessionId") ?? false; @@ -693,12 +694,14 @@ export function VirtualizedLogsTable({ > {t("logs.columns.model")}
-
- {t("logs.columns.reasoningEffort")} -
+ {hideReasoningEffortColumn ? null : ( +
+ {t("logs.columns.reasoningEffort")} +
+ )} {hideTokensColumn ? null : (
- {/* Codex Reasoning Effort */} -
- -
+ {/* Thinking Effort */} + {hideReasoningEffortColumn ? null : ( +
+ +
+ )} {/* Tokens */} {hideTokensColumn ? null : ( 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 56d47ae15..1969576cf 100644 --- a/src/app/[locale]/settings/config/_components/system-settings-form.tsx +++ b/src/app/[locale]/settings/config/_components/system-settings-form.tsx @@ -9,10 +9,12 @@ import { Eye, FileCode, Filter, + Gauge, Globe, MapPin, Network, Pencil, + Repeat, Route, Terminal, Thermometer, @@ -95,6 +97,8 @@ interface SystemSettingsFormProps { | "fakeStreamingWhitelist" | "streamGateMode" | "affinityIgnoreClientSessionId" + | "replayEnabled" + | "cacheEffectivenessEnabled" | "enableCodexSessionIdCompletion" | "enableClaudeMetadataUserIdInjection" | "enableResponseFixer" @@ -210,6 +214,12 @@ export function SystemSettingsForm({ const [affinityIgnoreClientSessionId, setAffinityIgnoreClientSessionId] = useState( initialSettings.affinityIgnoreClientSessionId ); + // null = 跟随环境变量(Replay 默认关 / 缓存模拟默认开):未触碰开关时按 null 原样保存, + // 避免无关字段的保存把覆写写死为布尔值;仅用户切换后才落显式值 + const [replayEnabled, setReplayEnabled] = useState(initialSettings.replayEnabled); + const [cacheEffectivenessEnabled, setCacheEffectivenessEnabled] = useState( + initialSettings.cacheEffectivenessEnabled + ); const [enableThinkingBudgetRectifier, setEnableThinkingBudgetRectifier] = useState( initialSettings.enableThinkingBudgetRectifier ); @@ -395,6 +405,8 @@ export function SystemSettingsForm({ fakeStreamingWhitelist: sanitizedFakeStreamingWhitelist, streamGateMode, affinityIgnoreClientSessionId, + replayEnabled, + cacheEffectivenessEnabled, enableThinkingBudgetRectifier, enableThinkingEffortConflictRectifier, enableGeminiFunctionIdRectifier, @@ -459,6 +471,8 @@ export function SystemSettingsForm({ ); setStreamGateMode(result.data.streamGateMode); setAffinityIgnoreClientSessionId(result.data.affinityIgnoreClientSessionId); + setReplayEnabled(result.data.replayEnabled ?? null); + setCacheEffectivenessEnabled(result.data.cacheEffectivenessEnabled ?? null); setEnableThinkingBudgetRectifier(result.data.enableThinkingBudgetRectifier); setEnableThinkingEffortConflictRectifier(result.data.enableThinkingEffortConflictRectifier); setEnableGeminiFunctionIdRectifier(result.data.enableGeminiFunctionIdRectifier); @@ -1159,6 +1173,50 @@ export function SystemSettingsForm({ />
+ {/* F2 Request Replay */} +
+
+
+ +
+
+

{t("replayEnabled")}

+

{t("replayEnabledDesc")}

+
+
+ setReplayEnabled(checked)} + disabled={isPending} + /> +
+ + {/* F3b Cache Effectiveness Simulation */} +
+
+
+ +
+
+

+ {t("cacheEffectivenessEnabled")} +

+

+ {t("cacheEffectivenessEnabledDesc")} +

+
+
+ setCacheEffectivenessEnabled(checked)} + disabled={isPending} + /> +
+ {/* Enable Codex Session ID Completion */}
diff --git a/src/app/[locale]/settings/config/page.tsx b/src/app/[locale]/settings/config/page.tsx index 192369a1e..7da0a1264 100644 --- a/src/app/[locale]/settings/config/page.tsx +++ b/src/app/[locale]/settings/config/page.tsx @@ -80,6 +80,8 @@ async function SettingsConfigContent({ locale }: { locale: string }) { fakeStreamingWhitelist: settings.fakeStreamingWhitelist, streamGateMode: settings.streamGateMode, affinityIgnoreClientSessionId: settings.affinityIgnoreClientSessionId, + replayEnabled: settings.replayEnabled, + cacheEffectivenessEnabled: settings.cacheEffectivenessEnabled, enableCodexSessionIdCompletion: settings.enableCodexSessionIdCompletion, enableClaudeMetadataUserIdInjection: settings.enableClaudeMetadataUserIdInjection, enableResponseFixer: settings.enableResponseFixer, diff --git a/src/app/v1/_lib/proxy/affinity/affinity-recorder.ts b/src/app/v1/_lib/proxy/affinity/affinity-recorder.ts index 1f212e40b..8eca681c8 100644 --- a/src/app/v1/_lib/proxy/affinity/affinity-recorder.ts +++ b/src/app/v1/_lib/proxy/affinity/affinity-recorder.ts @@ -11,7 +11,8 @@ import { fingerprintTip } from "./fingerprint"; */ /** - * 成功终态写回:tip + sys 两键绑定到胜出供应商,滑动 TTL。 + * 成功终态写回:tip 单键绑定到胜出供应商,滑动 TTL。 + * 不写 F_sys 键:仅系统提示词相同不构成前缀亲和(防跨对话过宽匹配)。 * 调用点:流式 commitSideEffects(计费持久化成功后)与非流式成功分支。 * replay serve / 竞速败者 / 失败重试不得调用。 */ @@ -24,10 +25,11 @@ export async function recordAffinityWinner( try { if (!(await isAffinityRoutingEnabled())) return; const tip = fingerprintTip(affinity.chain); + // tip 落在系统段(无会话消息)时不写绑定:与查找侧的 sys 排除保持一致 + if (tip.depth === 0) return; await getAffinityStore().put( affinity.scopeTag, tip.fp, - affinity.chain.sys.fp, providerId, getEnvConfig().PREFIX_AFFINITY_TTL_SECONDS ); diff --git a/src/app/v1/_lib/proxy/affinity/affinity-store.ts b/src/app/v1/_lib/proxy/affinity/affinity-store.ts index e4f6c4b6b..08306fc8f 100644 --- a/src/app/v1/_lib/proxy/affinity/affinity-store.ts +++ b/src/app/v1/_lib/proxy/affinity/affinity-store.ts @@ -40,8 +40,6 @@ const TOMBSTONE_TTL_SECONDS = 60; export interface AffinityHint { providerId: number; - /** 命中的边界在传入序列中的位置换算出的深度语义 */ - tier: "conversation" | "system"; matchedFp: string; /** 0-based:0 = 最深(tip),越大越浅;仅用于观测 */ matchedIndex: number; @@ -76,7 +74,8 @@ export class AffinityStore { } /** - * 最长前缀查找。fpsDeepestFirst 为最深->最浅指纹序列(最后一个是 F_sys)。 + * 最长前缀查找。fpsDeepestFirst 为最深->最浅的会话消息边界指纹序列 + * (不含 F_sys:仅系统提示词相同不构成前缀命中)。 * 命中活跃绑定即返回并滑动续期;墓碑被 Lua 跳过继续向浅。 */ async lookup( @@ -111,8 +110,6 @@ export class AffinityStore { providerId, matchedIndex, matchedFp: fpsDeepestFirst[matchedIndex] ?? "", - // 最后一个键是 F_sys:仅系统提示词命中 - tier: matchedIndex >= fpsDeepestFirst.length - 1 ? "system" : "conversation", }; } catch (error) { logger.warn("[AffinityStore] lookup failed, falling back to no-affinity", { @@ -124,13 +121,13 @@ export class AffinityStore { } /** - * 成功终态写回:只写 tip + sys 两键(对话推进天然累积链条,无需写全窗口)。 + * 成功终态写回:只写 tip 一键(对话推进天然累积链条,无需写全窗口)。 + * 不写 F_sys 键:仅系统提示词相同的跨对话请求不应互相粘连。 * 仅 owner 成功请求调用;replay serve / 竞速败者 / 失败重试不写。 */ async put( scopeTag: string, tipFp: string, - sysFp: string, providerId: number, ttlSeconds: number ): Promise { @@ -141,9 +138,6 @@ export class AffinityStore { const value = `1|${providerId}`; try { await redis.set(this.buildKey(scopeTag, tipFp), value, "EX", ttlSeconds); - if (sysFp && sysFp !== tipFp) { - await redis.set(this.buildKey(scopeTag, sysFp), value, "EX", ttlSeconds); - } } catch (error) { logger.warn("[AffinityStore] put failed", { error: error instanceof Error ? error.message : String(error), diff --git a/src/app/v1/_lib/proxy/affinity/fingerprint.ts b/src/app/v1/_lib/proxy/affinity/fingerprint.ts index cd6be60ad..82041026b 100644 --- a/src/app/v1/_lib/proxy/affinity/fingerprint.ts +++ b/src/app/v1/_lib/proxy/affinity/fingerprint.ts @@ -48,13 +48,17 @@ export function fingerprintTip(chain: FingerprintChain): FingerprintBoundary { return chain.tail.length > 0 ? chain.tail[chain.tail.length - 1] : chain.sys; } -/** 供查找使用的最深 -> 最浅指纹序列(最后一个永远是 Sys)。 */ +/** + * 供查找使用的最深 -> 最浅指纹序列。 + * + * 只包含会话消息边界(tail),不含 F_sys:仅系统提示词 + 工具相同的 + * 跨对话请求不应命中亲和(过宽匹配会把无关对话粘到同一供应商)。 + */ export function fingerprintsDeepestFirst(chain: FingerprintChain): string[] { const out: string[] = []; for (let i = chain.tail.length - 1; i >= 0; i--) { out.push(chain.tail[i].fp); } - out.push(chain.sys.fp); return out; } diff --git a/src/app/v1/_lib/proxy/forwarder.ts b/src/app/v1/_lib/proxy/forwarder.ts index b7b40fd3a..f587d8ba9 100644 --- a/src/app/v1/_lib/proxy/forwarder.ts +++ b/src/app/v1/_lib/proxy/forwarder.ts @@ -119,6 +119,7 @@ import { resolveStreamGateCaps, resolveStreamGateMode, runStreamContentGate, + StreamPrecommitError, } from "./stream-gate/stream-content-gate"; import { detectThinkingBudgetRectifierTrigger, @@ -330,6 +331,8 @@ type StreamingHedgeAttempt = { * usage lives in the first chunk). */ firstChunk: Uint8Array | null; + /** F1 门控提交标记(该 attempt 门控提交时记录,随 hedge_winner 链条目落库)。 */ + gateAudit?: ProviderChainItem["streamGate"]; /** * Billing context snapshot for the INITIAL provider's losing attempt, captured BEFORE * commitWinner overwrites the shared session's model/context with the winner's. Null for @@ -565,6 +568,35 @@ export function mergeAnthropicCacheTtlBetaFlag(existing: string | null | undefin return Array.from(betaFlags).join(", "); } +/** + * 门控等待期静默超时 -> 524 streaming_idle_timeout。 + * 与提交后的静默超时同构:错误规则/熔断/切换逻辑无需区分静默发生在门控前后; + * 串行与 hedge 竞速路径共用,保证同一故障归类一致。 + */ +function buildStreamingIdleTimeoutError(provider: { + id: number; + name: string; + streamingIdleTimeoutMs: number; +}): ProxyError { + const parsed = { + error: { + type: "streaming_idle_timeout", + message: `Provider stopped sending data for ${provider.streamingIdleTimeoutMs}ms`, + timeout_ms: provider.streamingIdleTimeoutMs, + }, + }; + return new ProxyError( + `供应商流式响应静默超时: ${provider.streamingIdleTimeoutMs}ms 内未收到新数据`, + 524, + { + body: JSON.stringify(parsed), + parsed, + providerId: provider.id, + providerName: provider.name, + } + ); +} + function clampRetryAttempts(value: number): number { const numeric = Number(value); if (!Number.isFinite(numeric)) return RETRY_LIMITS.MIN; @@ -1655,6 +1687,7 @@ export class ProxyForwarder { // - 首字节计时器(doForward 设置,response-handler 读到首字节才清除) // 在门控期间继续生效,天然升级为「首个有效内容超时」。 let streamingResponse = response; + let gateChainAudit: ProviderChainItem["streamGate"]; const gateMode = resolveStreamGateMode(); if ( gateMode === "enforce" && @@ -1663,20 +1696,27 @@ export class ProxyForwarder { ) { const gateFamily = mapProviderTypeToFamily(currentProvider.providerType); if (gateFamily) { + const runtime = session as ProxySession & { + responseController?: AbortController; + clearResponseTimeout?: () => void; + releaseAgent?: () => void; + }; const gateReader = response.body.getReader(); + const gateStartedAt = Date.now(); const gate = await runStreamContentGate(gateReader, { family: gateFamily, providerId: currentProvider.id, providerName: currentProvider.name, ...resolveStreamGateCaps(), + // 首字节到达即清除首字节计时器,保持「首字节超时」的原始语义—— + // 思考型模型可在首个内容帧前长时间输出中性帧,不应触发该计时器 + onFirstByte: () => runtime.clearResponseTimeout?.(), + // 门控等待期沿用供应商静默超时(与提交后 response-handler 的行为对齐) + idleTimeoutMs: currentProvider.streamingIdleTimeoutMs, + captureCommitMarker: !session.isHighConcurrencyModeEnabled(), }); if (!gate.committed) { - const runtime = session as ProxySession & { - responseController?: AbortController; - clearResponseTimeout?: () => void; - releaseAgent?: () => void; - }; // 先于清理读取超时来源:区分首字节/首内容超时与客户端断开 const timedOutBeforeContent = runtime.responseController?.signal.aborted === true && @@ -1687,6 +1727,13 @@ export class ProxyForwarder { runtime.clearResponseTimeout?.(); runtime.releaseAgent?.(); + if ( + gate.error instanceof StreamPrecommitError && + gate.error.gateReason === "idle_timeout" + ) { + throw buildStreamingIdleTimeoutError(currentProvider); + } + if (timedOutBeforeContent) { throw new ProxyError( `供应商首个有效内容超时: 门控在收到有效内容帧前被首字节计时器中止`, @@ -1707,12 +1754,26 @@ export class ProxyForwarder { throw gate.error; } + if (gate.commitMarker) { + gateChainAudit = { + ...gate.commitMarker, + gateWaitMs: Date.now() - gateStartedAt, + }; + } + logger.info("ProxyForwarder: Stream content gate committed", { providerId: currentProvider.id, providerName: currentProvider.name, framesSeen: gate.framesSeen, prefixChunks: gate.prefixChunks.length, readerDone: gate.readerDone, + ...(gateChainAudit + ? { + commitEventName: gateChainAudit.eventName, + gateWaitMs: gateChainAudit.gateWaitMs, + echoExcludedBytes: gateChainAudit.echoExcludedBytes, + } + : {}), }); streamingResponse = new Response( @@ -1727,6 +1788,7 @@ export class ProxyForwarder { } setDeferredStreamingFinalization(session, { + ...(gateChainAudit ? { streamGate: gateChainAudit } : {}), providerId: currentProvider.id, providerName: currentProvider.name, providerPriority: currentProvider.priority || 0, @@ -4655,15 +4717,32 @@ export class ProxyForwarder { : null; if (hedgeGateFamily) { + const gateStartedAt = Date.now(); const gate = await runStreamContentGate(attempt.reader, { family: hedgeGateFamily, providerId: attempt.provider.id, providerName: attempt.provider.name, ...resolveStreamGateCaps(), + // 竞速路径首字节计时器已在响应头到达时清除;门控等待期沿用供应商静默超时 + idleTimeoutMs: attempt.provider.streamingIdleTimeoutMs, + captureCommitMarker: !session.isHighConcurrencyModeEnabled(), }); if (!gate.committed) { + if ( + gate.error instanceof StreamPrecommitError && + gate.error.gateReason === "idle_timeout" + ) { + // 与串行路径同一 524 归类,熔断/超时判定不因 hedge 模式而漂移 + throw buildStreamingIdleTimeoutError(attempt.provider); + } throw gate.error; } + if (gate.commitMarker) { + attempt.gateAudit = { + ...gate.commitMarker, + gateWaitMs: Date.now() - gateStartedAt, + }; + } // 保留完整门控前缀:若本 attempt 落败且需要计费,drain 时补回前缀里的 usage。 attempt.firstChunk = concatChunks(gate.prefixChunks); await commitWinner(attempt, gate.prefixChunks); @@ -5006,6 +5085,7 @@ export class ProxyForwarder { attemptNumber: attempt.sequence, statusCode: attempt.response.status, modelRedirect: getAttemptModelRedirect(attempt), + streamGate: attempt.gateAudit, }); abortAllAttempts(attempt, "hedge_loser"); diff --git a/src/app/v1/_lib/proxy/provider-selector.ts b/src/app/v1/_lib/proxy/provider-selector.ts index 2df37b345..464b5205e 100644 --- a/src/app/v1/_lib/proxy/provider-selector.ts +++ b/src/app/v1/_lib/proxy/provider-selector.ts @@ -6,7 +6,10 @@ import { logger } from "@/lib/logger"; import { RateLimitService } from "@/lib/rate-limit"; import { buildScopeTag } from "@/lib/request-identity"; import { SessionManager } from "@/lib/session-manager"; -import { getProxyRuntimeSettings } from "@/lib/system-settings/proxy-runtime"; +import { + getProxyRuntimeSettings, + isCacheEffectivenessEnabled, +} from "@/lib/system-settings/proxy-runtime"; import { parseProviderGroups, resolveBillingProviderGroups, @@ -367,8 +370,14 @@ export class ProxyProviderResolver { attempt: attemptCount, }); - // 只在首次选择时记录到决策链(重试时的记录由 forwarder.ts 在请求完成后统一记录) - if (attemptCount === 1) { + // 只在首次选择时记录到决策链(重试时的记录由 forwarder.ts 在请求完成后统一记录)。 + // 供应商由会话复用/亲和提名预先确定时,对应链条目已写入且携带真实 selectionMethod, + // 不得再补一条 initial_selection(否则决策链看起来全部是加权随机初选)。 + const lastChainItem = session.getProviderChain().at(-1); + const stickySelectionRecorded = + lastChainItem?.id === session.provider.id && + (lastChainItem.reason === "session_reuse" || lastChainItem.reason === "affinity_hit"); + if (attemptCount === 1 && !stickySelectionRecorded) { const successContext = session.getLastSelectionContext(); session.addProviderToChain(session.provider, { reason: "initial_selection", @@ -537,7 +546,7 @@ export class ProxyProviderResolver { if (!keyId) return false; const env = getEnvConfig(); - if (!affinityRoutingEnabled && !env.ENABLE_CACHE_EFFECTIVENESS) return false; + if (!affinityRoutingEnabled && !isCacheEffectivenessEnabled()) return false; const chain = computeFingerprintChain( session.request.message, @@ -551,7 +560,6 @@ export class ProxyProviderResolver { chain, nominatedProviderId: null, matchedFp: null, - matchedTier: null, }; return true; } @@ -577,7 +585,6 @@ export class ProxyProviderResolver { if (!hint) return; affinity.matchedFp = hint.matchedFp; - affinity.matchedTier = hint.tier; const provider = await ProxyProviderResolver.validateAffinityCandidate( session, @@ -587,17 +594,26 @@ export class ProxyProviderResolver { // 候选不过硬校验:软回落,不写墓碑(可能只是临时熔断/调度窗口外) logger.debug("ProviderSelector: Affinity candidate rejected by hard validation", { providerId: hint.providerId, - tier: hint.tier, + matchedIndex: hint.matchedIndex, }); return; } + // 命中边界详情:随决策链落库,供请求详情展示「具体匹配到哪个前缀」 + const matchedBoundary = + affinity.chain.tail.find((boundary) => boundary.fp === hint.matchedFp) ?? null; + affinity.nominatedProviderId = provider.id; session.setProvider(provider); session.addProviderToChain(provider, { reason: "affinity_hit", selectionMethod: "prefix_affinity", circuitState: getCircuitState(provider.id), + affinity: { + matchedDepth: matchedBoundary?.depth ?? null, + matchedPrefixBytes: matchedBoundary?.prefixBytes ?? null, + matchedFp: hint.matchedFp, + }, decisionContext: { totalProviders: 0, enabledProviders: 0, @@ -624,8 +640,8 @@ export class ProxyProviderResolver { logger.info("ProviderSelector: Prefix affinity nomination accepted", { providerId: provider.id, providerName: provider.name, - tier: hint.tier, matchedIndex: hint.matchedIndex, + matchedDepth: matchedBoundary?.depth ?? null, }); } catch (error) { // 亲和路径任何异常都不影响主选路 diff --git a/src/app/v1/_lib/proxy/replay/replay-guard.ts b/src/app/v1/_lib/proxy/replay/replay-guard.ts index c91fe78aa..065366192 100644 --- a/src/app/v1/_lib/proxy/replay/replay-guard.ts +++ b/src/app/v1/_lib/proxy/replay/replay-guard.ts @@ -3,6 +3,7 @@ import { db } from "@/drizzle/db"; import { messageRequest } from "@/drizzle/schema"; import { getEnvConfig } from "@/lib/config/env.schema"; import { logger } from "@/lib/logger"; +import { getProxyRuntimeSettings } from "@/lib/system-settings/proxy-runtime"; import type { ProxySession } from "../session"; import { deriveReplayIdentity, REPLAY_BYPASS_HEADER, type ReplayIdentity } from "./replay-identity"; import { getReplayStore, type ReplayMeta, type ReplayStore } from "./replay-store"; @@ -37,6 +38,9 @@ const ATTACH_MAX_WAIT_MS = 10 * 60 * 1000; export class ProxyReplayGuard { static async ensure(session: ProxySession): Promise { try { + // guard 位于 provider 步骤之前:先刷新运行时覆写快照,管理端刚保存的 + // replayEnabled 首个请求即生效(底层系统设置缓存有 TTL,常态为缓存命中) + await getProxyRuntimeSettings(); const identity = deriveReplayIdentity(session); if (!identity) return null; diff --git a/src/app/v1/_lib/proxy/replay/replay-identity.ts b/src/app/v1/_lib/proxy/replay/replay-identity.ts index e07a7cb67..652eef17e 100644 --- a/src/app/v1/_lib/proxy/replay/replay-identity.ts +++ b/src/app/v1/_lib/proxy/replay/replay-identity.ts @@ -1,5 +1,6 @@ import { getEnvConfig } from "@/lib/config/env.schema"; import { buildScopeTag, sha256Hex, stableStringify } from "@/lib/request-identity"; +import { getCachedProxyRuntimeSettings } from "@/lib/system-settings/proxy-runtime"; import type { ClientFormat } from "../format-mapper"; import type { ProxySession } from "../session"; @@ -35,10 +36,18 @@ export interface ReplayIdentity { export const REPLAY_BYPASS_HEADER = "x-cch-no-replay"; +/** F2 有效开关:系统设置覆写优先(同步快照),null/无快照时跟随 env。 */ +export function isReplayEnabled(): boolean { + try { + return getCachedProxyRuntimeSettings()?.replayEnabled ?? getEnvConfig().ENABLE_REQUEST_REPLAY; + } catch { + return false; + } +} + export function deriveReplayIdentity(session: ProxySession): ReplayIdentity | null { try { - const env = getEnvConfig(); - if (!env.ENABLE_REQUEST_REPLAY) return null; + if (!isReplayEnabled()) return null; if (session.getEndpointPolicy().kind !== "default") return null; if (session.method !== "POST") return null; diff --git a/src/app/v1/_lib/proxy/replay/replay-spool.ts b/src/app/v1/_lib/proxy/replay/replay-spool.ts index baa20cbe9..ab7f1b9f4 100644 --- a/src/app/v1/_lib/proxy/replay/replay-spool.ts +++ b/src/app/v1/_lib/proxy/replay/replay-spool.ts @@ -1,7 +1,7 @@ import { getEnvConfig } from "@/lib/config/env.schema"; import { logger } from "@/lib/logger"; import type { ProxySession } from "../session"; -import type { ReplayIdentity } from "./replay-identity"; +import { isReplayEnabled, type ReplayIdentity } from "./replay-identity"; import { getReplayStore, type ReplayMeta } from "./replay-store"; /** @@ -344,7 +344,7 @@ export function createReplaySpoolIfOwner( }; try { const env = getEnvConfig(); - if (!env.ENABLE_REQUEST_REPLAY) return declineOwnership(); + if (!isReplayEnabled()) return declineOwnership(); if (activeSpoolCount >= env.REPLAY_MAX_CONCURRENT_SPOOLS) { logger.debug("[ReplaySpool] concurrent spool cap reached, skipping replay", { active: activeSpoolCount, diff --git a/src/app/v1/_lib/proxy/response-handler.ts b/src/app/v1/_lib/proxy/response-handler.ts index f628f390c..5825ef903 100644 --- a/src/app/v1/_lib/proxy/response-handler.ts +++ b/src/app/v1/_lib/proxy/response-handler.ts @@ -18,6 +18,7 @@ import type { SessionBindingSnapshot } from "@/lib/redis/session-binding"; import { SessionManager } from "@/lib/session-manager"; import { SessionTracker } from "@/lib/session-tracker"; import { CODEX_1M_CONTEXT_TOKEN_THRESHOLD } from "@/lib/special-attributes"; +import { isCacheEffectivenessEnabled } from "@/lib/system-settings/proxy-runtime"; import type { CostBreakdown, RequestCostCalculationOptions, @@ -2132,6 +2133,7 @@ function finalizeDeferredStreamingFinalizationIfNeeded( reason: meta.isFirstAttempt ? "request_success" : "retry_success", attemptNumber: meta.attemptNumber, statusCode: meta.upstreamStatusCode, + streamGate: meta.streamGate, }); } @@ -4456,7 +4458,7 @@ export class ProxyResponseHandler { latestStreamCommitSideEffects = postTerminalSideEffects; // F3b 缓存模拟列:仅开关开启时派生(关闭时保持 undefined,不落值) - const cacheScoreFields = getEnvConfig().ENABLE_CACHE_EFFECTIVENESS + const cacheScoreFields = isCacheEffectivenessEnabled() ? computeCacheScoreFields({ affinity: session.affinity, succeeded: effectiveStatusCode >= 200 && effectiveStatusCode < 300, diff --git a/src/app/v1/_lib/proxy/session.ts b/src/app/v1/_lib/proxy/session.ts index b14042f5c..0c864af8e 100644 --- a/src/app/v1/_lib/proxy/session.ts +++ b/src/app/v1/_lib/proxy/session.ts @@ -63,7 +63,6 @@ export interface SessionAffinityState { nominatedProviderId: number | null; /** 查找命中的边界指纹(未命中为 null) */ matchedFp: string | null; - matchedTier: "conversation" | "system" | null; } /** @@ -731,6 +730,8 @@ export class ProxySession { endpointFilterStats?: ProviderChainItem["endpointFilterStats"]; // endpoint filter statistics modelRedirect?: ProviderChainItem["modelRedirect"]; rawCrossProviderFallbackEnabled?: boolean; + streamGate?: ProviderChainItem["streamGate"]; // F1 门控提交标记 + affinity?: ProviderChainItem["affinity"]; // F3a 亲和命中详情 } ): void { const item: ProviderChainItem = { @@ -762,6 +763,8 @@ export class ProxySession { endpointFilterStats: metadata?.endpointFilterStats, modelRedirect: metadata?.modelRedirect ?? this.getCurrentModelRedirect(provider.id), rawCrossProviderFallbackEnabled: metadata?.rawCrossProviderFallbackEnabled, + streamGate: metadata?.streamGate, + affinity: metadata?.affinity, }; // 避免重复添加同一个供应商 diff --git a/src/app/v1/_lib/proxy/stream-finalization.ts b/src/app/v1/_lib/proxy/stream-finalization.ts index 383266c0d..6f34eb6db 100644 --- a/src/app/v1/_lib/proxy/stream-finalization.ts +++ b/src/app/v1/_lib/proxy/stream-finalization.ts @@ -1,4 +1,5 @@ import type { SessionBindingSnapshot } from "@/lib/redis/session-binding"; +import type { ProviderChainItem } from "@/types/message"; import type { ProxySession } from "./session"; export type DeferredStreamingDiscoveryLease = { @@ -70,6 +71,8 @@ export type DeferredStreamingFinalization = { hedgeBindingAuthorityPromise?: Promise; /** ResponseHandler-owned runtime lifecycle; attached when streaming starts. */ hedgeBindingHeartbeat?: DeferredStreamingBindingHeartbeat; + /** F1 门控提交标记:随成功链条目落库(高并发模式下为空)。 */ + streamGate?: ProviderChainItem["streamGate"]; }; const deferredMeta = new WeakMap(); diff --git a/src/app/v1/_lib/proxy/stream-gate/frame-classifier.ts b/src/app/v1/_lib/proxy/stream-gate/frame-classifier.ts index 7913f7c00..2bbfb65b6 100644 --- a/src/app/v1/_lib/proxy/stream-gate/frame-classifier.ts +++ b/src/app/v1/_lib/proxy/stream-gate/frame-classifier.ts @@ -254,6 +254,33 @@ export function mapProviderTypeToFamily( } } +/** + * 请求回显帧:openai-responses 家族的生命周期首帧(response.created / + * response.in_progress / response.queued)会在 data.response 里回显完整请求体 + * (instructions + input),大上下文请求单帧即可达数百 KB。 + * 门控 prebuffer 的字节计数应排除这类帧,避免把「请求大」误判成「流异常」。 + */ +const REQUEST_ECHO_EVENTS: Partial>> = { + "openai-responses": new Set(["response.created", "response.in_progress", "response.queued"]), +}; + +export function isRequestEchoFrame( + family: ProtocolFamily, + eventName: string | null, + data: string +): boolean { + const events = REQUEST_ECHO_EVENTS[family]; + if (!events) return false; + const effective = (eventName ?? "").trim(); + if (effective !== "") return events.has(effective); + // 无 event 行时嗅探 data 头部的 type 字段(上游实践中 type 总在最前) + const head = data.slice(0, 64); + for (const event of events) { + if (head.includes(`"type":"${event}"`)) return true; + } + return false; +} + /** * 对单个完整 SSE 帧分类。 * diff --git a/src/app/v1/_lib/proxy/stream-gate/stream-content-gate.ts b/src/app/v1/_lib/proxy/stream-gate/stream-content-gate.ts index 9f3db0e17..e286306a4 100644 --- a/src/app/v1/_lib/proxy/stream-gate/stream-content-gate.ts +++ b/src/app/v1/_lib/proxy/stream-gate/stream-content-gate.ts @@ -2,7 +2,12 @@ import { getEnvConfig } from "@/lib/config/env.schema"; import { logger } from "@/lib/logger"; import { getCachedProxyRuntimeSettings } from "@/lib/system-settings/proxy-runtime"; import { ProxyError } from "../errors"; -import { classifyFrame, type FrameVerdict, type ProtocolFamily } from "./frame-classifier"; +import { + classifyFrame, + type FrameVerdict, + isRequestEchoFrame, + type ProtocolFamily, +} from "./frame-classifier"; import { SseFrameParser } from "./sse-frames"; /** @@ -12,6 +17,8 @@ import { SseFrameParser } from "./sse-frames"; * - error / malformed 帧 -> precommit 失败:调用方抛错走现有供应商切换循环 * - terminal 先于 content / 流提前结束 -> 空流失败 * - neutral 帧入缓冲;超过 event/byte 上限 -> prebuffer_overflow 失败 + * (请求回显帧不计入字节上限,见 isRequestEchoFrame) + * - 读间隔超过 idleTimeoutMs -> idle_timeout 失败(调用方按静默超时归类) * - read 拒绝(首字节超时 abort / 客户端断开)-> 原样返回错误,由调用方按来源归类 * * 客户端在提交前收到的字节数恒为 0:失败时整段前缀被丢弃。 @@ -21,7 +28,8 @@ export type StreamGateFailureReason = | "gate_error" | "decode_error" | "empty_stream" - | "prebuffer_overflow"; + | "prebuffer_overflow" + | "idle_timeout"; /** * 门控 precommit 错误。继承 ProxyError(statusCode 502)—— @@ -41,6 +49,7 @@ export class StreamPrecommitError extends ProxyError { frameData?: string; framesSeen?: number; bufferedBytes?: number; + echoExcludedBytes?: number; } ) { const message = `Stream content gate rejected upstream before first valid content (${reason})`; @@ -61,6 +70,7 @@ function buildGateErrorBody( frameData?: string; framesSeen?: number; bufferedBytes?: number; + echoExcludedBytes?: number; } ): string { if (reason === "gate_error" && detail.frameData) { @@ -74,6 +84,7 @@ function buildGateErrorBody( family: detail.family, frames_seen: detail.framesSeen, buffered_bytes: detail.bufferedBytes, + ...(detail.echoExcludedBytes ? { echo_excluded_bytes: detail.echoExcludedBytes } : {}), ...(detail.frameData ? { frame_preview: detail.frameData.slice(0, 500) } : {}), }, }); @@ -106,7 +117,7 @@ export function resolveStreamGateCaps(): StreamGateCaps { prebufferByteCap: env.STREAM_GATE_PREBUFFER_BYTE_CAP, }; } catch { - return { prebufferEventCap: 64, prebufferByteCap: 256 * 1024 }; + return { prebufferEventCap: 64, prebufferByteCap: 10 * 1024 * 1024 }; } } @@ -114,10 +125,36 @@ export interface StreamGateOptions extends StreamGateCaps { family: ProtocolFamily; providerId: number; providerName: string; + /** 首个非空上游 chunk 到达时回调一次(调用方用于清除首字节计时器,恢复其原始语义) */ + onFirstByte?: () => void; + /** 门控等待期的读间隔静默上限(毫秒;<=0 或未设不启用),对齐提交后 response-handler 的静默超时 */ + idleTimeoutMs?: number; + /** 记录触发提交的帧信息(高并发模式下关闭以省开销) */ + captureCommitMarker?: boolean; +} + +/** 触发门控提交的帧/chunk 标记(用于 Message 详情可观测性)。 */ +export interface StreamGateCommitMarker { + /** 触发提交的帧序号(1-based,含中性前缀帧) */ + frameIndex: number; + /** 触发提交的帧所在网络 chunk 序号(1-based) */ + chunkIndex: number; + /** 触发提交的 SSE event 名(无事件行时为 null) */ + eventName: string | null; + /** 提交时已缓冲的前缀字节数 */ + bufferedBytes: number; + /** 被排除出字节计数的请求回显帧字节数 */ + echoExcludedBytes: number; } export type StreamGateResult = - | { committed: true; prefixChunks: Uint8Array[]; framesSeen: number; readerDone: boolean } + | { + committed: true; + prefixChunks: Uint8Array[]; + framesSeen: number; + readerDone: boolean; + commitMarker: StreamGateCommitMarker | null; + } | { committed: false; error: Error }; /** @@ -134,7 +171,10 @@ export async function runStreamContentGate( const parser = new SseFrameParser(); const buffered: Uint8Array[] = []; let bufferedBytes = 0; + let echoExcludedBytes = 0; let framesSeen = 0; + let chunkIndex = 0; + let firstByteSeen = false; const failure = (reason: StreamGateFailureReason, frameData?: string): StreamGateResult => ({ committed: false, @@ -145,13 +185,28 @@ export async function runStreamContentGate( frameData, framesSeen, bufferedBytes, + echoExcludedBytes, }), }); + const commit = (eventName: string | null, readerDone: boolean): StreamGateResult => ({ + committed: true, + prefixChunks: buffered, + framesSeen, + readerDone, + commitMarker: options.captureCommitMarker + ? { frameIndex: framesSeen, chunkIndex, eventName, bufferedBytes, echoExcludedBytes } + : null, + }); + while (true) { let readResult: ReadableStreamReadResult; try { - readResult = await reader.read(); + const raced = await readWithIdleTimeout(reader, options.idleTimeoutMs); + if (raced === IDLE_TIMEOUT) { + return failure("idle_timeout"); + } + readResult = raced; } catch (readError) { // 首字节超时 abort / 客户端断开 / 传输错误:原样上抛,调用方按来源归类 return { @@ -166,7 +221,7 @@ export async function runStreamContentGate( framesSeen++; const verdict = classifyFrame(options.family, frame.eventName, frame.data); if (verdict === "content") { - return { committed: true, prefixChunks: buffered, framesSeen, readerDone: true }; + return commit(frame.eventName, true); } if (verdict === "error") return failure("gate_error", frame.data); if (verdict === "malformed") return failure("decode_error", frame.data); @@ -178,6 +233,12 @@ export async function runStreamContentGate( if (!chunk || chunk.byteLength === 0) { continue; } + if (!firstByteSeen) { + firstByteSeen = true; + // 上游已开始响应:调用方在此清除首字节计时器(保持「首字节」而非「首内容」语义) + options.onFirstByte?.(); + } + chunkIndex++; buffered.push(chunk); bufferedBytes += chunk.byteLength; @@ -185,7 +246,7 @@ export async function runStreamContentGate( framesSeen++; const verdict: FrameVerdict = classifyFrame(options.family, frame.eventName, frame.data); if (verdict === "content") { - return { committed: true, prefixChunks: buffered, framesSeen, readerDone: false }; + return commit(frame.eventName, false); } if (verdict === "error") { return failure("gate_error", frame.data); @@ -197,18 +258,52 @@ export async function runStreamContentGate( // 干净终止先于任何内容 = 空流 return failure("empty_stream", frame.data); } - // neutral: 继续缓冲;event 上限为逐帧硬上限(单 chunk 大量小帧也会触发) + // neutral: 继续缓冲;请求回显帧的载荷不计入字节上限(豁免额度另有上限,见下方判定) + if (isRequestEchoFrame(options.family, frame.eventName, frame.data)) { + echoExcludedBytes += Buffer.byteLength(frame.data, "utf8"); + } + // event 上限为逐帧硬上限(单 chunk 大量小帧也会触发) if (framesSeen > options.prebufferEventCap) { return failure("prebuffer_overflow"); } } - if (bufferedBytes > options.prebufferByteCap) { + // 豁免额度以 cap 为自身上限:伪装成回显的中性帧最多把缓冲总量抬到 2×cap,不会无界占用内存 + const cappedEchoExcluded = Math.min(echoExcludedBytes, options.prebufferByteCap); + if (bufferedBytes - cappedEchoExcluded > options.prebufferByteCap) { return failure("prebuffer_overflow"); } } } +const IDLE_TIMEOUT = Symbol("stream_gate_idle_timeout"); + +/** + * 单次 read 与静默计时器竞速。计时器胜出时挂起的 read 由调用方随后的 + * reader.cancel 收尾;对其附加空 catch 防止孤儿 rejection。 + */ +async function readWithIdleTimeout( + reader: ReadableStreamDefaultReader, + idleTimeoutMs: number | undefined +): Promise | typeof IDLE_TIMEOUT> { + if (!idleTimeoutMs || idleTimeoutMs <= 0) { + return reader.read(); + } + const readPromise = reader.read(); + let timer: NodeJS.Timeout | undefined; + try { + return await Promise.race([ + readPromise, + new Promise((resolve) => { + timer = setTimeout(() => resolve(IDLE_TIMEOUT), idleTimeoutMs); + }), + ]); + } finally { + clearTimeout(timer); + readPromise.catch(() => undefined); + } +} + /** 拼接门控前缀字节(供竞速败者计费 drain 恢复 usage 时复用现有单块逻辑)。 */ export function concatChunks(chunks: Uint8Array[]): Uint8Array | null { if (chunks.length === 0) return null; diff --git a/src/components/ui/__tests__/code-display.test.tsx b/src/components/ui/__tests__/code-display.test.tsx index 836bf1793..d3397ce5e 100644 --- a/src/components/ui/__tests__/code-display.test.tsx +++ b/src/components/ui/__tests__/code-display.test.tsx @@ -329,4 +329,31 @@ describe("CodeDisplay", () => { expect(container.textContent).toContain(dashboardMessages.sessions.codeDisplay.hardLimit.title); unmount(); }); + + test("sse keeps pretty event view for content over the pretty-mode char cap", () => { + // 请求回显帧场景:单个 SSE 事件即可超过 100K 字符,事件视图不得降级为 raw + const hugeEcho = `event: response.created\ndata: {"payload":"${"x".repeat(150_000)}"}\n\n`; + const sse = `${hugeEcho}event: response.output_text.delta\ndata: {"delta":"hi"}\n\n`; + const { container, unmount } = renderWithIntl( + + ); + + expect(container.querySelectorAll("[data-testid='code-display-sse-row']").length).toBe(2); + unmount(); + }); + + test("sse ignores the line cap and renders events beyond the max-lines limit", () => { + // 多 data 行事件轻松超过文本行数上限;SSE 视图不得因行数上限整体拒绝渲染 + const dataLines = Array.from({ length: 300 }, (_, i) => `data: line-${i}`).join("\n"); + const sse = `event: big\n${dataLines}\n\n`; + const { container, unmount } = renderWithIntl( + + ); + + expect(container.textContent).not.toContain( + dashboardMessages.sessions.codeDisplay.hardLimit.title + ); + expect(container.querySelectorAll("[data-testid='code-display-sse-row']").length).toBe(1); + unmount(); + }); }); diff --git a/src/components/ui/code-display.tsx b/src/components/ui/code-display.tsx index c08002d20..4face0d32 100644 --- a/src/components/ui/code-display.tsx +++ b/src/components/ui/code-display.tsx @@ -26,6 +26,10 @@ export type CodeDisplayLanguage = "json" | "sse" | "text"; const DEFAULT_MAX_CONTENT_BYTES = 1_000_000; // 1MB const DEFAULT_MAX_LINES = 10_000; const PRETTY_MODE_DEFAULT_MAX_CHARS = 100_000; +// SSE 事件视图为虚拟滚动 + 折叠预览,渲染成本与内容体积解耦: +// 不受 PRETTY_MODE_DEFAULT_MAX_CHARS 强制降级与行数上限约束, +// 字节上限对齐流式统计缓冲(response-handler STREAM_STATS_MAX_BUFFER_BYTES) +const SSE_MAX_CONTENT_BYTES = 10_000_000; export interface CodeDisplayProps { content: string; @@ -89,14 +93,20 @@ export function CodeDisplay({ }: CodeDisplayProps) { const t = useTranslations("dashboard.sessions"); const tActions = useTranslations("dashboard.actions"); - const resolvedMaxContentBytes = maxContentBytes ?? DEFAULT_MAX_CONTENT_BYTES; + const isSse = language === "sse"; + const resolvedMaxContentBytes = + maxContentBytes ?? (isSse ? SSE_MAX_CONTENT_BYTES : DEFAULT_MAX_CONTENT_BYTES); const resolvedMaxLines = maxLines ?? DEFAULT_MAX_LINES; const contentBytes = useMemo(() => new Blob([content]).size, [content]); const isOverMaxBytes = contentBytes > resolvedMaxContentBytes; const [mode, setMode] = useState<"raw" | "pretty">(() => { const defaultMode = getDefaultMode(language); - if (defaultMode === "pretty" && content.length > PRETTY_MODE_DEFAULT_MAX_CHARS) { + if ( + defaultMode === "pretty" && + language !== "sse" && + content.length > PRETTY_MODE_DEFAULT_MAX_CHARS + ) { return "raw"; } return defaultMode; @@ -124,18 +134,18 @@ export function CodeDisplay({ useEffect(() => { if (mode !== "pretty") return; - if (language === "text") return; + if (language === "text" || language === "sse") return; if (content.length <= PRETTY_MODE_DEFAULT_MAX_CHARS) return; setMode("raw"); }, [content, language, mode]); const lineCount = useMemo(() => { - if (isOverMaxBytes) return 0; + if (isOverMaxBytes || isSse) return 0; return countLinesUpTo(content, resolvedMaxLines + 1); - }, [content, isOverMaxBytes, resolvedMaxLines]); + }, [content, isOverMaxBytes, isSse, resolvedMaxLines]); const isLargeContent = content.length > 4000 || lineCount > 200; const isExpanded = expanded || !isLargeContent; - const isHardLimited = isOverMaxBytes || lineCount > resolvedMaxLines; + const isHardLimited = isOverMaxBytes || (!isSse && lineCount > resolvedMaxLines); const formattedJson = useMemo(() => { if (language !== "json") return content; diff --git a/src/drizzle/schema.ts b/src/drizzle/schema.ts index 60312e8e3..a4c0b6463 100644 --- a/src/drizzle/schema.ts +++ b/src/drizzle/schema.ts @@ -1020,6 +1020,12 @@ export const systemSettings = pgTable('system_settings', { .notNull() .default(true), + // F2 Replay 开关覆写(null = 跟随环境变量 ENABLE_REQUEST_REPLAY) + replayEnabled: boolean('replay_enabled'), + + // F3b 最长前缀匹配缓存模拟开关覆写(null = 跟随环境变量 ENABLE_CACHE_EFFECTIVENESS) + cacheEffectivenessEnabled: boolean('cache_effectiveness_enabled'), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(), }); diff --git a/src/instrumentation.ts b/src/instrumentation.ts index 2ba076002..8c9d6c38b 100644 --- a/src/instrumentation.ts +++ b/src/instrumentation.ts @@ -262,15 +262,18 @@ async function startCacheEffectivenessScheduler(): Promise { } try { - const { getEnvConfig } = await import("@/lib/config/env.schema"); - if (!getEnvConfig().ENABLE_CACHE_EFFECTIVENESS) { - return; - } + // 开关支持系统设置运行时覆写:调度器常驻,每 tick 异步刷新有效开关—— + // 低流量实例没有请求路径保鲜快照,只读同步快照会一直陈旧 + const { getProxyRuntimeSettings } = await import("@/lib/system-settings/proxy-runtime"); const { aggregateCacheEffectiveness } = await import("@/lib/cache-effectiveness/service"); const intervalMs = 5 * 60 * 1000; instrumentationState.__CCH_CACHE_EFFECTIVENESS_INTERVAL_ID__ = setInterval(() => { - void aggregateCacheEffectiveness().catch((error) => { + void (async () => { + const settings = await getProxyRuntimeSettings(); + if (!settings.cacheEffectivenessEnabled) return; + await aggregateCacheEffectiveness(); + })().catch((error) => { logger.warn("[Instrumentation] Cache effectiveness aggregation tick failed", { error: error instanceof Error ? error.message : String(error), }); diff --git a/src/lib/api-client/v1/openapi-types.gen.ts b/src/lib/api-client/v1/openapi-types.gen.ts index c8bc16148..0e14d5feb 100644 --- a/src/lib/api-client/v1/openapi-types.gen.ts +++ b/src/lib/api-client/v1/openapi-types.gen.ts @@ -12240,6 +12240,10 @@ export interface operations { streamGateMode: "off" | "shadow" | "enforce"; /** @description Whether fingerprintable requests force longest-prefix affinity for provider stickiness, skipping client session id binding. */ affinityIgnoreClientSessionId: boolean; + /** @description Request replay (response caching and upstream connection reuse) override. Null follows the ENABLE_REQUEST_REPLAY environment variable. */ + replayEnabled: boolean | null; + /** @description Longest-prefix cache-effectiveness simulation override (observability only). Null follows the ENABLE_CACHE_EFFECTIVENESS environment variable. */ + cacheEffectivenessEnabled: boolean | null; /** * Format: date-time * @description Creation time. @@ -12522,6 +12526,10 @@ export interface operations { streamGateMode?: "off" | "shadow" | "enforce"; /** @description Whether fingerprintable requests force longest-prefix affinity for provider stickiness, skipping client session id binding. */ affinityIgnoreClientSessionId?: boolean; + /** @description Request replay (response caching and upstream connection reuse) override. Null follows the ENABLE_REQUEST_REPLAY environment variable. */ + replayEnabled?: boolean | null; + /** @description Longest-prefix cache-effectiveness simulation override (observability only). Null follows the ENABLE_CACHE_EFFECTIVENESS environment variable. */ + cacheEffectivenessEnabled?: boolean | null; }; }; }; @@ -12679,6 +12687,10 @@ export interface operations { streamGateMode: "off" | "shadow" | "enforce"; /** @description Whether fingerprintable requests force longest-prefix affinity for provider stickiness, skipping client session id binding. */ affinityIgnoreClientSessionId: boolean; + /** @description Request replay (response caching and upstream connection reuse) override. Null follows the ENABLE_REQUEST_REPLAY environment variable. */ + replayEnabled: boolean | null; + /** @description Longest-prefix cache-effectiveness simulation override (observability only). Null follows the ENABLE_CACHE_EFFECTIVENESS environment variable. */ + cacheEffectivenessEnabled: boolean | null; /** * Format: date-time * @description Creation time. diff --git a/src/lib/api/v1/schemas/system-config.ts b/src/lib/api/v1/schemas/system-config.ts index bdb409111..fd0887a41 100644 --- a/src/lib/api/v1/schemas/system-config.ts +++ b/src/lib/api/v1/schemas/system-config.ts @@ -222,6 +222,18 @@ export const SystemSettingsSchema = z .describe( "Whether fingerprintable requests force longest-prefix affinity for provider stickiness, skipping client session id binding." ), + replayEnabled: z + .boolean() + .nullable() + .describe( + "Request replay (response caching and upstream connection reuse) override. Null follows the ENABLE_REQUEST_REPLAY environment variable." + ), + cacheEffectivenessEnabled: z + .boolean() + .nullable() + .describe( + "Longest-prefix cache-effectiveness simulation override (observability only). Null follows the ENABLE_CACHE_EFFECTIVENESS environment variable." + ), createdAt: IsoDateTimeStringSchema.describe("Creation time."), updatedAt: IsoDateTimeStringSchema.describe("Last update time."), }) diff --git a/src/lib/column-visibility.test.ts b/src/lib/column-visibility.test.ts index 59df779bb..0c0036538 100644 --- a/src/lib/column-visibility.test.ts +++ b/src/lib/column-visibility.test.ts @@ -52,8 +52,14 @@ describe("column-visibility", () => { vi.restoreAllMocks(); }); - test("keeps reasoning effort visible beside the billing model", () => { - expect(ALWAYS_VISIBLE_COLUMNS).toEqual(["time", "model", "reasoningEffort", "status"]); + test("keeps only structural columns always visible", () => { + expect(ALWAYS_VISIBLE_COLUMNS).toEqual(["time", "model", "status"]); + }); + + test("makes reasoning effort toggleable and visible by default", () => { + expect(DEFAULT_VISIBLE_COLUMNS).toContain("reasoningEffort"); + expect(DEFAULT_HIDDEN_COLUMNS).not.toContain("reasoningEffort"); + expect(getVisibleColumns(userId, tableId)).toContain("reasoningEffort"); }); describe("getHiddenColumns", () => { @@ -199,6 +205,17 @@ describe("column-visibility", () => { expect(visibleAfterToggleBack).not.toContain("cost"); expect(getVisibleColumns(userId, tableId)).toContain("cost"); }); + + test("toggles reasoning effort column visibility and persists it", () => { + const hiddenAfterToggle = toggleColumn(userId, tableId, "reasoningEffort"); + expect(hiddenAfterToggle).toContain("reasoningEffort"); + expect(getVisibleColumns(userId, tableId)).not.toContain("reasoningEffort"); + expect(mockStorage[storageKey]).toContain("reasoningEffort"); + + const visibleAfterToggleBack = toggleColumn(userId, tableId, "reasoningEffort"); + expect(visibleAfterToggleBack).not.toContain("reasoningEffort"); + expect(getVisibleColumns(userId, tableId)).toContain("reasoningEffort"); + }); }); describe("resetColumns", () => { @@ -228,6 +245,7 @@ describe("column-visibility", () => { expect(DEFAULT_VISIBLE_COLUMNS).toContain("key"); expect(DEFAULT_VISIBLE_COLUMNS).toContain("sessionId"); expect(DEFAULT_VISIBLE_COLUMNS).toContain("provider"); + expect(DEFAULT_VISIBLE_COLUMNS).toContain("reasoningEffort"); expect(DEFAULT_VISIBLE_COLUMNS).toContain("tokens"); expect(DEFAULT_VISIBLE_COLUMNS).toContain("cost"); expect(DEFAULT_VISIBLE_COLUMNS).toContain("cache"); diff --git a/src/lib/column-visibility.ts b/src/lib/column-visibility.ts index db580587e..dd28e7011 100644 --- a/src/lib/column-visibility.ts +++ b/src/lib/column-visibility.ts @@ -16,6 +16,7 @@ export type LogsTableColumn = | "sessionId" | "ip" | "provider" + | "reasoningEffort" | "tokens" | "cache" | "performance" @@ -30,6 +31,7 @@ export const DEFAULT_VISIBLE_COLUMNS: LogsTableColumn[] = [ "sessionId", "ip", "provider", + "reasoningEffort", "tokens", "cache", "performance", @@ -44,7 +46,7 @@ export const DEFAULT_HIDDEN_COLUMNS: LogsTableColumn[] = ["ip"]; /** * Columns that cannot be hidden (always visible) */ -export const ALWAYS_VISIBLE_COLUMNS = ["time", "model", "reasoningEffort", "status"] as const; +export const ALWAYS_VISIBLE_COLUMNS = ["time", "model", "status"] as const; /** * Get the storage key for a specific user and table diff --git a/src/lib/config/env.schema.ts b/src/lib/config/env.schema.ts index a7088249c..c974a0a51 100644 --- a/src/lib/config/env.schema.ts +++ b/src/lib/config/env.schema.ts @@ -187,13 +187,14 @@ export const EnvSchema = z.object({ // 流式内容门控:off=关闭;shadow=旁路分类只记录分歧;enforce=首个有效内容帧前缓冲+failover STREAM_GATE_MODE: z.enum(["off", "shadow", "enforce"]).default("off"), // 门控 precommit 缓冲上限:超限即视为该供应商流异常,failover 释放内存 + // (字节计数排除请求回显帧,见 stream-gate/frame-classifier.ts isRequestEchoFrame) STREAM_GATE_PREBUFFER_EVENT_CAP: z.coerce.number().int().min(1).max(4096).default(64), STREAM_GATE_PREBUFFER_BYTE_CAP: z.coerce .number() .int() .min(1024) - .max(16 * 1024 * 1024) - .default(256 * 1024), + .max(64 * 1024 * 1024) + .default(10 * 1024 * 1024), // 请求分离 + Replay:客户端断开后上游继续引流缓存,相同请求体重发续传 ENABLE_REQUEST_REPLAY: z.string().default("false").transform(booleanTransform), // owner 客户端仍在线时的并发相同请求去重(attached-live);关闭后仅 detached/completed 可命中 diff --git a/src/lib/config/system-settings-cache.ts b/src/lib/config/system-settings-cache.ts index b090337c4..cc26ffb48 100644 --- a/src/lib/config/system-settings-cache.ts +++ b/src/lib/config/system-settings-cache.ts @@ -215,6 +215,8 @@ export async function getCachedSystemSettings(): Promise { DEFAULT_SETTINGS.publicStatusAggregationIntervalMinutes, streamGateMode: DEFAULT_SETTINGS.streamGateMode, affinityIgnoreClientSessionId: DEFAULT_SETTINGS.affinityIgnoreClientSessionId, + replayEnabled: null, + cacheEffectivenessEnabled: null, discoveryEnabled: DEFAULT_SETTINGS.discoveryEnabled, discoveryConcurrency: DEFAULT_SETTINGS.discoveryConcurrency, maxDiscoveryRounds: DEFAULT_SETTINGS.maxDiscoveryRounds, diff --git a/src/lib/system-settings/proxy-runtime.ts b/src/lib/system-settings/proxy-runtime.ts index cf9e3879f..94d1a5604 100644 --- a/src/lib/system-settings/proxy-runtime.ts +++ b/src/lib/system-settings/proxy-runtime.ts @@ -10,6 +10,8 @@ import { getCachedSystemSettings } from "@/lib/config/system-settings-cache"; * - affinityIgnoreClientSessionId:F3a「忽略客户端 Session ID」开关(默认开)—— * 可指纹化的请求强制使用最长前缀亲和做供应商粘性,跳过 session-ID 绑定读取; * 不可指纹化的请求仍走既有 session 复用。 + * - replayEnabled:F2 Replay 有效开关(系统设置覆写优先,null 时跟随 env) + * - cacheEffectivenessEnabled:F3b 缓存模拟有效开关(同上) * * 读取约定:热路径用 getCachedProxyRuntimeSettings()(同步、最近快照), * 异步场景用 getProxyRuntimeSettings()(带 TTL 缓存)。 @@ -17,20 +19,45 @@ import { getCachedSystemSettings } from "@/lib/config/system-settings-cache"; export interface ProxyRuntimeSettings { streamGateMode: "off" | "shadow" | "enforce"; affinityIgnoreClientSessionId: boolean; + replayEnabled: boolean; + cacheEffectivenessEnabled: boolean; } // 最近一次成功读取的快照;同步热路径消费,异步读取与开机预热负责保鲜。 let lastKnown: ProxyRuntimeSettings | null = null; +function envReplayDefault(): boolean { + try { + return getEnvConfig().ENABLE_REQUEST_REPLAY; + } catch { + return false; + } +} + +function envCacheEffectivenessDefault(): boolean { + try { + return getEnvConfig().ENABLE_CACHE_EFFECTIVENESS; + } catch { + return true; + } +} + function envFallback(): ProxyRuntimeSettings { try { const env = getEnvConfig(); return { streamGateMode: env.STREAM_GATE_MODE, affinityIgnoreClientSessionId: true, + replayEnabled: env.ENABLE_REQUEST_REPLAY, + cacheEffectivenessEnabled: env.ENABLE_CACHE_EFFECTIVENESS, }; } catch { - return { streamGateMode: "off", affinityIgnoreClientSessionId: true }; + return { + streamGateMode: "off", + affinityIgnoreClientSessionId: true, + replayEnabled: false, + cacheEffectivenessEnabled: true, + }; } } @@ -40,6 +67,9 @@ export async function getProxyRuntimeSettings(): Promise { lastKnown = { streamGateMode: settings.streamGateMode, affinityIgnoreClientSessionId: settings.affinityIgnoreClientSessionId, + replayEnabled: settings.replayEnabled ?? envReplayDefault(), + cacheEffectivenessEnabled: + settings.cacheEffectivenessEnabled ?? envCacheEffectivenessDefault(), }; return lastKnown; } catch { @@ -54,3 +84,8 @@ export async function getProxyRuntimeSettings(): Promise { export function getCachedProxyRuntimeSettings(): ProxyRuntimeSettings | null { return lastKnown; } + +/** F3b 有效开关(同步):系统设置覆写优先,无快照时跟随 env。 */ +export function isCacheEffectivenessEnabled(): boolean { + return lastKnown?.cacheEffectivenessEnabled ?? envCacheEffectivenessDefault(); +} diff --git a/src/lib/utils/thinking-effort.ts b/src/lib/utils/thinking-effort.ts new file mode 100644 index 000000000..aad4d3279 --- /dev/null +++ b/src/lib/utils/thinking-effort.ts @@ -0,0 +1,50 @@ +import { extractAnthropicEffortInfo } from "@/lib/utils/anthropic-effort"; +import { extractCodexReasoningEffortInfo } from "@/lib/utils/codex-reasoning-effort"; +import type { SpecialSetting } from "@/types/special-settings"; + +/** 思考强度审计来源:Codex 的 reasoning.effort 或 Anthropic 的 output_config.effort。 */ +export type ThinkingEffortSource = "codex" | "anthropic"; + +/** 任意模型统一后的思考强度展示信息,供列表列与请求详情共用。 */ +export interface ThinkingEffortInfo { + source: ThinkingEffortSource; + /** 客户端请求声明的思考强度;历史记录可能缺失。 */ + requestedEffort: string | null; + /** 实际转发给上游的思考强度;供应商覆写移除该参数时为 null。 */ + effectiveEffort: string | null; + isOverridden: boolean; +} + +/** + * 从 specialSettings 中提取任意模型的思考强度。 + * + * 复用 Codex 与 Anthropic 两个提取器并统一返回结构:Codex 审计优先, + * 其次回退到 Anthropic effort,两者都无则返回 null。 + */ +export function extractThinkingEffortInfo( + specialSettings: SpecialSetting[] | null | undefined +): ThinkingEffortInfo | null { + const codexInfo = extractCodexReasoningEffortInfo(specialSettings); + if (codexInfo) { + return { + source: "codex", + requestedEffort: codexInfo.requestedEffort, + effectiveEffort: codexInfo.effectiveEffort, + isOverridden: codexInfo.isOverridden, + }; + } + + const anthropicInfo = extractAnthropicEffortInfo(specialSettings); + if (anthropicInfo) { + return { + source: "anthropic", + requestedEffort: anthropicInfo.originalEffort, + effectiveEffort: anthropicInfo.isOverridden + ? anthropicInfo.overriddenEffort + : anthropicInfo.originalEffort, + isOverridden: anthropicInfo.isOverridden, + }; + } + + return null; +} diff --git a/src/lib/validation/schemas.ts b/src/lib/validation/schemas.ts index 6617cb1f0..c5bba2c71 100644 --- a/src/lib/validation/schemas.ts +++ b/src/lib/validation/schemas.ts @@ -1118,6 +1118,10 @@ export const UpdateSystemSettingsSchema = z .optional(), // 忽略客户端 Session ID(可选) affinityIgnoreClientSessionId: z.boolean().optional(), + // F2 Replay 响应缓存与复用(可选;null = 跟随环境变量) + replayEnabled: z.boolean().nullable().optional(), + // F3b 最长前缀匹配缓存模拟(可选;null = 跟随环境变量) + cacheEffectivenessEnabled: z.boolean().nullable().optional(), // Codex Session ID 补全(可选) enableCodexSessionIdCompletion: z.boolean().optional(), // Claude metadata.user_id 注入(可选) diff --git a/src/repository/_shared/transformers.ts b/src/repository/_shared/transformers.ts index 5dfbf830b..e57c69a43 100644 --- a/src/repository/_shared/transformers.ts +++ b/src/repository/_shared/transformers.ts @@ -316,6 +316,8 @@ export function toSystemSettings(dbSettings: any): SystemSettings { ? dbSettings.streamGateMode : "enforce", affinityIgnoreClientSessionId: dbSettings?.affinityIgnoreClientSessionId ?? true, + replayEnabled: dbSettings?.replayEnabled ?? null, + cacheEffectivenessEnabled: dbSettings?.cacheEffectivenessEnabled ?? null, createdAt: dbSettings?.createdAt ? new Date(dbSettings.createdAt) : new Date(), updatedAt: dbSettings?.updatedAt ? new Date(dbSettings.updatedAt) : new Date(), }; diff --git a/src/repository/system-config.ts b/src/repository/system-config.ts index a672d1a9d..ae2a0eb1d 100644 --- a/src/repository/system-config.ts +++ b/src/repository/system-config.ts @@ -202,6 +202,8 @@ function createFallbackSettings(): SystemSettings { ipGeoLookupEnabled: true, streamGateMode: "enforce", affinityIgnoreClientSessionId: true, + replayEnabled: null, + cacheEffectivenessEnabled: null, createdAt: now, updatedAt: now, }; @@ -280,6 +282,19 @@ const RECENT_COLUMN_LADDER: ReadonlyArray<{ // 本层更新失败(仍有列缺失)时记录的告警 updateWarn: string; }> = [ + { + key: "cacheEffectivenessEnabled", + column: systemSettings.cacheEffectivenessEnabled, + selectWarn: + "system_settings 表除 cacheEffectivenessEnabled 外仍有列缺失,继续回退到上一代字段集。", + updateWarn: "system_settings 表除 cacheEffectivenessEnabled 外仍有列缺失,继续降级更新。", + }, + { + key: "replayEnabled", + column: systemSettings.replayEnabled, + selectWarn: "system_settings 表除 replayEnabled 外仍有列缺失,继续回退到上一代字段集。", + updateWarn: "system_settings 表除 replayEnabled 外仍有列缺失,继续降级更新。", + }, { key: "affinityIgnoreClientSessionId", column: systemSettings.affinityIgnoreClientSessionId, @@ -876,6 +891,16 @@ export async function updateSystemSettings( updates.affinityIgnoreClientSessionId = payload.affinityIgnoreClientSessionId; } + // F2 Replay 开关覆写(如果提供;null = 清除覆写跟随环境变量) + if (payload.replayEnabled !== undefined) { + updates.replayEnabled = payload.replayEnabled; + } + + // F3b 缓存模拟开关覆写(如果提供;null = 清除覆写跟随环境变量) + if (payload.cacheEffectivenessEnabled !== undefined) { + updates.cacheEffectivenessEnabled = payload.cacheEffectivenessEnabled; + } + let updated; try { [updated] = await executor diff --git a/src/types/message.ts b/src/types/message.ts index 64d19730c..64e5ce130 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -248,6 +248,23 @@ export interface ProviderChainItem { | "ws_not_yet_implemented" | "ws_error_pre_first_event"; }; + + // === F1 流式内容门控提交标记(enforce 提交成功时记录;高并发模式下省略) === + streamGate?: { + frameIndex: number; // 触发提交的帧序号(1-based,含中性前缀帧) + chunkIndex: number; // 触发提交的帧所在网络 chunk 序号(1-based) + eventName: string | null; // 触发提交的 SSE event 名 + bufferedBytes: number; // 提交时已缓冲的前缀字节数 + echoExcludedBytes: number; // 被排除出字节计数的请求回显帧字节数 + gateWaitMs: number; // 门控等待时长(首字节到提交) + }; + + // === F3a 亲和命中详情(reason === "affinity_hit" 时记录) === + affinity?: { + matchedDepth: number | null; // 命中边界的消息深度(null = 无法定位) + matchedPrefixBytes: number | null; // 命中边界的规范化前缀字节数 + matchedFp: string; // 命中的链式指纹(截断哈希) + }; } /** diff --git a/src/types/system-config.ts b/src/types/system-config.ts index 79441b62b..f3bfe0e42 100644 --- a/src/types/system-config.ts +++ b/src/types/system-config.ts @@ -158,6 +158,14 @@ export interface SystemSettings { // 不可指纹化的请求仍走会话复用 affinityIgnoreClientSessionId: boolean; + // F2 Replay(响应缓存与上游连接复用)开关覆写 + // null = 跟随环境变量 ENABLE_REQUEST_REPLAY(默认 false) + replayEnabled: boolean | null; + + // F3b 最长前缀匹配缓存模拟(理论 vs 实际缓存命中率,仅观测不影响路由)开关覆写 + // null = 跟随环境变量 ENABLE_CACHE_EFFECTIVENESS(默认 true) + cacheEffectivenessEnabled: boolean | null; + /** Bounded streaming Discovery settings. */ discoveryEnabled: boolean; discoveryConcurrency: number; @@ -284,4 +292,10 @@ export interface UpdateSystemSettingsInput { // 忽略客户端 Session ID(可选) affinityIgnoreClientSessionId?: boolean; + + // F2 Replay 开关(可选;null = 清除覆写跟随环境变量) + replayEnabled?: boolean | null; + + // F3b 缓存模拟开关(可选;null = 清除覆写跟随环境变量) + cacheEffectivenessEnabled?: boolean | null; } diff --git a/tests/unit/proxy/affinity-fingerprint.test.ts b/tests/unit/proxy/affinity-fingerprint.test.ts index 377ebc144..95f6c566b 100644 --- a/tests/unit/proxy/affinity-fingerprint.test.ts +++ b/tests/unit/proxy/affinity-fingerprint.test.ts @@ -314,7 +314,8 @@ describe("computeFingerprintChain - edge cases", () => { expect(chain.tail).toHaveLength(0); expect(chain.sys.fp).toMatch(HEX32); expect(fingerprintTip(chain)).toBe(chain.sys); - expect(fingerprintsDeepestFirst(chain)).toEqual([chain.sys.fp]); + // 仅系统提示词不构成前缀亲和:查找序列不含 F_sys + expect(fingerprintsDeepestFirst(chain)).toEqual([]); }); it("missing system and tools still produce a valid chain", () => { @@ -471,10 +472,11 @@ describe("computeFingerprintChain - gemini formats", () => { }); describe("fingerprintsDeepestFirst", () => { - it("orders tail deepest-first with sys always last", () => { + it("orders tail deepest-first and excludes sys (system-only prefixes never match)", () => { const chain = mustChain(claudeBody([U1, A1, U2])); const fps = fingerprintsDeepestFirst(chain); - expect(fps).toEqual([chain.tail[2].fp, chain.tail[1].fp, chain.tail[0].fp, chain.sys.fp]); + expect(fps).toEqual([chain.tail[2].fp, chain.tail[1].fp, chain.tail[0].fp]); + expect(fps).not.toContain(chain.sys.fp); }); }); diff --git a/tests/unit/proxy/affinity-recorder.test.ts b/tests/unit/proxy/affinity-recorder.test.ts index 5605f4208..03915f606 100644 --- a/tests/unit/proxy/affinity-recorder.test.ts +++ b/tests/unit/proxy/affinity-recorder.test.ts @@ -59,7 +59,6 @@ function makeAffinity(overrides: Partial = {}): SessionAff chain: makeChain(), nominatedProviderId: null, matchedFp: null, - matchedTier: null, ...overrides, }; } @@ -75,15 +74,15 @@ beforeEach(() => { }); describe("recordAffinityWinner", () => { - it("writes tip + sys bindings for the winning provider with the configured TTL", async () => { + it("writes a single tip binding for the winning provider with the configured TTL", async () => { await recordAffinityWinner(makeSession(makeAffinity()), 42); expect(storeMocks.put).toHaveBeenCalledTimes(1); - expect(storeMocks.put).toHaveBeenCalledWith("scope123", "fp2", "sysfp", 42, 3600); + expect(storeMocks.put).toHaveBeenCalledWith("scope123", "fp2", 42, 3600); }); - it("uses sys as tip when the chain has no conversation boundaries", async () => { + it("skips writing when the chain has no conversation boundaries (system-only tip)", async () => { await recordAffinityWinner(makeSession(makeAffinity({ chain: makeChain(0) })), 7); - expect(storeMocks.put).toHaveBeenCalledWith("scope123", "sysfp", "sysfp", 7, 3600); + expect(storeMocks.put).not.toHaveBeenCalled(); }); it("is a no-op when both the env flag and the ignore-session setting are off", async () => { @@ -97,7 +96,7 @@ describe("recordAffinityWinner", () => { envControl.enabled = false; settingsControl.ignoreClientSessionId = true; await recordAffinityWinner(makeSession(makeAffinity()), 42); - expect(storeMocks.put).toHaveBeenCalledWith("scope123", "fp2", "sysfp", 42, 3600); + expect(storeMocks.put).toHaveBeenCalledWith("scope123", "fp2", 42, 3600); }); it("is a no-op without affinity state or with a non-positive provider id", async () => { diff --git a/tests/unit/proxy/affinity-store.test.ts b/tests/unit/proxy/affinity-store.test.ts index 2a9254ebd..a9689b085 100644 --- a/tests/unit/proxy/affinity-store.test.ts +++ b/tests/unit/proxy/affinity-store.test.ts @@ -52,7 +52,6 @@ describe("AffinityStore.lookup", () => { providerId: 42, matchedIndex: 0, matchedFp: "deep", - tier: "conversation", }); }); @@ -79,14 +78,13 @@ describe("AffinityStore.lookup", () => { providerId: 7, matchedIndex: 1, matchedFp: "mid", - tier: "conversation", }); }); - it("maps a match on the last (sys) fingerprint to the system tier", async () => { - const { client } = createLuaFakeRedis({ [key("s1", "sysf")]: "1|9" }); - const hint = await makeStore(client).lookup("s1", ["deep", "mid", "sysf"], 600); - expect(hint?.tier).toBe("system"); + it("matches the shallowest boundary when only it is active", async () => { + const { client } = createLuaFakeRedis({ [key("s1", "shallow")]: "1|9" }); + const hint = await makeStore(client).lookup("s1", ["deep", "mid", "shallow"], 600); + expect(hint?.matchedFp).toBe("shallow"); expect(hint?.matchedIndex).toBe(2); }); @@ -133,31 +131,20 @@ describe("AffinityStore.lookup", () => { }); describe("AffinityStore.put", () => { - it("writes only tip + sys boundaries with the active encoding and TTL", async () => { + it("writes only the tip boundary with the active encoding and TTL", async () => { const { client } = createLuaFakeRedis(); - await makeStore(client).put("s1", "tipfp", "sysfp", 42, 900); - expect(client.set).toHaveBeenCalledTimes(2); - expect(client.set).toHaveBeenNthCalledWith(1, key("s1", "tipfp"), "1|42", "EX", 900); - expect(client.set).toHaveBeenNthCalledWith(2, key("s1", "sysfp"), "1|42", "EX", 900); - }); - - it("writes a single key when tip and sys collide or sys is empty", async () => { - const { client } = createLuaFakeRedis(); - const store = makeStore(client); - await store.put("s1", "same", "same", 42, 900); - await store.put("s1", "solo", "", 42, 900); - expect(client.set).toHaveBeenCalledTimes(2); - expect(client.set).toHaveBeenNthCalledWith(1, key("s1", "same"), "1|42", "EX", 900); - expect(client.set).toHaveBeenNthCalledWith(2, key("s1", "solo"), "1|42", "EX", 900); + await makeStore(client).put("s1", "tipfp", 42, 900); + expect(client.set).toHaveBeenCalledTimes(1); + expect(client.set).toHaveBeenCalledWith(key("s1", "tipfp"), "1|42", "EX", 900); }); it("ignores invalid arguments", async () => { const { client } = createLuaFakeRedis(); const store = makeStore(client); - await store.put("", "tip", "sys", 42, 900); - await store.put("s1", "", "sys", 42, 900); - await store.put("s1", "tip", "sys", 0, 900); - await store.put("s1", "tip", "sys", 42, 0); + await store.put("", "tip", 42, 900); + await store.put("s1", "", 42, 900); + await store.put("s1", "tip", 0, 900); + await store.put("s1", "tip", 42, 0); expect(client.set).not.toHaveBeenCalled(); }); }); @@ -188,28 +175,19 @@ describe("AffinityStore.tombstone", () => { }); describe("AffinityStore round-trip through the fake Lua", () => { - it("put -> lookup hits, tombstone on tip falls back to sys, tombstone on sys misses", async () => { + it("put -> lookup hits the tip; tombstone on tip misses (no sys fallback)", async () => { const { client } = createLuaFakeRedis(); const store = makeStore(client); - await store.put("s1", "tip", "sysf", 42, 600); - expect(await store.lookup("s1", ["tip", "sysf"], 600)).toEqual({ + await store.put("s1", "tip", 42, 600); + expect(await store.lookup("s1", ["tip"], 600)).toEqual({ providerId: 42, matchedIndex: 0, matchedFp: "tip", - tier: "conversation", }); await store.tombstone("s1", "tip", "failover"); - expect(await store.lookup("s1", ["tip", "sysf"], 600)).toEqual({ - providerId: 42, - matchedIndex: 1, - matchedFp: "sysf", - tier: "system", - }); - - await store.tombstone("s1", "sysf", "failover"); - expect(await store.lookup("s1", ["tip", "sysf"], 600)).toBeNull(); + expect(await store.lookup("s1", ["tip"], 600)).toBeNull(); }); }); @@ -217,14 +195,14 @@ describe("AffinityStore fail-open behavior", () => { it("fails open when redis is unavailable or not ready", async () => { const nullStore = makeStore(null); expect(await nullStore.lookup("s1", ["fp"], 600)).toBeNull(); - await expect(nullStore.put("s1", "tip", "sys", 42, 600)).resolves.toBeUndefined(); + await expect(nullStore.put("s1", "tip", 42, 600)).resolves.toBeUndefined(); await expect(nullStore.tombstone("s1", "fp", "r")).resolves.toBeUndefined(); const { client } = createLuaFakeRedis(); client.status = "connecting"; const store = makeStore(client); expect(await store.lookup("s1", ["fp"], 600)).toBeNull(); - await store.put("s1", "tip", "sys", 42, 600); + await store.put("s1", "tip", 42, 600); await store.tombstone("s1", "fp", "r"); expect(client.eval).not.toHaveBeenCalled(); expect(client.set).not.toHaveBeenCalled(); @@ -236,7 +214,7 @@ describe("AffinityStore fail-open behavior", () => { client.set.mockRejectedValue(new Error("boom")); const store = makeStore(client); expect(await store.lookup("s1", ["fp"], 600)).toBeNull(); - await expect(store.put("s1", "tip", "sys", 42, 600)).resolves.toBeUndefined(); + await expect(store.put("s1", "tip", 42, 600)).resolves.toBeUndefined(); await expect(store.tombstone("s1", "fp", "r")).resolves.toBeUndefined(); }); @@ -246,7 +224,7 @@ describe("AffinityStore fail-open behavior", () => { client.set.mockRejectedValue("string failure"); const store = makeStore(client); expect(await store.lookup("s1", ["fp"], 600)).toBeNull(); - await expect(store.put("s1", "tip", "sys", 42, 600)).resolves.toBeUndefined(); + await expect(store.put("s1", "tip", 42, 600)).resolves.toBeUndefined(); await expect(store.tombstone("s1", "fp", "r")).resolves.toBeUndefined(); }); }); diff --git a/tests/unit/proxy/provider-selector-affinity-ignore-session.test.ts b/tests/unit/proxy/provider-selector-affinity-ignore-session.test.ts index dc11811e1..a5ba6a7bd 100644 --- a/tests/unit/proxy/provider-selector-affinity-ignore-session.test.ts +++ b/tests/unit/proxy/provider-selector-affinity-ignore-session.test.ts @@ -91,6 +91,7 @@ vi.mock("@/lib/system-settings/proxy-runtime", () => ({ streamGateMode: "off" as const, affinityIgnoreClientSessionId: settingsControl.ignoreClientSessionId, })), + isCacheEffectivenessEnabled: () => envControl.cacheEffectiveness, })); vi.mock("@/lib/config/env.schema", async (importOriginal) => { const actual = await importOriginal(); @@ -163,6 +164,7 @@ function makeSession(overrides: Record = {}): any { session.provider = p; }, addProviderToChain: vi.fn(), + getProviderChain: vi.fn(() => []), setLastSelectionContext: vi.fn((ctx: unknown) => { session._ctx = ctx; }), diff --git a/tests/unit/proxy/provider-selector-affinity-priority.test.ts b/tests/unit/proxy/provider-selector-affinity-priority.test.ts index b1d81963d..5199567bd 100644 --- a/tests/unit/proxy/provider-selector-affinity-priority.test.ts +++ b/tests/unit/proxy/provider-selector-affinity-priority.test.ts @@ -86,6 +86,8 @@ vi.mock("@/lib/system-settings/proxy-runtime", () => ({ streamGateMode: "off" as const, affinityIgnoreClientSessionId: settingsControl.ignoreClientSessionId, })), + + isCacheEffectivenessEnabled: () => false, })); vi.mock("@/lib/config/env.schema", async (importOriginal) => { const actual = await importOriginal(); @@ -143,6 +145,8 @@ const claudeMessage = { // Minimal ProxySession stub; loose typing matches sibling selector tests. function makeSession(overrides: Record = {}): any { + // 链条目联动:addProviderToChain 推入、getProviderChain 读出(覆盖粘性选择去重逻辑) + const chainItems: Array> = []; const session: any = { sessionId: null, provider: null, @@ -158,7 +162,10 @@ function makeSession(overrides: Record = {}): any { setProvider(p: Provider) { session.provider = p; }, - addProviderToChain: vi.fn(), + addProviderToChain: vi.fn((provider: Provider, metadata: Record = {}) => { + chainItems.push({ id: provider.id, ...metadata }); + }), + getProviderChain: vi.fn(() => chainItems), setLastSelectionContext: vi.fn((ctx: unknown) => { session._ctx = ctx; }), @@ -232,6 +239,10 @@ describe("ensure() nomination priority", () => { expect.objectContaining({ id: 42 }), expect.objectContaining({ reason: "affinity_hit", selectionMethod: "prefix_affinity" }) ); + // 亲和提名已写入链:ensure 不得再补 initial_selection(否则决策链显示为加权随机初选) + expect(session.getProviderChain().map((item: { reason?: string }) => item.reason)).toEqual([ + "affinity_hit", + ]); const [, luaKeysCount] = storeMocks.lookup.mock.calls[0] as unknown as [string, string[]]; expect(Array.isArray(luaKeysCount)).toBe(true); diff --git a/tests/unit/proxy/provider-selector-select-provider-by-type.test.ts b/tests/unit/proxy/provider-selector-select-provider-by-type.test.ts index 284bae735..b98a2d509 100644 --- a/tests/unit/proxy/provider-selector-select-provider-by-type.test.ts +++ b/tests/unit/proxy/provider-selector-select-provider-by-type.test.ts @@ -159,6 +159,7 @@ describe("ProxyProviderResolver.ensure - 分组倍率", () => { getLastSelectionContext: vi.fn(() => null), setGroupCostMultiplier, addProviderToChain: vi.fn(), + getProviderChain: vi.fn(() => []), getOriginalModel: vi.fn(() => "gpt-5.5"), } as unknown as Parameters[0]; @@ -243,6 +244,7 @@ describe("ProxyProviderResolver.ensure - 分组倍率", () => { getLastSelectionContext: vi.fn(() => context), setGroupCostMultiplier, addProviderToChain: vi.fn(), + getProviderChain: vi.fn(() => []), getOriginalModel: vi.fn(() => "gpt-5.5"), recordProviderSessionRef: vi.fn(), } as unknown as Parameters[0]; diff --git a/tests/unit/proxy/replay-guard.test.ts b/tests/unit/proxy/replay-guard.test.ts index 27e024759..699b5c0d7 100644 --- a/tests/unit/proxy/replay-guard.test.ts +++ b/tests/unit/proxy/replay-guard.test.ts @@ -60,6 +60,17 @@ vi.mock("@/lib/config/env.schema", async (importOriginal) => { }; }); +vi.mock("@/lib/system-settings/proxy-runtime", () => ({ + // ensure() 每请求刷新一次快照;单测让同步快照为空,isReplayEnabled 走上方 env mock + getProxyRuntimeSettings: vi.fn(async () => ({ + streamGateMode: "off", + affinityIgnoreClientSessionId: true, + replayEnabled: false, + cacheEffectivenessEnabled: true, + })), + getCachedProxyRuntimeSettings: () => null, +})); + vi.mock("@/app/v1/_lib/proxy/replay/replay-store", () => ({ getReplayStore: () => storeControl, })); diff --git a/tests/unit/proxy/stream-gate-content-gate.test.ts b/tests/unit/proxy/stream-gate-content-gate.test.ts index ff1c5a09d..10cb9066d 100644 --- a/tests/unit/proxy/stream-gate-content-gate.test.ts +++ b/tests/unit/proxy/stream-gate-content-gate.test.ts @@ -248,3 +248,120 @@ describe("StreamPrecommitError classification", () => { expect(error).not.toBeInstanceOf(EmptyResponseError); }); }); + +describe("request echo frame byte-cap exclusion", () => { + const RESPONSES_OPTIONS = { + ...GATE_OPTIONS, + family: "openai-responses" as const, + prebufferByteCap: 1024, + }; + const bigPayload = "x".repeat(4096); + const ECHO_FRAME = `event: response.created\ndata: {"type":"response.created","response":{"instructions":"${bigPayload}"}}\n\n`; + const RESPONSES_DELTA = + 'event: response.output_text.delta\ndata: {"type":"response.output_text.delta","delta":"hi"}\n\n'; + + it("does not count request echo frames against the byte cap", async () => { + // cap 4096 < 帧总字节(约 4186):若不豁免必溢出;回显在豁免额度(=cap)内则放行 + const reader = readerFromChunks([ECHO_FRAME, RESPONSES_DELTA]); + const result = await runStreamContentGate(reader, { + ...RESPONSES_OPTIONS, + prebufferByteCap: 4096, + }); + expect(result.committed).toBe(true); + if (!result.committed) return; + expect(await drainPrefix(result.prefixChunks)).toContain("response.created"); + }); + + it("caps the echo exemption at prebufferByteCap so echo floods still overflow", async () => { + // 豁免额度上限 = cap(1024):4KB 回显超出上限部分照常计入,缓冲总量被压在 2×cap 内 + const reader = readerFromChunks([ECHO_FRAME, RESPONSES_DELTA]); + const result = await runStreamContentGate(reader, RESPONSES_OPTIONS); + expect(result.committed).toBe(false); + if (result.committed) return; + expect(result.error).toBeInstanceOf(StreamPrecommitError); + expect((result.error as StreamPrecommitError).gateReason).toBe("prebuffer_overflow"); + }); + + it("still overflows on oversized non-echo neutral frames", async () => { + const bigNeutral = `event: response.output_item.added\ndata: {"type":"response.output_item.added","item":"${bigPayload}"}\n\n`; + const reader = readerFromChunks([bigNeutral, RESPONSES_DELTA]); + const result = await runStreamContentGate(reader, RESPONSES_OPTIONS); + expect(result.committed).toBe(false); + if (result.committed) return; + expect(result.error).toBeInstanceOf(StreamPrecommitError); + expect((result.error as StreamPrecommitError).gateReason).toBe("prebuffer_overflow"); + }); + + it("reports echo-excluded bytes in the overflow error body", async () => { + const bigEchoFrame = `event: response.in_progress\ndata: {"type":"response.in_progress","response":{"instructions":"${bigPayload}"}}\n\n`; + const oversizedTail = `event: response.output_item.added\ndata: {"item":"${"y".repeat(4096)}"}\n\n`; + const reader = readerFromChunks([bigEchoFrame, oversizedTail, RESPONSES_DELTA]); + const result = await runStreamContentGate(reader, RESPONSES_OPTIONS); + expect(result.committed).toBe(false); + if (result.committed) return; + const body = JSON.parse((result.error as StreamPrecommitError).upstreamError?.body ?? "{}"); + expect(body.error.echo_excluded_bytes).toBeGreaterThan(4000); + }); +}); + +describe("gate idle timeout", () => { + it("fails with idle_timeout when no chunk arrives within idleTimeoutMs", async () => { + const neverEnding = new ReadableStream({ pull: () => new Promise(() => {}) }); + const result = await runStreamContentGate(neverEnding.getReader(), { + ...GATE_OPTIONS, + idleTimeoutMs: 20, + }); + expect(result.committed).toBe(false); + if (result.committed) return; + expect((result.error as StreamPrecommitError).gateReason).toBe("idle_timeout"); + }); + + it("does not time out while chunks keep arriving", async () => { + const reader = readerFromChunks([PING, MESSAGE_START, TEXT_DELTA]); + const result = await runStreamContentGate(reader, { ...GATE_OPTIONS, idleTimeoutMs: 5000 }); + expect(result.committed).toBe(true); + }); +}); + +describe("commit marker and first-byte callback", () => { + it("captures the committing frame/chunk marker when enabled", async () => { + const reader = readerFromChunks([PING, MESSAGE_START, TEXT_DELTA]); + const result = await runStreamContentGate(reader, { + ...GATE_OPTIONS, + captureCommitMarker: true, + }); + expect(result.committed).toBe(true); + if (!result.committed) return; + expect(result.commitMarker).toMatchObject({ + frameIndex: 3, + chunkIndex: 3, + eventName: "content_block_delta", + echoExcludedBytes: 0, + }); + expect(result.commitMarker?.bufferedBytes).toBeGreaterThan(0); + }); + + it("omits the marker when capture is disabled (high-concurrency mode)", async () => { + const reader = readerFromChunks([MESSAGE_START, TEXT_DELTA]); + const result = await runStreamContentGate(reader, { + ...GATE_OPTIONS, + captureCommitMarker: false, + }); + expect(result.committed).toBe(true); + if (!result.committed) return; + expect(result.commitMarker).toBeNull(); + }); + + it("invokes onFirstByte exactly once on the first non-empty chunk", async () => { + let calls = 0; + const reader = readerFromChunks([PING, MESSAGE_START, TEXT_DELTA]); + const result = await runStreamContentGate(reader, { + ...GATE_OPTIONS, + onFirstByte: () => { + calls += 1; + }, + }); + expect(result.committed).toBe(true); + expect(calls).toBe(1); + }); +}); diff --git a/tests/unit/proxy/stream-gate-forwarder-integration.test.ts b/tests/unit/proxy/stream-gate-forwarder-integration.test.ts index ea4c9d510..61adc58f0 100644 --- a/tests/unit/proxy/stream-gate-forwarder-integration.test.ts +++ b/tests/unit/proxy/stream-gate-forwarder-integration.test.ts @@ -369,7 +369,15 @@ describe("F1 stream content gate x ProxyForwarder sequential path", () => { }); return createSseResponse([PING_FRAME, ERROR_FRAME]); }); - doForward.mockImplementationOnce(async () => createSseResponse(WINNER_FRAMES)); + const clearResponseTimeout2 = vi.fn(); + const releaseAgent2 = vi.fn(); + doForward.mockImplementationOnce(async (attemptSession) => { + attachAttemptRuntime(attemptSession, { + clearResponseTimeout: clearResponseTimeout2, + releaseAgent: releaseAgent2, + }); + return createSseResponse(WINNER_FRAMES); + }); const response = await ProxyForwarder.send(session); const text = await response.text(); @@ -383,8 +391,9 @@ describe("F1 stream content gate x ProxyForwarder sequential path", () => { expect(text).not.toContain("overloaded_error"); // precommit 失败按 PROVIDER_ERROR 结算:计入熔断器并清理计时器 / agent 引用 + // 首字节到达即清一次(onFirstByte 保持首字节超时语义),失败清理再兜底一次 expect(mocks.recordFailure).toHaveBeenCalledWith(provider1.id, expect.any(Error)); - expect(clearResponseTimeout1).toHaveBeenCalledTimes(1); + expect(clearResponseTimeout1).toHaveBeenCalledTimes(2); expect(releaseAgent1).toHaveBeenCalledTimes(1); expect(session.provider?.id).toBe(provider2.id); diff --git a/tests/unit/proxy/stream-gate-frame-classifier.test.ts b/tests/unit/proxy/stream-gate-frame-classifier.test.ts index 6fcd89b9c..ecc3f4d7e 100644 --- a/tests/unit/proxy/stream-gate-frame-classifier.test.ts +++ b/tests/unit/proxy/stream-gate-frame-classifier.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { classifyFrame, + isRequestEchoFrame, mapProviderTypeToFamily, } from "@/app/v1/_lib/proxy/stream-gate/frame-classifier"; @@ -400,3 +401,33 @@ describe("classifyFrame: shared edge cases", () => { expect(classifyFrame("openai-chat", null, "[]")).toBe("neutral"); }); }); + +describe("isRequestEchoFrame", () => { + it("recognizes openai-responses lifecycle echo frames by event name", () => { + expect(isRequestEchoFrame("openai-responses", "response.created", "{}")).toBe(true); + expect(isRequestEchoFrame("openai-responses", "response.in_progress", "{}")).toBe(true); + expect(isRequestEchoFrame("openai-responses", "response.queued", "{}")).toBe(true); + expect(isRequestEchoFrame("openai-responses", "response.output_text.delta", "{}")).toBe(false); + }); + + it("sniffs the data head when the event line is absent", () => { + expect( + isRequestEchoFrame("openai-responses", null, '{"type":"response.created","response":{}}') + ).toBe(true); + expect(isRequestEchoFrame("openai-responses", null, '{"type":"other"}')).toBe(false); + // type 不在头部 64 字节内则不嗅探(上游实践中 type 总在最前) + expect( + isRequestEchoFrame( + "openai-responses", + null, + `{"pad":"${"z".repeat(80)}","type":"response.created"}` + ) + ).toBe(false); + }); + + it("never matches for families without echo frames", () => { + expect(isRequestEchoFrame("anthropic", "response.created", "{}")).toBe(false); + expect(isRequestEchoFrame("openai-chat", null, '{"type":"response.created"}')).toBe(false); + expect(isRequestEchoFrame("gemini", null, '{"type":"response.created"}')).toBe(false); + }); +}); diff --git a/tests/unit/repository/system-config-degradation-ladder.test.ts b/tests/unit/repository/system-config-degradation-ladder.test.ts index ea75e70d7..9afac3765 100644 --- a/tests/unit/repository/system-config-degradation-ladder.test.ts +++ b/tests/unit/repository/system-config-degradation-ladder.test.ts @@ -7,6 +7,8 @@ import type { UpdateSystemSettingsInput } from "@/types/system-config"; // 近代新增列(最新在前),降级链按引入顺序逐层累计剥离。 const RECENT_COLUMNS = [ + "cacheEffectivenessEnabled", + "replayEnabled", "affinityIgnoreClientSessionId", "streamGateMode", "stickyTimeoutCooldownMs", @@ -27,6 +29,8 @@ const RECENT_COLUMNS = [ // 全量字段集(46 列)。 const FULL_COLUMNS = [ + "cacheEffectivenessEnabled", + "replayEnabled", "affinityIgnoreClientSessionId", "streamGateMode", "discoveryEnabled", @@ -142,7 +146,7 @@ function createResolvingSelectQuery(rows: unknown[]) { } describe("SystemSettings:列降级阶梯的尝试序列锁定", () => { - test("getSystemSettings 全部列缺失时按既定顺序尝试 14 套字段集", async () => { + test("getSystemSettings 全部列缺失时按既定顺序尝试全部字段集", async () => { vi.resetModules(); const selections: string[][] = []; @@ -185,7 +189,7 @@ describe("SystemSettings:列降级阶梯的尝试序列锁定", () => { const selectMock = vi.fn((selection: Record) => { selections.push(sortedKeys(selection)); callIndex += 1; - if (callIndex < 18) { + if (callIndex < 20) { return createRejectingSelectQuery({ code: "42703" }); } return createResolvingSelectQuery([ @@ -218,14 +222,14 @@ describe("SystemSettings:列降级阶梯的尝试序列锁定", () => { const result = await getSystemSettings(); - expect(selectMock).toHaveBeenCalledTimes(18); - // 第 17 次(近代链末层)不含这些新列;第 18 次(passThrough 世代)重新包含旧列。 - expect(selections[16]).not.toContain("enableThinkingEffortConflictRectifier"); - expect(selections[16]).not.toContain("allowNonConversationEndpointProviderFallback"); - expect(selections[16]).toContain("passThroughUpstreamErrorMessage"); - expect(selections[17]).toContain("enableThinkingEffortConflictRectifier"); - expect(selections[17]).toContain("allowNonConversationEndpointProviderFallback"); - expect(selections[17]).not.toContain("passThroughUpstreamErrorMessage"); + expect(selectMock).toHaveBeenCalledTimes(20); + // 第 19 次(近代链末层)不含这些新列;第 20 次(passThrough 世代)重新包含旧列。 + expect(selections[18]).not.toContain("enableThinkingEffortConflictRectifier"); + expect(selections[18]).not.toContain("allowNonConversationEndpointProviderFallback"); + expect(selections[18]).toContain("passThroughUpstreamErrorMessage"); + expect(selections[19]).toContain("enableThinkingEffortConflictRectifier"); + expect(selections[19]).toContain("allowNonConversationEndpointProviderFallback"); + expect(selections[19]).not.toContain("passThroughUpstreamErrorMessage"); // 世代字段集选出的真实值要透传,缺失列由 transformer 落默认值。 expect(result.siteTitle).toBe("Era Row"); @@ -236,7 +240,7 @@ describe("SystemSettings:列降级阶梯的尝试序列锁定", () => { expect(result.passThroughUpstreamErrorMessage).toBe(true); }); - test("updateSystemSettings 全部列缺失时按既定顺序尝试 13 套 set/returning 组合", async () => { + test("updateSystemSettings 全部列缺失时按既定顺序尝试全部 set/returning 组合", async () => { vi.resetModules(); const now = new Date("2026-01-04T00:00:00.000Z"); @@ -305,7 +309,7 @@ describe("SystemSettings:列降级阶梯的尝试序列锁定", () => { "system_settings 表列缺失,请执行数据库迁移以升级数据库结构。" ); - expect(updateMock).toHaveBeenCalledTimes(20); + expect(updateMock).toHaveBeenCalledTimes(22); const expectedReturningSequence = [ [...FULL_COLUMNS], diff --git a/tests/unit/repository/system-config-update-missing-columns.test.ts b/tests/unit/repository/system-config-update-missing-columns.test.ts index b602dc559..b03d055dc 100644 --- a/tests/unit/repository/system-config-update-missing-columns.test.ts +++ b/tests/unit/repository/system-config-update-missing-columns.test.ts @@ -291,7 +291,7 @@ describe("SystemSettings:数据库缺列时的保存兜底", () => { vi.useRealTimers(); }); - test("getSystemSettings 在仅缺 affinity_ignore_client_session_id 新列时应降级读取并默认开启", async () => { + test("getSystemSettings 在仅缺 cache_effectiveness_enabled 新列时应降级读取", async () => { vi.resetModules(); const now = new Date("2026-01-04T00:00:00.000Z"); @@ -299,7 +299,7 @@ describe("SystemSettings:数据库缺列时的保存兜底", () => { vi.setSystemTime(now); // 第一次 select(fullSelection) 因新列缺失而抛 42703; - // 第二次 select(selectionWithoutAffinityIgnore) 命中——验证新列已加入降级链最外层。 + // 第二次 select(去掉 cacheEffectivenessEnabled)命中——验证新列已加入降级链最外层。 const selectMock = vi .fn() .mockReturnValueOnce(createRejectedThenableQuery({ code: "42703" })) @@ -344,7 +344,9 @@ describe("SystemSettings:数据库缺列时的保存兜底", () => { // 关键回归保护:第二次 select 必须恰好剥离了最新列(最外层降级), // 而非旧行为先剥离更早引入的列。若新列未加入降级链最外层,下面断言会失败。 const secondSelection = selectMock.mock.calls[1]?.[0] as Record; - expect(secondSelection).not.toHaveProperty("affinityIgnoreClientSessionId"); + expect(secondSelection).not.toHaveProperty("cacheEffectivenessEnabled"); + expect(secondSelection).toHaveProperty("replayEnabled"); + expect(secondSelection).toHaveProperty("affinityIgnoreClientSessionId"); expect(secondSelection).toHaveProperty("streamGateMode"); expect(secondSelection).toHaveProperty("stickyTimeoutCooldownMs"); expect(secondSelection).toHaveProperty("racingTotalTimeoutMs"); diff --git a/tests/unit/settings/system-settings-form-replay-cache-toggles.test.tsx b/tests/unit/settings/system-settings-form-replay-cache-toggles.test.tsx new file mode 100644 index 000000000..e38519796 --- /dev/null +++ b/tests/unit/settings/system-settings-form-replay-cache-toggles.test.tsx @@ -0,0 +1,193 @@ +import fs from "node:fs"; +import path from "node:path"; +import type { ReactNode } from "react"; +import { act } from "react"; +import { createRoot } from "react-dom/client"; +import { NextIntlClientProvider } from "next-intl"; +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { SystemSettingsForm } from "@/app/[locale]/settings/config/_components/system-settings-form"; +import type { SystemSettings } from "@/types/system-config"; + +vi.mock("next/navigation", () => ({ + useRouter: () => ({ refresh: vi.fn() }), +})); + +const systemConfigActionMocks = vi.hoisted(() => ({ + saveSystemSettings: vi.fn(async () => ({ ok: true })), +})); +vi.mock("@/actions/system-config", () => systemConfigActionMocks); + +const requestFiltersActionMocks = vi.hoisted(() => ({ + getDistinctProviderGroupsAction: vi.fn(async () => ({ ok: true, data: [] })), +})); +vi.mock("@/actions/request-filters", () => requestFiltersActionMocks); + +const sonnerMocks = vi.hoisted(() => ({ + toast: { + success: vi.fn(), + error: vi.fn(), + warning: vi.fn(), + }, +})); +vi.mock("sonner", () => sonnerMocks); + +const baseSettings = { + siteTitle: "Claude Code Hub", + allowGlobalUsageView: true, + currencyDisplay: "USD", + billingModelSource: "original", + codexPriorityBillingSource: "requested", + timezone: "UTC", + verboseProviderError: false, + passThroughUpstreamErrorMessage: true, + enableHttp2: true, + enableHighConcurrencyMode: false, + interceptAnthropicWarmupRequests: false, + enableThinkingSignatureRectifier: true, + enableThinkingBudgetRectifier: true, + enableBillingHeaderRectifier: true, + enableResponseInputRectifier: true, + enableCodexSessionIdCompletion: true, + enableClaudeMetadataUserIdInjection: true, + enableResponseFixer: true, + allowNonConversationEndpointProviderFallback: true, + fakeStreamingWhitelist: [], + responseFixerConfig: { + fixEncoding: true, + fixSseFormat: true, + fixTruncatedJson: true, + }, + quotaDbRefreshIntervalSeconds: 10, + quotaLeasePercent5h: 0.05, + quotaLeasePercentDaily: 0.05, + quotaLeasePercentWeekly: 0.05, + quotaLeasePercentMonthly: 0.05, + quotaLeaseCapUsd: null, + ipGeoLookupEnabled: true, + ipExtractionConfig: null, + // null = 跟随环境变量:本组用例的核心前置 + replayEnabled: null, + cacheEffectivenessEnabled: null, +} satisfies Pick< + SystemSettings, + | "siteTitle" + | "allowGlobalUsageView" + | "currencyDisplay" + | "billingModelSource" + | "codexPriorityBillingSource" + | "timezone" + | "verboseProviderError" + | "passThroughUpstreamErrorMessage" + | "enableHttp2" + | "enableHighConcurrencyMode" + | "interceptAnthropicWarmupRequests" + | "enableThinkingSignatureRectifier" + | "enableThinkingBudgetRectifier" + | "enableBillingHeaderRectifier" + | "enableResponseInputRectifier" + | "enableCodexSessionIdCompletion" + | "enableClaudeMetadataUserIdInjection" + | "enableResponseFixer" + | "allowNonConversationEndpointProviderFallback" + | "fakeStreamingWhitelist" + | "responseFixerConfig" + | "quotaDbRefreshIntervalSeconds" + | "quotaLeasePercent5h" + | "quotaLeasePercentDaily" + | "quotaLeasePercentWeekly" + | "quotaLeasePercentMonthly" + | "quotaLeaseCapUsd" + | "ipGeoLookupEnabled" + | "ipExtractionConfig" + | "replayEnabled" + | "cacheEffectivenessEnabled" +>; + +function loadMessages(locale: string) { + const base = path.join(process.cwd(), `messages/${locale}/settings`); + const read = (name: string) => JSON.parse(fs.readFileSync(path.join(base, name), "utf8")); + + return { + settings: { + common: read("common.json"), + config: read("config.json"), + requestFilters: read("requestFilters.json"), + }, + }; +} + +function render(node: ReactNode) { + const container = document.createElement("div"); + document.body.appendChild(container); + const root = createRoot(container); + + act(() => { + root.render( + + {node} + + ); + }); + + return { + unmount: () => { + act(() => root.unmount()); + container.remove(); + }, + }; +} + +async function submitForm() { + const form = document.body.querySelector("form"); + if (!form) throw new Error("未找到系统设置表单"); + + await act(async () => { + form.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true })); + await Promise.resolve(); + await new Promise((resolve) => setTimeout(resolve, 0)); + }); +} + +describe("SystemSettingsForm replay/cache-effectiveness null 三态", () => { + beforeEach(() => { + document.body.innerHTML = ""; + vi.clearAllMocks(); + }); + + test("未触碰开关时保存保持 null(跟随环境变量),不写死布尔覆写", async () => { + const { unmount } = render(); + + await submitForm(); + + expect(systemConfigActionMocks.saveSystemSettings).toHaveBeenCalledWith( + expect.objectContaining({ + replayEnabled: null, + cacheEffectivenessEnabled: null, + }) + ); + + unmount(); + }); + + test("用户切换开关后保存为显式覆写,未动的另一开关仍为 null", async () => { + const { unmount } = render(); + + const switchEl = document.getElementById("replay-enabled"); + if (!switchEl) throw new Error("未找到 replay-enabled 开关"); + await act(async () => { + (switchEl as HTMLElement).click(); + await Promise.resolve(); + }); + + await submitForm(); + + expect(systemConfigActionMocks.saveSystemSettings).toHaveBeenCalledWith( + expect.objectContaining({ + replayEnabled: true, + cacheEffectivenessEnabled: null, + }) + ); + + unmount(); + }); +});