diff --git a/drizzle/0077_nappy_giant_man.sql b/drizzle/0077_nappy_giant_man.sql new file mode 100644 index 000000000..0ef03b6d5 --- /dev/null +++ b/drizzle/0077_nappy_giant_man.sql @@ -0,0 +1,3 @@ +ALTER TABLE "providers" ADD COLUMN "active_time_start" varchar(5);--> statement-breakpoint +ALTER TABLE "providers" ADD COLUMN "active_time_end" varchar(5);--> statement-breakpoint +ALTER TABLE "providers" DROP COLUMN "join_claude_pool"; \ No newline at end of file diff --git a/drizzle/meta/0077_snapshot.json b/drizzle/meta/0077_snapshot.json new file mode 100644 index 000000000..d2e46bcce --- /dev/null +++ b/drizzle/meta/0077_snapshot.json @@ -0,0 +1,3908 @@ +{ + "id": "22eb3652-56d7-4a04-9845-5fa18210ef90", + "prevId": "ea98464d-5d4d-45e1-b04f-9d5b5b280601", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.error_rules": { + "name": "error_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "pattern": { + "name": "pattern", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "match_type": { + "name": "match_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'regex'" + }, + "category": { + "name": "category", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "override_response": { + "name": "override_response", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "override_status_code": { + "name": "override_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_error_rules_enabled": { + "name": "idx_error_rules_enabled", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "unique_pattern": { + "name": "unique_pattern", + "columns": [ + { + "expression": "pattern", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_category": { + "name": "idx_category", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_match_type": { + "name": "idx_match_type", + "columns": [ + { + "expression": "match_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.keys": { + "name": "keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp 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_daily_usd": { + "name": "limit_daily_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "daily_reset_mode": { + "name": "daily_reset_mode", + "type": "daily_reset_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'fixed'" + }, + "daily_reset_time": { + "name": "daily_reset_time", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'00:00'" + }, + "limit_weekly_usd": { + "name": "limit_weekly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_monthly_usd": { + "name": "limit_monthly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_total_usd": { + "name": "limit_total_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_concurrent_sessions": { + "name": "limit_concurrent_sessions", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "provider_group": { + "name": "provider_group", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false, + "default": "'default'" + }, + "cache_ttl_preference": { + "name": "cache_ttl_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_keys_user_id": { + "name": "idx_keys_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_keys_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 + }, + "session_id": { + "name": "session_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "request_sequence": { + "name": "request_sequence", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "provider_chain": { + "name": "provider_chain", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "api_type": { + "name": "api_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "endpoint": { + "name": "endpoint", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "original_model": { + "name": "original_model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "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 + }, + "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_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": {} + } + }, + "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_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(50)", + "primaryKey": false, + "notNull": false + }, + "provider_type": { + "name": "provider_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'claude'" + }, + "preserve_client_ip": { + "name": "preserve_client_ip", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "model_redirects": { + "name": "model_redirects", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "allowed_models": { + "name": "allowed_models", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'null'::jsonb" + }, + "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_daily_usd": { + "name": "limit_daily_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "daily_reset_mode": { + "name": "daily_reset_mode", + "type": "daily_reset_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'fixed'" + }, + "daily_reset_time": { + "name": "daily_reset_time", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'00:00'" + }, + "limit_weekly_usd": { + "name": "limit_weekly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_monthly_usd": { + "name": "limit_monthly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_total_usd": { + "name": "limit_total_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "total_cost_reset_at": { + "name": "total_cost_reset_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "limit_concurrent_sessions": { + "name": "limit_concurrent_sessions", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "max_retry_attempts": { + "name": "max_retry_attempts", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "circuit_breaker_failure_threshold": { + "name": "circuit_breaker_failure_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5 + }, + "circuit_breaker_open_duration": { + "name": "circuit_breaker_open_duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1800000 + }, + "circuit_breaker_half_open_success_threshold": { + "name": "circuit_breaker_half_open_success_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 2 + }, + "proxy_url": { + "name": "proxy_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "proxy_fallback_to_direct": { + "name": "proxy_fallback_to_direct", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "first_byte_timeout_streaming_ms": { + "name": "first_byte_timeout_streaming_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "streaming_idle_timeout_ms": { + "name": "streaming_idle_timeout_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "request_timeout_non_streaming_ms": { + "name": "request_timeout_non_streaming_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "website_url": { + "name": "website_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "favicon_url": { + "name": "favicon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cache_ttl_preference": { + "name": "cache_ttl_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "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 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_request_filters_enabled": { + "name": "idx_request_filters_enabled", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_request_filters_scope": { + "name": "idx_request_filters_scope", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_request_filters_action": { + "name": "idx_request_filters_action", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_request_filters_binding": { + "name": "idx_request_filters_binding", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "binding_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sensitive_words": { + "name": "sensitive_words", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "word": { + "name": "word", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "match_type": { + "name": "match_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'contains'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_sensitive_words_enabled": { + "name": "idx_sensitive_words_enabled", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "match_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sensitive_words_created_at": { + "name": "idx_sensitive_words_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.system_settings": { + "name": "system_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "site_title": { + "name": "site_title", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "default": "'Claude Code Hub'" + }, + "allow_global_usage_view": { + "name": "allow_global_usage_view", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "currency_display": { + "name": "currency_display", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "billing_model_source": { + "name": "billing_model_source", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'original'" + }, + "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 + }, + "enable_http2": { + "name": "enable_http2", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "intercept_anthropic_warmup_requests": { + "name": "intercept_anthropic_warmup_requests", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_thinking_signature_rectifier": { + "name": "enable_thinking_signature_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_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_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 + }, + "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 + }, + "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 + }, + "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 + }, + "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 + }, + "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": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "varchar", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "rpm_limit": { + "name": "rpm_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "daily_limit_usd": { + "name": "daily_limit_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "provider_group": { + "name": "provider_group", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false, + "default": "'default'" + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "limit_5h_usd": { + "name": "limit_5h_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_weekly_usd": { + "name": "limit_weekly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_monthly_usd": { + "name": "limit_monthly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_total_usd": { + "name": "limit_total_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_concurrent_sessions": { + "name": "limit_concurrent_sessions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "daily_reset_mode": { + "name": "daily_reset_mode", + "type": "daily_reset_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'fixed'" + }, + "daily_reset_time": { + "name": "daily_reset_time", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'00:00'" + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "allowed_clients": { + "name": "allowed_clients", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "allowed_models": { + "name": "allowed_models", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "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 482164998..fb7b5a646 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -540,6 +540,13 @@ "when": 1771954926766, "tag": "0076_mighty_lionheart", "breakpoints": true + }, + { + "idx": 77, + "version": "7", + "when": 1772219877045, + "tag": "0077_nappy_giant_man", + "breakpoints": true } ] } \ No newline at end of file diff --git a/messages/en/settings/providers/batchEdit.json b/messages/en/settings/providers/batchEdit.json index 24fc6ec90..cf192932e 100644 --- a/messages/en/settings/providers/batchEdit.json +++ b/messages/en/settings/providers/batchEdit.json @@ -46,7 +46,9 @@ "modelRedirects": "Model Redirects", "allowedModels": "Allowed Models", "thinkingBudget": "Thinking Budget", - "adaptiveThinking": "Adaptive Thinking" + "adaptiveThinking": "Adaptive Thinking", + "activeTimeStart": "Active Start Time", + "activeTimeEnd": "Active End Time" }, "affectedProviders": { "title": "Affected Providers", diff --git a/messages/en/settings/providers/form/sections.json b/messages/en/settings/providers/form/sections.json index 59548820e..aec6461f4 100644 --- a/messages/en/settings/providers/form/sections.json +++ b/messages/en/settings/providers/form/sections.json @@ -368,6 +368,16 @@ "placeholder": "1" } }, + "activeTime": { + "title": "Scheduled Active Time", + "description": "Automatically activate provider during specific time windows", + "toggleLabel": "Enable Schedule", + "toggleDescription": "When enabled, this provider is only active during the specified time window", + "startLabel": "Start Time", + "endLabel": "End Time", + "timezoneNote": "Times are based on system timezone", + "crossDayHint": "Cross-day schedule: active from {start} to next day {end}" + }, "summary": { "models": "{count} whitelisted models", "none": "Not configured", diff --git a/messages/en/settings/providers/list.json b/messages/en/settings/providers/list.json index f1656c930..0b3c13284 100644 --- a/messages/en/settings/providers/list.json +++ b/messages/en/settings/providers/list.json @@ -41,5 +41,6 @@ "actionResetCircuit": "Reset Circuit", "actionResetUsage": "Reset Usage", "actionDelete": "Delete", - "selectProvider": "Select {name}" + "selectProvider": "Select {name}", + "schedule": "Schedule" } diff --git a/messages/ja/settings/providers/batchEdit.json b/messages/ja/settings/providers/batchEdit.json index 166fa5d68..94e160152 100644 --- a/messages/ja/settings/providers/batchEdit.json +++ b/messages/ja/settings/providers/batchEdit.json @@ -46,7 +46,9 @@ "modelRedirects": "モデルリダイレクト", "allowedModels": "許可モデル", "thinkingBudget": "思考バジェット", - "adaptiveThinking": "アダプティブ思考" + "adaptiveThinking": "アダプティブ思考", + "activeTimeStart": "スケジュール開始時刻", + "activeTimeEnd": "スケジュール終了時刻" }, "affectedProviders": { "title": "影響を受けるプロバイダー", diff --git a/messages/ja/settings/providers/form/sections.json b/messages/ja/settings/providers/form/sections.json index 619801caf..3ec41f904 100644 --- a/messages/ja/settings/providers/form/sections.json +++ b/messages/ja/settings/providers/form/sections.json @@ -369,6 +369,16 @@ "placeholder": "1" } }, + "activeTime": { + "title": "スケジュール有効時間", + "description": "指定した時間帯に自動的にプロバイダーを有効化します", + "toggleLabel": "スケジュールを有効化", + "toggleDescription": "有効にすると、指定した時間帯のみプロバイダーがアクティブになります", + "startLabel": "開始時刻", + "endLabel": "終了時刻", + "timezoneNote": "時刻はシステムのタイムゾーンに基づきます", + "crossDayHint": "日跨ぎスケジュール: {start} から翌日 {end} まで有効" + }, "summary": { "models": "許可モデル {count} 件", "none": "未設定", diff --git a/messages/ja/settings/providers/list.json b/messages/ja/settings/providers/list.json index a0005c2df..aeb44a4d2 100644 --- a/messages/ja/settings/providers/list.json +++ b/messages/ja/settings/providers/list.json @@ -41,5 +41,6 @@ "actionResetCircuit": "サーキットリセット", "actionResetUsage": "使用量リセット", "actionDelete": "削除", - "selectProvider": "{name} を選択" + "selectProvider": "{name} を選択", + "schedule": "スケジュール" } diff --git a/messages/ru/settings/providers/batchEdit.json b/messages/ru/settings/providers/batchEdit.json index b601c8b87..026e2275f 100644 --- a/messages/ru/settings/providers/batchEdit.json +++ b/messages/ru/settings/providers/batchEdit.json @@ -46,7 +46,9 @@ "modelRedirects": "Перенаправление моделей", "allowedModels": "Разрешённые модели", "thinkingBudget": "Бюджет мышления", - "adaptiveThinking": "Адаптивное мышление" + "adaptiveThinking": "Адаптивное мышление", + "activeTimeStart": "Время начала расписания", + "activeTimeEnd": "Время окончания расписания" }, "affectedProviders": { "title": "Затронутые поставщики", diff --git a/messages/ru/settings/providers/form/sections.json b/messages/ru/settings/providers/form/sections.json index 30367befc..594eef96f 100644 --- a/messages/ru/settings/providers/form/sections.json +++ b/messages/ru/settings/providers/form/sections.json @@ -369,6 +369,16 @@ "placeholder": "1" } }, + "activeTime": { + "title": "Расписание активности", + "description": "Автоматически активировать провайдера в указанные временные окна", + "toggleLabel": "Включить расписание", + "toggleDescription": "При включении провайдер активен только в указанном временном окне", + "startLabel": "Время начала", + "endLabel": "Время окончания", + "timezoneNote": "Время указано по системному часовому поясу", + "crossDayHint": "Расписание через полночь: активен с {start} до {end} следующего дня" + }, "summary": { "models": "{count} моделей в белом списке", "none": "Не настроено", diff --git a/messages/ru/settings/providers/list.json b/messages/ru/settings/providers/list.json index 7ae099a72..8de44f407 100644 --- a/messages/ru/settings/providers/list.json +++ b/messages/ru/settings/providers/list.json @@ -41,5 +41,6 @@ "actionResetCircuit": "Сбросить автоматический выключатель", "actionResetUsage": "Сбросить использование", "actionDelete": "Удалить", - "selectProvider": "Выбрать {name}" + "selectProvider": "Выбрать {name}", + "schedule": "Расписание" } diff --git a/messages/zh-CN/settings/providers/batchEdit.json b/messages/zh-CN/settings/providers/batchEdit.json index 64ce1ad86..dff49239a 100644 --- a/messages/zh-CN/settings/providers/batchEdit.json +++ b/messages/zh-CN/settings/providers/batchEdit.json @@ -46,7 +46,9 @@ "modelRedirects": "模型重定向", "allowedModels": "允许的模型", "thinkingBudget": "思维预算", - "adaptiveThinking": "自适应思维" + "adaptiveThinking": "自适应思维", + "activeTimeStart": "调度开始时间", + "activeTimeEnd": "调度结束时间" }, "affectedProviders": { "title": "受影响的供应商", diff --git a/messages/zh-CN/settings/providers/form/sections.json b/messages/zh-CN/settings/providers/form/sections.json index efc9d7c8d..05a815026 100644 --- a/messages/zh-CN/settings/providers/form/sections.json +++ b/messages/zh-CN/settings/providers/form/sections.json @@ -100,6 +100,16 @@ "noGroups": "请先设置分组标签,才能配置分组优先级" } }, + "activeTime": { + "title": "调度时间窗口", + "description": "在指定时间窗口内自动激活供应商", + "toggleLabel": "启用时间调度", + "toggleDescription": "启用后,此供应商仅在指定时间窗口内处于活跃状态", + "startLabel": "开始时间", + "endLabel": "结束时间", + "timezoneNote": "时间基于系统时区", + "crossDayHint": "跨天调度: 从 {start} 到次日 {end}" + }, "cacheTtl": { "label": "Cache TTL 覆写", "options": { diff --git a/messages/zh-CN/settings/providers/list.json b/messages/zh-CN/settings/providers/list.json index 235ffd874..04331a7d9 100644 --- a/messages/zh-CN/settings/providers/list.json +++ b/messages/zh-CN/settings/providers/list.json @@ -41,5 +41,6 @@ "actionResetCircuit": "重置熔断", "actionResetUsage": "重置用量", "actionDelete": "删除", - "selectProvider": "选择 {name}" + "selectProvider": "选择 {name}", + "schedule": "调度" } diff --git a/messages/zh-TW/settings/providers/batchEdit.json b/messages/zh-TW/settings/providers/batchEdit.json index 664a23a52..2e4541364 100644 --- a/messages/zh-TW/settings/providers/batchEdit.json +++ b/messages/zh-TW/settings/providers/batchEdit.json @@ -46,7 +46,9 @@ "modelRedirects": "模型重新導向", "allowedModels": "允許的模型", "thinkingBudget": "思維預算", - "adaptiveThinking": "自適應思維" + "adaptiveThinking": "自適應思維", + "activeTimeStart": "排程開始時間", + "activeTimeEnd": "排程結束時間" }, "affectedProviders": { "title": "受影響的供應商", diff --git a/messages/zh-TW/settings/providers/form/sections.json b/messages/zh-TW/settings/providers/form/sections.json index e8c4333f7..49fc6b4f1 100644 --- a/messages/zh-TW/settings/providers/form/sections.json +++ b/messages/zh-TW/settings/providers/form/sections.json @@ -369,6 +369,16 @@ "placeholder": "1" } }, + "activeTime": { + "title": "排程時間窗口", + "description": "在指定時間窗口內自動啟用供應商", + "toggleLabel": "啟用時間排程", + "toggleDescription": "啟用後,此供應商僅在指定時間窗口內處於活躍狀態", + "startLabel": "開始時間", + "endLabel": "結束時間", + "timezoneNote": "時間依據系統時區", + "crossDayHint": "跨天排程: 從 {start} 到隔日 {end}" + }, "summary": { "models": "{count} 個允許模型", "none": "未設定", diff --git a/messages/zh-TW/settings/providers/list.json b/messages/zh-TW/settings/providers/list.json index 8a43c28ff..00ed50510 100644 --- a/messages/zh-TW/settings/providers/list.json +++ b/messages/zh-TW/settings/providers/list.json @@ -41,5 +41,6 @@ "actionResetCircuit": "重置熔斷", "actionResetUsage": "重設用量", "actionDelete": "刪除", - "selectProvider": "選擇 {name}" + "selectProvider": "選擇 {name}", + "schedule": "排程" } diff --git a/src/actions/providers.ts b/src/actions/providers.ts index a859ae03f..25ac7f2d7 100644 --- a/src/actions/providers.ts +++ b/src/actions/providers.ts @@ -272,6 +272,8 @@ export async function getProviders(): Promise { providerVendorId: provider.providerVendorId, preserveClientIp: provider.preserveClientIp, modelRedirects: provider.modelRedirects, + activeTimeStart: provider.activeTimeStart, + activeTimeEnd: provider.activeTimeEnd, allowedModels: provider.allowedModels, allowedClients: provider.allowedClients, blockedClients: provider.blockedClients, @@ -478,6 +480,8 @@ export async function addProvider(data: { provider_type?: ProviderType; preserve_client_ip?: boolean; model_redirects?: Record | null; + active_time_start?: string | null; + active_time_end?: string | null; allowed_models?: string[] | null; allowed_clients?: string[] | null; blocked_clients?: string[] | null; @@ -651,6 +655,8 @@ export async function editProvider( provider_type?: ProviderType; preserve_client_ip?: boolean; model_redirects?: Record | null; + active_time_start?: string | null; + active_time_end?: string | null; allowed_models?: string[] | null; allowed_clients?: string[] | null; blocked_clients?: string[] | null; @@ -1259,6 +1265,8 @@ const SINGLE_EDIT_PREIMAGE_FIELD_TO_PROVIDER_KEY: Record group_priorities: "groupPriorities", provider_type: "providerType", preserve_client_ip: "preserveClientIp", + active_time_start: "activeTimeStart", + active_time_end: "activeTimeEnd", model_redirects: "modelRedirects", allowed_models: "allowedModels", limit_5h_usd: "limit5hUsd", @@ -1420,6 +1428,12 @@ function mapApplyUpdatesToRepositoryFormat( if (applyUpdates.preserve_client_ip !== undefined) { result.preserveClientIp = applyUpdates.preserve_client_ip; } + if (applyUpdates.active_time_start !== undefined) { + result.activeTimeStart = applyUpdates.active_time_start; + } + if (applyUpdates.active_time_end !== undefined) { + result.activeTimeEnd = applyUpdates.active_time_end; + } if (applyUpdates.group_priorities !== undefined) { result.groupPriorities = applyUpdates.group_priorities; } @@ -1529,6 +1543,8 @@ const PATCH_FIELD_TO_PROVIDER_KEY: Record> SET_ADAPTIVE_THINKING_MODEL_MATCH_MODE: "routing.anthropicAdaptiveThinking", SET_ADAPTIVE_THINKING_MODELS: "routing.anthropicAdaptiveThinking", SET_GEMINI_GOOGLE_SEARCH: "routing.geminiGoogleSearchPreference", + SET_ACTIVE_TIME_START: "routing.activeTimeStart", + SET_ACTIVE_TIME_END: "routing.activeTimeEnd", SET_LIMIT_5H_USD: "rateLimit.limit5hUsd", SET_LIMIT_DAILY_USD: "rateLimit.limitDailyUsd", SET_DAILY_RESET_MODE: "rateLimit.dailyResetMode", @@ -110,6 +112,8 @@ export function createInitialState( anthropicThinkingBudgetPreference: "inherit", anthropicAdaptiveThinking: null, geminiGoogleSearchPreference: "inherit", + activeTimeStart: null, + activeTimeEnd: null, }, rateLimit: { limit5hUsd: null, @@ -190,6 +194,8 @@ export function createInitialState( sourceProvider?.anthropicThinkingBudgetPreference ?? "inherit", anthropicAdaptiveThinking: sourceProvider?.anthropicAdaptiveThinking ?? null, geminiGoogleSearchPreference: sourceProvider?.geminiGoogleSearchPreference ?? "inherit", + activeTimeStart: sourceProvider?.activeTimeStart ?? null, + activeTimeEnd: sourceProvider?.activeTimeEnd ?? null, }, rateLimit: { limit5hUsd: sourceProvider?.limit5hUsd ?? null, @@ -375,6 +381,16 @@ export function providerFormReducer( ...state, routing: { ...state.routing, geminiGoogleSearchPreference: action.payload }, }; + case "SET_ACTIVE_TIME_START": + return { + ...state, + routing: { ...state.routing, activeTimeStart: action.payload }, + }; + case "SET_ACTIVE_TIME_END": + return { + ...state, + routing: { ...state.routing, activeTimeEnd: action.payload }, + }; // Rate limit case "SET_LIMIT_5H_USD": diff --git a/src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts b/src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts index cd7d3dfcc..e6acc79bb 100644 --- a/src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts +++ b/src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts @@ -62,6 +62,9 @@ export interface RoutingState { anthropicAdaptiveThinking: AnthropicAdaptiveThinkingConfig | null; // Gemini-specific geminiGoogleSearchPreference: GeminiGoogleSearchPreference; + // Scheduled active time window (HH:mm format, null = always active) + activeTimeStart: string | null; + activeTimeEnd: string | null; } export interface RateLimitState { @@ -153,6 +156,8 @@ export type ProviderFormAction = | { type: "SET_ADAPTIVE_THINKING_MODELS"; payload: string[] } | { type: "SET_ADAPTIVE_THINKING_ENABLED"; payload: boolean } | { type: "SET_GEMINI_GOOGLE_SEARCH"; payload: GeminiGoogleSearchPreference } + | { type: "SET_ACTIVE_TIME_START"; payload: string | null } + | { type: "SET_ACTIVE_TIME_END"; payload: string | null } // Rate limit actions | { type: "SET_LIMIT_5H_USD"; payload: number | null } | { type: "SET_LIMIT_DAILY_USD"; payload: number | null } diff --git a/src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx b/src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx index 8a86edf86..9a1e94cfd 100644 --- a/src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx +++ b/src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx @@ -1,7 +1,7 @@ "use client"; import { motion } from "framer-motion"; -import { Info, Layers, Route, Scale, Settings, Timer } from "lucide-react"; +import { Clock, Info, Layers, Route, Scale, Settings, Timer } from "lucide-react"; import { useTranslations } from "next-intl"; import { useEffect, useState } from "react"; import { toast } from "sonner"; @@ -817,6 +817,74 @@ export function RoutingSection() { )} + + {/* Scheduled Active Time */} + +
+ + { + if (checked) { + dispatch({ type: "SET_ACTIVE_TIME_START", payload: "09:00" }); + dispatch({ type: "SET_ACTIVE_TIME_END", payload: "22:00" }); + } else { + dispatch({ type: "SET_ACTIVE_TIME_START", payload: null }); + dispatch({ type: "SET_ACTIVE_TIME_END", payload: null }); + } + }} + disabled={state.ui.isPending} + /> + + + {state.routing.activeTimeStart !== null && state.routing.activeTimeEnd !== null && ( +
+
+ + + dispatch({ type: "SET_ACTIVE_TIME_START", payload: e.target.value }) + } + disabled={state.ui.isPending} + /> + + + + dispatch({ type: "SET_ACTIVE_TIME_END", payload: e.target.value }) + } + disabled={state.ui.isPending} + /> + +
+

+ {t("sections.routing.activeTime.timezoneNote")} +

+ {state.routing.activeTimeStart > state.routing.activeTimeEnd && ( +

+ {t("sections.routing.activeTime.crossDayHint", { + start: state.routing.activeTimeStart, + end: state.routing.activeTimeEnd, + })} +

+ )} +
+ )} +
+
); diff --git a/src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx b/src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx index afd79455d..5681eb8de 100644 --- a/src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx +++ b/src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx @@ -4,6 +4,7 @@ import { useQueryClient } from "@tanstack/react-query"; import { AlertTriangle, CheckCircle, + Clock, Copy, Edit, Globe, @@ -539,6 +540,13 @@ export function ProviderRichListItem({ {tList("endpointCircuitBroken")} )} + {/* Schedule badge */} + {provider.activeTimeStart && provider.activeTimeEnd && ( + + + {provider.activeTimeStart}-{provider.activeTimeEnd} + + )} {/* Mobile: metrics row */} @@ -738,6 +746,13 @@ export function ProviderRichListItem({ {tList("endpointCircuitBroken")} )} + {/* Schedule badge */} + {provider.activeTimeStart && provider.activeTimeEnd && ( + + + {provider.activeTimeStart}-{provider.activeTimeEnd} + + )}
{/* Vendor & Endpoints OR Legacy URL */} diff --git a/src/app/v1/_lib/proxy/provider-selector.ts b/src/app/v1/_lib/proxy/provider-selector.ts index 617ac4349..788d010b8 100644 --- a/src/app/v1/_lib/proxy/provider-selector.ts +++ b/src/app/v1/_lib/proxy/provider-selector.ts @@ -3,6 +3,8 @@ import { PROVIDER_GROUP } from "@/lib/constants/provider.constants"; import { logger } from "@/lib/logger"; import { RateLimitService } from "@/lib/rate-limit"; import { SessionManager } from "@/lib/session-manager"; +import { isProviderActiveNow } from "@/lib/utils/provider-schedule"; +import { resolveSystemTimezone } from "@/lib/utils/timezone"; import { isVendorTypeCircuitOpen } from "@/lib/vendor-type-circuit-breaker"; import { findAllProviders, findProviderById } from "@/repository/provider"; import { getSystemSettings } from "@/repository/system-config"; @@ -536,6 +538,20 @@ export class ProxyProviderResolver { return null; } + // 调度时间窗口检查:防止会话复用绕过时间调度 + const systemTimezone = await resolveSystemTimezone(); + if (!isProviderActiveNow(provider.activeTimeStart, provider.activeTimeEnd, systemTimezone)) { + logger.debug("ProviderSelector: Session provider outside active schedule", { + sessionId: session.sessionId, + providerId: provider.id, + activeTimeStart: provider.activeTimeStart, + activeTimeEnd: provider.activeTimeEnd, + timezone: systemTimezone, + }); + await SessionManager.clearSessionProvider(session.sessionId); + return null; + } + // 临时熔断(vendor+type):防止会话复用绕过故障隔离 if ( provider.providerVendorId && @@ -844,6 +860,9 @@ export class ProxyProviderResolver { visibleProviders = clientFilteredProviders; } + // Resolve system timezone once for active time checks + const systemTimezone = await resolveSystemTimezone(); + // Step 2: 基础过滤 + 格式/模型匹配(使用 visibleProviders) const enabledProviders = visibleProviders.filter((provider) => { // 2a. 基础过滤 @@ -851,6 +870,11 @@ export class ProxyProviderResolver { return false; } + // 2a-2. 调度时间窗口过滤 + if (!isProviderActiveNow(provider.activeTimeStart, provider.activeTimeEnd, systemTimezone)) { + return false; + } + // 2b. 格式类型匹配(新增) // 根据 session.originalFormat 限制候选供应商类型,避免格式错配 if (session?.originalFormat) { @@ -885,6 +909,7 @@ export class ProxyProviderResolver { | "type_mismatch" | "model_not_allowed" | "context_1m_disabled" + | "schedule_inactive" | "disabled" = "disabled"; let details = ""; @@ -894,6 +919,9 @@ export class ProxyProviderResolver { } else if (excludeIds.includes(p.id)) { reason = "excluded"; details = "已在前序尝试中失败"; + } else if (!isProviderActiveNow(p.activeTimeStart, p.activeTimeEnd, systemTimezone)) { + reason = "schedule_inactive"; + details = `outside active window ${p.activeTimeStart}-${p.activeTimeEnd}`; } else if ( session?.originalFormat && !checkFormatProviderTypeCompatibility(session.originalFormat, p.providerType) @@ -1252,9 +1280,13 @@ export class ProxyProviderResolver { ); } - // 按 providerType 精确过滤 + // 按 providerType 精确过滤 + 调度时间窗口 + const systemTimezone = await resolveSystemTimezone(); const typeFiltered = visibleProviders.filter( - (p) => p.isEnabled && p.providerType === providerType + (p) => + p.isEnabled && + p.providerType === providerType && + isProviderActiveNow(p.activeTimeStart, p.activeTimeEnd, systemTimezone) ); // 将 providerType 映射为 decisionContext 允许的 targetType diff --git a/src/drizzle/schema.ts b/src/drizzle/schema.ts index 607e6d922..60a1f8e17 100644 --- a/src/drizzle/schema.ts +++ b/src/drizzle/schema.ts @@ -204,9 +204,10 @@ export const providers = pgTable('providers', { allowedClients: jsonb('allowed_clients').$type().notNull().default([]), blockedClients: jsonb('blocked_clients').$type().notNull().default([]), - // 加入 Claude 调度池:仅对非 Anthropic 提供商有效 - // 启用后,如果该提供商配置了重定向到 claude-* 模型,可以加入 claude 调度池 - joinClaudePool: boolean('join_claude_pool').default(false), + // Scheduled active time window (HH:mm format) + // Both null = always active; both set = active during window only + activeTimeStart: varchar('active_time_start', { length: 5 }), + activeTimeEnd: varchar('active_time_end', { length: 5 }), // Codex instructions 策略(已废弃):历史字段保留以兼容旧数据 // 当前运行时对 Codex 请求的 instructions 一律透传,不再读取/生效此配置 diff --git a/src/lib/provider-patch-contract.ts b/src/lib/provider-patch-contract.ts index d80401f79..e208eee80 100644 --- a/src/lib/provider-patch-contract.ts +++ b/src/lib/provider-patch-contract.ts @@ -36,6 +36,8 @@ const PATCH_FIELDS: ProviderBatchPatchField[] = [ "anthropic_thinking_budget_preference", "anthropic_adaptive_thinking", // Routing + "active_time_start", + "active_time_end", "preserve_client_ip", "group_priorities", "cache_ttl_preference", @@ -86,6 +88,8 @@ const CLEARABLE_FIELDS: Record = { anthropic_thinking_budget_preference: true, anthropic_adaptive_thinking: true, // Routing + active_time_start: true, + active_time_end: true, preserve_client_ip: false, group_priorities: true, cache_ttl_preference: true, @@ -232,6 +236,9 @@ function isValidSetValue(field: ProviderBatchPatchField, value: unknown): boolea case "proxy_url": case "mcp_passthrough_url": return typeof value === "string"; + case "active_time_start": + case "active_time_end": + return typeof value === "string" && /^([01][0-9]|2[0-3]):[0-5][0-9]$/.test(value); case "group_priorities": return isNumberRecord(value); case "cache_ttl_preference": @@ -421,6 +428,12 @@ export function normalizeProviderBatchPatchDraft( if (!adaptiveThinking.ok) return adaptiveThinking; // Routing + const activeTimeStart = normalizePatchField("active_time_start", typedDraft.active_time_start); + if (!activeTimeStart.ok) return activeTimeStart; + + const activeTimeEnd = normalizePatchField("active_time_end", typedDraft.active_time_end); + if (!activeTimeEnd.ok) return activeTimeEnd; + const preserveClientIp = normalizePatchField("preserve_client_ip", typedDraft.preserve_client_ip); if (!preserveClientIp.ok) return preserveClientIp; @@ -584,6 +597,8 @@ export function normalizeProviderBatchPatchDraft( anthropic_thinking_budget_preference: thinkingBudget.data, anthropic_adaptive_thinking: adaptiveThinking.data, // Routing + active_time_start: activeTimeStart.data, + active_time_end: activeTimeEnd.data, preserve_client_ip: preserveClientIp.data, group_priorities: groupPriorities.data, cache_ttl_preference: cacheTtlPref.data, @@ -672,6 +687,12 @@ function applyPatchField( patch.value as ProviderBatchApplyUpdates["anthropic_adaptive_thinking"]; return { ok: true, data: undefined }; // Routing + case "active_time_start": + updates.active_time_start = patch.value as ProviderBatchApplyUpdates["active_time_start"]; + return { ok: true, data: undefined }; + case "active_time_end": + updates.active_time_end = patch.value as ProviderBatchApplyUpdates["active_time_end"]; + return { ok: true, data: undefined }; case "preserve_client_ip": updates.preserve_client_ip = patch.value as ProviderBatchApplyUpdates["preserve_client_ip"]; return { ok: true, data: undefined }; @@ -813,6 +834,13 @@ function applyPatchField( case "anthropic_adaptive_thinking": updates.anthropic_adaptive_thinking = null; return { ok: true, data: undefined }; + // Routing - active time clear to null + case "active_time_start": + updates.active_time_start = null; + return { ok: true, data: undefined }; + case "active_time_end": + updates.active_time_end = null; + return { ok: true, data: undefined }; // Routing - preference fields clear to "inherit" case "cache_ttl_preference": updates.cache_ttl_preference = "inherit"; @@ -893,6 +921,8 @@ export function buildProviderBatchApplyUpdates( ["anthropic_thinking_budget_preference", patch.anthropic_thinking_budget_preference], ["anthropic_adaptive_thinking", patch.anthropic_adaptive_thinking], // Routing + ["active_time_start", patch.active_time_start], + ["active_time_end", patch.active_time_end], ["preserve_client_ip", patch.preserve_client_ip], ["group_priorities", patch.group_priorities], ["cache_ttl_preference", patch.cache_ttl_preference], @@ -956,6 +986,8 @@ export function hasProviderBatchPatchChanges(patch: ProviderBatchPatch): boolean patch.anthropic_thinking_budget_preference.mode !== "no_change" || patch.anthropic_adaptive_thinking.mode !== "no_change" || // Routing + patch.active_time_start.mode !== "no_change" || + patch.active_time_end.mode !== "no_change" || patch.preserve_client_ip.mode !== "no_change" || patch.group_priorities.mode !== "no_change" || patch.cache_ttl_preference.mode !== "no_change" || diff --git a/src/lib/utils/provider-schedule.ts b/src/lib/utils/provider-schedule.ts new file mode 100644 index 000000000..a3b34c5d1 --- /dev/null +++ b/src/lib/utils/provider-schedule.ts @@ -0,0 +1,79 @@ +/** + * Provider Schedule Utilities + * + * Determines whether a provider is currently within its configured active time window. + * Supports same-day and cross-day (overnight) schedules using the system timezone. + */ + +/** + * Check if a provider is currently active based on its schedule configuration. + * + * @param startTime - HH:mm format start time, or null (always active) + * @param endTime - HH:mm format end time, or null (always active) + * @param timezone - IANA timezone identifier (e.g., "Asia/Shanghai") + * @param now - Optional Date override for testing + * @returns true if the provider is currently active + * + * Rules: + * - Both null -> always active (true) + * - Either null -> always active (true) (defensive; validation ensures both-or-neither) + * - start === end -> false (zero-width window; validation blocks this input) + * - Same-day (start < end): start <= now < end + * - Cross-day (start > end): now >= start || now < end + */ +export function isProviderActiveNow( + startTime: string | null, + endTime: string | null, + timezone: string, + now: Date = new Date() +): boolean { + if (startTime == null || endTime == null) { + return true; + } + + if (startTime === endTime) { + return false; + } + + const nowMinutes = getCurrentMinutesInTimezone(now, timezone); + const startMinutes = parseHHMM(startTime); + const endMinutes = parseHHMM(endTime); + + // Fail-open: if DB contains malformed time values, treat provider as always active + if (Number.isNaN(startMinutes) || Number.isNaN(endMinutes)) { + return true; + } + + if (startMinutes < endMinutes) { + // Same-day: start <= now < end + return nowMinutes >= startMinutes && nowMinutes < endMinutes; + } + + // Cross-day: now >= start || now < end + return nowMinutes >= startMinutes || nowMinutes < endMinutes; +} + +const HHMM_RE = /^([01]\d|2[0-3]):([0-5]\d)$/; + +function parseHHMM(time: string): number { + const match = HHMM_RE.exec(time); + if (!match) { + return Number.NaN; + } + return Number.parseInt(match[1], 10) * 60 + Number.parseInt(match[2], 10); +} + +function getCurrentMinutesInTimezone(now: Date, timezone: string): number { + const formatter = new Intl.DateTimeFormat("en-US", { + timeZone: timezone, + hour: "2-digit", + minute: "2-digit", + hour12: false, + }); + + const parts = formatter.formatToParts(now); + const hour = parseInt(parts.find((p) => p.type === "hour")?.value ?? "0", 10); + const minute = parseInt(parts.find((p) => p.type === "minute")?.value ?? "0", 10); + + return hour * 60 + minute; +} diff --git a/src/lib/validation/schemas.ts b/src/lib/validation/schemas.ts index 97dcae39a..aee7785e4 100644 --- a/src/lib/validation/schemas.ts +++ b/src/lib/validation/schemas.ts @@ -448,6 +448,17 @@ export const CreateProviderSchema = z .default("claude"), preserve_client_ip: z.boolean().optional().default(false), model_redirects: z.record(z.string(), z.string()).nullable().optional(), + // Scheduled active time window (HH:mm format) + active_time_start: z + .string() + .regex(/^([01][0-9]|2[0-3]):[0-5][0-9]$/, "active_time_start must be HH:mm format") + .nullable() + .optional(), + active_time_end: z + .string() + .regex(/^([01][0-9]|2[0-3]):[0-5][0-9]$/, "active_time_end must be HH:mm format") + .nullable() + .optional(), allowed_models: z.array(z.string()).nullable().optional(), allowed_clients: OPTIONAL_CLIENT_PATTERN_ARRAY_WITH_DEFAULT_SCHEMA, blocked_clients: OPTIONAL_CLIENT_PATTERN_ARRAY_WITH_DEFAULT_SCHEMA, @@ -621,6 +632,23 @@ export const CreateProviderSchema = z }); } } + // active_time_start and active_time_end must be both set or both null + const hasStart = data.active_time_start != null; + const hasEnd = data.active_time_end != null; + if (hasStart !== hasEnd) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "active_time_start and active_time_end must be both set or both cleared", + path: [hasStart ? "active_time_end" : "active_time_start"], + }); + } + if (hasStart && hasEnd && data.active_time_start === data.active_time_end) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "active_time_start and active_time_end must not be the same", + path: ["active_time_end"], + }); + } }); /** @@ -656,6 +684,16 @@ export const UpdateProviderSchema = z .optional(), preserve_client_ip: z.boolean().optional(), model_redirects: z.record(z.string(), z.string()).nullable().optional(), + active_time_start: z + .string() + .regex(/^([01][0-9]|2[0-3]):[0-5][0-9]$/, "active_time_start must be HH:mm format") + .nullable() + .optional(), + active_time_end: z + .string() + .regex(/^([01][0-9]|2[0-3]):[0-5][0-9]$/, "active_time_end must be HH:mm format") + .nullable() + .optional(), allowed_models: z.array(z.string()).nullable().optional(), allowed_clients: OPTIONAL_CLIENT_PATTERN_ARRAY_SCHEMA, blocked_clients: OPTIONAL_CLIENT_PATTERN_ARRAY_SCHEMA, @@ -824,6 +862,23 @@ export const UpdateProviderSchema = z }); } } + // active_time_start and active_time_end must be both set or both null + const hasStart = data.active_time_start != null; + const hasEnd = data.active_time_end != null; + if (hasStart !== hasEnd) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "active_time_start and active_time_end must be both set or both cleared", + path: [hasStart ? "active_time_end" : "active_time_start"], + }); + } + if (hasStart && hasEnd && data.active_time_start === data.active_time_end) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "active_time_start and active_time_end must not be the same", + path: ["active_time_end"], + }); + } }); /** diff --git a/src/repository/_shared/transformers.ts b/src/repository/_shared/transformers.ts index d06cd86ed..562ee92d2 100644 --- a/src/repository/_shared/transformers.ts +++ b/src/repository/_shared/transformers.ts @@ -92,6 +92,8 @@ export function toProvider(dbProvider: any): Provider { providerType: dbProvider?.providerType ?? "claude", preserveClientIp: dbProvider?.preserveClientIp ?? false, modelRedirects: dbProvider?.modelRedirects ?? null, + activeTimeStart: dbProvider?.activeTimeStart ?? null, + activeTimeEnd: dbProvider?.activeTimeEnd ?? null, mcpPassthroughType: dbProvider?.mcpPassthroughType ?? "none", mcpPassthroughUrl: dbProvider?.mcpPassthroughUrl ?? null, limit5hUsd: dbProvider?.limit5hUsd ? parseFloat(dbProvider.limit5hUsd) : null, diff --git a/src/repository/provider.ts b/src/repository/provider.ts index 77b5fe216..00e5c2bb3 100644 --- a/src/repository/provider.ts +++ b/src/repository/provider.ts @@ -187,6 +187,8 @@ export async function createProvider(providerData: CreateProviderData): Promise< allowedModels: providerData.allowed_models, allowedClients: providerData.allowed_clients ?? [], blockedClients: providerData.blocked_clients ?? [], + activeTimeStart: providerData.active_time_start ?? null, + activeTimeEnd: providerData.active_time_end ?? null, mcpPassthroughType: providerData.mcp_passthrough_type ?? "none", mcpPassthroughUrl: providerData.mcp_passthrough_url ?? null, limit5hUsd: providerData.limit_5h_usd != null ? providerData.limit_5h_usd.toString() : null, @@ -264,6 +266,8 @@ export async function createProvider(providerData: CreateProviderData): Promise< allowedModels: providers.allowedModels, allowedClients: providers.allowedClients, blockedClients: providers.blockedClients, + activeTimeStart: providers.activeTimeStart, + activeTimeEnd: providers.activeTimeEnd, mcpPassthroughType: providers.mcpPassthroughType, mcpPassthroughUrl: providers.mcpPassthroughUrl, limit5hUsd: providers.limit5hUsd, @@ -346,6 +350,8 @@ export async function findProviderList( allowedModels: providers.allowedModels, allowedClients: providers.allowedClients, blockedClients: providers.blockedClients, + activeTimeStart: providers.activeTimeStart, + activeTimeEnd: providers.activeTimeEnd, mcpPassthroughType: providers.mcpPassthroughType, mcpPassthroughUrl: providers.mcpPassthroughUrl, limit5hUsd: providers.limit5hUsd, @@ -428,6 +434,8 @@ export async function findAllProvidersFresh(): Promise { allowedModels: providers.allowedModels, allowedClients: providers.allowedClients, blockedClients: providers.blockedClients, + activeTimeStart: providers.activeTimeStart, + activeTimeEnd: providers.activeTimeEnd, mcpPassthroughType: providers.mcpPassthroughType, mcpPassthroughUrl: providers.mcpPassthroughUrl, limit5hUsd: providers.limit5hUsd, @@ -514,6 +522,8 @@ export async function findProviderById(id: number): Promise { allowedModels: providers.allowedModels, allowedClients: providers.allowedClients, blockedClients: providers.blockedClients, + activeTimeStart: providers.activeTimeStart, + activeTimeEnd: providers.activeTimeEnd, mcpPassthroughType: providers.mcpPassthroughType, mcpPassthroughUrl: providers.mcpPassthroughUrl, limit5hUsd: providers.limit5hUsd, @@ -596,6 +606,10 @@ export async function updateProvider( dbData.allowedClients = providerData.allowed_clients ?? []; if (providerData.blocked_clients !== undefined) dbData.blockedClients = providerData.blocked_clients ?? []; + if (providerData.active_time_start !== undefined) + dbData.activeTimeStart = providerData.active_time_start ?? null; + if (providerData.active_time_end !== undefined) + dbData.activeTimeEnd = providerData.active_time_end ?? null; if (providerData.mcp_passthrough_type !== undefined) dbData.mcpPassthroughType = providerData.mcp_passthrough_type; if (providerData.mcp_passthrough_url !== undefined) @@ -743,6 +757,8 @@ export async function updateProvider( allowedModels: providers.allowedModels, allowedClients: providers.allowedClients, blockedClients: providers.blockedClients, + activeTimeStart: providers.activeTimeStart, + activeTimeEnd: providers.activeTimeEnd, mcpPassthroughType: providers.mcpPassthroughType, mcpPassthroughUrl: providers.mcpPassthroughUrl, limit5hUsd: providers.limit5hUsd, @@ -998,6 +1014,8 @@ export interface BatchProviderUpdates { anthropicAdaptiveThinking?: AnthropicAdaptiveThinkingConfig | null; // Routing preserveClientIp?: boolean; + activeTimeStart?: string | null; + activeTimeEnd?: string | null; groupPriorities?: Record | null; cacheTtlPreference?: string | null; swapCacheTtlBilling?: boolean; @@ -1082,6 +1100,12 @@ export async function updateProvidersBatch( if (updates.preserveClientIp !== undefined) { setClauses.preserveClientIp = updates.preserveClientIp; } + if (updates.activeTimeStart !== undefined) { + setClauses.activeTimeStart = updates.activeTimeStart; + } + if (updates.activeTimeEnd !== undefined) { + setClauses.activeTimeEnd = updates.activeTimeEnd; + } if (updates.groupPriorities !== undefined) { setClauses.groupPriorities = updates.groupPriorities; } diff --git a/src/types/message.ts b/src/types/message.ts index f99ca47b9..21a6552a7 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -172,6 +172,7 @@ export interface ProviderChainItem { | "type_mismatch" | "model_not_allowed" | "context_1m_disabled" // 供应商禁用了 1M 上下文功能 + | "schedule_inactive" // 供应商不在调度时间窗口内 | "disabled" | "client_restriction"; // Provider filtered due to client restriction details?: string; // 额外信息(如费用:$15.2/$15) diff --git a/src/types/provider.ts b/src/types/provider.ts index 875341206..e251ba1b1 100644 --- a/src/types/provider.ts +++ b/src/types/provider.ts @@ -69,7 +69,9 @@ export type ProviderBatchPatchField = | "blocked_clients" | "anthropic_thinking_budget_preference" | "anthropic_adaptive_thinking" - // Routing + // Routing / Schedule + | "active_time_start" + | "active_time_end" | "preserve_client_ip" | "group_priorities" | "cache_ttl_preference" @@ -118,7 +120,9 @@ export interface ProviderBatchPatchDraft { blocked_clients?: ProviderPatchDraftInput; anthropic_thinking_budget_preference?: ProviderPatchDraftInput; anthropic_adaptive_thinking?: ProviderPatchDraftInput; - // Routing + // Routing / Schedule + active_time_start?: ProviderPatchDraftInput; + active_time_end?: ProviderPatchDraftInput; preserve_client_ip?: ProviderPatchDraftInput; group_priorities?: ProviderPatchDraftInput>; cache_ttl_preference?: ProviderPatchDraftInput; @@ -168,7 +172,9 @@ export interface ProviderBatchPatch { blocked_clients: ProviderPatchOperation; anthropic_thinking_budget_preference: ProviderPatchOperation; anthropic_adaptive_thinking: ProviderPatchOperation; - // Routing + // Routing / Schedule + active_time_start: ProviderPatchOperation; + active_time_end: ProviderPatchOperation; preserve_client_ip: ProviderPatchOperation; group_priorities: ProviderPatchOperation>; cache_ttl_preference: ProviderPatchOperation; @@ -218,7 +224,9 @@ export interface ProviderBatchApplyUpdates { blocked_clients?: string[]; anthropic_thinking_budget_preference?: AnthropicThinkingBudgetPreference | null; anthropic_adaptive_thinking?: AnthropicAdaptiveThinkingConfig | null; - // Routing + // Routing / Schedule + active_time_start?: string | null; + active_time_end?: string | null; preserve_client_ip?: boolean; group_priorities?: Record | null; cache_ttl_preference?: CacheTtlPreference | null; @@ -288,6 +296,10 @@ export interface Provider { preserveClientIp: boolean; modelRedirects: Record | null; + // Scheduled active time window (HH:mm format, null = always active) + activeTimeStart: string | null; + activeTimeEnd: string | null; + // 模型列表:双重语义 // - Anthropic 提供商:白名单(管理员限制可调度的模型,可选) // - 非 Anthropic 提供商:声明列表(提供商声称支持的模型,可选) @@ -398,6 +410,9 @@ export interface ProviderDisplay { // 是否透传客户端 IP preserveClientIp: boolean; modelRedirects: Record | null; + // Scheduled active time window + activeTimeStart: string | null; + activeTimeEnd: string | null; // 模型列表(双重语义) allowedModels: string[] | null; allowedClients: string[]; // Allowed client patterns (empty = no restriction) @@ -490,6 +505,8 @@ export interface CreateProviderData { provider_type?: ProviderType; preserve_client_ip?: boolean; model_redirects?: Record | null; + active_time_start?: string | null; + active_time_end?: string | null; allowed_models?: string[] | null; allowed_clients?: string[] | null; blocked_clients?: string[] | null; @@ -566,6 +583,8 @@ export interface UpdateProviderData { provider_type?: ProviderType; preserve_client_ip?: boolean; model_redirects?: Record | null; + active_time_start?: string | null; + active_time_end?: string | null; allowed_models?: string[] | null; allowed_clients?: string[] | null; blocked_clients?: string[] | null; diff --git a/tests/unit/actions/providers-patch-contract.test.ts b/tests/unit/actions/providers-patch-contract.test.ts index 1d919f38d..6ea46db3b 100644 --- a/tests/unit/actions/providers-patch-contract.test.ts +++ b/tests/unit/actions/providers-patch-contract.test.ts @@ -909,6 +909,122 @@ describe("provider patch contract", () => { expect(hasProviderBatchPatchChanges(normalized.data)).toBe(false); }); + + it("detects change on active_time_start", () => { + const normalized = normalizeProviderBatchPatchDraft({ + active_time_start: { set: "09:00" }, + }); + + expect(normalized.ok).toBe(true); + if (!normalized.ok) return; + + expect(hasProviderBatchPatchChanges(normalized.data)).toBe(true); + }); + + it("detects change on active_time_end", () => { + const normalized = normalizeProviderBatchPatchDraft({ + active_time_end: { set: "17:00" }, + }); + + expect(normalized.ok).toBe(true); + if (!normalized.ok) return; + + expect(hasProviderBatchPatchChanges(normalized.data)).toBe(true); + }); + }); + + describe("active_time_start / active_time_end batch patch", () => { + it("accepts active_time_start as string and maps to apply payload", () => { + const result = prepareProviderBatchApplyUpdates({ + active_time_start: { set: "09:00" }, + }); + + expect(result.ok).toBe(true); + if (!result.ok) return; + + expect(result.data.active_time_start).toBe("09:00"); + }); + + it("clears active_time_start to null", () => { + const result = prepareProviderBatchApplyUpdates({ + active_time_start: { clear: true }, + }); + + expect(result.ok).toBe(true); + if (!result.ok) return; + + expect(result.data.active_time_start).toBeNull(); + }); + + it("accepts active_time_end as string and maps to apply payload", () => { + const result = prepareProviderBatchApplyUpdates({ + active_time_end: { set: "17:00" }, + }); + + expect(result.ok).toBe(true); + if (!result.ok) return; + + expect(result.data.active_time_end).toBe("17:00"); + }); + + it("clears active_time_end to null", () => { + const result = prepareProviderBatchApplyUpdates({ + active_time_end: { clear: true }, + }); + + expect(result.ok).toBe(true); + if (!result.ok) return; + + expect(result.data.active_time_end).toBeNull(); + }); + + it("rejects non-string value for active_time_start", () => { + const result = normalizeProviderBatchPatchDraft({ + active_time_start: { set: 900 } as never, + }); + + expect(result.ok).toBe(false); + if (result.ok) return; + + expect(result.error.code).toBe(PROVIDER_PATCH_ERROR_CODES.INVALID_PATCH_SHAPE); + expect(result.error.field).toBe("active_time_start"); + }); + + it("rejects non-string value for active_time_end", () => { + const result = normalizeProviderBatchPatchDraft({ + active_time_end: { set: 900 } as never, + }); + + expect(result.ok).toBe(false); + if (result.ok) return; + + expect(result.error.code).toBe(PROVIDER_PATCH_ERROR_CODES.INVALID_PATCH_SHAPE); + expect(result.error.field).toBe("active_time_end"); + }); + + it("rejects invalid HH:mm format for active_time_start", () => { + const result = normalizeProviderBatchPatchDraft({ + active_time_start: { set: "9:00" }, + }); + + expect(result.ok).toBe(false); + if (result.ok) return; + + expect(result.error.code).toBe(PROVIDER_PATCH_ERROR_CODES.INVALID_PATCH_SHAPE); + expect(result.error.field).toBe("active_time_start"); + }); + + it("rejects out-of-range time for active_time_end", () => { + const result = normalizeProviderBatchPatchDraft({ + active_time_end: { set: "25:00" }, + }); + + expect(result.ok).toBe(false); + if (result.ok) return; + + expect(result.error.code).toBe(PROVIDER_PATCH_ERROR_CODES.INVALID_PATCH_SHAPE); + expect(result.error.field).toBe("active_time_end"); + }); }); describe("combined set across all categories", () => { @@ -936,6 +1052,9 @@ describe("provider patch contract", () => { // mcp mcp_passthrough_type: { set: "minimax" }, mcp_passthrough_url: { set: "https://api.minimaxi.com" }, + // schedule + active_time_start: { set: "09:00" }, + active_time_end: { set: "17:00" }, }); expect(result.ok).toBe(true); @@ -957,6 +1076,8 @@ describe("provider patch contract", () => { expect(result.data.first_byte_timeout_streaming_ms).toBe(15000); expect(result.data.mcp_passthrough_type).toBe("minimax"); expect(result.data.mcp_passthrough_url).toBe("https://api.minimaxi.com"); + expect(result.data.active_time_start).toBe("09:00"); + expect(result.data.active_time_end).toBe("17:00"); }); }); }); diff --git a/tests/unit/lib/utils/provider-schedule.test.ts b/tests/unit/lib/utils/provider-schedule.test.ts new file mode 100644 index 000000000..e0eda21aa --- /dev/null +++ b/tests/unit/lib/utils/provider-schedule.test.ts @@ -0,0 +1,178 @@ +import { describe, expect, it } from "vitest"; +import { isProviderActiveNow } from "@/lib/utils/provider-schedule"; + +describe("isProviderActiveNow", () => { + // Helper: create a Date at a specific time in a given timezone + function makeDate(hh: number, mm: number, timezone: string): Date { + // Build a date string in the target timezone, then convert back to UTC + const now = new Date(); + const year = now.getFullYear(); + const month = now.getMonth(); + const day = now.getDate(); + + // Create a date formatter for the target timezone + const formatter = new Intl.DateTimeFormat("en-US", { + timeZone: timezone, + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }); + + // Get what the current time is in that timezone + const parts = formatter.formatToParts(now); + const getPart = (type: string) => parts.find((p) => p.type === type)?.value ?? "0"; + const currentHourInTz = parseInt(getPart("hour"), 10); + const currentMinuteInTz = parseInt(getPart("minute"), 10); + + // Compute the offset in ms we need to shift + const targetMinutes = hh * 60 + mm; + const currentMinutes = currentHourInTz * 60 + currentMinuteInTz; + const diffMs = (targetMinutes - currentMinutes) * 60 * 1000; + + return new Date(now.getTime() + diffMs); + } + + describe("null/undefined inputs (always active)", () => { + it("returns true when both start and end are null", () => { + expect(isProviderActiveNow(null, null, "UTC")).toBe(true); + }); + + it("returns true when start is null and end is non-null", () => { + expect(isProviderActiveNow(null, "18:00", "UTC")).toBe(true); + }); + + it("returns true when start is non-null and end is null", () => { + expect(isProviderActiveNow("09:00", null, "UTC")).toBe(true); + }); + }); + + describe("same-day schedule (start < end)", () => { + const cases = [ + { start: "09:00", end: "17:00", hour: 9, min: 0, expected: true, desc: "at start boundary" }, + { start: "09:00", end: "17:00", hour: 12, min: 30, expected: true, desc: "middle of window" }, + { start: "09:00", end: "17:00", hour: 16, min: 59, expected: true, desc: "just before end" }, + { + start: "09:00", + end: "17:00", + hour: 17, + min: 0, + expected: false, + desc: "at end boundary (exclusive)", + }, + { + start: "09:00", + end: "17:00", + hour: 8, + min: 59, + expected: false, + desc: "just before start", + }, + { start: "09:00", end: "17:00", hour: 23, min: 0, expected: false, desc: "well after end" }, + { start: "09:00", end: "17:00", hour: 0, min: 0, expected: false, desc: "midnight" }, + { start: "00:00", end: "23:59", hour: 12, min: 0, expected: true, desc: "nearly full day" }, + ]; + + for (const { start, end, hour, min, expected, desc } of cases) { + it(`${desc}: ${start}-${end} at ${String(hour).padStart(2, "0")}:${String(min).padStart(2, "0")} -> ${expected}`, () => { + const now = makeDate(hour, min, "UTC"); + expect(isProviderActiveNow(start, end, "UTC", now)).toBe(expected); + }); + } + }); + + describe("cross-day schedule (start > end)", () => { + const cases = [ + { start: "22:00", end: "08:00", hour: 22, min: 0, expected: true, desc: "at start boundary" }, + { start: "22:00", end: "08:00", hour: 23, min: 30, expected: true, desc: "late night" }, + { start: "22:00", end: "08:00", hour: 0, min: 0, expected: true, desc: "midnight" }, + { start: "22:00", end: "08:00", hour: 3, min: 0, expected: true, desc: "early morning" }, + { start: "22:00", end: "08:00", hour: 7, min: 59, expected: true, desc: "just before end" }, + { + start: "22:00", + end: "08:00", + hour: 8, + min: 0, + expected: false, + desc: "at end boundary (exclusive)", + }, + { start: "22:00", end: "08:00", hour: 12, min: 0, expected: false, desc: "midday" }, + { + start: "22:00", + end: "08:00", + hour: 21, + min: 59, + expected: false, + desc: "just before start", + }, + ]; + + for (const { start, end, hour, min, expected, desc } of cases) { + it(`${desc}: ${start}-${end} at ${String(hour).padStart(2, "0")}:${String(min).padStart(2, "0")} -> ${expected}`, () => { + const now = makeDate(hour, min, "UTC"); + expect(isProviderActiveNow(start, end, "UTC", now)).toBe(expected); + }); + } + }); + + describe("edge cases", () => { + it("start === end returns false (zero-width window)", () => { + const now = makeDate(22, 0, "UTC"); + expect(isProviderActiveNow("22:00", "22:00", "UTC", now)).toBe(false); + }); + + it("start === end returns false even at different time", () => { + const now = makeDate(10, 0, "UTC"); + expect(isProviderActiveNow("22:00", "22:00", "UTC", now)).toBe(false); + }); + }); + + describe("timezone support", () => { + it("same UTC time yields different results in different timezones", () => { + // At UTC 06:00, in Asia/Shanghai (UTC+8) it's 14:00 + // Schedule 09:00-17:00 should be active in Shanghai but not in UTC + const utcDate = makeDate(6, 0, "UTC"); + + // In UTC at 06:00 with schedule 09:00-17:00 -> inactive + expect(isProviderActiveNow("09:00", "17:00", "UTC", utcDate)).toBe(false); + + // In Asia/Shanghai at 14:00 with schedule 09:00-17:00 -> active + expect(isProviderActiveNow("09:00", "17:00", "Asia/Shanghai", utcDate)).toBe(true); + }); + }); + + describe("malformed input defense", () => { + it("returns true (fail-open) for malformed start time", () => { + const now = makeDate(12, 0, "UTC"); + expect(isProviderActiveNow("garbage", "17:00", "UTC", now)).toBe(true); + }); + + it("returns true (fail-open) for malformed end time", () => { + const now = makeDate(12, 0, "UTC"); + expect(isProviderActiveNow("09:00", "not-a-time", "UTC", now)).toBe(true); + }); + + it("returns true (fail-open) for both times malformed", () => { + const now = makeDate(12, 0, "UTC"); + expect(isProviderActiveNow("bad", "worse", "UTC", now)).toBe(true); + }); + + it("returns true (fail-open) for out-of-range hour (24:00)", () => { + const now = makeDate(12, 0, "UTC"); + expect(isProviderActiveNow("24:00", "17:00", "UTC", now)).toBe(true); + }); + + it("returns true (fail-open) for single-digit hour (9:00)", () => { + const now = makeDate(12, 0, "UTC"); + expect(isProviderActiveNow("9:00", "17:00", "UTC", now)).toBe(true); + }); + + it("returns true (fail-open) for out-of-range minutes (99:99)", () => { + const now = makeDate(12, 0, "UTC"); + expect(isProviderActiveNow("99:99", "17:00", "UTC", now)).toBe(true); + }); + }); +});