diff --git a/drizzle/0102_useful_lionheart.sql b/drizzle/0102_useful_lionheart.sql new file mode 100644 index 000000000..314f98869 --- /dev/null +++ b/drizzle/0102_useful_lionheart.sql @@ -0,0 +1 @@ +ALTER TABLE "system_settings" ADD COLUMN IF NOT EXISTS "bill_non_successful_requests" boolean DEFAULT false NOT NULL; \ No newline at end of file diff --git a/drizzle/meta/0102_snapshot.json b/drizzle/meta/0102_snapshot.json new file mode 100644 index 000000000..91be4a5ab --- /dev/null +++ b/drizzle/meta/0102_snapshot.json @@ -0,0 +1,4497 @@ +{ + "id": "278b6561-15fa-4eac-804d-228f8480e173", + "prevId": "f475eec8-aa3e-4c45-8b70-85400b73a776", + "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.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 + }, + "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 + }, + "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 + }, + "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": "'litellm'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_model_prices_latest": { + "name": "idx_model_prices_latest", + "columns": [ + { + "expression": "model_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_model_prices_model_name": { + "name": "idx_model_prices_model_name", + "columns": [ + { + "expression": "model_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_model_prices_created_at": { + "name": "idx_model_prices_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_model_prices_source": { + "name": "idx_model_prices_source", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_settings": { + "name": "notification_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "use_legacy_mode": { + "name": "use_legacy_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "circuit_breaker_enabled": { + "name": "circuit_breaker_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "circuit_breaker_webhook": { + "name": "circuit_breaker_webhook", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "daily_leaderboard_enabled": { + "name": "daily_leaderboard_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "daily_leaderboard_webhook": { + "name": "daily_leaderboard_webhook", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "daily_leaderboard_time": { + "name": "daily_leaderboard_time", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false, + "default": "'09:00'" + }, + "daily_leaderboard_top_n": { + "name": "daily_leaderboard_top_n", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5 + }, + "cost_alert_enabled": { + "name": "cost_alert_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cost_alert_webhook": { + "name": "cost_alert_webhook", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "cost_alert_threshold": { + "name": "cost_alert_threshold", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false, + "default": "'0.80'" + }, + "cost_alert_check_interval": { + "name": "cost_alert_check_interval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 60 + }, + "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_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_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.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 + }, + "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_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 + }, + "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" + } + ], + "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" + } + ], + "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" + } + ], + "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 e1cebfb58..35050d3d0 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -715,6 +715,13 @@ "when": 1777362451735, "tag": "0101_worthless_gauntlet", "breakpoints": true + }, + { + "idx": 102, + "version": "7", + "when": 1777739658761, + "tag": "0102_useful_lionheart", + "breakpoints": true } ] } \ No newline at end of file diff --git a/messages/en/auditLogs.json b/messages/en/auditLogs.json index 3f597b671..4bb77e766 100644 --- a/messages/en/auditLogs.json +++ b/messages/en/auditLogs.json @@ -59,7 +59,8 @@ "key": { "create": "Create key", "update": "Update key", - "delete": "Delete key" + "delete": "Delete key", + "key_reveal": "Reveal user key" }, "notification": { "update": "Update notification" diff --git a/messages/en/settings/config.json b/messages/en/settings/config.json index 83c989935..17faa0378 100644 --- a/messages/en/settings/config.json +++ b/messages/en/settings/config.json @@ -108,6 +108,9 @@ "siteTitleRequired": "Site title cannot be empty", "ipLoggingInvalidJson": "IP extraction config is not valid JSON: {message}", "ipLoggingInvalidShape": "IP extraction config must be an object with a `headers` array.", + "billNonSuccessfulRequests": "Bill Non-2xx Requests by Token Usage", + "billNonSuccessfulRequestsDesc": "When enabled, requests with non-success status (e.g., 499 client cancellation) are billed by token usage if upstream returned positive usage data. Default off.", + "billNonSuccessfulRequestsTooltip": "Useful when an upstream provider counts tokens regardless of the final status (e.g., aborted streaming responses). Fake-200 upstream errors remain unbilled.", "verboseProviderError": "Verbose Provider Error", "verboseProviderErrorDesc": "When enabled, CCH may return detailed diagnostic information for some upstream failure types in `error.details` (for example provider availability diagnostics or sanitized upstream snippets).", "verboseProviderErrorTooltip": "May expose provider names, internal routing clues, upstream failure reasons, and other diagnostic details. Enable only if clients are allowed to see low-level troubleshooting context.", diff --git a/messages/ja/auditLogs.json b/messages/ja/auditLogs.json index 2b5ea2f7f..6e21cdf4e 100644 --- a/messages/ja/auditLogs.json +++ b/messages/ja/auditLogs.json @@ -59,7 +59,8 @@ "key": { "create": "キー作成", "update": "キー更新", - "delete": "キー削除" + "delete": "キー削除", + "key_reveal": "ユーザーキーを表示" }, "notification": { "update": "通知更新" diff --git a/messages/ja/settings/config.json b/messages/ja/settings/config.json index bc4b18b3d..2da14986a 100644 --- a/messages/ja/settings/config.json +++ b/messages/ja/settings/config.json @@ -110,6 +110,9 @@ "siteTitleRequired": "サイトタイトルは空にできません", "ipLoggingInvalidJson": "IP 抽出設定が有効な JSON ではありません:{message}", "ipLoggingInvalidShape": "IP 抽出設定は `headers` 配列を持つオブジェクトである必要があります。", + "billNonSuccessfulRequests": "非成功リクエストを Token 使用量で課金", + "billNonSuccessfulRequestsDesc": "有効にすると、非 2xx ステータス(例: クライアント中断による 499)のリクエストでも、上流が正の token 使用量を返した場合は使用量に応じて課金されます。既定はオフ。", + "billNonSuccessfulRequestsTooltip": "上流プロバイダーがレスポンス失敗時にも token をカウントするケース(ストリーム中断でも token を計上する等)に有用です。fake-200 の偽成功エラー応答は引き続き課金されません。", "verboseProviderError": "詳細なプロバイダーエラー", "verboseProviderErrorDesc": "有効にすると、一部の上流障害タイプで `error.details` により詳細な診断情報(プロバイダー可用性の診断やサニタイズ済み上流断片など)を含める場合があります。", "verboseProviderErrorTooltip": "この設定を有効にすると、プロバイダー名、内部ルーティングの手掛かり、上流障害の理由などの診断情報が露出する可能性があります。クライアントに低レベルのトラブルシュート文脈を見せてもよい場合にのみ有効化してください。", diff --git a/messages/ru/auditLogs.json b/messages/ru/auditLogs.json index 1cd4e0e63..e05922d7c 100644 --- a/messages/ru/auditLogs.json +++ b/messages/ru/auditLogs.json @@ -59,7 +59,8 @@ "key": { "create": "Создание ключа", "update": "Обновление ключа", - "delete": "Удаление ключа" + "delete": "Удаление ключа", + "key_reveal": "Просмотр ключа пользователя" }, "notification": { "update": "Обновление уведомления" diff --git a/messages/ru/settings/config.json b/messages/ru/settings/config.json index 04f62d47b..509d5fe7e 100644 --- a/messages/ru/settings/config.json +++ b/messages/ru/settings/config.json @@ -110,6 +110,9 @@ "siteTitleRequired": "Название сайта не может быть пустым", "ipLoggingInvalidJson": "Конфигурация извлечения IP не является валидным JSON: {message}", "ipLoggingInvalidShape": "Конфигурация извлечения IP должна быть объектом с массивом `headers`.", + "billNonSuccessfulRequests": "Тарифицировать неуспешные запросы по токенам", + "billNonSuccessfulRequestsDesc": "Если включено, запросы с не-2xx статусом (например, 499 при отмене клиентом) будут тарифицироваться по фактическому количеству токенов, если апстрим вернул положительные данные об использовании. По умолчанию выключено.", + "billNonSuccessfulRequestsTooltip": "Полезно, когда апстрим считает токены даже при неудачном статусе (например, прерванные стриминговые ответы). Поддельные ответы 200 (fake-200) по-прежнему не тарифицируются.", "verboseProviderError": "Подробные ошибки провайдеров", "verboseProviderErrorDesc": "При включении CCH может добавлять более подробную диагностику некоторых типов сбоев апстрима в `error.details` (например, диагностику доступности провайдеров или очищенные фрагменты ответа апстрима).", "verboseProviderErrorTooltip": "Может раскрывать названия провайдеров, внутренние подсказки маршрутизации, причины сбоев апстрима и другие диагностические детали. Включайте только если клиентам допустимо видеть низкоуровневый контекст отладки.", diff --git a/messages/zh-CN/auditLogs.json b/messages/zh-CN/auditLogs.json index b7c7c01f5..8fad548e4 100644 --- a/messages/zh-CN/auditLogs.json +++ b/messages/zh-CN/auditLogs.json @@ -59,7 +59,8 @@ "key": { "create": "创建密钥", "update": "更新密钥", - "delete": "删除密钥" + "delete": "删除密钥", + "key_reveal": "查看用户密钥" }, "notification": { "update": "更新通知" diff --git a/messages/zh-CN/settings/config.json b/messages/zh-CN/settings/config.json index b6301f65d..a6085902b 100644 --- a/messages/zh-CN/settings/config.json +++ b/messages/zh-CN/settings/config.json @@ -41,6 +41,9 @@ }, "allowGlobalView": "允许查看全站使用量", "allowGlobalViewDesc": "关闭后,普通用户在仪表盘仅能查看自己密钥的使用统计。", + "billNonSuccessfulRequests": "非成功请求按 Token 用量计费", + "billNonSuccessfulRequestsDesc": "开启后,对于响应非 2xx 状态码(例如客户端中断的 499)的请求,只要上游返回了正向 token 用量,仍按用量计费。默认关闭。", + "billNonSuccessfulRequestsTooltip": "适用于上游供应商即使响应失败也按 token 计费的场景(如流式中断时仍计入 token)。fake-200 假成功错误响应仍不会计费。", "verboseProviderError": "详细供应商错误信息", "verboseProviderErrorDesc": "开启后,CCH 会在某些上游失败类型下于 `error.details` 返回更详细的诊断信息(例如供应商可用性诊断或脱敏后的上游片段)。", "verboseProviderErrorTooltip": "该选项可能暴露供应商名称、内部路由线索、上游失败原因等诊断信息。仅建议在客户端可以查看底层排障上下文时开启。", diff --git a/messages/zh-TW/auditLogs.json b/messages/zh-TW/auditLogs.json index e0864f360..c63323140 100644 --- a/messages/zh-TW/auditLogs.json +++ b/messages/zh-TW/auditLogs.json @@ -59,7 +59,8 @@ "key": { "create": "建立金鑰", "update": "更新金鑰", - "delete": "刪除金鑰" + "delete": "刪除金鑰", + "key_reveal": "查看用戶金鑰" }, "notification": { "update": "更新通知" diff --git a/messages/zh-TW/settings/config.json b/messages/zh-TW/settings/config.json index 08fe73139..4f99a9021 100644 --- a/messages/zh-TW/settings/config.json +++ b/messages/zh-TW/settings/config.json @@ -110,6 +110,9 @@ "siteTitleRequired": "站台標題不能為空", "ipLoggingInvalidJson": "IP 擷取設定不是合法的 JSON:{message}", "ipLoggingInvalidShape": "IP 擷取設定必須是包含 `headers` 陣列的物件。", + "billNonSuccessfulRequests": "非成功請求按 Token 用量計費", + "billNonSuccessfulRequestsDesc": "開啟後,對於回應非 2xx 狀態碼(例如客戶端中斷的 499)的請求,只要上游回報了正向 token 用量,仍會按用量計費。預設關閉。", + "billNonSuccessfulRequestsTooltip": "適用於上游供應商即使回應失敗也按 token 計費的情境(例如串流中斷時仍記入 token)。fake-200 偽成功錯誤響應仍不會計費。", "verboseProviderError": "詳細供應商錯誤資訊", "verboseProviderErrorDesc": "開啟後,CCH 會在某些上游失敗類型下於 `error.details` 返回較詳細的診斷資訊(例如供應商可用性診斷或脫敏後的上游片段)。", "verboseProviderErrorTooltip": "此選項可能暴露供應商名稱、內部路由線索、上游失敗原因等診斷資訊。僅建議在客戶端可以查看底層排障上下文時開啟。", diff --git a/src/actions/keys.ts b/src/actions/keys.ts index f9d8c1752..a00882d42 100644 --- a/src/actions/keys.ts +++ b/src/actions/keys.ts @@ -828,6 +828,69 @@ export async function getKeysWithStatistics( } } +/** + * 获取密钥的未脱敏值 + * - 管理员:可查看任意用户的密钥 + * - 普通用户:仅可查看自己拥有的密钥(与列表 canReveal/canCopy 契约保持一致) + * 用于安全展示和复制完整 Key + */ +export async function getUnmaskedKey(keyId: number): Promise> { + try { + const session = await getSession(); + if (!session) { + return { ok: false, error: "未登录" }; + } + + const key = await findKeyById(keyId); + if (!key) { + return { ok: false, error: "密钥不存在" }; + } + + const isAdmin = session.user.role === "admin"; + const isOwner = session.user.id === key.userId; + if (!isAdmin && !isOwner) { + return { ok: false, error: "无权限执行此操作" }; + } + + // 记录查看行为(不记录密钥内容) + logger.info("User viewed key", { + viewerId: session.user.id, + viewerRole: session.user.role, + keyId, + keyName: key.name, + keyOwnerId: key.userId, + }); + emitActionAudit({ + category: "key", + action: "key.key_reveal", + targetType: "key", + targetId: String(key.id), + targetName: key.name, + after: { + id: key.id, + name: key.name, + userId: key.userId, + }, + success: true, + redactExtraKeys: ["key"], + }); + + return { ok: true, data: { key: key.key } }; + } catch (error) { + logger.error("获取密钥失败:", error); + const message = error instanceof Error ? error.message : "获取密钥失败"; + emitActionAudit({ + category: "key", + action: "key.key_reveal", + targetType: "key", + targetId: String(keyId), + success: false, + errorMessage: "KEY_REVEAL_FAILED", + }); + return { ok: false, error: message }; + } +} + /** * 获取密钥的限额使用情况(实时数据) */ diff --git a/src/actions/system-config.ts b/src/actions/system-config.ts index badc67209..b1cac1f48 100644 --- a/src/actions/system-config.ts +++ b/src/actions/system-config.ts @@ -57,6 +57,7 @@ export async function saveSystemSettings(formData: { currencyDisplay?: string; billingModelSource?: string; codexPriorityBillingSource?: CodexPriorityBillingSource; + billNonSuccessfulRequests?: boolean; timezone?: string | null; enableAutoCleanup?: boolean; cleanupRetentionDays?: number; @@ -107,6 +108,7 @@ export async function saveSystemSettings(formData: { currencyDisplay: validated.currencyDisplay, billingModelSource: validated.billingModelSource, codexPriorityBillingSource: validated.codexPriorityBillingSource, + billNonSuccessfulRequests: validated.billNonSuccessfulRequests, timezone: validated.timezone, enableAutoCleanup: validated.enableAutoCleanup, cleanupRetentionDays: validated.cleanupRetentionDays, diff --git a/src/actions/users.ts b/src/actions/users.ts index 1cc239f36..1ffbe2c3e 100644 --- a/src/actions/users.ts +++ b/src/actions/users.ts @@ -417,7 +417,7 @@ export async function getUsers(params?: GetUsersBatchParams): Promise(null); const [expandedKeys, setExpandedKeys] = useState>(new Set()); const [visibleKeyIds, setVisibleKeyIds] = useState>(new Set()); const [clipboardAvailable, setClipboardAvailable] = useState(false); + // Cache unmasked keys after they have been revealed (lazy fetch via :reveal endpoint). + const [revealedKeys, setRevealedKeys] = useState>({}); + const [revealingKeyId, setRevealingKeyId] = useState(null); const canDeleteKeys = keys.length > 1; // 检测 clipboard 是否可用 @@ -48,6 +54,27 @@ export function KeyList({ setClipboardAvailable(isClipboardSupported()); }, []); + // Drop the reveal cache whenever the underlying key set changes so a removed + // / replaced id can't expose its previously cached plaintext on a new row. + useEffect(() => { + const currentIds = new Set(keys.map((k) => k.id)); + setRevealedKeys((prev) => { + const next: Record = {}; + for (const [idStr, value] of Object.entries(prev)) { + const id = Number(idStr); + if (currentIds.has(id)) next[id] = value; + } + return next; + }); + setVisibleKeyIds((prev) => { + const next = new Set(); + for (const id of prev) { + if (currentIds.has(id)) next.add(id); + } + return next; + }); + }, [keys]); + const toggleExpanded = (keyId: number) => { setExpandedKeys((prev) => { const newSet = new Set(prev); @@ -72,16 +99,51 @@ export function KeyList({ }); }; - const handleCopyKey = async (key: UserKeyDisplay) => { - if (!key.fullKey || !key.canCopy) return; + const fetchUnmaskedKey = async (keyId: number): Promise => { + if (revealedKeys[keyId]) return revealedKeys[keyId]; + setRevealingKeyId(keyId); + try { + const res = await getUnmaskedKey(keyId); + if (!res.ok) { + toast.error(res.error || t("copyFailedTooltip")); + return null; + } + if (!res.data?.key) { + toast.error(tCommon("copyFailed")); + return null; + } + setRevealedKeys((prev) => ({ ...prev, [keyId]: res.data.key })); + return res.data.key; + } catch (error) { + console.error("[KeyList] reveal failed", error); + toast.error(tCommon("copyFailed")); + return null; + } finally { + setRevealingKeyId(null); + } + }; - const success = await copyToClipboard(key.fullKey); + const handleCopyKey = async (key: UserKeyDisplay) => { + if (!key.canCopy) return; + const fullKey = await fetchUnmaskedKey(key.id); + if (!fullKey) return; + const success = await copyToClipboard(fullKey); if (success) { setCopiedKeyId(key.id); setTimeout(() => setCopiedKeyId(null), 2000); } }; + const handleToggleVisibility = async (keyId: number) => { + if (visibleKeyIds.has(keyId)) { + toggleKeyVisibility(keyId); + return; + } + const fullKey = await fetchUnmaskedKey(keyId); + if (!fullKey) return; + toggleKeyVisibility(keyId); + }; + const columns = [ TableColumnTypes.text("name", t("columns.name"), { render: (value, record) => { @@ -179,19 +241,21 @@ export function KeyList({ TableColumnTypes.text("maskedKey", t("columns.key"), { render: (_, record: UserKeyDisplay) => { const isVisible = visibleKeyIds.has(record.id); - const displayKey = isVisible && record.fullKey ? record.fullKey : record.maskedKey || "-"; + const fullKey = revealedKeys[record.id]; + const displayKey = isVisible && fullKey ? fullKey : record.maskedKey || "-"; + const isRevealing = revealingKeyId === record.id; return (
{displayKey}
- {record.canCopy && - record.fullKey && - (clipboardAvailable ? ( - // HTTPS 环境:显示复制按钮 + {record.canReveal && + (clipboardAvailable && record.canCopy ? ( + // HTTPS 环境:显示复制按钮(按需 fetch 完整 key) ) : ( - // HTTP 环境:显示显示/隐藏按钮 + // HTTP 环境(或不允许复制):显示显示/隐藏按钮(按需 fetch)
{/* Full Key Display Dialog */} - {keyData.fullKey && ( + {revealedKey && ( )} diff --git a/src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx b/src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx index 279adcea1..3fedefe77 100644 --- a/src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx +++ b/src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx @@ -640,7 +640,7 @@ export function UserKeyTableRow({ id: key.id, name: key.name, maskedKey: key.maskedKey, - fullKey: key.fullKey, + canReveal: key.canReveal, canCopy: key.canCopy, providerGroup: key.providerGroup, todayUsage: key.todayUsage, diff --git a/src/app/[locale]/dashboard/users/users-page-client.tsx b/src/app/[locale]/dashboard/users/users-page-client.tsx index 76656adff..2673fc466 100644 --- a/src/app/[locale]/dashboard/users/users-page-client.tsx +++ b/src/app/[locale]/dashboard/users/users-page-client.tsx @@ -378,7 +378,6 @@ function UsersPageContent({ currentUser }: UsersPageClientProps) { hasSearch && (key.name.toLowerCase().includes(normalizedTerm) || key.maskedKey.toLowerCase().includes(normalizedTerm) || - (key.fullKey || "").toLowerCase().includes(normalizedTerm) || (key.providerGroup || "").toLowerCase().includes(normalizedTerm)); const matchesKeyGroup = 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 8ea361909..7dc015849 100644 --- a/src/app/[locale]/settings/config/_components/system-settings-form.tsx +++ b/src/app/[locale]/settings/config/_components/system-settings-form.tsx @@ -5,6 +5,7 @@ import { ChevronDown, CircleHelp, Clock, + Coins, Eye, FileCode, Globe, @@ -65,6 +66,7 @@ interface SystemSettingsFormProps { | "currencyDisplay" | "billingModelSource" | "codexPriorityBillingSource" + | "billNonSuccessfulRequests" | "timezone" | "verboseProviderError" | "passThroughUpstreamErrorMessage" @@ -124,6 +126,9 @@ export function SystemSettingsForm({ initialSettings }: SystemSettingsFormProps) ); const [codexPriorityBillingSource, setCodexPriorityBillingSource] = useState(initialSettings.codexPriorityBillingSource); + const [billNonSuccessfulRequests, setBillNonSuccessfulRequests] = useState( + initialSettings.billNonSuccessfulRequests + ); const [timezone, setTimezone] = useState(initialSettings.timezone); const [verboseProviderError, setVerboseProviderError] = useState( initialSettings.verboseProviderError @@ -298,6 +303,7 @@ export function SystemSettingsForm({ initialSettings }: SystemSettingsFormProps) currencyDisplay, billingModelSource, codexPriorityBillingSource, + billNonSuccessfulRequests, timezone, verboseProviderError, passThroughUpstreamErrorMessage, @@ -336,6 +342,7 @@ export function SystemSettingsForm({ initialSettings }: SystemSettingsFormProps) setCurrencyDisplay(result.data.currencyDisplay); setBillingModelSource(result.data.billingModelSource); setCodexPriorityBillingSource(result.data.codexPriorityBillingSource); + setBillNonSuccessfulRequests(result.data.billNonSuccessfulRequests); setTimezone(result.data.timezone); setVerboseProviderError(result.data.verboseProviderError); setPassThroughUpstreamErrorMessage(result.data.passThroughUpstreamErrorMessage); @@ -541,6 +548,46 @@ export function SystemSettingsForm({ initialSettings }: SystemSettingsFormProps) /> + {/* Bill Non-Successful Requests */} +
+
+
+ +
+
+
+

+ {t("billNonSuccessfulRequests")} +

+ + + + + + {t("billNonSuccessfulRequestsTooltip")} + + +
+

+ {t("billNonSuccessfulRequestsDesc")} +

+
+
+ setBillNonSuccessfulRequests(checked)} + disabled={isPending} + /> +
+ {/* Verbose Provider Error */}
diff --git a/src/app/[locale]/settings/config/page.tsx b/src/app/[locale]/settings/config/page.tsx index a3acb0978..03958076e 100644 --- a/src/app/[locale]/settings/config/page.tsx +++ b/src/app/[locale]/settings/config/page.tsx @@ -50,6 +50,7 @@ async function SettingsConfigContent({ locale }: { locale: string }) { currencyDisplay: settings.currencyDisplay, billingModelSource: settings.billingModelSource, codexPriorityBillingSource: settings.codexPriorityBillingSource, + billNonSuccessfulRequests: settings.billNonSuccessfulRequests, timezone: settings.timezone, verboseProviderError: settings.verboseProviderError, passThroughUpstreamErrorMessage: settings.passThroughUpstreamErrorMessage, diff --git a/src/app/api/actions/[...route]/route.ts b/src/app/api/actions/[...route]/route.ts index ac3348128..1e2db649d 100644 --- a/src/app/api/actions/[...route]/route.ts +++ b/src/app/api/actions/[...route]/route.ts @@ -182,7 +182,7 @@ const userKeyListItemSchema = z.object({ id: z.number().describe("密钥 ID"), name: z.string().describe("密钥名称"), maskedKey: z.string().describe("脱敏后的密钥"), - fullKey: z.string().optional().describe("完整密钥(有权限时返回)"), + canReveal: z.boolean().describe("是否允许查看完整密钥(需通过 /api/v1/keys/{id}:reveal 拉取)"), canCopy: z.boolean().describe("是否允许复制完整密钥"), expiresAt: z.string().describe("过期时间展示值"), status: z.enum(["enabled", "disabled"]).describe("密钥状态"), diff --git a/src/app/api/v1/resources/keys/handlers.ts b/src/app/api/v1/resources/keys/handlers.ts index 5c045abf6..b6c3b12dd 100644 --- a/src/app/api/v1/resources/keys/handlers.ts +++ b/src/app/api/v1/resources/keys/handlers.ts @@ -135,6 +135,20 @@ export async function renewKey(c: Context): Promise { ); } +export async function revealKey(c: Context): Promise { + const params = parseKeyParams(c); + if (params instanceof Response) return params; + const actions = await import("@/actions/keys"); + const result = await callAction( + c, + actions.getUnmaskedKey, + [params.keyId] as never[], + c.get("auth") + ); + if (!result.ok) return actionError(c, result); + return jsonResponse(result.data, { headers: withNoStoreHeaders() }); +} + export async function resetKeyLimits(c: Context): Promise { const params = parseKeyParams(c); if (params instanceof Response) return params; @@ -206,7 +220,7 @@ function parseUserParams(c: Context): { userId: number } | Response { } function parseKeyParams(c: Context): { keyId: number } | Response { - const rawKeyId = (c.req.param("keyId") ?? "").replace(/:(enable|renew)$/, ""); + const rawKeyId = (c.req.param("keyId") ?? "").replace(/:(enable|renew|reveal)$/, ""); const params = KeyIdParamSchema.safeParse({ keyId: rawKeyId }); if (!params.success) return fromZodError(params.error, new URL(c.req.url).pathname); return params.data; diff --git a/src/app/api/v1/resources/keys/router.ts b/src/app/api/v1/resources/keys/router.ts index 7dddbe329..fbe05095a 100644 --- a/src/app/api/v1/resources/keys/router.ts +++ b/src/app/api/v1/resources/keys/router.ts @@ -10,6 +10,7 @@ import { KeyListQuerySchema, KeyListResponseSchema, KeyRenewSchema, + KeyRevealResponseSchema, KeysBatchUpdateSchema, KeyUpdateSchema, PatchKeyLimitParamSchema, @@ -28,6 +29,7 @@ import { patchKeyLimit, renewKey, resetKeyLimits, + revealKey, updateKey, } from "./handlers"; @@ -220,6 +222,30 @@ const renewKeyRoute = createRoute({ keysRouter.openAPIRegistry.registerPath(renewKeyRoute); keysRouter.post("/keys/:keyId{[0-9]+:renew}", requireAuth("admin"), renewKey); +const revealKeyRoute = createRoute({ + method: "get", + path: "/keys/{keyId}:reveal", + tags: ["Keys"], + summary: "Reveal key", + description: + "Returns the unmasked user API key. Admins may reveal any key; regular users may reveal only the keys they own. Non-owners receive 403. Writes the existing audit log on every call.", + "x-required-access": "read", + security, + request: { params: KeyIdParamSchema }, + responses: { + 200: { + description: "Unmasked key.", + content: { "application/json": { schema: KeyRevealResponseSchema } }, + }, + ...problemResponses, + }, +}); + +keysRouter.openAPIRegistry.registerPath(revealKeyRoute); +// Auth tier is "read" so the action's per-request ownership check can +// apply (admin OR key owner). Non-owners are rejected at the action layer. +keysRouter.get("/keys/:keyId{[0-9]+:reveal}", requireAuth("read"), revealKey); + keysRouter.openapi( createRoute({ method: "post", diff --git a/src/app/v1/_lib/proxy/response-handler.ts b/src/app/v1/_lib/proxy/response-handler.ts index 4d8662f37..3de917794 100644 --- a/src/app/v1/_lib/proxy/response-handler.ts +++ b/src/app/v1/_lib/proxy/response-handler.ts @@ -1,6 +1,7 @@ import { ResponseFixer } from "@/app/v1/_lib/proxy/response-fixer"; import { AsyncTaskManager } from "@/lib/async-task-manager"; import { getEnvConfig } from "@/lib/config/env.schema"; +import { getCachedSystemSettings } from "@/lib/config/system-settings-cache"; import { emitProxyLangfuseTrace } from "@/lib/langfuse/emit-proxy-trace"; import { logger } from "@/lib/logger"; import { requestCloudPriceTableSync } from "@/lib/price-sync/cloud-price-updater"; @@ -378,7 +379,21 @@ function hasBillableInputCostPerRequest(priceData: { input_cost_per_request?: un ); } -async function resolveBillableUsageMetricsForCost( +function hasPositiveBillableTokens(usage: UsageMetrics | null): boolean { + if (!usage) return false; + const tokens = + (usage.input_tokens ?? 0) + + (usage.output_tokens ?? 0) + + (usage.cache_creation_input_tokens ?? 0) + + (usage.cache_creation_5m_input_tokens ?? 0) + + (usage.cache_creation_1h_input_tokens ?? 0) + + (usage.cache_read_input_tokens ?? 0) + + (usage.input_image_tokens ?? 0) + + (usage.output_image_tokens ?? 0); + return tokens > 0; +} + +export async function resolveBillableUsageMetricsForCost( session: ProxySession, provider: Provider | null, usageMetrics: UsageMetrics | null, @@ -390,7 +405,37 @@ async function resolveBillableUsageMetricsForCost( } if (statusCode < 200 || statusCode >= 300) { - return null; + // 默认行为:非 2xx 不计费,避免对失败/中断的请求重复扣费。 + // 当 billNonSuccessfulRequests 开关打开时,只要上游已回报正向 token 用量 + // (典型场景:499 客户端中断但上游已计算 token),仍按 usage 计费。 + let allowBillingNonSuccess = false; + try { + const settings = await getCachedSystemSettings(); + allowBillingNonSuccess = settings.billNonSuccessfulRequests === true; + } catch (error) { + logger.warn( + "[CostCalculation] Failed to read billNonSuccessfulRequests setting, defaulting to skip", + { error: error instanceof Error ? error.message : String(error) } + ); + } + + if (!allowBillingNonSuccess) { + return null; + } + + if (!hasPositiveBillableTokens(usageMetrics)) { + return null; + } + + logger.info("[CostCalculation] Billing non-2xx request per system setting", { + statusCode, + providerId: provider?.id, + providerName: provider?.name, + originalModel: session.getOriginalModel(), + redirectedModel: session.getCurrentModel(), + }); + + return usageMetrics; } if (responseText !== undefined && responseText !== null) { diff --git a/src/drizzle/schema.ts b/src/drizzle/schema.ts index 1cbf4c7b7..426f4d63e 100644 --- a/src/drizzle/schema.ts +++ b/src/drizzle/schema.ts @@ -736,6 +736,11 @@ export const systemSettings = pgTable('system_settings', { .notNull() .default('requested'), + // 非成功请求按 token 用量计费(默认关闭) + // 关闭:返回 4xx/5xx 时即使上游回报了 token 用量也不计费(当前行为) + // 开启:只要上游返回了正向 token 用量,无论响应状态码如何都按用量计费(fake-200 错误检测仍然生效) + billNonSuccessfulRequests: boolean('bill_non_successful_requests').notNull().default(false), + // 系统时区配置 (IANA timezone identifier) // 用于统一后端时间边界计算和前端日期/时间显示 // null 表示使用环境变量 TZ 或默认 UTC diff --git a/src/lib/api-client/v1/actions/keys.ts b/src/lib/api-client/v1/actions/keys.ts index f9d95b6bf..05b2299ff 100644 --- a/src/lib/api-client/v1/actions/keys.ts +++ b/src/lib/api-client/v1/actions/keys.ts @@ -61,3 +61,7 @@ export function renewKeyExpiresAt(keyId: number, data: unknown) { export function patchKeyLimit(keyId: number, field: PatchKeyLimitField, value: unknown) { return toActionResult(apiPatch(`/api/v1/keys/${keyId}/limits/${field}`, { value })); } + +export function getUnmaskedKey(keyId: number) { + return toActionResult(apiGet<{ key: string }>(`/api/v1/keys/${keyId}:reveal`)); +} diff --git a/src/lib/api-client/v1/openapi-types.gen.ts b/src/lib/api-client/v1/openapi-types.gen.ts index 4b1eccc4a..bf789d248 100644 --- a/src/lib/api-client/v1/openapi-types.gen.ts +++ b/src/lib/api-client/v1/openapi-types.gen.ts @@ -2536,6 +2536,26 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/keys/{keyId}:reveal": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Reveal key + * @description Returns the unmasked user API key. Admins may reveal any key; regular users may reveal only the keys they own. Non-owners receive 403. Writes the existing audit log on every call. + */ + get: operations["getKeysByKeyidReveal"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/keys/{keyId}/limits:reset": { parameters: { query?: never; @@ -11559,6 +11579,8 @@ export interface operations { * @enum {string} */ codexPriorityBillingSource: "requested" | "actual"; + /** @description Whether non-2xx responses (e.g., 499) that report token usage should be billed normally. */ + billNonSuccessfulRequests: boolean; /** @description Configured system timezone, or null for default. */ timezone: string | null; /** @description Whether usage-log cleanup is enabled. */ @@ -11813,6 +11835,8 @@ export interface operations { * @enum {string} */ codexPriorityBillingSource?: "requested" | "actual"; + /** @description Whether non-2xx responses (e.g., 499) that report token usage should be billed normally. */ + billNonSuccessfulRequests?: boolean; /** @description System timezone, or null to use default. */ timezone?: string | null; /** @description Whether usage-log cleanup is enabled. */ @@ -11940,6 +11964,8 @@ export interface operations { * @enum {string} */ codexPriorityBillingSource: "requested" | "actual"; + /** @description Whether non-2xx responses (e.g., 499) that report token usage should be billed normally. */ + billNonSuccessfulRequests: boolean; /** @description Configured system timezone, or null for default. */ timezone: string | null; /** @description Whether usage-log cleanup is enabled. */ @@ -33669,6 +33695,180 @@ export interface operations { }; }; }; + getKeysByKeyidReveal: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Key id. */ + keyId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Unmasked key. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @description Unmasked key value. Returned only to admin callers. */ + key: string; + }; + }; + }; + /** @description Invalid request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": { + /** @description Stable problem type URI or URN. */ + type: string; + /** @description Short problem title. */ + title: string; + /** @description HTTP status code. */ + status: number; + /** @description Human-readable error detail. */ + detail: string; + /** @description Request path that produced the problem. */ + instance: string; + /** @description Application error code for frontend i18n. */ + errorCode: string; + /** @description Optional i18n parameters. */ + errorParams?: { + [key: string]: unknown; + }; + /** @description Optional request trace identifier. */ + traceId?: string; + /** @description Validation failure details. */ + invalidParams?: { + /** @description Path to the invalid input field. */ + path: (string | number)[]; + /** @description Machine-readable validation error code. */ + code: string; + /** @description Validation error message. */ + message: string; + }[]; + }; + }; + }; + /** @description Authentication required. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": { + /** @description Stable problem type URI or URN. */ + type: string; + /** @description Short problem title. */ + title: string; + /** @description HTTP status code. */ + status: number; + /** @description Human-readable error detail. */ + detail: string; + /** @description Request path that produced the problem. */ + instance: string; + /** @description Application error code for frontend i18n. */ + errorCode: string; + /** @description Optional i18n parameters. */ + errorParams?: { + [key: string]: unknown; + }; + /** @description Optional request trace identifier. */ + traceId?: string; + /** @description Validation failure details. */ + invalidParams?: { + /** @description Path to the invalid input field. */ + path: (string | number)[]; + /** @description Machine-readable validation error code. */ + code: string; + /** @description Validation error message. */ + message: string; + }[]; + }; + }; + }; + /** @description Access denied. */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": { + /** @description Stable problem type URI or URN. */ + type: string; + /** @description Short problem title. */ + title: string; + /** @description HTTP status code. */ + status: number; + /** @description Human-readable error detail. */ + detail: string; + /** @description Request path that produced the problem. */ + instance: string; + /** @description Application error code for frontend i18n. */ + errorCode: string; + /** @description Optional i18n parameters. */ + errorParams?: { + [key: string]: unknown; + }; + /** @description Optional request trace identifier. */ + traceId?: string; + /** @description Validation failure details. */ + invalidParams?: { + /** @description Path to the invalid input field. */ + path: (string | number)[]; + /** @description Machine-readable validation error code. */ + code: string; + /** @description Validation error message. */ + message: string; + }[]; + }; + }; + }; + /** @description Key not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": { + /** @description Stable problem type URI or URN. */ + type: string; + /** @description Short problem title. */ + title: string; + /** @description HTTP status code. */ + status: number; + /** @description Human-readable error detail. */ + detail: string; + /** @description Request path that produced the problem. */ + instance: string; + /** @description Application error code for frontend i18n. */ + errorCode: string; + /** @description Optional i18n parameters. */ + errorParams?: { + [key: string]: unknown; + }; + /** @description Optional request trace identifier. */ + traceId?: string; + /** @description Validation failure details. */ + invalidParams?: { + /** @description Path to the invalid input field. */ + path: (string | number)[]; + /** @description Machine-readable validation error code. */ + code: string; + /** @description Validation error message. */ + message: string; + }[]; + }; + }; + }; + }; + }; postKeysByKeyidLimitsReset: { parameters: { query?: never; diff --git a/src/lib/api/v1/action-migration-matrix.ts b/src/lib/api/v1/action-migration-matrix.ts index a8ca3b395..f7ec9f3c4 100644 --- a/src/lib/api/v1/action-migration-matrix.ts +++ b/src/lib/api/v1/action-migration-matrix.ts @@ -45,7 +45,11 @@ export const ACTION_MIGRATION_MATRIX = [ module: "keys", sourceFile: "keys.ts", resource: "keys", - endpointFamilies: ["/api/v1/users/{userId}/keys", "/api/v1/keys/{keyId}"], + endpointFamilies: [ + "/api/v1/users/{userId}/keys", + "/api/v1/keys/{keyId}", + "/api/v1/keys/{keyId}:reveal", + ], access: "admin", exportPolicy: "all-action-exports", }, diff --git a/src/lib/api/v1/schemas/keys.ts b/src/lib/api/v1/schemas/keys.ts index 827fea415..c22aa619b 100644 --- a/src/lib/api/v1/schemas/keys.ts +++ b/src/lib/api/v1/schemas/keys.ts @@ -129,6 +129,10 @@ export const KeyListResponseSchema = z.object({ items: z.array(z.unknown()).describe("Keys for the user."), }); +export const KeyRevealResponseSchema = z.object({ + key: z.string().describe("Unmasked key value. Returned only to admin callers."), +}); + export type KeyCreateInput = z.infer; export type KeyUpdateInput = z.infer; export type KeyRenewInput = z.infer; diff --git a/src/lib/api/v1/schemas/system-config.ts b/src/lib/api/v1/schemas/system-config.ts index 94be16c24..0f826e21c 100644 --- a/src/lib/api/v1/schemas/system-config.ts +++ b/src/lib/api/v1/schemas/system-config.ts @@ -88,6 +88,11 @@ export const SystemSettingsSchema = z currencyDisplay: CurrencyCodeSchema, billingModelSource: BillingModelSourceSchema, codexPriorityBillingSource: CodexPriorityBillingSourceSchema, + billNonSuccessfulRequests: z + .boolean() + .describe( + "Whether non-2xx responses (e.g., 499) that report token usage should be billed normally." + ), timezone: TimeZoneSchema.nullable().describe( "Configured system timezone, or null for default." ), diff --git a/src/lib/config/system-settings-cache.ts b/src/lib/config/system-settings-cache.ts index 533702cc6..9bf871478 100644 --- a/src/lib/config/system-settings-cache.ts +++ b/src/lib/config/system-settings-cache.ts @@ -132,6 +132,7 @@ export async function getCachedSystemSettings(): Promise { currencyDisplay: "USD", billingModelSource: "original", codexPriorityBillingSource: DEFAULT_SETTINGS.codexPriorityBillingSource, + billNonSuccessfulRequests: false, timezone: null, verboseProviderError: false, passThroughUpstreamErrorMessage: DEFAULT_SETTINGS.passThroughUpstreamErrorMessage, diff --git a/src/lib/validation/schemas.ts b/src/lib/validation/schemas.ts index 8ee3caf93..298741097 100644 --- a/src/lib/validation/schemas.ts +++ b/src/lib/validation/schemas.ts @@ -986,6 +986,8 @@ export const UpdateSystemSettingsSchema = z.object({ passThroughUpstreamErrorMessage: z.boolean().optional(), // 启用 HTTP/2 连接供应商(可选) enableHttp2: z.boolean().optional(), + // 非成功请求按 token 用量计费(可选;默认关闭) + billNonSuccessfulRequests: z.boolean().optional(), // 启用 OpenAI Responses WebSocket 支持(可选,仅 Codex 类型供应商生效) enableOpenaiResponsesWebsocket: z.boolean().optional(), // 高并发模式(可选) diff --git a/src/repository/_shared/transformers.ts b/src/repository/_shared/transformers.ts index 7dbedc438..1cf0353d2 100644 --- a/src/repository/_shared/transformers.ts +++ b/src/repository/_shared/transformers.ts @@ -250,6 +250,7 @@ export function toSystemSettings(dbSettings: any): SystemSettings { dbSettings?.codexPriorityBillingSource === "actual" ? dbSettings.codexPriorityBillingSource : "requested", + billNonSuccessfulRequests: dbSettings?.billNonSuccessfulRequests ?? false, timezone: dbSettings?.timezone ?? null, enableAutoCleanup: dbSettings?.enableAutoCleanup ?? false, cleanupRetentionDays: dbSettings?.cleanupRetentionDays ?? 30, diff --git a/src/repository/system-config.ts b/src/repository/system-config.ts index 6a465f891..d56034697 100644 --- a/src/repository/system-config.ts +++ b/src/repository/system-config.ts @@ -147,6 +147,7 @@ function createFallbackSettings(): SystemSettings { currencyDisplay: "USD", billingModelSource: "original", codexPriorityBillingSource: "requested", + billNonSuccessfulRequests: false, timezone: null, enableAutoCleanup: false, cleanupRetentionDays: 30, @@ -278,6 +279,7 @@ export async function getSystemSettings(): Promise { updatedAt: systemSettings.updatedAt, }; const fullSelection = { + billNonSuccessfulRequests: systemSettings.billNonSuccessfulRequests, passThroughUpstreamErrorMessage: systemSettings.passThroughUpstreamErrorMessage, fakeStreamingWhitelist: systemSettings.fakeStreamingWhitelist, enableOpenaiResponsesWebsocket: systemSettings.enableOpenaiResponsesWebsocket, @@ -298,11 +300,35 @@ export async function getSystemSettings(): Promise { error, }); + // 最新降级:移除最近新增的 billNonSuccessfulRequests 列。 + const { + billNonSuccessfulRequests: _omitBillNonSuccessful, + ...selectionWithoutBillNonSuccessful + } = fullSelection; + + try { + const [row] = await db + .select(selectionWithoutBillNonSuccessful) + .from(systemSettings) + .orderBy(asc(systemSettings.id)) + .limit(1); + return row ?? null; + } catch (billNonSuccessfulFallbackError) { + if (!isUndefinedColumnError(billNonSuccessfulFallbackError)) { + throw billNonSuccessfulFallbackError; + } + + logger.warn( + "system_settings 表除 billNonSuccessfulRequests 外仍有列缺失,继续回退到上一代字段集。", + { error: billNonSuccessfulFallbackError } + ); + } + // 第零层降级:仅移除最新增加的 enableOpenaiResponsesWebsocket 列。 const { enableOpenaiResponsesWebsocket: _omitOpenaiResponsesWebsocket, ...selectionWithoutOpenaiResponsesWebsocket - } = fullSelection; + } = selectionWithoutBillNonSuccessful; try { const [row] = await db @@ -590,6 +616,7 @@ export async function updateSystemSettings( updatedAt: systemSettings.updatedAt, }; const fullReturning = { + billNonSuccessfulRequests: systemSettings.billNonSuccessfulRequests, passThroughUpstreamErrorMessage: systemSettings.passThroughUpstreamErrorMessage, fakeStreamingWhitelist: systemSettings.fakeStreamingWhitelist, enableOpenaiResponsesWebsocket: systemSettings.enableOpenaiResponsesWebsocket, @@ -625,6 +652,11 @@ export async function updateSystemSettings( updates.codexPriorityBillingSource = payload.codexPriorityBillingSource; } + // 非成功请求按 token 用量计费开关(如果提供) + if (payload.billNonSuccessfulRequests !== undefined) { + updates.billNonSuccessfulRequests = payload.billNonSuccessfulRequests; + } + // 系统时区配置字段(如果提供) if (payload.timezone !== undefined) { updates.timezone = payload.timezone; @@ -784,31 +816,59 @@ export async function updateSystemSettings( error, }); - // 第零层降级:仅移除最新增加的 enableOpenaiResponsesWebsocket 列。 + // 最新降级:移除最近新增的 billNonSuccessfulRequests 列。 const { - enableOpenaiResponsesWebsocket: _omitUpdateOpenaiResponsesWebsocket, - ...updatesWithoutOpenaiResponsesWebsocket + billNonSuccessfulRequests: _omitUpdateBillNonSuccessful, + ...updatesWithoutBillNonSuccessful } = updates; const { - enableOpenaiResponsesWebsocket: _omitReturningOpenaiResponsesWebsocket, - ...returningWithoutOpenaiResponsesWebsocket + billNonSuccessfulRequests: _omitReturningBillNonSuccessful, + ...returningWithoutBillNonSuccessful } = fullReturning; try { [updated] = await executor .update(systemSettings) - .set(updatesWithoutOpenaiResponsesWebsocket) + .set(updatesWithoutBillNonSuccessful) .where(eq(systemSettings.id, current.id)) - .returning(returningWithoutOpenaiResponsesWebsocket); - } catch (openaiResponsesWebsocketFallbackError) { - if (!isUndefinedColumnError(openaiResponsesWebsocketFallbackError)) { - throw openaiResponsesWebsocketFallbackError; + .returning(returningWithoutBillNonSuccessful); + } catch (billNonSuccessfulFallbackError) { + if (!isUndefinedColumnError(billNonSuccessfulFallbackError)) { + throw billNonSuccessfulFallbackError; } - logger.warn( - "system_settings 表除 enableOpenaiResponsesWebsocket 外仍有列缺失,继续降级更新。", - { error: openaiResponsesWebsocketFallbackError } - ); + logger.warn("system_settings 表除 billNonSuccessfulRequests 外仍有列缺失,继续降级更新。", { + error: billNonSuccessfulFallbackError, + }); + } + + // 第零层降级:仅移除最新增加的 enableOpenaiResponsesWebsocket 列。 + const { + enableOpenaiResponsesWebsocket: _omitUpdateOpenaiResponsesWebsocket, + ...updatesWithoutOpenaiResponsesWebsocket + } = updatesWithoutBillNonSuccessful; + const { + enableOpenaiResponsesWebsocket: _omitReturningOpenaiResponsesWebsocket, + ...returningWithoutOpenaiResponsesWebsocket + } = returningWithoutBillNonSuccessful; + + if (!updated) { + try { + [updated] = await executor + .update(systemSettings) + .set(updatesWithoutOpenaiResponsesWebsocket) + .where(eq(systemSettings.id, current.id)) + .returning(returningWithoutOpenaiResponsesWebsocket); + } catch (openaiResponsesWebsocketFallbackError) { + if (!isUndefinedColumnError(openaiResponsesWebsocketFallbackError)) { + throw openaiResponsesWebsocketFallbackError; + } + + logger.warn( + "system_settings 表除 enableOpenaiResponsesWebsocket 外仍有列缺失,继续降级更新。", + { error: openaiResponsesWebsocketFallbackError } + ); + } } // 第一层降级:再移除 fakeStreamingWhitelist 列。 diff --git a/src/types/system-config.ts b/src/types/system-config.ts index aa38e01bd..30e39fffd 100644 --- a/src/types/system-config.ts +++ b/src/types/system-config.ts @@ -43,6 +43,11 @@ export interface SystemSettings { // Codex Priority 单独计费口径 codexPriorityBillingSource: CodexPriorityBillingSource; + // 非成功请求按 token 用量计费(默认关闭) + // 开启后:返回非 2xx 状态(如 499 客户端中断)但上游仍回报了正向 token 用量时按 usage 计费; + // fake-200 上游错误识别仍生效,保证假成功响应不会被错误计费。 + billNonSuccessfulRequests: boolean; + // 系统时区配置 (IANA timezone identifier) // 用于统一后端时间边界计算和前端日期/时间显示 // null 表示使用环境变量 TZ 或默认 UTC @@ -149,6 +154,9 @@ export interface UpdateSystemSettingsInput { // Codex Priority 单独计费口径(可选) codexPriorityBillingSource?: CodexPriorityBillingSource; + // 非成功请求按 token 用量计费(可选) + billNonSuccessfulRequests?: boolean; + // 系统时区配置(可选) timezone?: string | null; diff --git a/src/types/user.ts b/src/types/user.ts index 91333f8bb..76d1dde92 100644 --- a/src/types/user.ts +++ b/src/types/user.ts @@ -102,7 +102,11 @@ export interface UserKeyDisplay { id: number; name: string; maskedKey: string; - fullKey?: string; // 仅管理员可见的完整密钥 + /** + * 当前查看者是否被允许查看 / 复制完整密钥(前端据此显示按钮)。 + * 实际密钥不再下发到列表 payload,需通过 GET /api/v1/keys/{id}:reveal 获取。 + */ + canReveal: boolean; canCopy: boolean; // 是否可以复制完整密钥 expiresAt: string; // 格式化后的日期字符串或"永不过期" status: "enabled" | "disabled"; diff --git a/tests/unit/actions/keys-reveal.test.ts b/tests/unit/actions/keys-reveal.test.ts new file mode 100644 index 000000000..8a589a056 --- /dev/null +++ b/tests/unit/actions/keys-reveal.test.ts @@ -0,0 +1,135 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const getSessionMock = vi.fn(); +vi.mock("@/lib/auth", () => ({ + getSession: getSessionMock, +})); + +vi.mock("next/cache", () => ({ + revalidatePath: vi.fn(), +})); + +vi.mock("next-intl/server", () => ({ + getTranslations: vi.fn(async () => (key: string) => key), +})); + +const findKeyByIdMock = vi.fn(); +vi.mock("@/repository/key", () => ({ + countActiveKeysByUser: vi.fn(async () => 1), + createKey: vi.fn(async () => ({})), + deleteKey: vi.fn(async () => true), + findActiveKeyByUserIdAndName: vi.fn(async () => null), + findKeyById: findKeyByIdMock, + findKeyList: vi.fn(async () => []), + findKeysWithStatistics: vi.fn(async () => []), + resetKeyCostResetAt: vi.fn(), + updateKey: vi.fn(async () => ({})), +})); + +vi.mock("@/repository/user", () => ({ + findUserById: vi.fn(), +})); + +vi.mock("@/actions/users", () => ({ + syncUserProviderGroupFromKeys: vi.fn(async () => undefined), +})); + +const emitActionAuditMock = vi.fn(); +vi.mock("@/lib/audit/emit", () => ({ + emitActionAudit: emitActionAuditMock, +})); + +describe("getUnmaskedKey action", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns the unmasked key for an admin caller and writes a redacted audit event", async () => { + getSessionMock.mockResolvedValue({ user: { id: 1, role: "admin" } }); + findKeyByIdMock.mockResolvedValueOnce({ + id: 42, + name: "user-key", + key: "sk-actual-secret-value", + userId: 99, + }); + + const { getUnmaskedKey } = await import("@/actions/keys"); + const result = await getUnmaskedKey(42); + + expect(result).toEqual({ ok: true, data: { key: "sk-actual-secret-value" } }); + expect(emitActionAuditMock).toHaveBeenCalledWith( + expect.objectContaining({ + category: "key", + action: "key.key_reveal", + targetType: "key", + targetId: "42", + targetName: "user-key", + success: true, + redactExtraKeys: ["key"], + }) + ); + expect(JSON.stringify(emitActionAuditMock.mock.calls)).not.toContain("sk-actual-secret-value"); + }); + + it("returns the unmasked key for the key owner (non-admin)", async () => { + getSessionMock.mockResolvedValue({ user: { id: 99, role: "user" } }); + findKeyByIdMock.mockResolvedValueOnce({ + id: 42, + name: "owner-key", + key: "sk-owner-secret", + userId: 99, + }); + + const { getUnmaskedKey } = await import("@/actions/keys"); + const result = await getUnmaskedKey(42); + + expect(result).toEqual({ ok: true, data: { key: "sk-owner-secret" } }); + expect(emitActionAuditMock).toHaveBeenCalledWith( + expect.objectContaining({ + category: "key", + action: "key.key_reveal", + success: true, + }) + ); + }); + + it("rejects a non-admin caller asking for someone else's key", async () => { + getSessionMock.mockResolvedValue({ user: { id: 7, role: "user" } }); + findKeyByIdMock.mockResolvedValueOnce({ + id: 42, + name: "other-user-key", + key: "sk-other", + userId: 99, + }); + + const { getUnmaskedKey } = await import("@/actions/keys"); + const result = await getUnmaskedKey(42); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error).toContain("权限"); + } + }); + + it("rejects unauthenticated callers", async () => { + getSessionMock.mockResolvedValue(null); + + const { getUnmaskedKey } = await import("@/actions/keys"); + const result = await getUnmaskedKey(42); + + expect(result.ok).toBe(false); + }); + + it("returns 404 when the key cannot be found", async () => { + getSessionMock.mockResolvedValue({ user: { id: 1, role: "admin" } }); + findKeyByIdMock.mockResolvedValueOnce(null); + + const { getUnmaskedKey } = await import("@/actions/keys"); + const result = await getUnmaskedKey(404); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error).toContain("不存在"); + } + }); +}); diff --git a/tests/unit/proxy/model-redirect-fallback.test.ts b/tests/unit/proxy/model-redirect-fallback.test.ts new file mode 100644 index 000000000..630c3ffee --- /dev/null +++ b/tests/unit/proxy/model-redirect-fallback.test.ts @@ -0,0 +1,284 @@ +/** + * Regression test for model redirect across provider fallback. + * + * Behavioral contract: + * - When a request falls back from Provider A to Provider B, Provider B's + * redirect rules must match against the user's ORIGINAL pre-redirect model + * (NOT against the model Provider A rewrote it to). + * - If Provider B has no rule for the original model, request.model must be + * reset back to the original model before forwarding. + * + * This file documents the contract and pins it as a regression test, since the + * "redirect leaks across fallback" complaint is hard to reproduce in production. + */ + +import { describe, expect, test, vi } from "vitest"; +import { resolveEndpointPolicy } from "@/app/v1/_lib/proxy/endpoint-policy"; +import { ModelRedirector } from "@/app/v1/_lib/proxy/model-redirector"; +import { ProxySession } from "@/app/v1/_lib/proxy/session"; +import type { Provider } from "@/types/provider"; + +vi.mock("@/lib/logger", () => ({ + logger: { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + trace: vi.fn(), + error: vi.fn(), + fatal: vi.fn(), + }, +})); + +function createProvider(overrides: Partial = {}): Provider { + return { + id: 1, + name: "p1", + url: "https://provider.example.com", + key: "k", + providerVendorId: null, + isEnabled: true, + weight: 1, + priority: 0, + groupPriorities: null, + costMultiplier: 1, + groupTag: null, + providerType: "claude", + preserveClientIp: false, + modelRedirects: null, + allowedModels: null, + mcpPassthroughType: "none", + mcpPassthroughUrl: null, + limit5hUsd: null, + limitDailyUsd: null, + dailyResetMode: "fixed", + dailyResetTime: "00:00", + limitWeeklyUsd: null, + limitMonthlyUsd: null, + limitTotalUsd: null, + totalCostResetAt: null, + limitConcurrentSessions: 0, + maxRetryAttempts: 1, + circuitBreakerFailureThreshold: 5, + circuitBreakerOpenDuration: 1_800_000, + circuitBreakerHalfOpenSuccessThreshold: 2, + proxyUrl: null, + proxyFallbackToDirect: false, + firstByteTimeoutStreamingMs: 100, + streamingIdleTimeoutMs: 0, + requestTimeoutNonStreamingMs: 0, + websiteUrl: null, + faviconUrl: null, + cacheTtlPreference: null, + context1mPreference: null, + codexReasoningEffortPreference: null, + codexReasoningSummaryPreference: null, + codexTextVerbosityPreference: null, + codexParallelToolCallsPreference: null, + codexServiceTierPreference: null, + anthropicMaxTokensPreference: null, + anthropicThinkingBudgetPreference: null, + anthropicAdaptiveThinking: null, + geminiGoogleSearchPreference: null, + tpm: 0, + rpm: 0, + rpd: 0, + cc: 0, + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: null, + ...overrides, + }; +} + +function createSession(initialModel: string): ProxySession { + const headers = new Headers(); + const session = Object.create(ProxySession.prototype); + Object.assign(session, { + startTime: Date.now(), + method: "POST", + requestUrl: new URL("https://example.com/v1/messages"), + headers, + originalHeaders: new Headers(headers), + headerLog: JSON.stringify(Object.fromEntries(headers.entries())), + request: { + model: initialModel, + log: "(test)", + message: { + model: initialModel, + stream: true, + messages: [{ role: "user", content: "hi" }], + }, + }, + userAgent: null, + context: null, + clientAbortSignal: null, + userName: "test-user", + authState: { success: true, user: null, key: null, apiKey: null }, + provider: null, + messageContext: null, + sessionId: "sess-fallback-test", + requestSequence: 1, + originalFormat: "claude", + providerType: null, + originalModelName: null, + originalUrlPathname: null, + providerChain: [], + cacheTtlResolved: null, + context1mApplied: false, + specialSettings: [], + cachedPriceData: undefined, + cachedBillingModelSource: undefined, + endpointPolicy: resolveEndpointPolicy("/v1/messages"), + isHeaderModified: () => false, + }); + return session as ProxySession; +} + +describe("Model redirect across provider fallback", () => { + const REQUESTED_MODEL = "claude-3-5-sonnet-20241022"; + const PROVIDER_A_REDIRECT = "glm-4.6"; + const PROVIDER_B_REDIRECT_FROM_ORIGINAL = "kimi-k2"; + + test("Provider B redirect rule on the ORIGINAL model fires after Provider A failed", () => { + const providerA = createProvider({ + id: 100, + name: "A", + modelRedirects: [ + { matchType: "exact", source: REQUESTED_MODEL, target: PROVIDER_A_REDIRECT }, + ], + }); + const providerB = createProvider({ + id: 200, + name: "B", + modelRedirects: [ + { + matchType: "exact", + source: REQUESTED_MODEL, + target: PROVIDER_B_REDIRECT_FROM_ORIGINAL, + }, + ], + }); + + const session = createSession(REQUESTED_MODEL); + session.setProvider(providerA); + session.addProviderToChain(providerA, { reason: "initial_selection" }); + + // Provider A redirects "claude-3-5-sonnet" -> "glm-4.6" + expect(ModelRedirector.apply(session, providerA)).toBe(true); + expect(session.request.model).toBe(PROVIDER_A_REDIRECT); + expect(session.getOriginalModel()).toBe(REQUESTED_MODEL); + + // Simulate Provider A failing -> fallback to Provider B + session.setProvider(providerB); + session.addProviderToChain(providerB, { + reason: "retry_failed", + attemptNumber: 2, + }); + + // Provider B's rule SHOULD match against the ORIGINAL model + // (NOT against "glm-4.6" left in request.model from Provider A) + expect(ModelRedirector.apply(session, providerB)).toBe(true); + expect(session.request.model).toBe(PROVIDER_B_REDIRECT_FROM_ORIGINAL); + expect(session.request.message.model).toBe(PROVIDER_B_REDIRECT_FROM_ORIGINAL); + expect(session.getOriginalModel()).toBe(REQUESTED_MODEL); + }); + + test("Provider B redirect rule keyed on Provider A's REDIRECTED name does NOT fire", () => { + // Common pitfall: someone configures Provider B with a rule like + // "glm-4.6 -> kimi-k2" + // expecting it to chain after Provider A's redirect. The contract says no: + // each provider matches against the user-requested model. + const providerA = createProvider({ + id: 100, + name: "A", + modelRedirects: [ + { matchType: "exact", source: REQUESTED_MODEL, target: PROVIDER_A_REDIRECT }, + ], + }); + const providerB = createProvider({ + id: 200, + name: "B", + modelRedirects: [{ matchType: "exact", source: PROVIDER_A_REDIRECT, target: "kimi-k2" }], + }); + + const session = createSession(REQUESTED_MODEL); + session.setProvider(providerA); + session.addProviderToChain(providerA, { reason: "initial_selection" }); + expect(ModelRedirector.apply(session, providerA)).toBe(true); + expect(session.request.model).toBe(PROVIDER_A_REDIRECT); + + // Fallback to Provider B + session.setProvider(providerB); + session.addProviderToChain(providerB, { + reason: "retry_failed", + attemptNumber: 2, + }); + // Provider B rule keyed on "glm-4.6" should NOT match because we're matching + // the ORIGINAL "claude-3-5-sonnet" model, not "glm-4.6". + expect(ModelRedirector.apply(session, providerB)).toBe(false); + // Model must be reset to the original on Provider B + expect(session.request.model).toBe(REQUESTED_MODEL); + expect(session.request.message.model).toBe(REQUESTED_MODEL); + }); + + test("Provider B without redirect rules resets request.model to the original", () => { + const providerA = createProvider({ + id: 100, + name: "A", + modelRedirects: [ + { matchType: "exact", source: REQUESTED_MODEL, target: PROVIDER_A_REDIRECT }, + ], + }); + const providerB = createProvider({ id: 200, name: "B", modelRedirects: null }); + + const session = createSession(REQUESTED_MODEL); + session.setProvider(providerA); + session.addProviderToChain(providerA, { reason: "initial_selection" }); + expect(ModelRedirector.apply(session, providerA)).toBe(true); + expect(session.request.model).toBe(PROVIDER_A_REDIRECT); + + session.setProvider(providerB); + session.addProviderToChain(providerB, { + reason: "retry_failed", + attemptNumber: 2, + }); + expect(ModelRedirector.apply(session, providerB)).toBe(false); + expect(session.request.model).toBe(REQUESTED_MODEL); + // resetToOriginal also rewrites request.message.model — guard against + // regressions that only reset request.model. + expect(session.request.message.model).toBe(REQUESTED_MODEL); + }); + + test("Provider B redirects to a model different from Provider A's target", () => { + // Both providers have rules on the original model but redirect to different targets. + // The fallback path must select Provider B's target, not carry over Provider A's. + const providerA = createProvider({ + id: 100, + name: "A", + modelRedirects: [{ matchType: "exact", source: REQUESTED_MODEL, target: "a-target" }], + }); + const providerB = createProvider({ + id: 200, + name: "B", + modelRedirects: [{ matchType: "exact", source: REQUESTED_MODEL, target: "b-target" }], + }); + + const session = createSession(REQUESTED_MODEL); + session.setProvider(providerA); + session.addProviderToChain(providerA, { reason: "initial_selection" }); + expect(ModelRedirector.apply(session, providerA)).toBe(true); + expect(session.request.model).toBe("a-target"); + + session.setProvider(providerB); + session.addProviderToChain(providerB, { + reason: "retry_failed", + attemptNumber: 2, + }); + expect(ModelRedirector.apply(session, providerB)).toBe(true); + expect(session.request.model).toBe("b-target"); + expect(session.request.message.model).toBe("b-target"); + + // Original is preserved across both attempts (used for billing). + expect(session.getOriginalModel()).toBe(REQUESTED_MODEL); + }); +}); diff --git a/tests/unit/proxy/response-handler-bill-non-success.test.ts b/tests/unit/proxy/response-handler-bill-non-success.test.ts new file mode 100644 index 000000000..56dcc9389 --- /dev/null +++ b/tests/unit/proxy/response-handler-bill-non-success.test.ts @@ -0,0 +1,190 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("@/lib/config/system-settings-cache", () => ({ + getCachedSystemSettings: vi.fn(), +})); + +vi.mock("@/lib/logger", () => ({ + logger: { + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, + trace: () => {}, + }, +})); + +vi.mock("@/lib/async-task-manager", () => ({ + AsyncTaskManager: { + register: () => new AbortController(), + cleanup: () => {}, + cancel: () => {}, + }, +})); + +vi.mock("@/lib/utils/upstream-error-detection", () => ({ + detectUpstreamErrorFromSseOrJsonText: vi.fn(() => ({ isError: false })), + inferUpstreamErrorStatusCodeFromText: vi.fn(() => null), +})); + +vi.mock(import("@/lib/utils/performance-formatter"), async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + isNonBillingEndpoint: vi.fn(() => false), + }; +}); + +import { resolveBillableUsageMetricsForCost } from "@/app/v1/_lib/proxy/response-handler"; +import { getCachedSystemSettings } from "@/lib/config/system-settings-cache"; +import { detectUpstreamErrorFromSseOrJsonText } from "@/lib/utils/upstream-error-detection"; + +const mockGetCachedSystemSettings = getCachedSystemSettings as unknown as ReturnType; + +type MinimalSession = { + getEndpoint: () => string | null; + getOriginalModel: () => string | null; + getCurrentModel: () => string | null; + getResolvedPricingByBillingSource: (provider: unknown) => Promise; +}; + +function makeSession(): MinimalSession { + return { + getEndpoint: () => "/v1/messages", + getOriginalModel: () => "claude-3-5-sonnet", + getCurrentModel: () => "claude-3-5-sonnet", + getResolvedPricingByBillingSource: async () => null, + }; +} + +describe("resolveBillableUsageMetricsForCost — bill-non-success toggle", () => { + beforeEach(() => { + mockGetCachedSystemSettings.mockReset(); + (detectUpstreamErrorFromSseOrJsonText as unknown as ReturnType).mockReset(); + (detectUpstreamErrorFromSseOrJsonText as unknown as ReturnType).mockReturnValue({ + isError: false, + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("returns null on 499 when toggle is OFF (default)", async () => { + mockGetCachedSystemSettings.mockResolvedValue({ billNonSuccessfulRequests: false }); + + const session = makeSession(); + const result = await resolveBillableUsageMetricsForCost( + // biome-ignore lint/suspicious/noExplicitAny: minimal session for test + session as any, + null, + { input_tokens: 100, output_tokens: 50 }, + 499 + ); + + expect(result).toBeNull(); + }); + + it("returns usage on 499 when toggle is ON and tokens are positive", async () => { + mockGetCachedSystemSettings.mockResolvedValue({ billNonSuccessfulRequests: true }); + + const session = makeSession(); + const usage = { input_tokens: 100, output_tokens: 50 }; + const result = await resolveBillableUsageMetricsForCost( + // biome-ignore lint/suspicious/noExplicitAny: minimal session for test + session as any, + null, + usage, + 499 + ); + + expect(result).toEqual(usage); + }); + + it("returns null on 499 when toggle is ON but usage is null", async () => { + mockGetCachedSystemSettings.mockResolvedValue({ billNonSuccessfulRequests: true }); + + const session = makeSession(); + const result = await resolveBillableUsageMetricsForCost( + // biome-ignore lint/suspicious/noExplicitAny: minimal session for test + session as any, + null, + null, + 499 + ); + + expect(result).toBeNull(); + }); + + it("returns null on 499 when toggle is ON but tokens are all zero", async () => { + mockGetCachedSystemSettings.mockResolvedValue({ billNonSuccessfulRequests: true }); + + const session = makeSession(); + const result = await resolveBillableUsageMetricsForCost( + // biome-ignore lint/suspicious/noExplicitAny: minimal session for test + session as any, + null, + { input_tokens: 0, output_tokens: 0 }, + 499 + ); + + expect(result).toBeNull(); + }); + + it("counts cache tokens as positive when toggle is ON", async () => { + mockGetCachedSystemSettings.mockResolvedValue({ billNonSuccessfulRequests: true }); + + const session = makeSession(); + const usage = { + input_tokens: 0, + output_tokens: 0, + cache_read_input_tokens: 200, + }; + const result = await resolveBillableUsageMetricsForCost( + // biome-ignore lint/suspicious/noExplicitAny: minimal session for test + session as any, + null, + usage, + 499 + ); + + expect(result).toEqual(usage); + }); + + it("does NOT bypass fake-200 detector even when toggle is ON (fake-200 only checked for 2xx)", async () => { + // For status 200 with fake error payload: still skipped regardless of toggle. + mockGetCachedSystemSettings.mockResolvedValue({ billNonSuccessfulRequests: true }); + (detectUpstreamErrorFromSseOrJsonText as unknown as ReturnType).mockReturnValue({ + isError: true, + code: 401, + detail: "unauthorized", + }); + + const session = makeSession(); + const result = await resolveBillableUsageMetricsForCost( + // biome-ignore lint/suspicious/noExplicitAny: minimal session for test + session as any, + null, + { input_tokens: 100, output_tokens: 50 }, + 200, + "fake error body" + ); + + expect(result).toBeNull(); + }); + + it("falls back to skip-billing if reading the setting throws", async () => { + mockGetCachedSystemSettings.mockRejectedValue(new Error("redis down")); + + const session = makeSession(); + const result = await resolveBillableUsageMetricsForCost( + // biome-ignore lint/suspicious/noExplicitAny: minimal session for test + session as any, + null, + { input_tokens: 100, output_tokens: 50 }, + 499 + ); + + expect(result).toBeNull(); + }); +});