diff --git a/drizzle/0074_wide_retro_girl.sql b/drizzle/0074_wide_retro_girl.sql new file mode 100644 index 000000000..df9235243 --- /dev/null +++ b/drizzle/0074_wide_retro_girl.sql @@ -0,0 +1,3 @@ +ALTER TABLE "providers" ADD COLUMN "allowed_clients" jsonb DEFAULT '[]'::jsonb NOT NULL;--> statement-breakpoint +ALTER TABLE "providers" ADD COLUMN "blocked_clients" jsonb DEFAULT '[]'::jsonb NOT NULL;--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN "blocked_clients" jsonb DEFAULT '[]'::jsonb NOT NULL; \ No newline at end of file diff --git a/drizzle/0075_faithful_speed_demon.sql b/drizzle/0075_faithful_speed_demon.sql new file mode 100644 index 000000000..671525977 --- /dev/null +++ b/drizzle/0075_faithful_speed_demon.sql @@ -0,0 +1,5 @@ +DROP INDEX IF EXISTS "idx_usage_ledger_key_cost";--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "idx_message_request_session_user_info" ON "message_request" USING btree ("session_id","created_at","user_id","key") WHERE "message_request"."deleted_at" IS NULL;--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "idx_usage_ledger_user_cost_cover" ON "usage_ledger" USING btree ("user_id","created_at","cost_usd") WHERE "usage_ledger"."blocked_by" IS NULL;--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "idx_usage_ledger_provider_cost_cover" ON "usage_ledger" USING btree ("final_provider_id","created_at","cost_usd") WHERE "usage_ledger"."blocked_by" IS NULL;--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "idx_usage_ledger_key_cost" ON "usage_ledger" USING btree ("key","created_at","cost_usd") WHERE "usage_ledger"."blocked_by" IS NULL; \ No newline at end of file diff --git a/drizzle/meta/0074_snapshot.json b/drizzle/meta/0074_snapshot.json new file mode 100644 index 000000000..ca17a71b0 --- /dev/null +++ b/drizzle/meta/0074_snapshot.json @@ -0,0 +1,3723 @@ +{ + "id": "132bc4b6-86f3-43e1-b980-5ff62835bd1d", + "prevId": "0a7b1169-a126-4a17-a3e5-ce96ffcb0d59", + "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": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_prices": { + "name": "model_prices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "model_name": { + "name": "model_name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "price_data": { + "name": "price_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'litellm'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_model_prices_latest": { + "name": "idx_model_prices_latest", + "columns": [ + { + "expression": "model_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_model_prices_model_name": { + "name": "idx_model_prices_model_name", + "columns": [ + { + "expression": "model_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_model_prices_created_at": { + "name": "idx_model_prices_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_model_prices_source": { + "name": "idx_model_prices_source", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_settings": { + "name": "notification_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "use_legacy_mode": { + "name": "use_legacy_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "circuit_breaker_enabled": { + "name": "circuit_breaker_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "circuit_breaker_webhook": { + "name": "circuit_breaker_webhook", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "daily_leaderboard_enabled": { + "name": "daily_leaderboard_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "daily_leaderboard_webhook": { + "name": "daily_leaderboard_webhook", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "daily_leaderboard_time": { + "name": "daily_leaderboard_time", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false, + "default": "'09:00'" + }, + "daily_leaderboard_top_n": { + "name": "daily_leaderboard_top_n", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5 + }, + "cost_alert_enabled": { + "name": "cost_alert_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cost_alert_webhook": { + "name": "cost_alert_webhook", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "cost_alert_threshold": { + "name": "cost_alert_threshold", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false, + "default": "'0.80'" + }, + "cost_alert_check_interval": { + "name": "cost_alert_check_interval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 60 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_target_bindings": { + "name": "notification_target_bindings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "notification_type": { + "name": "notification_type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "schedule_cron": { + "name": "schedule_cron", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "schedule_timezone": { + "name": "schedule_timezone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "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" + }, + "join_claude_pool": { + "name": "join_claude_pool", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "codex_instructions_strategy": { + "name": "codex_instructions_strategy", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "default": "'auto'" + }, + "mcp_passthrough_type": { + "name": "mcp_passthrough_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "mcp_passthrough_url": { + "name": "mcp_passthrough_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "limit_5h_usd": { + "name": "limit_5h_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_daily_usd": { + "name": "limit_daily_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "daily_reset_mode": { + "name": "daily_reset_mode", + "type": "daily_reset_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'fixed'" + }, + "daily_reset_time": { + "name": "daily_reset_time", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'00:00'" + }, + "limit_weekly_usd": { + "name": "limit_weekly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_monthly_usd": { + "name": "limit_monthly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_total_usd": { + "name": "limit_total_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "total_cost_reset_at": { + "name": "total_cost_reset_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "limit_concurrent_sessions": { + "name": "limit_concurrent_sessions", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "max_retry_attempts": { + "name": "max_retry_attempts", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "circuit_breaker_failure_threshold": { + "name": "circuit_breaker_failure_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5 + }, + "circuit_breaker_open_duration": { + "name": "circuit_breaker_open_duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1800000 + }, + "circuit_breaker_half_open_success_threshold": { + "name": "circuit_breaker_half_open_success_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 2 + }, + "proxy_url": { + "name": "proxy_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "proxy_fallback_to_direct": { + "name": "proxy_fallback_to_direct", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "first_byte_timeout_streaming_ms": { + "name": "first_byte_timeout_streaming_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "streaming_idle_timeout_ms": { + "name": "streaming_idle_timeout_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "request_timeout_non_streaming_ms": { + "name": "request_timeout_non_streaming_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "website_url": { + "name": "website_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "favicon_url": { + "name": "favicon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cache_ttl_preference": { + "name": "cache_ttl_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "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": "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" + ] + }, + "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/0075_snapshot.json b/drizzle/meta/0075_snapshot.json new file mode 100644 index 000000000..72868bccc --- /dev/null +++ b/drizzle/meta/0075_snapshot.json @@ -0,0 +1,3819 @@ +{ + "id": "61c4da35-57cf-4629-88de-a1af77c8ae3b", + "prevId": "132bc4b6-86f3-43e1-b980-5ff62835bd1d", + "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 + }, + "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" + }, + "join_claude_pool": { + "name": "join_claude_pool", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "codex_instructions_strategy": { + "name": "codex_instructions_strategy", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "default": "'auto'" + }, + "mcp_passthrough_type": { + "name": "mcp_passthrough_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "mcp_passthrough_url": { + "name": "mcp_passthrough_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "limit_5h_usd": { + "name": "limit_5h_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_daily_usd": { + "name": "limit_daily_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "daily_reset_mode": { + "name": "daily_reset_mode", + "type": "daily_reset_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'fixed'" + }, + "daily_reset_time": { + "name": "daily_reset_time", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'00:00'" + }, + "limit_weekly_usd": { + "name": "limit_weekly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_monthly_usd": { + "name": "limit_monthly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_total_usd": { + "name": "limit_total_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "total_cost_reset_at": { + "name": "total_cost_reset_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "limit_concurrent_sessions": { + "name": "limit_concurrent_sessions", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "max_retry_attempts": { + "name": "max_retry_attempts", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "circuit_breaker_failure_threshold": { + "name": "circuit_breaker_failure_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5 + }, + "circuit_breaker_open_duration": { + "name": "circuit_breaker_open_duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1800000 + }, + "circuit_breaker_half_open_success_threshold": { + "name": "circuit_breaker_half_open_success_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 2 + }, + "proxy_url": { + "name": "proxy_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "proxy_fallback_to_direct": { + "name": "proxy_fallback_to_direct", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "first_byte_timeout_streaming_ms": { + "name": "first_byte_timeout_streaming_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "streaming_idle_timeout_ms": { + "name": "streaming_idle_timeout_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "request_timeout_non_streaming_ms": { + "name": "request_timeout_non_streaming_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "website_url": { + "name": "website_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "favicon_url": { + "name": "favicon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cache_ttl_preference": { + "name": "cache_ttl_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "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" + ] + }, + "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 4540079c8..e477b9578 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -519,6 +519,20 @@ "when": 1771527016184, "tag": "0073_magical_manta", "breakpoints": true + }, + { + "idx": 74, + "version": "7", + "when": 1771600203231, + "tag": "0074_wide_retro_girl", + "breakpoints": true + }, + { + "idx": 75, + "version": "7", + "when": 1771688588623, + "tag": "0075_faithful_speed_demon", + "breakpoints": true } ] } \ No newline at end of file diff --git a/messages/en/dashboard.json b/messages/en/dashboard.json index 1b21eca95..084d29071 100644 --- a/messages/en/dashboard.json +++ b/messages/en/dashboard.json @@ -1824,7 +1824,15 @@ "label": "Client Restrictions", "description": "Restrict which CLI/IDE clients can use this account. Empty = no restriction.", "customLabel": "Custom Client Pattern", - "customPlaceholder": "Enter pattern (e.g., 'xcode', 'my-ide')" + "customPlaceholder": "Enter pattern (e.g., 'xcode', 'my-ide')", + "customHelp": "Custom patterns match User-Agent by case-insensitive substring. '-' and '_' are treated as equivalent." + }, + "blockedClients": { + "label": "Blocked Clients", + "description": "Clients matching these patterns will be rejected, even if they match allowed clients.", + "customLabel": "Custom Block Pattern", + "customPlaceholder": "Enter pattern (e.g., 'xcode', 'my-ide')", + "customHelp": "Custom patterns match User-Agent by case-insensitive substring. '-' and '_' are treated as equivalent." }, "allowedModels": { "label": "Model Restrictions", @@ -1845,11 +1853,22 @@ "processing": "Processing..." } }, + "actions": { + "allow": "Allow", + "block": "Block" + }, "presetClients": { "claude-cli": "Claude Code CLI", "gemini-cli": "Gemini CLI", "factory-cli": "Droid CLI", - "codex-cli": "Codex CLI" + "codex-cli": "Codex CLI", + "claude-code": "Claude Code (all)", + "claude-code-cli": "Claude Code CLI (builtin)", + "claude-code-cli-sdk": "Claude Code CLI SDK", + "claude-code-vscode": "Claude Code VSCode", + "claude-code-sdk-ts": "Claude Code SDK (TypeScript)", + "claude-code-sdk-py": "Claude Code SDK (Python)", + "claude-code-gh-action": "Claude Code GitHub Action" } }, "keyEditSection": { diff --git a/messages/en/provider-chain.json b/messages/en/provider-chain.json index ba9d4c47f..67ba8dd64 100644 --- a/messages/en/provider-chain.json +++ b/messages/en/provider-chain.json @@ -55,7 +55,8 @@ "session_reuse": "Session Reuse", "initial_selection": "Initial Selection", "endpoint_pool_exhausted": "Endpoint Pool Exhausted", - "vendor_type_all_timeout": "Vendor-Type All Endpoints Timeout" + "vendor_type_all_timeout": "Vendor-Type All Endpoints Timeout", + "client_restriction_filtered": "Client Restricted" }, "filterReasons": { "rate_limited": "Rate Limited", @@ -70,13 +71,21 @@ "group_mismatch": "Group Mismatch", "health_check_failed": "Health Check Failed", "endpoint_circuit_open": "Endpoint Circuit Open", - "endpoint_disabled": "Endpoint Disabled" + "endpoint_disabled": "Endpoint Disabled", + "client_restriction": "Client Restriction" }, "filterDetails": { "vendor_type_circuit_open": "Vendor-type temporarily circuit-broken", "circuit_open": "Circuit breaker open", "circuit_half_open": "Circuit breaker half-open", - "rate_limited": "Rate limited" + "rate_limited": "Rate limited", + "provider_client_restriction": "Provider skipped due to client restriction", + "session_reuse_client_restriction": "Session reuse rejected: client restriction", + "blocklist_hit": "Blocked by pattern: {pattern}", + "allowlist_miss": "Not in allowed list", + "detectedClient": "Detected: {client}", + "providerAllowlist": "Allowlist: {list}", + "providerBlocklist": "Blocklist: {list}" }, "details": { "selectionMethod": "Selection", diff --git a/messages/en/settings/providers/form/sections.json b/messages/en/settings/providers/form/sections.json index 135d192bc..315d2b860 100644 --- a/messages/en/settings/providers/form/sections.json +++ b/messages/en/settings/providers/form/sections.json @@ -305,6 +305,25 @@ "selectedOnly": "Only the selected {count} models are allowed. Other models will not be routed to this provider.", "title": "Model Allowlist" }, + "clientRestrictions": { + "allowedLabel": "Allowed Clients", + "allowedPlaceholder": "e.g. claude-code-cli", + "blockedLabel": "Blocked Clients", + "blockedPlaceholder": "e.g. gemini-cli", + "allowAction": "Allow", + "blockAction": "Block", + "customAllowedLabel": "Custom Allowed Patterns", + "customAllowedPlaceholder": "e.g. my-ide, internal-tool", + "customBlockedLabel": "Custom Blocked Patterns", + "customBlockedPlaceholder": "e.g. legacy-client", + "customHelp": "Custom patterns use case-insensitive User-Agent contains matching. '-' and '_' are treated as equivalent.", + "presetClients": { + "claude-code": "Claude Code (all)", + "gemini-cli": "Gemini CLI", + "factory-cli": "Droid CLI", + "codex-cli": "Codex CLI" + } + }, "preserveClientIp": { "desc": "Pass x-forwarded-for / x-real-ip to upstream providers (may expose real client IP)", "help": "Keep off by default for privacy. Enable only when upstream must see the end-user IP.", diff --git a/messages/ja/dashboard.json b/messages/ja/dashboard.json index 795ecc363..d6776bb09 100644 --- a/messages/ja/dashboard.json +++ b/messages/ja/dashboard.json @@ -1760,7 +1760,15 @@ "label": "クライアント制限", "description": "このアカウントを使用できるCLI/IDEクライアントを制限します。空欄は制限なし。", "customLabel": "カスタムクライアントパターン", - "customPlaceholder": "パターンを入力(例:'xcode', 'my-ide')" + "customPlaceholder": "パターンを入力(例:'xcode', 'my-ide')", + "customHelp": "カスタムパターンは User-Agent の部分一致で判定されます(大文字小文字を区別しません)。'-' と '_' は同等として扱います。" + }, + "blockedClients": { + "label": "ブロックするクライアント", + "description": "これらのパターンに一致するクライアントは、許可リストに一致しても拒否されます。", + "customLabel": "カスタムブロックパターン", + "customPlaceholder": "パターンを入力(例: 'xcode'、'my-ide')", + "customHelp": "カスタムパターンは User-Agent の部分一致で判定されます(大文字小文字を区別しません)。'-' と '_' は同等として扱います。" }, "allowedModels": { "label": "モデル制限", @@ -1781,11 +1789,22 @@ "processing": "処理中..." } }, + "actions": { + "allow": "許可", + "block": "ブロック" + }, "presetClients": { "claude-cli": "Claude Code CLI", "gemini-cli": "Gemini CLI", "factory-cli": "Droid CLI", - "codex-cli": "Codex CLI" + "codex-cli": "Codex CLI", + "claude-code": "Claude Code (全て)", + "claude-code-cli": "Claude Code CLI (厳密検出)", + "claude-code-cli-sdk": "Claude Code CLI SDK", + "claude-code-vscode": "Claude Code VSCode", + "claude-code-sdk-ts": "Claude Code SDK (TypeScript)", + "claude-code-sdk-py": "Claude Code SDK (Python)", + "claude-code-gh-action": "Claude Code GitHub Action" } }, "keyEditSection": { diff --git a/messages/ja/provider-chain.json b/messages/ja/provider-chain.json index e8793c5fc..701a2a4b9 100644 --- a/messages/ja/provider-chain.json +++ b/messages/ja/provider-chain.json @@ -55,7 +55,8 @@ "session_reuse": "セッション再利用", "initial_selection": "初期選択", "endpoint_pool_exhausted": "エンドポイントプール枯渇", - "vendor_type_all_timeout": "ベンダータイプ全エンドポイントタイムアウト" + "vendor_type_all_timeout": "ベンダータイプ全エンドポイントタイムアウト", + "client_restriction_filtered": "クライアント制限" }, "filterReasons": { "rate_limited": "レート制限", @@ -70,13 +71,21 @@ "group_mismatch": "グループ不一致", "health_check_failed": "ヘルスチェック失敗", "endpoint_circuit_open": "エンドポイントサーキットオープン", - "endpoint_disabled": "エンドポイント無効" + "endpoint_disabled": "エンドポイント無効", + "client_restriction": "クライアント制限" }, "filterDetails": { "vendor_type_circuit_open": "ベンダータイプ一時サーキットブレイク", "circuit_open": "サーキットブレーカーオープン", "circuit_half_open": "サーキットブレーカーハーフオープン", - "rate_limited": "レート制限" + "rate_limited": "レート制限", + "provider_client_restriction": "クライアント制限によりプロバイダーをスキップ", + "session_reuse_client_restriction": "Session reuse rejected: client restriction", + "blocklist_hit": "Blocked by pattern: {pattern}", + "allowlist_miss": "Not in allowed list", + "detectedClient": "Detected: {client}", + "providerAllowlist": "Allowlist: {list}", + "providerBlocklist": "Blocklist: {list}" }, "details": { "selectionMethod": "選択方法", diff --git a/messages/ja/settings/providers/form/sections.json b/messages/ja/settings/providers/form/sections.json index 1356c87e4..d7d23a3c2 100644 --- a/messages/ja/settings/providers/form/sections.json +++ b/messages/ja/settings/providers/form/sections.json @@ -306,6 +306,25 @@ "selectedOnly": "選択した {count} 件のモデルのみ許可します。他のモデルはこのプロバイダーにルーティングされません。", "title": "モデル許可リスト" }, + "clientRestrictions": { + "allowedLabel": "許可クライアント", + "allowedPlaceholder": "例: claude-code-cli", + "blockedLabel": "ブロッククライアント", + "blockedPlaceholder": "例: gemini-cli", + "allowAction": "許可", + "blockAction": "ブロック", + "customAllowedLabel": "カスタム許可パターン", + "customAllowedPlaceholder": "例: my-ide、internal-tool", + "customBlockedLabel": "カスタムブロックパターン", + "customBlockedPlaceholder": "例: legacy-client", + "customHelp": "カスタムパターンは User-Agent の部分一致で判定されます(大文字小文字を区別しません)。'-' と '_' は同等として扱います。", + "presetClients": { + "claude-code": "Claude Code(すべて)", + "gemini-cli": "Gemini CLI", + "factory-cli": "Droid CLI", + "codex-cli": "Codex CLI" + } + }, "preserveClientIp": { "desc": "x-forwarded-for / x-real-ip を上流に渡します(実際の IP が露出する可能性)", "help": "プライバシー保護のためデフォルトはオフ。上流側で端末 IP が必要な場合のみ有効化してください。", diff --git a/messages/ru/dashboard.json b/messages/ru/dashboard.json index 7634ae3fa..c98716709 100644 --- a/messages/ru/dashboard.json +++ b/messages/ru/dashboard.json @@ -1808,7 +1808,15 @@ "label": "Ограничения клиентов", "description": "Ограничьте, какие CLI/IDE клиенты могут использовать эту учетную запись. Пусто = без ограничений.", "customLabel": "Пользовательские шаблоны клиентов", - "customPlaceholder": "Введите шаблон (например, 'xcode', 'my-ide')" + "customPlaceholder": "Введите шаблон (например, 'xcode', 'my-ide')", + "customHelp": "Пользовательские шаблоны проверяются по вхождению в User-Agent без учёта регистра. '-' и '_' считаются эквивалентными." + }, + "blockedClients": { + "label": "Заблокированные клиенты", + "description": "Клиенты, соответствующие этим шаблонам, будут отклонены, даже если они соответствуют разрешённым.", + "customLabel": "Пользовательский шаблон блокировки", + "customPlaceholder": "Введите шаблон (например, 'xcode', 'my-ide')", + "customHelp": "Пользовательские шаблоны проверяются по вхождению в User-Agent без учёта регистра. '-' и '_' считаются эквивалентными." }, "allowedModels": { "label": "Ограничения моделей", @@ -1829,11 +1837,22 @@ "processing": "Обработка..." } }, + "actions": { + "allow": "Разрешить", + "block": "Блокировать" + }, "presetClients": { "claude-cli": "Claude Code CLI", "gemini-cli": "Gemini CLI", "factory-cli": "Droid CLI", - "codex-cli": "Codex CLI" + "codex-cli": "Codex CLI", + "claude-code": "Claude Code (все)", + "claude-code-cli": "Claude Code CLI (точное обнаружение)", + "claude-code-cli-sdk": "Claude Code CLI SDK", + "claude-code-vscode": "Claude Code VSCode", + "claude-code-sdk-ts": "Claude Code SDK (TypeScript)", + "claude-code-sdk-py": "Claude Code SDK (Python)", + "claude-code-gh-action": "Claude Code GitHub Action" } }, "keyEditSection": { diff --git a/messages/ru/provider-chain.json b/messages/ru/provider-chain.json index 0e86f022f..ebe5d8629 100644 --- a/messages/ru/provider-chain.json +++ b/messages/ru/provider-chain.json @@ -55,7 +55,8 @@ "session_reuse": "Повторное использование сессии", "initial_selection": "Первоначальный выбор", "endpoint_pool_exhausted": "Пул конечных точек исчерпан", - "vendor_type_all_timeout": "Тайм-аут всех конечных точек типа поставщика" + "vendor_type_all_timeout": "Тайм-аут всех конечных точек типа поставщика", + "client_restriction_filtered": "Клиент ограничен" }, "filterReasons": { "rate_limited": "Ограничение скорости", @@ -70,13 +71,21 @@ "group_mismatch": "Несоответствие группы", "health_check_failed": "Проверка состояния не пройдена", "endpoint_circuit_open": "Автомат конечной точки открыт", - "endpoint_disabled": "Эндпоинт отключен" + "endpoint_disabled": "Эндпоинт отключен", + "client_restriction": "Ограничение клиента" }, "filterDetails": { "vendor_type_circuit_open": "Временное размыкание типа поставщика", "circuit_open": "Размыкатель открыт", "circuit_half_open": "Размыкатель полуоткрыт", - "rate_limited": "Ограничение скорости" + "rate_limited": "Ограничение скорости", + "provider_client_restriction": "Провайдер пропущен из-за ограничения клиента", + "session_reuse_client_restriction": "Session reuse rejected: client restriction", + "blocklist_hit": "Blocked by pattern: {pattern}", + "allowlist_miss": "Not in allowed list", + "detectedClient": "Detected: {client}", + "providerAllowlist": "Allowlist: {list}", + "providerBlocklist": "Blocklist: {list}" }, "details": { "selectionMethod": "Метод выбора", diff --git a/messages/ru/settings/providers/form/sections.json b/messages/ru/settings/providers/form/sections.json index b59ec1a4f..aac6a6fea 100644 --- a/messages/ru/settings/providers/form/sections.json +++ b/messages/ru/settings/providers/form/sections.json @@ -306,6 +306,25 @@ "selectedOnly": "Разрешены только выбранные {count} моделей. Другие модели не будут направляться к этому провайдеру.", "title": "Список разрешённых моделей" }, + "clientRestrictions": { + "allowedLabel": "Разрешённые клиенты", + "allowedPlaceholder": "напр. claude-code-cli", + "blockedLabel": "Заблокированные клиенты", + "blockedPlaceholder": "напр. gemini-cli", + "allowAction": "Разрешить", + "blockAction": "Блокировать", + "customAllowedLabel": "Пользовательские разрешённые шаблоны", + "customAllowedPlaceholder": "напр. my-ide, internal-tool", + "customBlockedLabel": "Пользовательские шаблоны блокировки", + "customBlockedPlaceholder": "напр. legacy-client", + "customHelp": "Пользовательские шаблоны проверяются по вхождению в User-Agent без учёта регистра. '-' и '_' считаются эквивалентными.", + "presetClients": { + "claude-code": "Claude Code (все)", + "gemini-cli": "Gemini CLI", + "factory-cli": "Droid CLI", + "codex-cli": "Codex CLI" + } + }, "preserveClientIp": { "desc": "Передавать x-forwarded-for / x-real-ip в апстрим (может раскрыть реальный IP клиента)", "help": "По умолчанию выключено для приватности. Включайте только если апстриму нужен IP пользователя.", diff --git a/messages/zh-CN/dashboard.json b/messages/zh-CN/dashboard.json index a641a3aca..740766f85 100644 --- a/messages/zh-CN/dashboard.json +++ b/messages/zh-CN/dashboard.json @@ -1783,7 +1783,15 @@ "label": "客户端限制", "description": "限制哪些 CLI/IDE 客户端可以使用此账户。留空表示无限制。", "customLabel": "自定义客户端模式", - "customPlaceholder": "输入自定义模式(如:'xcode', 'my-ide')" + "customPlaceholder": "输入自定义模式(如:'xcode', 'my-ide')", + "customHelp": "自定义模式按 User-Agent 包含匹配(不区分大小写),并将 '-' 与 '_' 视为等价。" + }, + "blockedClients": { + "label": "黑名单客户端", + "description": "匹配这些模式的客户端将被拒绝,即使它们也匹配白名单。", + "customLabel": "自定义黑名单模式", + "customPlaceholder": "输入模式(如 'xcode'、'my-ide')", + "customHelp": "自定义模式按 User-Agent 包含匹配(不区分大小写),并将 '-' 与 '_' 视为等价。" }, "allowedModels": { "label": "模型限制", @@ -1804,11 +1812,22 @@ "processing": "处理中..." } }, + "actions": { + "allow": "允许", + "block": "阻止" + }, "presetClients": { "claude-cli": "Claude Code CLI", "gemini-cli": "Gemini CLI", "factory-cli": "Droid CLI", - "codex-cli": "Codex CLI" + "codex-cli": "Codex CLI", + "claude-code": "Claude Code(全部)", + "claude-code-cli": "Claude Code CLI (精确检测)", + "claude-code-cli-sdk": "Claude Code CLI SDK", + "claude-code-vscode": "Claude Code VSCode", + "claude-code-sdk-ts": "Claude Code SDK(TypeScript)", + "claude-code-sdk-py": "Claude Code SDK(Python)", + "claude-code-gh-action": "Claude Code GitHub Action" } }, "keyEditSection": { diff --git a/messages/zh-CN/provider-chain.json b/messages/zh-CN/provider-chain.json index c1b287503..eecf293af 100644 --- a/messages/zh-CN/provider-chain.json +++ b/messages/zh-CN/provider-chain.json @@ -55,7 +55,8 @@ "session_reuse": "会话复用", "initial_selection": "首次选择", "endpoint_pool_exhausted": "端点池耗尽", - "vendor_type_all_timeout": "供应商类型全端点超时" + "vendor_type_all_timeout": "供应商类型全端点超时", + "client_restriction_filtered": "客户端受限" }, "filterReasons": { "rate_limited": "速率限制", @@ -70,13 +71,21 @@ "group_mismatch": "分组不匹配", "health_check_failed": "健康检查失败", "endpoint_circuit_open": "端点已熔断", - "endpoint_disabled": "端点已禁用" + "endpoint_disabled": "端点已禁用", + "client_restriction": "客户端限制" }, "filterDetails": { "vendor_type_circuit_open": "供应商类型临时熔断", "circuit_open": "熔断器打开", "circuit_half_open": "熔断器半开", - "rate_limited": "速率限制" + "rate_limited": "速率限制", + "provider_client_restriction": "供应商因客户端限制被跳过", + "session_reuse_client_restriction": "Session reuse rejected: client restriction", + "blocklist_hit": "Blocked by pattern: {pattern}", + "allowlist_miss": "Not in allowed list", + "detectedClient": "Detected: {client}", + "providerAllowlist": "Allowlist: {list}", + "providerBlocklist": "Blocklist: {list}" }, "details": { "selectionMethod": "选择方式", diff --git a/messages/zh-CN/settings/providers/form/sections.json b/messages/zh-CN/settings/providers/form/sections.json index 3e54ef4ab..59ecf6d46 100644 --- a/messages/zh-CN/settings/providers/form/sections.json +++ b/messages/zh-CN/settings/providers/form/sections.json @@ -49,6 +49,25 @@ "selectedOnly": "仅允许选中的 {count} 个模型。其他模型的请求不会调度到此供应商。", "moreModels": "+{count} 更多" }, + "clientRestrictions": { + "allowedLabel": "白名单客户端", + "allowedPlaceholder": "例如 claude-code-cli", + "blockedLabel": "黑名单客户端", + "blockedPlaceholder": "例如 gemini-cli", + "allowAction": "允许", + "blockAction": "阻止", + "customAllowedLabel": "自定义白名单模式", + "customAllowedPlaceholder": "例如 my-ide、internal-tool", + "customBlockedLabel": "自定义黑名单模式", + "customBlockedPlaceholder": "例如 legacy-client", + "customHelp": "自定义模式使用 User-Agent 包含匹配(不区分大小写),并将 '-' 与 '_' 视为等价。", + "presetClients": { + "claude-code": "Claude Code(全部)", + "gemini-cli": "Gemini CLI", + "factory-cli": "Droid CLI", + "codex-cli": "Codex CLI" + } + }, "scheduleParams": { "title": "调度参数", "priority": { diff --git a/messages/zh-TW/dashboard.json b/messages/zh-TW/dashboard.json index eabcac79d..96e47b627 100644 --- a/messages/zh-TW/dashboard.json +++ b/messages/zh-TW/dashboard.json @@ -1768,7 +1768,15 @@ "label": "用戶端限制", "description": "限制哪些 CLI/IDE 用戶端可以使用此帳戶。留空表示無限制。", "customLabel": "自訂用戶端模式", - "customPlaceholder": "輸入自訂模式(如:'xcode', 'my-ide')" + "customPlaceholder": "輸入自訂模式(如:'xcode', 'my-ide')", + "customHelp": "自訂模式會以 User-Agent 包含比對(不區分大小寫),並將 '-' 與 '_' 視為等價。" + }, + "blockedClients": { + "label": "黑名單客戶端", + "description": "符合這些模式的客戶端將被拒絕,即使它們也符合白名單。", + "customLabel": "自訂黑名單模式", + "customPlaceholder": "輸入模式(如 'xcode'、'my-ide')", + "customHelp": "自訂模式會以 User-Agent 包含比對(不區分大小寫),並將 '-' 與 '_' 視為等價。" }, "allowedModels": { "label": "Model 限制", @@ -1789,11 +1797,22 @@ "processing": "處理中..." } }, + "actions": { + "allow": "允許", + "block": "封鎖" + }, "presetClients": { "claude-cli": "Claude Code CLI", "gemini-cli": "Gemini CLI", "factory-cli": "Droid CLI", - "codex-cli": "Codex CLI" + "codex-cli": "Codex CLI", + "claude-code": "Claude Code(全部)", + "claude-code-cli": "Claude Code CLI (精確檢測)", + "claude-code-cli-sdk": "Claude Code CLI SDK", + "claude-code-vscode": "Claude Code VSCode", + "claude-code-sdk-ts": "Claude Code SDK(TypeScript)", + "claude-code-sdk-py": "Claude Code SDK(Python)", + "claude-code-gh-action": "Claude Code GitHub Action" } }, "keyEditSection": { diff --git a/messages/zh-TW/provider-chain.json b/messages/zh-TW/provider-chain.json index 847d0bbd5..9ce531b7e 100644 --- a/messages/zh-TW/provider-chain.json +++ b/messages/zh-TW/provider-chain.json @@ -55,7 +55,8 @@ "session_reuse": "會話複用", "initial_selection": "首次選擇", "endpoint_pool_exhausted": "端點池耗盡", - "vendor_type_all_timeout": "供應商類型全端點逾時" + "vendor_type_all_timeout": "供應商類型全端點逾時", + "client_restriction_filtered": "客戶端受限" }, "filterReasons": { "rate_limited": "速率限制", @@ -70,13 +71,21 @@ "group_mismatch": "分組不匹配", "health_check_failed": "健康檢查失敗", "endpoint_circuit_open": "端點已熔斷", - "endpoint_disabled": "端點已停用" + "endpoint_disabled": "端點已停用", + "client_restriction": "客戶端限制" }, "filterDetails": { "vendor_type_circuit_open": "供應商類型臨時熔斷", "circuit_open": "熔斷器打開", "circuit_half_open": "熔斷器半開", - "rate_limited": "速率限制" + "rate_limited": "速率限制", + "provider_client_restriction": "供應商因客戶端限制被跳過", + "session_reuse_client_restriction": "Session reuse rejected: client restriction", + "blocklist_hit": "Blocked by pattern: {pattern}", + "allowlist_miss": "Not in allowed list", + "detectedClient": "Detected: {client}", + "providerAllowlist": "Allowlist: {list}", + "providerBlocklist": "Blocklist: {list}" }, "details": { "selectionMethod": "選擇方式", diff --git a/messages/zh-TW/settings/providers/form/sections.json b/messages/zh-TW/settings/providers/form/sections.json index c007c3941..70cd5707e 100644 --- a/messages/zh-TW/settings/providers/form/sections.json +++ b/messages/zh-TW/settings/providers/form/sections.json @@ -306,6 +306,25 @@ "selectedOnly": "僅允許所選的 {count} 個模型。其他模型將不會被路由到此供應商。", "title": "模型允許清單" }, + "clientRestrictions": { + "allowedLabel": "白名單客戶端", + "allowedPlaceholder": "例如 claude-code-cli", + "blockedLabel": "黑名單客戶端", + "blockedPlaceholder": "例如 gemini-cli", + "allowAction": "允許", + "blockAction": "封鎖", + "customAllowedLabel": "自訂白名單模式", + "customAllowedPlaceholder": "例如 my-ide、internal-tool", + "customBlockedLabel": "自訂黑名單模式", + "customBlockedPlaceholder": "例如 legacy-client", + "customHelp": "自訂模式使用 User-Agent 包含比對(不區分大小寫),並將 '-' 與 '_' 視為等價。", + "presetClients": { + "claude-code": "Claude Code(全部)", + "gemini-cli": "Gemini CLI", + "factory-cli": "Droid CLI", + "codex-cli": "Codex CLI" + } + }, "preserveClientIp": { "desc": "向上游轉發 x-forwarded-for / x-real-ip,可能暴露真實來源 IP", "help": "預設關閉以保護隱私;僅在需要上游感知終端 IP 時開啟。", diff --git a/src/actions/providers.ts b/src/actions/providers.ts index 181093158..a18e93980 100644 --- a/src/actions/providers.ts +++ b/src/actions/providers.ts @@ -273,6 +273,8 @@ export async function getProviders(): Promise { preserveClientIp: provider.preserveClientIp, modelRedirects: provider.modelRedirects, allowedModels: provider.allowedModels, + allowedClients: provider.allowedClients, + blockedClients: provider.blockedClients, mcpPassthroughType: provider.mcpPassthroughType, mcpPassthroughUrl: provider.mcpPassthroughUrl, limit5hUsd: provider.limit5hUsd, @@ -477,6 +479,8 @@ export async function addProvider(data: { preserve_client_ip?: boolean; model_redirects?: Record | null; allowed_models?: string[] | null; + allowed_clients?: string[] | null; + blocked_clients?: string[] | null; limit_5h_usd?: number | null; limit_daily_usd?: number | null; daily_reset_mode?: "fixed" | "rolling"; @@ -648,6 +652,8 @@ export async function editProvider( preserve_client_ip?: boolean; model_redirects?: Record | null; allowed_models?: string[] | null; + allowed_clients?: string[] | null; + blocked_clients?: string[] | null; limit_5h_usd?: number | null; limit_daily_usd?: number | null; daily_reset_time?: string; @@ -1399,6 +1405,12 @@ function mapApplyUpdatesToRepositoryFormat( if (applyUpdates.allowed_models !== undefined) { result.allowedModels = applyUpdates.allowed_models; } + if (applyUpdates.allowed_clients !== undefined) { + result.allowedClients = applyUpdates.allowed_clients ?? []; + } + if (applyUpdates.blocked_clients !== undefined) { + result.blockedClients = applyUpdates.blocked_clients ?? []; + } if (applyUpdates.anthropic_thinking_budget_preference !== undefined) { result.anthropicThinkingBudgetPreference = applyUpdates.anthropic_thinking_budget_preference; } @@ -1512,6 +1524,8 @@ const PATCH_FIELD_TO_PROVIDER_KEY: Record> = { + allowed_clients: [], + blocked_clients: [], anthropic_thinking_budget_preference: "inherit", cache_ttl_preference: "inherit", context_1m_preference: "inherit", @@ -2031,6 +2047,8 @@ export interface BatchUpdateProvidersParams { group_tag?: string | null; model_redirects?: Record | null; allowed_models?: string[] | null; + allowed_clients?: string[]; + blocked_clients?: string[]; anthropic_thinking_budget_preference?: AnthropicThinkingBudgetPreference | null; anthropic_adaptive_thinking?: AnthropicAdaptiveThinkingConfig | null; }; @@ -2079,6 +2097,12 @@ export async function batchUpdateProviders( ? null : updates.allowed_models; } + if (updates.allowed_clients !== undefined) { + repositoryUpdates.allowedClients = updates.allowed_clients; + } + if (updates.blocked_clients !== undefined) { + repositoryUpdates.blockedClients = updates.blocked_clients; + } if (updates.anthropic_thinking_budget_preference !== undefined) { repositoryUpdates.anthropicThinkingBudgetPreference = updates.anthropic_thinking_budget_preference; diff --git a/src/actions/users.ts b/src/actions/users.ts index 02eadd0bc..d12e0262d 100644 --- a/src/actions/users.ts +++ b/src/actions/users.ts @@ -254,6 +254,7 @@ export async function getUsers(): Promise { isEnabled: user.isEnabled, expiresAt: user.expiresAt ?? null, allowedClients: user.allowedClients || [], + blockedClients: user.blockedClients || [], allowedModels: user.allowedModels ?? [], keys: keys.map((key) => { const stats = statisticsLookup.get(key.id); @@ -320,6 +321,7 @@ export async function getUsers(): Promise { isEnabled: user.isEnabled, expiresAt: user.expiresAt ?? null, allowedClients: user.allowedClients || [], + blockedClients: user.blockedClients || [], allowedModels: user.allowedModels ?? [], keys: [], }; @@ -523,6 +525,7 @@ export async function getUsersBatch( isEnabled: user.isEnabled, expiresAt: user.expiresAt ?? null, allowedClients: user.allowedClients || [], + blockedClients: user.blockedClients || [], allowedModels: user.allowedModels ?? [], keys: keys.map((key) => { const stats = statisticsLookup.get(key.id); @@ -585,6 +588,7 @@ export async function getUsersBatch( isEnabled: user.isEnabled, expiresAt: user.expiresAt ?? null, allowedClients: user.allowedClients || [], + blockedClients: user.blockedClients || [], allowedModels: user.allowedModels ?? [], keys: [], }; @@ -750,6 +754,7 @@ export async function addUser(data: { isEnabled?: boolean; expiresAt?: Date | null; allowedClients?: string[]; + blockedClients?: string[]; allowedModels?: string[]; }): Promise< ActionResult<{ @@ -810,6 +815,7 @@ export async function addUser(data: { isEnabled: data.isEnabled, expiresAt: data.expiresAt, allowedClients: data.allowedClients || [], + blockedClients: data.blockedClients || [], allowedModels: data.allowedModels || [], }); @@ -869,6 +875,7 @@ export async function addUser(data: { isEnabled: validatedData.isEnabled, expiresAt: validatedData.expiresAt ?? null, allowedClients: validatedData.allowedClients ?? [], + blockedClients: validatedData.blockedClients ?? [], allowedModels: validatedData.allowedModels ?? [], }); @@ -942,6 +949,7 @@ export async function createUserOnly(data: { isEnabled?: boolean; expiresAt?: Date | null; allowedClients?: string[]; + blockedClients?: string[]; allowedModels?: string[]; }): Promise< ActionResult<{ @@ -995,6 +1003,7 @@ export async function createUserOnly(data: { isEnabled: data.isEnabled, expiresAt: data.expiresAt, allowedClients: data.allowedClients || [], + blockedClients: data.blockedClients || [], allowedModels: data.allowedModels || [], }); @@ -1053,6 +1062,7 @@ export async function createUserOnly(data: { isEnabled: validatedData.isEnabled, expiresAt: validatedData.expiresAt ?? null, allowedClients: validatedData.allowedClients ?? [], + blockedClients: validatedData.blockedClients ?? [], allowedModels: validatedData.allowedModels ?? [], }); @@ -1111,6 +1121,7 @@ export async function editUser( isEnabled?: boolean; expiresAt?: Date | null; allowedClients?: string[]; + blockedClients?: string[]; allowedModels?: string[]; } ): Promise { @@ -1211,6 +1222,7 @@ export async function editUser( isEnabled: validatedData.isEnabled, expiresAt: validatedData.expiresAt, allowedClients: validatedData.allowedClients, + blockedClients: validatedData.blockedClients, allowedModels: validatedData.allowedModels, }); diff --git a/src/app/[locale]/dashboard/_components/user/create-user-dialog.tsx b/src/app/[locale]/dashboard/_components/user/create-user-dialog.tsx index d83bca1de..32e8f73fe 100644 --- a/src/app/[locale]/dashboard/_components/user/create-user-dialog.tsx +++ b/src/app/[locale]/dashboard/_components/user/create-user-dialog.tsx @@ -42,6 +42,7 @@ const CreateUserSchema = UpdateUserSchema.extend({ name: z.string().min(1).max(64), providerGroup: z.string().max(200).nullable().optional(), allowedClients: z.array(z.string().max(64)).max(50).optional().default([]), + blockedClients: z.array(z.string().max(64)).max(50).optional().default([]), allowedModels: z.array(z.string().max(64)).max(50).optional().default([]), dailyQuota: z.number().nullable().optional(), }); @@ -89,6 +90,7 @@ function buildDefaultValues(): CreateFormValues { dailyResetMode: "fixed", dailyResetTime: "00:00", allowedClients: [], + blockedClients: [], allowedModels: [], }, key: { @@ -155,6 +157,7 @@ function CreateUserDialogInner({ onOpenChange, onSuccess }: CreateUserDialogProp dailyResetMode: data.user.dailyResetMode, dailyResetTime: data.user.dailyResetTime, allowedClients: data.user.allowedClients, + blockedClients: data.user.blockedClients, allowedModels: data.user.allowedModels, }); if (!userRes.ok) { @@ -363,6 +366,7 @@ function CreateUserDialogInner({ onOpenChange, onSuccess }: CreateUserDialogProp dailyResetMode: currentUserDraft.dailyResetMode ?? "fixed", dailyResetTime: currentUserDraft.dailyResetTime ?? "00:00", allowedClients: currentUserDraft.allowedClients || [], + blockedClients: currentUserDraft.blockedClients || [], allowedModels: currentUserDraft.allowedModels || [], }} isEnabled={true} diff --git a/src/app/[locale]/dashboard/_components/user/edit-user-dialog.tsx b/src/app/[locale]/dashboard/_components/user/edit-user-dialog.tsx index 5c963c72c..2a73c239b 100644 --- a/src/app/[locale]/dashboard/_components/user/edit-user-dialog.tsx +++ b/src/app/[locale]/dashboard/_components/user/edit-user-dialog.tsx @@ -50,6 +50,7 @@ const EditUserSchema = UpdateUserSchema.extend({ name: z.string().min(1).max(64), providerGroup: z.string().max(200).nullable().optional(), allowedClients: z.array(z.string().max(64)).max(50).optional().default([]), + blockedClients: z.array(z.string().max(64)).max(50).optional().default([]), allowedModels: z.array(z.string().max(64)).max(50).optional().default([]), dailyQuota: z.number().nullable().optional(), }); @@ -73,6 +74,7 @@ function buildDefaultValues(user: UserDisplay): EditUserValues { dailyResetMode: user.dailyResetMode ?? "fixed", dailyResetTime: user.dailyResetTime ?? "00:00", allowedClients: user.allowedClients || [], + blockedClients: user.blockedClients || [], allowedModels: user.allowedModels || [], }; } @@ -113,6 +115,7 @@ function EditUserDialogInner({ onOpenChange, user, onSuccess }: EditUserDialogPr dailyResetMode: data.dailyResetMode, dailyResetTime: data.dailyResetTime, allowedClients: data.allowedClients, + blockedClients: data.blockedClients, allowedModels: data.allowedModels, }); if (!userRes.ok) { @@ -270,6 +273,7 @@ function EditUserDialogInner({ onOpenChange, user, onSuccess }: EditUserDialogPr dailyResetMode: currentUserDraft.dailyResetMode ?? "fixed", dailyResetTime: currentUserDraft.dailyResetTime ?? "00:00", allowedClients: currentUserDraft.allowedClients || [], + blockedClients: currentUserDraft.blockedClients || [], allowedModels: currentUserDraft.allowedModels || [], }} isEnabled={user.isEnabled} diff --git a/src/app/[locale]/dashboard/_components/user/forms/access-restrictions-section.tsx b/src/app/[locale]/dashboard/_components/user/forms/access-restrictions-section.tsx index 26884c858..4b771001b 100644 --- a/src/app/[locale]/dashboard/_components/user/forms/access-restrictions-section.tsx +++ b/src/app/[locale]/dashboard/_components/user/forms/access-restrictions-section.tsx @@ -5,24 +5,24 @@ import { useCallback, useMemo } from "react"; import { ArrayTagInputField } from "@/components/form/form-field"; import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; +import { + CLIENT_RESTRICTION_PRESET_OPTIONS, + isPresetSelected, + mergePresetAndCustomClients, + removePresetValues, + splitPresetAndCustomClients, + togglePresetSelection, +} from "@/lib/client-restrictions/client-presets"; -// Preset client patterns -const PRESET_CLIENTS = [ - { value: "claude-cli", label: "Claude Code CLI" }, - { value: "gemini-cli", label: "Gemini CLI" }, - { value: "factory-cli", label: "Droid CLI" }, - { value: "codex-cli", label: "Codex CLI" }, -]; - -// Model name validation pattern: allows alphanumeric, dots, colons, slashes, underscores, hyphens -// Examples: gemini-1.5-pro, gpt-4.1, claude-3-opus-20240229, o1-mini +// Model name validation pattern const MODEL_NAME_PATTERN = /^[a-zA-Z0-9._:/-]+$/; export interface AccessRestrictionsSectionProps { allowedClients: string[]; + blockedClients: string[]; allowedModels: string[]; modelSuggestions: string[]; - onChange: (field: "allowedClients" | "allowedModels", value: string[]) => void; + onChange: (field: "allowedClients" | "blockedClients" | "allowedModels", value: string[]) => void; translations: { sections: { accessRestrictions: string; @@ -33,6 +33,14 @@ export interface AccessRestrictionsSectionProps { description: string; customLabel: string; customPlaceholder: string; + customHelp: string; + }; + blockedClients: { + label: string; + description: string; + customLabel: string; + customPlaceholder: string; + customHelp: string; }; allowedModels: { label: string; @@ -40,102 +48,149 @@ export interface AccessRestrictionsSectionProps { description: string; }; }; + actions: { + allow: string; + block: string; + }; presetClients: Record; }; } export function AccessRestrictionsSection({ allowedClients, + blockedClients, allowedModels, modelSuggestions, onChange, translations, }: AccessRestrictionsSectionProps) { - // Separate preset clients from custom clients - const { presetSelected, customClients } = useMemo(() => { - const presetValues = PRESET_CLIENTS.map((p) => p.value); - const preset = (allowedClients || []).filter((c) => presetValues.includes(c)); - const custom = (allowedClients || []).filter((c) => !presetValues.includes(c)); - return { presetSelected: preset, customClients: custom }; - }, [allowedClients]); - - const handlePresetChange = (clientValue: string, checked: boolean) => { - const currentClients = allowedClients || []; + const allowed = allowedClients || []; + const blocked = blockedClients || []; + + const { customValues: customAllowed } = useMemo( + () => splitPresetAndCustomClients(allowed), + [allowed] + ); + + const { customValues: customBlocked } = useMemo( + () => splitPresetAndCustomClients(blocked), + [blocked] + ); + + const handleAllowToggle = (presetValue: string, checked: boolean) => { + onChange("allowedClients", togglePresetSelection(allowed, presetValue, checked)); + if (checked) { + onChange("blockedClients", removePresetValues(blocked, presetValue)); + } + }; + + const handleBlockToggle = (presetValue: string, checked: boolean) => { + onChange("blockedClients", togglePresetSelection(blocked, presetValue, checked)); if (checked) { - onChange("allowedClients", [...currentClients, clientValue]); - } else { - onChange( - "allowedClients", - currentClients.filter((c) => c !== clientValue) - ); + onChange("allowedClients", removePresetValues(allowed, presetValue)); } }; - const handleCustomClientsChange = (newCustomClients: string[]) => { - // Merge preset clients with new custom clients - onChange("allowedClients", [...presetSelected, ...newCustomClients]); + const handleCustomAllowedChange = (newCustom: string[]) => { + onChange("allowedClients", mergePresetAndCustomClients(allowed, newCustom)); + }; + + const handleCustomBlockedChange = (newCustom: string[]) => { + onChange("blockedClients", mergePresetAndCustomClients(blocked, newCustom)); }; - // Custom validation for model names (allows dots, colons, slashes) const validateModelTag = useCallback( (tag: string): boolean => { if (!tag || tag.trim().length === 0) return false; if (tag.length > 64) return false; if (!MODEL_NAME_PATTERN.test(tag)) return false; - if (allowedModels.includes(tag)) return false; // duplicate check - if (allowedModels.length >= 50) return false; // max tags check + if (allowedModels.includes(tag)) return false; + if (allowedModels.length >= 50) return false; return true; }, [allowedModels] ); + const renderPresetRow = (value: string) => { + const isAllowed = isPresetSelected(allowed, value); + const isBlocked = isPresetSelected(blocked, value); + const displayLabel = translations.presetClients[value] ?? value; + + return ( +
+ {displayLabel} +
+
+ handleAllowToggle(value, checked === true)} + /> + +
+
+ handleBlockToggle(value, checked === true)} + /> + +
+
+
+ ); + }; + return ( -
-
-
))} diff --git a/src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx b/src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx index 59f5c9642..585b7836b 100644 --- a/src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx +++ b/src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx @@ -6,6 +6,7 @@ import { ChevronRight, InfoIcon, Link2, + MinusCircle, RefreshCw, XCircle, Zap, @@ -33,6 +34,8 @@ interface ProviderChainPopoverProps { * Determine if this is an actual request record (excluding intermediate states) */ function isActualRequest(item: ProviderChainItem): boolean { + if (item.reason === "client_restriction_filtered") return false; + if (item.reason === "concurrent_limit_failed") return true; if (item.reason === "retry_failed" || item.reason === "system_error") return true; @@ -101,6 +104,13 @@ function getItemStatus(item: ProviderChainItem): { bgColor: "bg-orange-50 dark:bg-orange-950/30", }; } + if (item.reason === "client_restriction_filtered") { + return { + icon: MinusCircle, + color: "text-muted-foreground", + bgColor: "bg-muted/30", + }; + } return { icon: RefreshCw, color: "text-slate-500", diff --git a/src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx b/src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx index 907ba3ff4..33430a398 100644 --- a/src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx +++ b/src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx @@ -318,6 +318,8 @@ function ProviderFormContent({ model_redirects: state.routing.modelRedirects, allowed_models: state.routing.allowedModels.length > 0 ? state.routing.allowedModels : null, + allowed_clients: state.routing.allowedClients, + blocked_clients: state.routing.blockedClients, priority: state.routing.priority, group_priorities: Object.keys(state.routing.groupPriorities).length > 0 diff --git a/src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx b/src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx index a8f79bc67..4b2272168 100644 --- a/src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx +++ b/src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx @@ -28,6 +28,8 @@ const ACTION_TO_FIELD_PATH: Partial> SET_PRESERVE_CLIENT_IP: "routing.preserveClientIp", SET_MODEL_REDIRECTS: "routing.modelRedirects", SET_ALLOWED_MODELS: "routing.allowedModels", + SET_ALLOWED_CLIENTS: "routing.allowedClients", + SET_BLOCKED_CLIENTS: "routing.blockedClients", SET_GROUP_PRIORITIES: "routing.groupPriorities", SET_CACHE_TTL_PREFERENCE: "routing.cacheTtlPreference", SET_SWAP_CACHE_TTL_BILLING: "routing.swapCacheTtlBilling", @@ -91,6 +93,8 @@ export function createInitialState( preserveClientIp: false, modelRedirects: {}, allowedModels: [], + allowedClients: [], + blockedClients: [], priority: 0, groupPriorities: {}, weight: 1, @@ -165,6 +169,8 @@ export function createInitialState( preserveClientIp: sourceProvider?.preserveClientIp ?? false, modelRedirects: sourceProvider?.modelRedirects ?? {}, allowedModels: sourceProvider?.allowedModels ?? [], + allowedClients: sourceProvider?.allowedClients ?? [], + blockedClients: sourceProvider?.blockedClients ?? [], priority: sourceProvider?.priority ?? 0, groupPriorities: sourceProvider?.groupPriorities ?? {}, weight: sourceProvider?.weight ?? 1, @@ -262,6 +268,10 @@ export function providerFormReducer( return { ...state, routing: { ...state.routing, modelRedirects: action.payload } }; case "SET_ALLOWED_MODELS": return { ...state, routing: { ...state.routing, allowedModels: action.payload } }; + case "SET_ALLOWED_CLIENTS": + return { ...state, routing: { ...state.routing, allowedClients: action.payload } }; + case "SET_BLOCKED_CLIENTS": + return { ...state, routing: { ...state.routing, blockedClients: action.payload } }; case "SET_PRIORITY": return { ...state, routing: { ...state.routing, priority: action.payload } }; case "SET_GROUP_PRIORITIES": 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 4bec44463..cd7d3dfcc 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 @@ -42,6 +42,8 @@ export interface RoutingState { preserveClientIp: boolean; modelRedirects: Record; allowedModels: string[]; + allowedClients: string[]; + blockedClients: string[]; priority: number; groupPriorities: Record; weight: number; @@ -128,6 +130,8 @@ export type ProviderFormAction = | { type: "SET_PRESERVE_CLIENT_IP"; payload: boolean } | { type: "SET_MODEL_REDIRECTS"; payload: Record } | { type: "SET_ALLOWED_MODELS"; payload: string[] } + | { type: "SET_ALLOWED_CLIENTS"; payload: string[] } + | { type: "SET_BLOCKED_CLIENTS"; payload: string[] } | { type: "SET_PRIORITY"; payload: number } | { type: "SET_GROUP_PRIORITIES"; payload: Record } | { type: "SET_WEIGHT"; payload: number } 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 59c0f7c4a..6b5458774 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 @@ -5,7 +5,9 @@ import { Info, Layers, Route, Scale, Settings, Timer } from "lucide-react"; import { useTranslations } from "next-intl"; import { toast } from "sonner"; import { Badge } from "@/components/ui/badge"; +import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; import { Select, SelectContent, @@ -16,6 +18,14 @@ import { import { Switch } from "@/components/ui/switch"; import { TagInput } from "@/components/ui/tag-input"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { + CLIENT_RESTRICTION_PRESET_OPTIONS, + isPresetSelected, + mergePresetAndCustomClients, + removePresetValues, + splitPresetAndCustomClients, + togglePresetSelection, +} from "@/lib/client-restrictions/client-presets"; import { getProviderTypeConfig } from "@/lib/provider-type-utils"; import type { CodexParallelToolCallsPreference, @@ -69,6 +79,44 @@ export function RoutingSection() { }; const providerTypes: ProviderType[] = ["claude", "codex", "gemini", "openai-compatible"]; + const allowedClients = state.routing.allowedClients; + const blockedClients = state.routing.blockedClients; + const { customValues: customAllowedClients } = splitPresetAndCustomClients(allowedClients); + const { customValues: customBlockedClients } = splitPresetAndCustomClients(blockedClients); + + const handleAllowToggle = (presetValue: string, checked: boolean) => { + const nextAllowed = togglePresetSelection(allowedClients, presetValue, checked); + dispatch({ type: "SET_ALLOWED_CLIENTS", payload: nextAllowed }); + + if (checked) { + const nextBlocked = removePresetValues(blockedClients, presetValue); + dispatch({ type: "SET_BLOCKED_CLIENTS", payload: nextBlocked }); + } + }; + + const handleBlockToggle = (presetValue: string, checked: boolean) => { + const nextBlocked = togglePresetSelection(blockedClients, presetValue, checked); + dispatch({ type: "SET_BLOCKED_CLIENTS", payload: nextBlocked }); + + if (checked) { + const nextAllowed = removePresetValues(allowedClients, presetValue); + dispatch({ type: "SET_ALLOWED_CLIENTS", payload: nextAllowed }); + } + }; + + const handleCustomAllowedChange = (customValues: string[]) => { + dispatch({ + type: "SET_ALLOWED_CLIENTS", + payload: mergePresetAndCustomClients(allowedClients, customValues), + }); + }; + + const handleCustomBlockedChange = (customValues: string[]) => { + dispatch({ + type: "SET_BLOCKED_CLIENTS", + payload: mergePresetAndCustomClients(blockedClients, customValues), + }); + }; return ( @@ -219,6 +267,85 @@ export function RoutingSection() {

+ + {/* Client Restrictions */} + +
+ {CLIENT_RESTRICTION_PRESET_OPTIONS.map((option) => { + const isAllowed = isPresetSelected(allowedClients, option.value); + const isBlocked = isPresetSelected(blockedClients, option.value); + return ( +
+ + {t(`sections.routing.clientRestrictions.presetClients.${option.value}`)} + +
+
+ + handleAllowToggle(option.value, checked === true) + } + /> + +
+
+ + handleBlockToggle(option.value, checked === true) + } + /> + +
+
+
+ ); + })} +
+
+ + + +

+ {t("sections.routing.clientRestrictions.customHelp")} +

+
+ + + +

+ {t("sections.routing.clientRestrictions.customHelp")} +

+
{/* Scheduling Parameters */} diff --git a/src/app/v1/_lib/proxy/client-detector.ts b/src/app/v1/_lib/proxy/client-detector.ts new file mode 100644 index 000000000..b11889fa0 --- /dev/null +++ b/src/app/v1/_lib/proxy/client-detector.ts @@ -0,0 +1,236 @@ +import type { ProxySession } from "./session"; + +export const CLAUDE_CODE_KEYWORD_PREFIX = "claude-code"; + +export const BUILTIN_CLIENT_KEYWORDS = new Set([ + "claude-code", + "claude-code-cli", + "claude-code-cli-sdk", + "claude-code-vscode", + "claude-code-sdk-ts", + "claude-code-sdk-py", + "claude-code-gh-action", +]); + +export interface ClientDetectionResult { + matched: boolean; + hubConfirmed: boolean; + subClient: string | null; + signals: string[]; + supplementary: string[]; +} + +export interface ClientRestrictionResult { + allowed: boolean; + matchType: "no_restriction" | "allowed" | "blocklist_hit" | "allowlist_miss"; + matchedPattern?: string; + detectedClient?: string; + checkedAllowlist: string[]; + checkedBlocklist: string[]; +} + +const normalize = (s: string) => s.toLowerCase().replace(/[-_]/g, ""); + +const ENTRYPOINT_MAP: Record = { + cli: "claude-code-cli", + "sdk-cli": "claude-code-cli-sdk", + "claude-vscode": "claude-code-vscode", + "sdk-ts": "claude-code-sdk-ts", + "sdk-py": "claude-code-sdk-py", + "claude-code-github-action": "claude-code-gh-action", +}; + +function confirmClaudeCodeSignals(session: ProxySession): { + confirmed: boolean; + signals: string[]; + supplementary: string[]; +} { + const signals: string[] = []; + const supplementary: string[] = []; + + if (session.headers.get("x-app") === "cli") { + signals.push("x-app-cli"); + } + + if (/^claude-cli\//i.test(session.userAgent ?? "")) { + signals.push("ua-prefix"); + } + + const betas = session.request.message["betas"]; + if ( + Array.isArray(betas) && + betas.some((beta) => typeof beta === "string" && /^claude-code-/i.test(beta)) + ) { + signals.push("betas-claude-code"); + } + + if (session.headers.get("anthropic-dangerous-direct-browser-access") === "true") { + supplementary.push("dangerous-browser-access"); + } + + return { + confirmed: signals.length === 3, + signals, + supplementary, + }; +} + +function extractSubClient(ua: string): string | null { + const match = /^claude-cli\/\S+\s+\(external,\s*([^,)]+)/i.exec(ua); + if (!match?.[1]) { + return null; + } + + const entrypoint = match[1].trim(); + return ENTRYPOINT_MAP[entrypoint] ?? null; +} + +export function isBuiltinKeyword(pattern: string): boolean { + return BUILTIN_CLIENT_KEYWORDS.has(pattern); +} + +export function matchClientPattern(session: ProxySession, pattern: string): boolean { + if (!isBuiltinKeyword(pattern)) { + const ua = session.userAgent?.trim(); + if (!ua) { + return false; + } + + const normalizedPattern = normalize(pattern); + if (normalizedPattern === "") { + return false; + } + + return normalize(ua).includes(normalizedPattern); + } + + const claudeCode = confirmClaudeCodeSignals(session); + if (!claudeCode.confirmed) { + return false; + } + + if (pattern === CLAUDE_CODE_KEYWORD_PREFIX) { + return true; + } + + const subClient = extractSubClient(session.userAgent ?? ""); + return subClient === pattern; +} + +export function detectClientFull(session: ProxySession, pattern: string): ClientDetectionResult { + const claudeCode = confirmClaudeCodeSignals(session); + const subClient = claudeCode.confirmed ? extractSubClient(session.userAgent ?? "") : null; + + let matched = false; + if (isBuiltinKeyword(pattern)) { + if (claudeCode.confirmed) { + matched = + pattern === CLAUDE_CODE_KEYWORD_PREFIX || (subClient !== null && subClient === pattern); + } + } else { + const ua = session.userAgent?.trim(); + if (ua) { + const normalizedPattern = normalize(pattern); + if (normalizedPattern !== "") { + matched = normalize(ua).includes(normalizedPattern); + } + } + } + + return { + matched, + hubConfirmed: claudeCode.confirmed, + subClient, + signals: claudeCode.signals, + supplementary: claudeCode.supplementary, + }; +} + +export function isClientAllowed( + session: ProxySession, + allowedClients: string[], + blockedClients?: string[] +): boolean { + return isClientAllowedDetailed(session, allowedClients, blockedClients).allowed; +} + +export function isClientAllowedDetailed( + session: ProxySession, + allowedClients: string[], + blockedClients?: string[] +): ClientRestrictionResult { + const checkedAllowlist = allowedClients; + const checkedBlocklist = blockedClients ?? []; + + const hasBlockList = checkedBlocklist.length > 0; + if (!hasBlockList && allowedClients.length === 0) { + return { + allowed: true, + matchType: "no_restriction", + checkedAllowlist, + checkedBlocklist, + }; + } + + // Pre-compute once to avoid repeated signal checks per pattern + const claudeCode = confirmClaudeCodeSignals(session); + const ua = session.userAgent?.trim() ?? ""; + const normalizedUa = normalize(ua); + const subClient = claudeCode.confirmed ? extractSubClient(ua) : null; + const detectedClient = subClient || ua || undefined; + + const matches = (pattern: string): boolean => { + if (!isBuiltinKeyword(pattern)) { + if (!ua) return false; + const normalizedPattern = normalize(pattern); + return normalizedPattern !== "" && normalizedUa.includes(normalizedPattern); + } + if (!claudeCode.confirmed) return false; + if (pattern === CLAUDE_CODE_KEYWORD_PREFIX) return true; + return subClient === pattern; + }; + + if (checkedBlocklist.length > 0) { + const blockedPattern = checkedBlocklist.find(matches); + if (blockedPattern) { + return { + allowed: false, + matchType: "blocklist_hit", + matchedPattern: blockedPattern, + detectedClient, + checkedAllowlist, + checkedBlocklist, + }; + } + } + + if (allowedClients.length === 0) { + return { + allowed: true, + matchType: "allowed", + detectedClient, + checkedAllowlist, + checkedBlocklist, + }; + } + + const allowedPattern = allowedClients.find(matches); + if (allowedPattern) { + return { + allowed: true, + matchType: "allowed", + matchedPattern: allowedPattern, + detectedClient, + checkedAllowlist, + checkedBlocklist, + }; + } + + return { + allowed: false, + matchType: "allowlist_miss", + detectedClient, + checkedAllowlist, + checkedBlocklist, + }; +} diff --git a/src/app/v1/_lib/proxy/client-guard.ts b/src/app/v1/_lib/proxy/client-guard.ts index f227132e6..7ffadb63a 100644 --- a/src/app/v1/_lib/proxy/client-guard.ts +++ b/src/app/v1/_lib/proxy/client-guard.ts @@ -1,21 +1,7 @@ +import { isClientAllowedDetailed } from "./client-detector"; import { ProxyResponses } from "./responses"; import type { ProxySession } from "./session"; -/** - * Client (CLI/IDE) restriction guard - * - * Validates that the client making the request is allowed based on User-Agent header matching. - * This check is ONLY performed when the user has configured client restrictions (allowedClients). - * - * Logic: - * - If allowedClients is empty or undefined: skip all checks, allow request - * - If allowedClients is non-empty: - * - Missing or empty User-Agent → 400 error - * - User-Agent doesn't match any allowed pattern → 400 error - * - User-Agent matches at least one pattern → allow request - * - * Matching: case-insensitive substring match - */ export class ProxyClientGuard { static async ensure(session: ProxySession): Promise { const user = session.authState?.user; @@ -24,18 +10,17 @@ export class ProxyClientGuard { return null; } - // Check if client restrictions are configured const allowedClients = user.allowedClients ?? []; - if (allowedClients.length === 0) { - // No restrictions configured - skip all checks + const blockedClients = user.blockedClients ?? []; + + if (allowedClients.length === 0 && blockedClients.length === 0) { return null; } - // Restrictions exist - now User-Agent is required - const userAgent = session.userAgent; - - // Missing or empty User-Agent when restrictions exist - if (!userAgent || userAgent.trim() === "") { + // User-Agent is only required when an allowlist is configured. + // Blocklist-only: no UA can't match any block pattern, so the request passes through. + const userAgent = session.userAgent?.trim(); + if (!userAgent && allowedClients.length > 0) { return ProxyResponses.buildError( 400, "Client not allowed. User-Agent header is required when client restrictions are configured.", @@ -43,23 +28,17 @@ export class ProxyClientGuard { ); } - // Case-insensitive substring match with hyphen/underscore normalization - // This handles variations like "gemini-cli" matching "GeminiCLI" or "gemini_cli" - const normalize = (s: string) => s.toLowerCase().replace(/[-_]/g, ""); - const userAgentNorm = normalize(userAgent); - const isAllowed = allowedClients.some((pattern) => { - const normalizedPattern = normalize(pattern); - // Skip empty patterns to prevent includes("") matching everything - if (normalizedPattern === "") return false; - return userAgentNorm.includes(normalizedPattern); - }); + const result = isClientAllowedDetailed(session, allowedClients, blockedClients); - if (!isAllowed) { - return ProxyResponses.buildError( - 400, - `Client not allowed. Your client is not in the allowed list.`, - "invalid_request_error" - ); + if (!result.allowed) { + const detected = result.detectedClient ? ` (detected: ${result.detectedClient})` : ""; + let message: string; + if (result.matchType === "blocklist_hit") { + message = `Client blocked by pattern: ${result.matchedPattern}${detected}`; + } else { + message = `Client not in allowed list: [${allowedClients.join(", ")}]${detected}`; + } + return ProxyResponses.buildError(400, message, "invalid_request_error"); } // Client is allowed diff --git a/src/app/v1/_lib/proxy/provider-selector.ts b/src/app/v1/_lib/proxy/provider-selector.ts index 9f108cd80..617ac4349 100644 --- a/src/app/v1/_lib/proxy/provider-selector.ts +++ b/src/app/v1/_lib/proxy/provider-selector.ts @@ -8,6 +8,7 @@ import { findAllProviders, findProviderById } from "@/repository/provider"; import { getSystemSettings } from "@/repository/system-config"; import type { ProviderChainItem } from "@/types/message"; import type { Provider } from "@/types/provider"; +import { isClientAllowedDetailed } from "./client-detector"; import type { ClientFormat } from "./format-mapper"; import { ProxyResponses } from "./responses"; import type { ProxySession } from "./session"; @@ -423,10 +424,15 @@ export class ProxyProviderResolver { const circuitOpen = filteredProviders.filter((p) => p.reason === "circuit_open"); const disabled = filteredProviders.filter((p) => p.reason === "disabled"); const modelNotAllowed = filteredProviders.filter((p) => p.reason === "model_not_allowed"); + const clientRestricted = filteredProviders.filter((p) => p.reason === "client_restriction"); // 计算可用供应商数量(排除禁用和模型不支持的) const unavailableCount = rateLimited.length + circuitOpen.length; - const totalEnabled = filteredProviders.length - disabled.length - modelNotAllowed.length; + const totalEnabled = + filteredProviders.length - + disabled.length - + modelNotAllowed.length - + clientRestricted.length; if ( rateLimited.length > 0 && @@ -473,11 +479,20 @@ export class ProxyProviderResolver { const filteredProviders = session.getLastSelectionContext()?.filteredProviders; if (filteredProviders) { + const clientRestricted = filteredProviders.filter((p) => p.reason === "client_restriction"); + // C-001: 脱敏供应商名称,仅暴露 id 和 reason details.filteredProviders = filteredProviders.map((p) => ({ id: p.id, reason: p.reason, })); + + if (clientRestricted.length > 0) { + details.clientRestrictedProviders = clientRestricted.map((p) => ({ + id: p.id, + reason: p.reason, + })); + } } return ProxyResponses.buildError(status, message, errorType, details); @@ -573,6 +588,55 @@ export class ProxyProviderResolver { return null; } + // Check provider-level client restrictions on session reuse + const providerAllowed = provider.allowedClients ?? []; + const providerBlocked = provider.blockedClients ?? []; + const clientResult = isClientAllowedDetailed(session, providerAllowed, providerBlocked); + if (!clientResult.allowed) { + logger.debug("ProviderSelector: Session provider blocked by client restrictions", { + sessionId: session.sessionId, + providerId: provider.id, + matchType: clientResult.matchType, + matchedPattern: clientResult.matchedPattern, + detectedClient: clientResult.detectedClient, + }); + session.addProviderToChain(provider, { + reason: "client_restriction_filtered", + decisionContext: { + totalProviders: 0, + enabledProviders: 0, + targetType: provider.providerType as NonNullable< + ProviderChainItem["decisionContext"] + >["targetType"], + requestedModel: session.getOriginalModel() || "", + groupFilterApplied: false, + beforeHealthCheck: 0, + afterHealthCheck: 0, + priorityLevels: [], + selectedPriority: 0, + candidatesAtPriority: [], + filteredProviders: [ + { + id: provider.id, + name: provider.name, + reason: "client_restriction", + details: + clientResult.matchType === "blocklist_hit" ? "blocklist_hit" : "allowlist_miss", + clientRestrictionContext: { + matchType: clientResult.matchType as "blocklist_hit" | "allowlist_miss", + matchedPattern: clientResult.matchedPattern, + detectedClient: clientResult.detectedClient, + providerAllowlist: clientResult.checkedAllowlist, + providerBlocklist: clientResult.checkedBlocklist, + }, + }, + ], + }, + }); + await SessionManager.clearSessionProvider(session.sessionId); + return null; + } + // 修复:检查用户分组权限(严格分组隔离 + 支持多分组) // Check if session provider matches user's group // Priority: key.providerGroup > user.providerGroup @@ -749,6 +813,37 @@ export class ProxyProviderResolver { excludedProviderIds: excludeIds.length > 0 ? excludeIds : undefined, }; + if (session) { + const clientFilteredProviders: typeof visibleProviders = []; + for (const p of visibleProviders) { + const providerAllowed = p.allowedClients ?? []; + const providerBlocked = p.blockedClients ?? []; + if (providerAllowed.length === 0 && providerBlocked.length === 0) { + clientFilteredProviders.push(p); + continue; + } + const result = isClientAllowedDetailed(session, providerAllowed, providerBlocked); + if (!result.allowed) { + context.filteredProviders?.push({ + id: p.id, + name: p.name, + reason: "client_restriction", + details: result.matchType === "blocklist_hit" ? "blocklist_hit" : "allowlist_miss", + clientRestrictionContext: { + matchType: result.matchType as "blocklist_hit" | "allowlist_miss", + matchedPattern: result.matchedPattern, + detectedClient: result.detectedClient, + providerAllowlist: result.checkedAllowlist, + providerBlocklist: result.checkedBlocklist, + }, + }); + continue; + } + clientFilteredProviders.push(p); + } + visibleProviders = clientFilteredProviders; + } + // Step 2: 基础过滤 + 格式/模型匹配(使用 visibleProviders) const enabledProviders = visibleProviders.filter((provider) => { // 2a. 基础过滤 diff --git a/src/app/v1/_lib/proxy/session.ts b/src/app/v1/_lib/proxy/session.ts index bebf1ba9e..74afe9400 100644 --- a/src/app/v1/_lib/proxy/session.ts +++ b/src/app/v1/_lib/proxy/session.ts @@ -449,7 +449,8 @@ export class ProxySession { | "client_error_non_retryable" // 不可重试的客户端错误(Prompt 超限、内容过滤、PDF 限制、Thinking 格式) | "http2_fallback" // HTTP/2 协议错误,回退到 HTTP/1.1(不切换供应商、不计入熔断器) | "endpoint_pool_exhausted" // 端点池耗尽(strict endpoint policy 阻止了 fallback) - | "vendor_type_all_timeout"; // 供应商类型全端点超时(524),触发 vendor-type 临时熔断 + | "vendor_type_all_timeout" // 供应商类型全端点超时(524),触发 vendor-type 临时熔断 + | "client_restriction_filtered"; // 供应商因客户端限制被跳过(会话复用路径) selectionMethod?: | "session_reuse" | "weighted_random" diff --git a/src/drizzle/schema.ts b/src/drizzle/schema.ts index 06ad18358..4c34b5a60 100644 --- a/src/drizzle/schema.ts +++ b/src/drizzle/schema.ts @@ -72,6 +72,10 @@ export const users = pgTable('users', { // Empty array = no restrictions, non-empty = only listed models allowed allowedModels: jsonb('allowed_models').$type().default([]), + // Blocked clients (CLI/IDE blocklist) + // Non-empty = listed patterns are denied even if allowedClients permits them + blockedClients: jsonb('blocked_clients').$type().notNull().default([]), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(), deletedAt: timestamp('deleted_at', { withTimezone: true }), @@ -193,6 +197,12 @@ export const providers = pgTable('providers', { // - null 或空数组:Anthropic 允许所有 claude 模型,非 Anthropic 允许任意模型 allowedModels: jsonb('allowed_models').$type().default(null), + // Client restrictions for this provider + // allowedClients: empty = no restriction; non-empty = only listed patterns allowed + // blockedClients: non-empty = listed patterns are denied + 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), @@ -542,6 +552,13 @@ export const messageRequest = pgTable('message_request', { messageRequestKeyCostActiveIdx: index('idx_message_request_key_cost_active') .on(table.key, table.costUsd) .where(sql`${table.deletedAt} IS NULL AND (${table.blockedBy} IS NULL OR ${table.blockedBy} <> 'warmup')`), + // #slow-query: composite index for session user-info LATERAL lookup + // Query: WHERE session_id = $1 AND deleted_at IS NULL ORDER BY created_at LIMIT 1 + // Provides seek + pre-sorted scan; user_id, key in index reduce heap columns to fetch. + // user_agent/api_type still require one heap fetch per session (LIMIT 1, negligible). + messageRequestSessionUserInfoIdx: index('idx_message_request_session_user_info') + .on(table.sessionId, table.createdAt, table.userId, table.key) + .where(sql`${table.deletedAt} IS NULL`), })); // Model Prices table @@ -881,8 +898,18 @@ export const usageLedger = pgTable('usage_ledger', { usageLedgerModelIdx: index('idx_usage_ledger_model') .on(table.model) .where(sql`${table.model} IS NOT NULL`), + // #slow-query: covering index for SUM(cost_usd) per key (replaces old key+cost, adds created_at for time range) usageLedgerKeyCostIdx: index('idx_usage_ledger_key_cost') - .on(table.key, table.costUsd) + .on(table.key, table.createdAt, table.costUsd) + .where(sql`${table.blockedBy} IS NULL`), + // #slow-query: covering index for SUM(cost_usd) per user (Quotas page + rate-limit total) + // Keys: user_id (equality), created_at (range filter), cost_usd (aggregation, index-only scan) + usageLedgerUserCostCoverIdx: index('idx_usage_ledger_user_cost_cover') + .on(table.userId, table.createdAt, table.costUsd) + .where(sql`${table.blockedBy} IS NULL`), + // #slow-query: covering index for SUM(cost_usd) per provider (rate-limit total) + usageLedgerProviderCostCoverIdx: index('idx_usage_ledger_provider_cost_cover') + .on(table.finalProviderId, table.createdAt, table.costUsd) .where(sql`${table.blockedBy} IS NULL`), })); diff --git a/src/lib/client-restrictions/client-presets.test.ts b/src/lib/client-restrictions/client-presets.test.ts new file mode 100644 index 000000000..041974ca7 --- /dev/null +++ b/src/lib/client-restrictions/client-presets.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, test } from "vitest"; +import { + isPresetClientValue, + isPresetSelected, + mergePresetAndCustomClients, + removePresetValues, + splitPresetAndCustomClients, + togglePresetSelection, +} from "./client-presets"; + +describe("client restriction presets", () => { + test("treats Claude Code sub-client values as preset-compatible aliases", () => { + expect(isPresetClientValue("claude-code")).toBe(true); + expect(isPresetClientValue("claude-code-cli")).toBe(true); + expect(isPresetSelected(["claude-code-cli-sdk"], "claude-code")).toBe(true); + }); + + test("splitPresetAndCustomClients keeps legacy aliases in presetValues", () => { + const result = splitPresetAndCustomClients(["claude-code-vscode", "my-ide"]); + expect(result).toEqual({ + presetValues: ["claude-code-vscode"], + customValues: ["my-ide"], + }); + }); + + test("togglePresetSelection adds canonical value for newly enabled preset", () => { + expect(togglePresetSelection(["gemini-cli"], "claude-code", true)).toEqual([ + "gemini-cli", + "claude-code", + ]); + }); + + test("togglePresetSelection removes canonical value and aliases when disabled", () => { + expect( + togglePresetSelection(["claude-code", "claude-code-cli", "my-ide"], "claude-code", false) + ).toEqual(["my-ide"]); + }); + + test("removePresetValues clears the whole preset group", () => { + expect(removePresetValues(["claude-code-gh-action", "codex-cli"], "claude-code")).toEqual([ + "codex-cli", + ]); + }); + + test("mergePresetAndCustomClients preserves legacy preset values without forcing migration", () => { + expect( + mergePresetAndCustomClients(["claude-code-sdk-ts", "codex-cli"], ["my-ide", "codex-cli"]) + ).toEqual(["claude-code-sdk-ts", "codex-cli", "my-ide"]); + }); +}); diff --git a/src/lib/client-restrictions/client-presets.ts b/src/lib/client-restrictions/client-presets.ts new file mode 100644 index 000000000..825186b10 --- /dev/null +++ b/src/lib/client-restrictions/client-presets.ts @@ -0,0 +1,89 @@ +export interface ClientRestrictionPresetOption { + value: string; + aliases: readonly string[]; +} + +const CLAUDE_CODE_ALIAS_VALUES = [ + "claude-code", + "claude-code-cli", + "claude-code-cli-sdk", + "claude-code-vscode", + "claude-code-sdk-ts", + "claude-code-sdk-py", + "claude-code-gh-action", +] as const; + +export const CLIENT_RESTRICTION_PRESET_OPTIONS: readonly ClientRestrictionPresetOption[] = [ + { value: "claude-code", aliases: CLAUDE_CODE_ALIAS_VALUES }, + { value: "gemini-cli", aliases: ["gemini-cli"] }, + { value: "factory-cli", aliases: ["factory-cli"] }, + { value: "codex-cli", aliases: ["codex-cli"] }, +]; + +const PRESET_OPTION_MAP = new Map( + CLIENT_RESTRICTION_PRESET_OPTIONS.map((option) => [option.value, option] as const) +); + +const PRESET_ALIAS_SET = new Set( + CLIENT_RESTRICTION_PRESET_OPTIONS.flatMap((option) => [...option.aliases]) +); + +function uniqueOrdered(values: string[]): string[] { + const seen = new Set(); + const result: string[] = []; + for (const value of values) { + if (seen.has(value)) continue; + seen.add(value); + result.push(value); + } + return result; +} + +function getPresetAliases(presetValue: string): readonly string[] { + return PRESET_OPTION_MAP.get(presetValue)?.aliases ?? [presetValue]; +} + +export function isPresetClientValue(value: string): boolean { + return PRESET_ALIAS_SET.has(value); +} + +export function isPresetSelected(values: string[], presetValue: string): boolean { + const aliases = getPresetAliases(presetValue); + return values.some((value) => aliases.includes(value)); +} + +export function removePresetValues(values: string[], presetValue: string): string[] { + const aliases = new Set(getPresetAliases(presetValue)); + return values.filter((value) => !aliases.has(value)); +} + +export function togglePresetSelection( + values: string[], + presetValue: string, + checked: boolean +): string[] { + if (!checked) { + return removePresetValues(values, presetValue); + } + + if (isPresetSelected(values, presetValue)) { + return uniqueOrdered(values); + } + + return uniqueOrdered([...values, presetValue]); +} + +export function splitPresetAndCustomClients(values: string[]): { + presetValues: string[]; + customValues: string[]; +} { + const presetValues = values.filter((value) => PRESET_ALIAS_SET.has(value)); + const customValues = values.filter((value) => !PRESET_ALIAS_SET.has(value)); + return { presetValues, customValues }; +} + +export function mergePresetAndCustomClients(values: string[], customValues: string[]): string[] { + const { presetValues } = splitPresetAndCustomClients(values); + const filteredCustomValues = customValues.filter((value) => !PRESET_ALIAS_SET.has(value)); + return uniqueOrdered([...presetValues, ...filteredCustomValues]); +} diff --git a/src/lib/database-backup/docker-executor.ts b/src/lib/database-backup/docker-executor.ts index 33095e934..6ebccbf5d 100644 --- a/src/lib/database-backup/docker-executor.ts +++ b/src/lib/database-backup/docker-executor.ts @@ -1,7 +1,7 @@ import { spawn } from "node:child_process"; import { createReadStream } from "node:fs"; -import { db } from "@/drizzle/db"; import { sql } from "drizzle-orm"; +import { db } from "@/drizzle/db"; import { logger } from "@/lib/logger"; import { getDatabaseConfig } from "./db-config"; diff --git a/src/lib/permissions/user-field-permissions.ts b/src/lib/permissions/user-field-permissions.ts index 2ce287023..2d0d7fb32 100644 --- a/src/lib/permissions/user-field-permissions.ts +++ b/src/lib/permissions/user-field-permissions.ts @@ -28,6 +28,7 @@ export const USER_FIELD_PERMISSIONS = { // Admin-only field (client restrictions) allowedClients: { requiredRole: "admin" }, + blockedClients: { requiredRole: "admin" }, // Admin-only field (model restrictions) allowedModels: { requiredRole: "admin" }, diff --git a/src/lib/provider-patch-contract.ts b/src/lib/provider-patch-contract.ts index 659176713..1e3c876d0 100644 --- a/src/lib/provider-patch-contract.ts +++ b/src/lib/provider-patch-contract.ts @@ -31,6 +31,8 @@ const PATCH_FIELDS: ProviderBatchPatchField[] = [ "group_tag", "model_redirects", "allowed_models", + "allowed_clients", + "blocked_clients", "anthropic_thinking_budget_preference", "anthropic_adaptive_thinking", // Routing @@ -79,6 +81,8 @@ const CLEARABLE_FIELDS: Record = { group_tag: true, model_redirects: true, allowed_models: true, + allowed_clients: true, + blocked_clients: true, anthropic_thinking_budget_preference: true, anthropic_adaptive_thinking: true, // Routing @@ -395,6 +399,12 @@ export function normalizeProviderBatchPatchDraft( const allowedModels = normalizePatchField("allowed_models", typedDraft.allowed_models); if (!allowedModels.ok) return allowedModels; + const allowedClients = normalizePatchField("allowed_clients", typedDraft.allowed_clients); + if (!allowedClients.ok) return allowedClients; + + const blockedClients = normalizePatchField("blocked_clients", typedDraft.blocked_clients); + if (!blockedClients.ok) return blockedClients; + const thinkingBudget = normalizePatchField( "anthropic_thinking_budget_preference", typedDraft.anthropic_thinking_budget_preference @@ -566,6 +576,8 @@ export function normalizeProviderBatchPatchDraft( group_tag: groupTag.data, model_redirects: modelRedirects.data, allowed_models: allowedModels.data, + allowed_clients: allowedClients.data, + blocked_clients: blockedClients.data, anthropic_thinking_budget_preference: thinkingBudget.data, anthropic_adaptive_thinking: adaptiveThinking.data, // Routing @@ -642,6 +654,12 @@ function applyPatchField( ? (patch.value as ProviderBatchApplyUpdates["allowed_models"]) : null; return { ok: true, data: undefined }; + case "allowed_clients": + updates.allowed_clients = patch.value as ProviderBatchApplyUpdates["allowed_clients"]; + return { ok: true, data: undefined }; + case "blocked_clients": + updates.blocked_clients = patch.value as ProviderBatchApplyUpdates["blocked_clients"]; + return { ok: true, data: undefined }; case "anthropic_thinking_budget_preference": updates.anthropic_thinking_budget_preference = patch.value as ProviderBatchApplyUpdates["anthropic_thinking_budget_preference"]; @@ -780,6 +798,12 @@ function applyPatchField( case "allowed_models": updates.allowed_models = null; return { ok: true, data: undefined }; + case "allowed_clients": + updates.allowed_clients = []; + return { ok: true, data: undefined }; + case "blocked_clients": + updates.blocked_clients = []; + return { ok: true, data: undefined }; case "anthropic_thinking_budget_preference": updates.anthropic_thinking_budget_preference = "inherit"; return { ok: true, data: undefined }; @@ -861,6 +885,8 @@ export function buildProviderBatchApplyUpdates( ["group_tag", patch.group_tag], ["model_redirects", patch.model_redirects], ["allowed_models", patch.allowed_models], + ["allowed_clients", patch.allowed_clients], + ["blocked_clients", patch.blocked_clients], ["anthropic_thinking_budget_preference", patch.anthropic_thinking_budget_preference], ["anthropic_adaptive_thinking", patch.anthropic_adaptive_thinking], // Routing @@ -922,6 +948,8 @@ export function hasProviderBatchPatchChanges(patch: ProviderBatchPatch): boolean patch.group_tag.mode !== "no_change" || patch.model_redirects.mode !== "no_change" || patch.allowed_models.mode !== "no_change" || + patch.allowed_clients.mode !== "no_change" || + patch.blocked_clients.mode !== "no_change" || patch.anthropic_thinking_budget_preference.mode !== "no_change" || patch.anthropic_adaptive_thinking.mode !== "no_change" || // Routing diff --git a/src/lib/utils/provider-chain-formatter.ts b/src/lib/utils/provider-chain-formatter.ts index 98e3188f5..1f7c72150 100644 --- a/src/lib/utils/provider-chain-formatter.ts +++ b/src/lib/utils/provider-chain-formatter.ts @@ -400,6 +400,27 @@ export function formatProviderTimeline( continue; } + // === Session reuse client restriction === + if (item.reason === "client_restriction_filtered" && ctx) { + timeline += `${t("filterDetails.session_reuse_client_restriction")}\n\n`; + timeline += `${t("timeline.provider", { provider: item.name })}\n`; + if (ctx.filteredProviders && ctx.filteredProviders.length > 0) { + const f = ctx.filteredProviders[0]; + if (f.clientRestrictionContext) { + const crc = f.clientRestrictionContext; + const detailKey = `filterDetails.${crc.matchType}`; + const detailsText = crc.matchedPattern + ? t(detailKey, { pattern: crc.matchedPattern }) + : t(detailKey); + timeline += `${detailsText}\n`; + if (crc.detectedClient) { + timeline += `${t("filterDetails.detectedClient", { client: crc.detectedClient })}\n`; + } + } + } + continue; + } + // === 首次选择 === if (item.reason === "initial_selection" && ctx) { timeline += `${t("timeline.initialSelectionTitle")}\n\n`; @@ -425,13 +446,28 @@ export function formatProviderTimeline( if (ctx.filteredProviders && ctx.filteredProviders.length > 0) { timeline += `\n${t("timeline.filtered")}:\n`; for (const f of ctx.filteredProviders) { - const icon = f.reason === "circuit_open" ? "⚡" : "💰"; + const icon = + f.reason === "circuit_open" ? "⚡" : f.reason === "client_restriction" ? "🚫" : "💰"; const detailsText = f.details ? t(`filterDetails.${f.details}`) !== `filterDetails.${f.details}` ? t(`filterDetails.${f.details}`) : f.details : f.reason; timeline += ` ${icon} ${f.name} (${detailsText})\n`; + + // Client restriction context details + if (f.clientRestrictionContext) { + const crc = f.clientRestrictionContext; + if (crc.detectedClient) { + timeline += ` ${t("filterDetails.detectedClient", { client: crc.detectedClient })}\n`; + } + if (crc.providerAllowlist.length > 0) { + timeline += ` ${t("filterDetails.providerAllowlist", { list: crc.providerAllowlist.join(", ") })}\n`; + } + if (crc.providerBlocklist.length > 0) { + timeline += ` ${t("filterDetails.providerBlocklist", { list: crc.providerBlocklist.join(", ") })}\n`; + } + } } } diff --git a/src/lib/validation/schemas.test.ts b/src/lib/validation/schemas.test.ts index f13fabbcd..ed63cd48d 100644 --- a/src/lib/validation/schemas.test.ts +++ b/src/lib/validation/schemas.test.ts @@ -1,6 +1,11 @@ import { describe, expect, test } from "vitest"; -import { CreateProviderSchema, UpdateProviderSchema } from "./schemas"; +import { + CreateProviderSchema, + CreateUserSchema, + UpdateProviderSchema, + UpdateUserSchema, +} from "./schemas"; describe("Provider schemas - priority/weight/costMultiplier 规则对齐", () => { describe("UpdateProviderSchema", () => { @@ -99,5 +104,55 @@ describe("Provider schemas - priority/weight/costMultiplier 规则对齐", () => ); // 注意: null 会被 coerce 转为 0 (Number(null) === 0),所以会通过 }); + + test("allowed_clients/blocked_clients 支持 null 并归一化为空数组", () => { + const base = { + name: "测试供应商", + url: "https://api.example.com", + key: "sk-test", + }; + + const parsed = CreateProviderSchema.parse({ + ...base, + allowed_clients: null, + blocked_clients: null, + }); + + expect(parsed.allowed_clients).toEqual([]); + expect(parsed.blocked_clients).toEqual([]); + }); + }); + + describe("client restrictions null normalization", () => { + test("UpdateProviderSchema 将 null 归一化为空数组", () => { + const parsed = UpdateProviderSchema.parse({ + allowed_clients: null, + blocked_clients: null, + }); + + expect(parsed.allowed_clients).toEqual([]); + expect(parsed.blocked_clients).toEqual([]); + }); + + test("CreateUserSchema 将 null 归一化为空数组", () => { + const parsed = CreateUserSchema.parse({ + name: "test-user", + allowedClients: null, + blockedClients: null, + }); + + expect(parsed.allowedClients).toEqual([]); + expect(parsed.blockedClients).toEqual([]); + }); + + test("UpdateUserSchema 将 null 归一化为空数组", () => { + const parsed = UpdateUserSchema.parse({ + allowedClients: null, + blockedClients: null, + }); + + expect(parsed.allowedClients).toEqual([]); + expect(parsed.blockedClients).toEqual([]); + }); }); }); diff --git a/src/lib/validation/schemas.ts b/src/lib/validation/schemas.ts index ebfbf1321..97dcae39a 100644 --- a/src/lib/validation/schemas.ts +++ b/src/lib/validation/schemas.ts @@ -74,6 +74,21 @@ const ANTHROPIC_ADAPTIVE_THINKING_CONFIG = z // - 'disabled': force remove googleSearch tool from request const GEMINI_GOOGLE_SEARCH_PREFERENCE = z.enum(["inherit", "enabled", "disabled"]); +const CLIENT_PATTERN_SCHEMA = z + .string() + .trim() + .min(1, "客户端模式不能为空") + .max(64, "客户端模式长度不能超过64个字符"); +const CLIENT_PATTERN_ARRAY_SCHEMA = z + .array(CLIENT_PATTERN_SCHEMA) + .max(50, "客户端模式数量不能超过50个"); +const OPTIONAL_CLIENT_PATTERN_ARRAY_SCHEMA = z.preprocess( + (value) => (value === null ? [] : value), + CLIENT_PATTERN_ARRAY_SCHEMA.optional() +); +const OPTIONAL_CLIENT_PATTERN_ARRAY_WITH_DEFAULT_SCHEMA = + OPTIONAL_CLIENT_PATTERN_ARRAY_SCHEMA.default([]); + /** * 用户创建数据验证schema */ @@ -197,11 +212,9 @@ export const CreateUserSchema = z.object({ .optional() .default("00:00"), // Allowed clients (CLI/IDE restrictions) - allowedClients: z - .array(z.string().max(64, "客户端模式长度不能超过64个字符")) - .max(50, "客户端模式数量不能超过50个") - .optional() - .default([]), + allowedClients: OPTIONAL_CLIENT_PATTERN_ARRAY_WITH_DEFAULT_SCHEMA, + // Blocked clients (CLI/IDE restrictions) + blockedClients: OPTIONAL_CLIENT_PATTERN_ARRAY_WITH_DEFAULT_SCHEMA, // Allowed models (AI model restrictions) allowedModels: z .array(z.string().max(64, "模型名称长度不能超过64个字符")) @@ -322,10 +335,9 @@ export const UpdateUserSchema = z.object({ .regex(/^([01]\d|2[0-3]):[0-5]\d$/, "重置时间格式必须为 HH:mm") .optional(), // Allowed clients (CLI/IDE restrictions) - allowedClients: z - .array(z.string().max(64, "客户端模式长度不能超过64个字符")) - .max(50, "客户端模式数量不能超过50个") - .optional(), + allowedClients: OPTIONAL_CLIENT_PATTERN_ARRAY_SCHEMA, + // Blocked clients (CLI/IDE restrictions) + blockedClients: OPTIONAL_CLIENT_PATTERN_ARRAY_SCHEMA, // Allowed models (AI model restrictions) allowedModels: z .array(z.string().max(64, "模型名称长度不能超过64个字符")) @@ -437,6 +449,8 @@ export const CreateProviderSchema = z preserve_client_ip: z.boolean().optional().default(false), model_redirects: z.record(z.string(), z.string()).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, // MCP 透传配置 mcp_passthrough_type: z.enum(["none", "minimax", "glm", "custom"]).optional().default("none"), mcp_passthrough_url: z @@ -643,6 +657,8 @@ export const UpdateProviderSchema = z preserve_client_ip: z.boolean().optional(), model_redirects: z.record(z.string(), z.string()).nullable().optional(), allowed_models: z.array(z.string()).nullable().optional(), + allowed_clients: OPTIONAL_CLIENT_PATTERN_ARRAY_SCHEMA, + blocked_clients: OPTIONAL_CLIENT_PATTERN_ARRAY_SCHEMA, // MCP 透传配置 mcp_passthrough_type: z.enum(["none", "minimax", "glm", "custom"]).optional(), mcp_passthrough_url: z diff --git a/src/repository/_shared/transformers.ts b/src/repository/_shared/transformers.ts index d3773b713..d06cd86ed 100644 --- a/src/repository/_shared/transformers.ts +++ b/src/repository/_shared/transformers.ts @@ -48,6 +48,7 @@ export function toUser(dbUser: any): User { isEnabled: dbUser?.isEnabled ?? true, expiresAt: dbUser?.expiresAt ? new Date(dbUser.expiresAt) : null, allowedClients: dbUser?.allowedClients ?? [], + blockedClients: dbUser?.blockedClients ?? [], allowedModels: dbUser?.allowedModels ?? [], createdAt: dbUser?.createdAt ? new Date(dbUser.createdAt) : new Date(), updatedAt: dbUser?.updatedAt ? new Date(dbUser.updatedAt) : new Date(), diff --git a/src/repository/message.ts b/src/repository/message.ts index d0baa0314..b5d2527ed 100644 --- a/src/repository/message.ts +++ b/src/repository/message.ts @@ -837,33 +837,63 @@ export async function aggregateMultipleSessionStats(sessionIds: string[]): Promi } // 4. 批量获取用户信息(每个 session 的第一条请求) - // 使用 DISTINCT ON + ORDER BY 优化 - const userInfoResults = await db - .select({ - sessionId: messageRequest.sessionId, - userName: users.name, - userId: users.id, - keyName: keysTable.name, - keyId: keysTable.id, - userAgent: messageRequest.userAgent, - apiType: messageRequest.apiType, - createdAt: messageRequest.createdAt, - }) - .from(messageRequest) - .innerJoin(users, eq(messageRequest.userId, users.id)) - .innerJoin(keysTable, eq(messageRequest.key, keysTable.key)) - .where(and(inArray(messageRequest.sessionId, sessionIds), isNull(messageRequest.deletedAt))) - .orderBy(messageRequest.sessionId, messageRequest.createdAt); - - // 创建 sessionId → userInfo 的 Map(取每个 session 最早的记录) - const userInfoMap = new Map(); - for (const info of userInfoResults) { - // 跳过 null sessionId(虽然 WHERE 条件已过滤,但需要满足 TypeScript 类型检查) - if (!info.sessionId) continue; - - if (!userInfoMap.has(info.sessionId)) { - userInfoMap.set(info.sessionId, info); + // LATERAL JOIN: 每个 session_id 做 1 次索引探测,无全局排序 + const sessionIdParams = sql.join( + sessionIds.map((id) => sql`${id}`), + sql.raw(", ") + ); + const userInfoRows = await db.execute(sql` + SELECT + sid AS session_id, + u.name AS user_name, + u.id AS user_id, + k.name AS key_name, + k.id AS key_id, + mr.user_agent, + mr.api_type + FROM unnest(ARRAY[${sessionIdParams}]::varchar[]) AS sid + CROSS JOIN LATERAL ( + SELECT user_id, key, user_agent, api_type + FROM message_request + WHERE session_id = sid AND deleted_at IS NULL + ORDER BY created_at + LIMIT 1 + ) mr + INNER JOIN users u ON mr.user_id = u.id + INNER JOIN keys k ON mr.key = k.key + `); + + // 创建 sessionId → userInfo 的 Map + const userInfoMap = new Map< + string, + { + sessionId: string; + userName: string; + userId: number; + keyName: string; + keyId: number; + userAgent: string | null; + apiType: string | null; } + >(); + for (const row of Array.from(userInfoRows) as Array<{ + session_id: string; + user_name: string; + user_id: number; + key_name: string; + key_id: number; + user_agent: string | null; + api_type: string | null; + }>) { + userInfoMap.set(row.session_id, { + sessionId: row.session_id, + userName: row.user_name, + userId: row.user_id, + keyName: row.key_name, + keyId: row.key_id, + userAgent: row.user_agent, + apiType: row.api_type, + }); } // 5. 组装最终结果 diff --git a/src/repository/provider.ts b/src/repository/provider.ts index 5204aab56..f9b6c32f3 100644 --- a/src/repository/provider.ts +++ b/src/repository/provider.ts @@ -181,6 +181,8 @@ export async function createProvider(providerData: CreateProviderData): Promise< preserveClientIp: providerData.preserve_client_ip ?? false, modelRedirects: providerData.model_redirects, allowedModels: providerData.allowed_models, + allowedClients: providerData.allowed_clients ?? [], + blockedClients: providerData.blocked_clients ?? [], mcpPassthroughType: providerData.mcp_passthrough_type ?? "none", mcpPassthroughUrl: providerData.mcp_passthrough_url ?? null, limit5hUsd: providerData.limit_5h_usd != null ? providerData.limit_5h_usd.toString() : null, @@ -256,6 +258,8 @@ export async function createProvider(providerData: CreateProviderData): Promise< preserveClientIp: providers.preserveClientIp, modelRedirects: providers.modelRedirects, allowedModels: providers.allowedModels, + allowedClients: providers.allowedClients, + blockedClients: providers.blockedClients, mcpPassthroughType: providers.mcpPassthroughType, mcpPassthroughUrl: providers.mcpPassthroughUrl, limit5hUsd: providers.limit5hUsd, @@ -336,6 +340,8 @@ export async function findProviderList( preserveClientIp: providers.preserveClientIp, modelRedirects: providers.modelRedirects, allowedModels: providers.allowedModels, + allowedClients: providers.allowedClients, + blockedClients: providers.blockedClients, mcpPassthroughType: providers.mcpPassthroughType, mcpPassthroughUrl: providers.mcpPassthroughUrl, limit5hUsd: providers.limit5hUsd, @@ -416,6 +422,8 @@ export async function findAllProvidersFresh(): Promise { preserveClientIp: providers.preserveClientIp, modelRedirects: providers.modelRedirects, allowedModels: providers.allowedModels, + allowedClients: providers.allowedClients, + blockedClients: providers.blockedClients, mcpPassthroughType: providers.mcpPassthroughType, mcpPassthroughUrl: providers.mcpPassthroughUrl, limit5hUsd: providers.limit5hUsd, @@ -500,6 +508,8 @@ export async function findProviderById(id: number): Promise { preserveClientIp: providers.preserveClientIp, modelRedirects: providers.modelRedirects, allowedModels: providers.allowedModels, + allowedClients: providers.allowedClients, + blockedClients: providers.blockedClients, mcpPassthroughType: providers.mcpPassthroughType, mcpPassthroughUrl: providers.mcpPassthroughUrl, limit5hUsd: providers.limit5hUsd, @@ -578,6 +588,10 @@ export async function updateProvider( if (providerData.model_redirects !== undefined) dbData.modelRedirects = providerData.model_redirects; if (providerData.allowed_models !== undefined) dbData.allowedModels = providerData.allowed_models; + if (providerData.allowed_clients !== undefined) + dbData.allowedClients = providerData.allowed_clients ?? []; + if (providerData.blocked_clients !== undefined) + dbData.blockedClients = providerData.blocked_clients ?? []; if (providerData.mcp_passthrough_type !== undefined) dbData.mcpPassthroughType = providerData.mcp_passthrough_type; if (providerData.mcp_passthrough_url !== undefined) @@ -723,6 +737,8 @@ export async function updateProvider( preserveClientIp: providers.preserveClientIp, modelRedirects: providers.modelRedirects, allowedModels: providers.allowedModels, + allowedClients: providers.allowedClients, + blockedClients: providers.blockedClients, mcpPassthroughType: providers.mcpPassthroughType, mcpPassthroughUrl: providers.mcpPassthroughUrl, limit5hUsd: providers.limit5hUsd, @@ -973,6 +989,8 @@ export interface BatchProviderUpdates { groupTag?: string | null; modelRedirects?: Record | null; allowedModels?: string[] | null; + allowedClients?: string[] | null; + blockedClients?: string[] | null; anthropicThinkingBudgetPreference?: string | null; anthropicAdaptiveThinking?: AnthropicAdaptiveThinkingConfig | null; // Routing @@ -1045,6 +1063,12 @@ export async function updateProvidersBatch( if (updates.allowedModels !== undefined) { setClauses.allowedModels = updates.allowedModels; } + if (updates.allowedClients !== undefined) { + setClauses.allowedClients = updates.allowedClients; + } + if (updates.blockedClients !== undefined) { + setClauses.blockedClients = updates.blockedClients; + } if (updates.anthropicThinkingBudgetPreference !== undefined) { setClauses.anthropicThinkingBudgetPreference = updates.anthropicThinkingBudgetPreference; } diff --git a/src/repository/statistics.ts b/src/repository/statistics.ts index fa00d0f50..7df64e5d0 100644 --- a/src/repository/statistics.ts +++ b/src/repository/statistics.ts @@ -1,5 +1,6 @@ import "server-only"; +import type { SQL } from "drizzle-orm"; import { and, eq, gte, inArray, isNull, lt, sql } from "drizzle-orm"; import { db } from "@/drizzle/db"; import { keys, messageRequest, usageLedger } from "@/drizzle/schema"; @@ -502,56 +503,83 @@ export async function sumUserTotalCost(userId: number, maxAgeDays: number = 365) } /** - * Batch query: all-time total cost grouped by user_id (single SQL query) + * Batch query: total cost grouped by user_id (single SQL query) * @param userIds - Array of user IDs + * @param maxAgeDays - Only include records newer than this many days (default 365, use Infinity to include all) * @returns Map of userId -> totalCost */ -export async function sumUserTotalCostBatch(userIds: number[]): Promise> { +export async function sumUserTotalCostBatch( + userIds: number[], + maxAgeDays: number = 365 +): Promise> { const result = new Map(); if (userIds.length === 0) return result; + const conditions: SQL[] = [inArray(usageLedger.userId, userIds), LEDGER_BILLING_CONDITION]; + if (Number.isFinite(maxAgeDays) && maxAgeDays > 0) { + const cutoffDate = new Date(Date.now() - Math.floor(maxAgeDays) * 24 * 60 * 60 * 1000); + conditions.push(gte(usageLedger.createdAt, cutoffDate)); + } + const rows = await db .select({ userId: usageLedger.userId, total: sql`COALESCE(SUM(${usageLedger.costUsd}), 0)`, }) .from(usageLedger) - .where(and(inArray(usageLedger.userId, userIds), LEDGER_BILLING_CONDITION)) + .where(and(...conditions)) .groupBy(usageLedger.userId); - for (const id of userIds) { - result.set(id, 0); - } - for (const row of rows) { - result.set(row.userId, Number(row.total || 0)); - } + for (const id of userIds) result.set(id, 0); + for (const row of rows) result.set(row.userId, Number(row.total || 0)); return result; } /** - * Batch query: all-time total cost grouped by key_id (single SQL query via JOIN) + * Batch query: total cost grouped by key_id using a two-step PK lookup then aggregate. + * Avoids varchar LEFT JOIN by first resolving key strings via PK, then aggregating on + * usage_ledger directly (hits idx_usage_ledger_key_cost index). * @param keyIds - Array of key IDs + * @param maxAgeDays - Only include records newer than this many days (default 365, use Infinity to include all) * @returns Map of keyId -> totalCost */ -export async function sumKeyTotalCostBatchByIds(keyIds: number[]): Promise> { +export async function sumKeyTotalCostBatchByIds( + keyIds: number[], + maxAgeDays: number = 365 +): Promise> { const result = new Map(); if (keyIds.length === 0) return result; + for (const id of keyIds) result.set(id, 0); + + // Step 1: PK lookup -> key strings + const keyMappings = await db + .select({ id: keys.id, key: keys.key }) + .from(keys) + .where(inArray(keys.id, keyIds)); + + const keyStringToId = new Map(keyMappings.map((k) => [k.key, k.id])); + const keyStrings = keyMappings.map((k) => k.key); + if (keyStrings.length === 0) return result; + + // Step 2: Aggregate on usage_ledger directly (hits idx_usage_ledger_key_cost) + const conditions: SQL[] = [inArray(usageLedger.key, keyStrings), LEDGER_BILLING_CONDITION]; + if (Number.isFinite(maxAgeDays) && maxAgeDays > 0) { + const cutoffDate = new Date(Date.now() - Math.floor(maxAgeDays) * 24 * 60 * 60 * 1000); + conditions.push(gte(usageLedger.createdAt, cutoffDate)); + } const rows = await db .select({ - keyId: keys.id, + key: usageLedger.key, total: sql`COALESCE(SUM(${usageLedger.costUsd}), 0)`, }) - .from(keys) - .leftJoin(usageLedger, and(eq(usageLedger.key, keys.key), LEDGER_BILLING_CONDITION)) - .where(inArray(keys.id, keyIds)) - .groupBy(keys.id); + .from(usageLedger) + .where(and(...conditions)) + .groupBy(usageLedger.key); - for (const id of keyIds) { - result.set(id, 0); - } for (const row of rows) { - result.set(row.keyId, Number(row.total || 0)); + const keyId = keyStringToId.get(row.key); + if (keyId !== undefined) result.set(keyId, Number(row.total || 0)); } return result; } diff --git a/src/repository/usage-ledger.ts b/src/repository/usage-ledger.ts index ed60cd4ec..8655c59aa 100644 --- a/src/repository/usage-ledger.ts +++ b/src/repository/usage-ledger.ts @@ -68,10 +68,13 @@ export async function sumLedgerTotalCost( /** * Batch total cost grouped by entity (single SQL query). * Returns Map of entityId (as string) -> totalCost. + * @param maxAgeDays - Only include ledger rows created within this many days (default 365). + * Pass Infinity or a non-positive number to include all-time records. */ export async function sumLedgerTotalCostBatch( entityType: "user" | "key", - entityIds: number[] | string[] + entityIds: number[] | string[], + maxAgeDays: number = 365 ): Promise> { const result = new Map(); if (entityIds.length === 0) return result; @@ -80,6 +83,12 @@ export async function sumLedgerTotalCostBatch( result.set(String(id), "0"); } + const timeConditions: ReturnType[] = []; + if (Number.isFinite(maxAgeDays) && maxAgeDays > 0) { + const cutoffDate = new Date(Date.now() - Math.floor(maxAgeDays) * 24 * 60 * 60 * 1000); + timeConditions.push(gte(usageLedger.createdAt, cutoffDate)); + } + if (entityType === "user") { const ids = entityIds as number[]; const rows = await db @@ -88,7 +97,7 @@ export async function sumLedgerTotalCostBatch( total: sql`COALESCE(SUM(${usageLedger.costUsd}), '0')`, }) .from(usageLedger) - .where(and(inArray(usageLedger.userId, ids), LEDGER_BILLING_CONDITION)) + .where(and(inArray(usageLedger.userId, ids), LEDGER_BILLING_CONDITION, ...timeConditions)) .groupBy(usageLedger.userId); for (const row of rows) { result.set(String(row.entityId), row.total ?? "0"); @@ -101,7 +110,7 @@ export async function sumLedgerTotalCostBatch( total: sql`COALESCE(SUM(${usageLedger.costUsd}), '0')`, }) .from(usageLedger) - .where(and(inArray(usageLedger.key, ids), LEDGER_BILLING_CONDITION)) + .where(and(inArray(usageLedger.key, ids), LEDGER_BILLING_CONDITION, ...timeConditions)) .groupBy(usageLedger.key); for (const row of rows) { result.set(row.entityId, row.total ?? "0"); diff --git a/src/repository/user.ts b/src/repository/user.ts index 350ccbf6c..a7de95da2 100644 --- a/src/repository/user.ts +++ b/src/repository/user.ts @@ -59,6 +59,7 @@ export async function createUser(userData: CreateUserData): Promise { isEnabled: userData.isEnabled ?? true, expiresAt: userData.expiresAt ?? null, allowedClients: userData.allowedClients ?? [], + blockedClients: userData.blockedClients ?? [], allowedModels: userData.allowedModels ?? [], }; @@ -84,6 +85,7 @@ export async function createUser(userData: CreateUserData): Promise { isEnabled: users.isEnabled, expiresAt: users.expiresAt, allowedClients: users.allowedClients, + blockedClients: users.blockedClients, allowedModels: users.allowedModels, }); @@ -116,6 +118,7 @@ export async function findUserList(limit: number = 50, offset: number = 0): Prom isEnabled: users.isEnabled, expiresAt: users.expiresAt, allowedClients: users.allowedClients, + blockedClients: users.blockedClients, allowedModels: users.allowedModels, }) .from(users) @@ -294,6 +297,7 @@ export async function findUserListBatch( isEnabled: users.isEnabled, expiresAt: users.expiresAt, allowedClients: users.allowedClients, + blockedClients: users.blockedClients, allowedModels: users.allowedModels, }) .from(users) @@ -338,6 +342,7 @@ export async function findUserById(id: number): Promise { isEnabled: users.isEnabled, expiresAt: users.expiresAt, allowedClients: users.allowedClients, + blockedClients: users.blockedClients, allowedModels: users.allowedModels, }) .from(users) @@ -371,6 +376,7 @@ export async function updateUser(id: number, userData: UpdateUserData): Promise< isEnabled?: boolean; expiresAt?: Date | null; allowedClients?: string[]; + blockedClients?: string[]; allowedModels?: string[]; } @@ -402,6 +408,7 @@ export async function updateUser(id: number, userData: UpdateUserData): Promise< if (userData.isEnabled !== undefined) dbData.isEnabled = userData.isEnabled; if (userData.expiresAt !== undefined) dbData.expiresAt = userData.expiresAt; if (userData.allowedClients !== undefined) dbData.allowedClients = userData.allowedClients; + if (userData.blockedClients !== undefined) dbData.blockedClients = userData.blockedClients; if (userData.allowedModels !== undefined) dbData.allowedModels = userData.allowedModels; const [user] = await db @@ -430,6 +437,7 @@ export async function updateUser(id: number, userData: UpdateUserData): Promise< isEnabled: users.isEnabled, expiresAt: users.expiresAt, allowedClients: users.allowedClients, + blockedClients: users.blockedClients, allowedModels: users.allowedModels, }); diff --git a/src/types/message.ts b/src/types/message.ts index c6833d290..f99ca47b9 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -34,7 +34,8 @@ export interface ProviderChainItem { | "client_error_non_retryable" // 不可重试的客户端错误(Prompt 超限、内容过滤、PDF 限制、Thinking 格式) | "http2_fallback" // HTTP/2 协议错误,回退到 HTTP/1.1(不切换供应商、不计入熔断器) | "endpoint_pool_exhausted" // 端点池耗尽(所有端点熔断或不可用,严格模式阻止降级) - | "vendor_type_all_timeout"; // 供应商类型全端点超时(524),触发 vendor-type 临时熔断 + | "vendor_type_all_timeout" // 供应商类型全端点超时(524),触发 vendor-type 临时熔断 + | "client_restriction_filtered"; // Provider skipped due to client restriction (neutral, no circuit breaker) // === 选择方法(细化) === selectionMethod?: @@ -171,8 +172,16 @@ export interface ProviderChainItem { | "type_mismatch" | "model_not_allowed" | "context_1m_disabled" // 供应商禁用了 1M 上下文功能 - | "disabled"; + | "disabled" + | "client_restriction"; // Provider filtered due to client restriction details?: string; // 额外信息(如费用:$15.2/$15) + clientRestrictionContext?: { + matchType: "blocklist_hit" | "allowlist_miss"; + matchedPattern?: string; + detectedClient?: string; + providerAllowlist: string[]; + providerBlocklist: string[]; + }; }>; // --- 优先级分层 --- diff --git a/src/types/provider.ts b/src/types/provider.ts index 94480e6d0..875341206 100644 --- a/src/types/provider.ts +++ b/src/types/provider.ts @@ -65,6 +65,8 @@ export type ProviderBatchPatchField = | "group_tag" | "model_redirects" | "allowed_models" + | "allowed_clients" + | "blocked_clients" | "anthropic_thinking_budget_preference" | "anthropic_adaptive_thinking" // Routing @@ -112,6 +114,8 @@ export interface ProviderBatchPatchDraft { group_tag?: ProviderPatchDraftInput; model_redirects?: ProviderPatchDraftInput>; allowed_models?: ProviderPatchDraftInput; + allowed_clients?: ProviderPatchDraftInput; + blocked_clients?: ProviderPatchDraftInput; anthropic_thinking_budget_preference?: ProviderPatchDraftInput; anthropic_adaptive_thinking?: ProviderPatchDraftInput; // Routing @@ -160,6 +164,8 @@ export interface ProviderBatchPatch { group_tag: ProviderPatchOperation; model_redirects: ProviderPatchOperation>; allowed_models: ProviderPatchOperation; + allowed_clients: ProviderPatchOperation; + blocked_clients: ProviderPatchOperation; anthropic_thinking_budget_preference: ProviderPatchOperation; anthropic_adaptive_thinking: ProviderPatchOperation; // Routing @@ -208,6 +214,8 @@ export interface ProviderBatchApplyUpdates { group_tag?: string | null; model_redirects?: Record | null; allowed_models?: string[] | null; + allowed_clients?: string[]; + blocked_clients?: string[]; anthropic_thinking_budget_preference?: AnthropicThinkingBudgetPreference | null; anthropic_adaptive_thinking?: AnthropicAdaptiveThinkingConfig | null; // Routing @@ -285,6 +293,8 @@ export interface Provider { // - 非 Anthropic 提供商:声明列表(提供商声称支持的模型,可选) // - null 或空数组:Anthropic 允许所有 claude 模型,非 Anthropic 允许任意模型 allowedModels: string[] | null; + allowedClients: string[]; // Allowed client patterns (empty = no restriction) + blockedClients: string[]; // Blocked client patterns (blacklist, checked before allowedClients) // MCP 透传类型:控制是否启用 MCP 透传功能 // 'none': 不启用(默认) @@ -390,6 +400,8 @@ export interface ProviderDisplay { modelRedirects: Record | null; // 模型列表(双重语义) allowedModels: string[] | null; + allowedClients: string[]; // Allowed client patterns (empty = no restriction) + blockedClients: string[]; // Blocked client patterns (blacklist, checked before allowedClients) // MCP 透传类型 mcpPassthroughType: McpPassthroughType; // MCP 透传 URL @@ -479,6 +491,8 @@ export interface CreateProviderData { preserve_client_ip?: boolean; model_redirects?: Record | null; allowed_models?: string[] | null; + allowed_clients?: string[] | null; + blocked_clients?: string[] | null; mcp_passthrough_type?: McpPassthroughType; mcp_passthrough_url?: string | null; @@ -553,6 +567,8 @@ export interface UpdateProviderData { preserve_client_ip?: boolean; model_redirects?: Record | null; allowed_models?: string[] | null; + allowed_clients?: string[] | null; + blocked_clients?: string[] | null; mcp_passthrough_type?: McpPassthroughType; mcp_passthrough_url?: string | null; diff --git a/src/types/user.ts b/src/types/user.ts index 1efcad8e3..7a1307c76 100644 --- a/src/types/user.ts +++ b/src/types/user.ts @@ -27,6 +27,7 @@ export interface User { expiresAt?: Date | null; // 用户过期时间 // Allowed clients (CLI/IDE restrictions) allowedClients?: string[]; // 允许的客户端模式(空数组=无限制) + blockedClients?: string[]; // Blocked client patterns (blacklist, checked before allowedClients) // Allowed models (AI model restrictions) allowedModels?: string[]; // 允许的AI模型(空数组=无限制) } @@ -55,6 +56,7 @@ export interface CreateUserData { expiresAt?: Date | null; // Allowed clients (CLI/IDE restrictions) allowedClients?: string[]; + blockedClients?: string[]; // Blocked client patterns (blacklist, checked before allowedClients) // Allowed models (AI model restrictions) allowedModels?: string[]; } @@ -83,6 +85,7 @@ export interface UpdateUserData { expiresAt?: Date | null; // Allowed clients (CLI/IDE restrictions) allowedClients?: string[]; + blockedClients?: string[]; // Blocked client patterns (blacklist, checked before allowedClients) // Allowed models (AI model restrictions) allowedModels?: string[]; } @@ -156,6 +159,7 @@ export interface UserDisplay { expiresAt?: Date | null; // 用户过期时间 // Allowed clients (CLI/IDE restrictions) allowedClients?: string[]; // 允许的客户端模式(空数组=无限制) + blockedClients?: string[]; // Blocked client patterns (blacklist, checked before allowedClients) // Allowed models (AI model restrictions) allowedModels?: string[]; // 允许的AI模型(空数组=无限制) } @@ -173,6 +177,7 @@ export interface KeyDialogUserContext { limitTotalUsd?: number | null; limitConcurrentSessions?: number; allowedClients?: string[]; + blockedClients?: string[]; // Blocked client patterns (blacklist, checked before allowedClients) allowedModels?: string[]; } diff --git a/tests/unit/proxy/client-detector.test.ts b/tests/unit/proxy/client-detector.test.ts new file mode 100644 index 000000000..818cd607d --- /dev/null +++ b/tests/unit/proxy/client-detector.test.ts @@ -0,0 +1,428 @@ +import { describe, expect, test } from "vitest"; +import { + BUILTIN_CLIENT_KEYWORDS, + CLAUDE_CODE_KEYWORD_PREFIX, + detectClientFull, + isBuiltinKeyword, + isClientAllowed, + isClientAllowedDetailed, + matchClientPattern, +} from "@/app/v1/_lib/proxy/client-detector"; +import type { ProxySession } from "@/app/v1/_lib/proxy/session"; + +type SessionOptions = { + userAgent?: string | null; + xApp?: string | null; + dangerousBrowserAccess?: string | null; + betas?: unknown; +}; + +function createMockSession(options: SessionOptions = {}): ProxySession { + const headers = new Headers(); + if (options.xApp !== undefined && options.xApp !== null) { + headers.set("x-app", options.xApp); + } + if (options.dangerousBrowserAccess !== undefined && options.dangerousBrowserAccess !== null) { + headers.set("anthropic-dangerous-direct-browser-access", options.dangerousBrowserAccess); + } + + const message: Record = {}; + if ("betas" in options) { + message.betas = options.betas; + } + + return { + userAgent: options.userAgent ?? null, + headers, + request: { + message, + }, + } as unknown as ProxySession; +} + +function createConfirmedClaudeCodeSession(userAgent: string): ProxySession { + return createMockSession({ + userAgent, + xApp: "cli", + betas: ["claude-code-test"], + }); +} + +describe("client-detector", () => { + describe("constants", () => { + test("CLAUDE_CODE_KEYWORD_PREFIX should be claude-code", () => { + expect(CLAUDE_CODE_KEYWORD_PREFIX).toBe("claude-code"); + }); + + test("BUILTIN_CLIENT_KEYWORDS should contain 7 items", () => { + expect(BUILTIN_CLIENT_KEYWORDS.size).toBe(7); + }); + }); + + describe("isBuiltinKeyword", () => { + test.each([ + "claude-code", + "claude-code-cli", + "claude-code-cli-sdk", + "claude-code-vscode", + "claude-code-sdk-ts", + "claude-code-sdk-py", + "claude-code-gh-action", + ])("should return true for builtin keyword: %s", (pattern) => { + expect(isBuiltinKeyword(pattern)).toBe(true); + }); + + test.each([ + "gemini-cli", + "codex-cli", + "custom-pattern", + ])("should return false for non-builtin keyword: %s", (pattern) => { + expect(isBuiltinKeyword(pattern)).toBe(false); + }); + }); + + describe("confirmClaudeCodeSignals via detectClientFull", () => { + test("should confirm when all 3 strong signals are present", () => { + const session = createMockSession({ + userAgent: "claude-cli/1.0.0 (external, cli)", + xApp: "cli", + betas: ["claude-code-cache-control-20260101"], + }); + + const result = detectClientFull(session, "claude-code"); + expect(result.hubConfirmed).toBe(true); + expect(result.signals).toEqual(["x-app-cli", "ua-prefix", "betas-claude-code"]); + expect(result.supplementary).toEqual([]); + }); + + test.each([ + { + name: "missing x-app", + options: { + userAgent: "claude-cli/1.0.0 (external, cli)", + betas: ["claude-code-foo"], + }, + }, + { + name: "missing ua-prefix", + options: { + userAgent: "GeminiCLI/1.0", + xApp: "cli", + betas: ["claude-code-foo"], + }, + }, + { + name: "missing betas-claude-code", + options: { + userAgent: "claude-cli/1.0.0 (external, cli)", + xApp: "cli", + betas: ["not-claude-code"], + }, + }, + ])("should not confirm with only 2-of-3 signals: $name", ({ options }) => { + const session = createMockSession(options); + const result = detectClientFull(session, "claude-code"); + expect(result.hubConfirmed).toBe(false); + expect(result.signals.length).toBe(2); + }); + + test("should not confirm with 0 strong signals", () => { + const session = createMockSession({ userAgent: "GeminiCLI/1.0", betas: "not-array" }); + const result = detectClientFull(session, "claude-code"); + + expect(result.hubConfirmed).toBe(false); + expect(result.signals).toEqual([]); + }); + + test("should collect supplementary signal without counting it", () => { + const session = createMockSession({ + userAgent: "claude-cli/1.0.0 (external, cli)", + xApp: "cli", + betas: ["not-claude-code"], + dangerousBrowserAccess: "true", + }); + + const result = detectClientFull(session, "claude-code"); + expect(result.hubConfirmed).toBe(false); + expect(result.signals).toEqual(["x-app-cli", "ua-prefix"]); + expect(result.supplementary).toEqual(["dangerous-browser-access"]); + }); + }); + + describe("extractSubClient via detectClientFull", () => { + test.each([ + ["cli", "claude-code-cli"], + ["sdk-cli", "claude-code-cli-sdk"], + ["claude-vscode", "claude-code-vscode"], + ["sdk-ts", "claude-code-sdk-ts"], + ["sdk-py", "claude-code-sdk-py"], + ["claude-code-github-action", "claude-code-gh-action"], + ])("should map entrypoint %s to %s", (entrypoint, expectedSubClient) => { + const session = createConfirmedClaudeCodeSession( + `claude-cli/1.2.3 (external, ${entrypoint})` + ); + const result = detectClientFull(session, "claude-code"); + + expect(result.hubConfirmed).toBe(true); + expect(result.subClient).toBe(expectedSubClient); + }); + + test("should return null for unknown entrypoint", () => { + const session = createConfirmedClaudeCodeSession( + "claude-cli/1.2.3 (external, unknown-entry)" + ); + const result = detectClientFull(session, "claude-code"); + + expect(result.hubConfirmed).toBe(true); + expect(result.subClient).toBeNull(); + }); + + test("should return null for malformed UA", () => { + const session = createConfirmedClaudeCodeSession("claude-cli 1.2.3 (external, cli)"); + const result = detectClientFull(session, "claude-code"); + + expect(result.hubConfirmed).toBe(false); + expect(result.subClient).toBeNull(); + }); + + test("should return null when UA has no parentheses section", () => { + const session = createMockSession({ + userAgent: "claude-cli/1.2.3 external, cli", + xApp: "cli", + betas: ["claude-code-a"], + }); + const result = detectClientFull(session, "claude-code"); + + expect(result.hubConfirmed).toBe(true); + expect(result.subClient).toBeNull(); + }); + }); + + describe("matchClientPattern builtin keyword path", () => { + test("should match wildcard claude-code when 3-of-3 is confirmed", () => { + const session = createConfirmedClaudeCodeSession("claude-cli/1.2.3 (external, cli)"); + expect(matchClientPattern(session, "claude-code")).toBe(true); + }); + + test("should match claude-code-cli for cli entrypoint", () => { + const session = createConfirmedClaudeCodeSession("claude-cli/1.2.3 (external, cli)"); + expect(matchClientPattern(session, "claude-code-cli")).toBe(true); + }); + + test("should match claude-code-vscode for claude-vscode entrypoint", () => { + const session = createConfirmedClaudeCodeSession( + "claude-cli/1.2.3 (external, claude-vscode, agent-sdk/0.1.0)" + ); + expect(matchClientPattern(session, "claude-code-vscode")).toBe(true); + }); + + test("should return false when sub-client does not match", () => { + const session = createConfirmedClaudeCodeSession("claude-cli/1.2.3 (external, sdk-py)"); + expect(matchClientPattern(session, "claude-code-sdk-ts")).toBe(false); + }); + + test("should return false when only 2-of-3 signals are present", () => { + const session = createMockSession({ + userAgent: "claude-cli/1.2.3 (external, cli)", + xApp: "cli", + betas: ["non-claude-code"], + }); + expect(matchClientPattern(session, "claude-code")).toBe(false); + }); + }); + + describe("matchClientPattern custom substring path", () => { + test("should match gemini-cli against GeminiCLI", () => { + const session = createMockSession({ userAgent: "GeminiCLI/1.0" }); + expect(matchClientPattern(session, "gemini-cli")).toBe(true); + }); + + test("should match codex-cli against codex_cli", () => { + const session = createMockSession({ userAgent: "codex_cli/2.0" }); + expect(matchClientPattern(session, "codex-cli")).toBe(true); + }); + + test("should return false when User-Agent is empty", () => { + const session = createMockSession({ userAgent: " " }); + expect(matchClientPattern(session, "gemini-cli")).toBe(false); + }); + + test("should return false when custom pattern is not found", () => { + const session = createMockSession({ userAgent: "Mozilla/5.0 Compatible" }); + expect(matchClientPattern(session, "gemini-cli")).toBe(false); + }); + + test("should return false when pattern normalizes to empty", () => { + const session = createMockSession({ userAgent: "AnyClient/1.0" }); + expect(matchClientPattern(session, "-_-")).toBe(false); + }); + }); + + describe("isClientAllowed", () => { + test("should reject when blocked matches even if allowed also matches", () => { + const session = createConfirmedClaudeCodeSession("claude-cli/1.2.3 (external, cli)"); + expect(isClientAllowed(session, ["claude-code"], ["claude-code"])).toBe(false); + }); + + test("should allow when allowedClients and blockedClients are both empty", () => { + const session = createMockSession({ userAgent: "AnyClient/1.0" }); + expect(isClientAllowed(session, [], [])).toBe(true); + }); + + test("should allow when allowedClients match", () => { + const session = createMockSession({ userAgent: "GeminiCLI/1.0" }); + expect(isClientAllowed(session, ["gemini-cli"])).toBe(true); + }); + + test("should reject when allowedClients are set but none match", () => { + const session = createMockSession({ userAgent: "UnknownClient/1.0" }); + expect(isClientAllowed(session, ["gemini-cli"])).toBe(false); + }); + + test("should reject when only blockedClients are set and blocked matches", () => { + const session = createMockSession({ userAgent: "GeminiCLI/1.0" }); + expect(isClientAllowed(session, [], ["gemini-cli"])).toBe(false); + }); + + test("should allow when only blockedClients are set and blocked does not match", () => { + const session = createMockSession({ userAgent: "GeminiCLI/1.0" }); + expect(isClientAllowed(session, [], ["codex-cli"])).toBe(true); + }); + + test("should allow when blocked does not match and allowed matches", () => { + const session = createMockSession({ userAgent: "codex_cli/2.0" }); + expect(isClientAllowed(session, ["codex-cli"], ["gemini-cli"])).toBe(true); + }); + }); + + describe("isClientAllowedDetailed", () => { + test("should return no_restriction when both lists are empty", () => { + const session = createMockSession({ userAgent: "AnyClient/1.0" }); + const result = isClientAllowedDetailed(session, [], []); + expect(result).toEqual({ + allowed: true, + matchType: "no_restriction", + matchedPattern: undefined, + detectedClient: undefined, + checkedAllowlist: [], + checkedBlocklist: [], + }); + }); + + test("should return blocklist_hit with matched pattern", () => { + const session = createMockSession({ userAgent: "GeminiCLI/1.0" }); + const result = isClientAllowedDetailed(session, [], ["gemini-cli"]); + expect(result.allowed).toBe(false); + expect(result.matchType).toBe("blocklist_hit"); + expect(result.matchedPattern).toBe("gemini-cli"); + expect(result.detectedClient).toBe("GeminiCLI/1.0"); + expect(result.checkedBlocklist).toEqual(["gemini-cli"]); + }); + + test("should return allowlist_miss when no allowlist pattern matches", () => { + const session = createMockSession({ userAgent: "UnknownClient/1.0" }); + const result = isClientAllowedDetailed(session, ["gemini-cli", "codex-cli"], []); + expect(result.allowed).toBe(false); + expect(result.matchType).toBe("allowlist_miss"); + expect(result.matchedPattern).toBeUndefined(); + expect(result.detectedClient).toBe("UnknownClient/1.0"); + expect(result.checkedAllowlist).toEqual(["gemini-cli", "codex-cli"]); + }); + + test("should return allowed when allowlist matches", () => { + const session = createMockSession({ userAgent: "GeminiCLI/1.0" }); + const result = isClientAllowedDetailed(session, ["gemini-cli"], []); + expect(result.allowed).toBe(true); + expect(result.matchType).toBe("allowed"); + expect(result.matchedPattern).toBe("gemini-cli"); + expect(result.detectedClient).toBe("GeminiCLI/1.0"); + }); + + test("blocklist takes precedence over allowlist", () => { + const session = createConfirmedClaudeCodeSession("claude-cli/1.2.3 (external, cli)"); + const result = isClientAllowedDetailed(session, ["claude-code"], ["claude-code"]); + expect(result.allowed).toBe(false); + expect(result.matchType).toBe("blocklist_hit"); + expect(result.matchedPattern).toBe("claude-code"); + }); + + test("should detect sub-client for builtin keywords", () => { + const session = createConfirmedClaudeCodeSession("claude-cli/1.2.3 (external, sdk-ts)"); + const result = isClientAllowedDetailed(session, ["claude-code"], []); + expect(result.allowed).toBe(true); + expect(result.matchType).toBe("allowed"); + expect(result.detectedClient).toBe("claude-code-sdk-ts"); + expect(result.matchedPattern).toBe("claude-code"); + }); + + test("should return allowed when only blocklist set and no match", () => { + const session = createMockSession({ userAgent: "CodexCLI/1.0" }); + const result = isClientAllowedDetailed(session, [], ["gemini-cli"]); + expect(result.allowed).toBe(true); + expect(result.matchType).toBe("allowed"); + expect(result.detectedClient).toBe("CodexCLI/1.0"); + }); + + test("should return no_restriction when blockedClients is undefined and allowlist empty", () => { + const session = createMockSession({ userAgent: "AnyClient/1.0" }); + const result = isClientAllowedDetailed(session, []); + expect(result.allowed).toBe(true); + expect(result.matchType).toBe("no_restriction"); + }); + + test("should capture first matching blocked pattern", () => { + const session = createMockSession({ userAgent: "GeminiCLI/1.0" }); + const result = isClientAllowedDetailed( + session, + [], + ["codex-cli", "gemini-cli", "factory-cli"] + ); + expect(result.allowed).toBe(false); + expect(result.matchType).toBe("blocklist_hit"); + expect(result.matchedPattern).toBe("gemini-cli"); + }); + }); + + describe("detectClientFull", () => { + test("should return matched=true for confirmed claude-code wildcard", () => { + const session = createConfirmedClaudeCodeSession("claude-cli/1.2.3 (external, sdk-ts)"); + const result = detectClientFull(session, "claude-code"); + + expect(result).toEqual({ + matched: true, + hubConfirmed: true, + subClient: "claude-code-sdk-ts", + signals: ["x-app-cli", "ua-prefix", "betas-claude-code"], + supplementary: [], + }); + }); + + test("should return matched=false for confirmed but different builtin sub-client", () => { + const session = createConfirmedClaudeCodeSession("claude-cli/1.2.3 (external, sdk-ts)"); + const result = detectClientFull(session, "claude-code-cli"); + + expect(result.hubConfirmed).toBe(true); + expect(result.subClient).toBe("claude-code-sdk-ts"); + expect(result.matched).toBe(false); + }); + + test("should use custom normalization path for non-builtin patterns", () => { + const session = createMockSession({ userAgent: "GeminiCLI/0.22.5" }); + const result = detectClientFull(session, "gemini-cli"); + + expect(result.matched).toBe(true); + expect(result.hubConfirmed).toBe(false); + expect(result.subClient).toBeNull(); + }); + + test("should return matched=false for custom pattern when User-Agent is missing", () => { + const session = createMockSession({ userAgent: null }); + const result = detectClientFull(session, "gemini-cli"); + + expect(result.matched).toBe(false); + expect(result.hubConfirmed).toBe(false); + expect(result.signals).toEqual([]); + expect(result.supplementary).toEqual([]); + }); + }); +}); diff --git a/tests/unit/proxy/client-guard.test.ts b/tests/unit/proxy/client-guard.test.ts index 83fda1b18..89b1633fb 100644 --- a/tests/unit/proxy/client-guard.test.ts +++ b/tests/unit/proxy/client-guard.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test, vi, beforeEach } from "vitest"; +import { describe, expect, test, vi } from "vitest"; import { ProxyClientGuard } from "@/app/v1/_lib/proxy/client-guard"; import type { ProxySession } from "@/app/v1/_lib/proxy/session"; @@ -13,13 +13,17 @@ vi.mock("@/app/v1/_lib/proxy/responses", () => ({ // Helper to create mock session function createMockSession( userAgent: string | undefined, - allowedClients: string[] = [] + allowedClients: string[] = [], + blockedClients: string[] = [] ): ProxySession { return { userAgent, + headers: new Headers(), + request: { message: {} }, authState: { user: { allowedClients, + blockedClients, }, }, } as unknown as ProxySession; @@ -57,6 +61,14 @@ describe("ProxyClientGuard", () => { }); }); + describe("when both allowedClients and blockedClients are empty", () => { + test("should allow request", async () => { + const session = createMockSession("AnyClient/1.0", [], []); + const result = await ProxyClientGuard.ensure(session); + expect(result).toBeNull(); + }); + }); + describe("when restrictions are configured", () => { test("should reject when User-Agent is missing", async () => { const session = createMockSession(undefined, ["claude-cli"]); @@ -196,4 +208,41 @@ describe("ProxyClientGuard", () => { expect(result).toBeNull(); }); }); + + describe("when blockedClients is configured", () => { + test("should reject when client matches blocked pattern", async () => { + const session = createMockSession("GeminiCLI/1.0", [], ["gemini-cli"]); + const result = await ProxyClientGuard.ensure(session); + expect(result).not.toBeNull(); + expect(result?.status).toBe(400); + }); + + test("should allow when client does not match blocked pattern", async () => { + const session = createMockSession("CodexCLI/1.0", [], ["gemini-cli"]); + const result = await ProxyClientGuard.ensure(session); + expect(result).toBeNull(); + }); + + test("should reject even when allowedClients matches", async () => { + const session = createMockSession("gemini-cli/1.0", ["gemini-cli"], ["gemini-cli"]); + const result = await ProxyClientGuard.ensure(session); + expect(result).not.toBeNull(); + expect(result?.status).toBe(400); + }); + }); + + describe("when only blockedClients is configured (no allowedClients)", () => { + test("should reject matching client", async () => { + const session = createMockSession("codex-cli/2.0", [], ["codex-cli"]); + const result = await ProxyClientGuard.ensure(session); + expect(result).not.toBeNull(); + expect(result?.status).toBe(400); + }); + + test("should allow non-matching client", async () => { + const session = createMockSession("claude-cli/1.0", [], ["codex-cli"]); + const result = await ProxyClientGuard.ensure(session); + expect(result).toBeNull(); + }); + }); }); diff --git a/tests/unit/settings/providers/provider-form-endpoint-pool.test.tsx b/tests/unit/settings/providers/provider-form-endpoint-pool.test.tsx index 18e8bb5d3..edf4f19c4 100644 --- a/tests/unit/settings/providers/provider-form-endpoint-pool.test.tsx +++ b/tests/unit/settings/providers/provider-form-endpoint-pool.test.tsx @@ -304,6 +304,12 @@ describe("ProviderForm: endpoint pool integration", () => { } expect(providersActionMocks.addProvider).toHaveBeenCalledTimes(1); + expect(providersActionMocks.addProvider).toHaveBeenCalledWith( + expect.objectContaining({ + allowed_clients: [], + blocked_clients: [], + }) + ); await flushTicks(3); expect(providerEndpointsActionMocks.addProviderEndpoint).toHaveBeenCalledTimes(0); diff --git a/tests/unit/user-dialogs.test.tsx b/tests/unit/user-dialogs.test.tsx index 32aa965c2..772947529 100644 --- a/tests/unit/user-dialogs.test.tsx +++ b/tests/unit/user-dialogs.test.tsx @@ -256,6 +256,14 @@ const messages = { description: "Restrict clients", customLabel: "Custom", customPlaceholder: "Custom client", + customHelp: "Custom help", + }, + blockedClients: { + label: "Blocked Clients", + description: "Blocked description", + customLabel: "Custom blocked", + customPlaceholder: "Blocked client", + customHelp: "Blocked help", }, allowedModels: { label: "Allowed Models", @@ -263,8 +271,12 @@ const messages = { description: "Restrict models", }, }, + actions: { + allow: "Allow", + block: "Block", + }, presetClients: { - "claude-cli": "Claude CLI", + "claude-code": "Claude Code", "gemini-cli": "Gemini CLI", "factory-cli": "Factory CLI", "codex-cli": "Codex CLI",