From 60eb8bdaf646cbf64f05ecb201c09458cbe69ac8 Mon Sep 17 00:00:00 2001 From: mci77777 Date: Tue, 5 May 2026 16:20:09 +0800 Subject: [PATCH 1/2] fix(public-status): persist hourly rollups --- drizzle/0103_public_status_hourly_rollups.sql | 27 + drizzle/meta/0103_snapshot.json | 4726 +++++++++++++++++ drizzle/meta/_journal.json | 7 + src/app/api/public-status/route.ts | 11 +- src/drizzle/schema.ts | 40 + src/lib/public-status/hourly-rollups.ts | 646 +++ src/lib/public-status/read-store.ts | 54 + src/lib/public-status/rebuild-worker.ts | 179 +- .../public-status/route-redis-only.test.ts | 46 + .../unit/public-status/hourly-rollups.test.ts | 262 + tests/unit/public-status/read-store.test.ts | 130 +- .../unit/public-status/rebuild-worker.test.ts | 83 +- 12 files changed, 6086 insertions(+), 125 deletions(-) create mode 100644 drizzle/0103_public_status_hourly_rollups.sql create mode 100644 drizzle/meta/0103_snapshot.json create mode 100644 src/lib/public-status/hourly-rollups.ts create mode 100644 tests/unit/public-status/hourly-rollups.test.ts diff --git a/drizzle/0103_public_status_hourly_rollups.sql b/drizzle/0103_public_status_hourly_rollups.sql new file mode 100644 index 000000000..812dbc85c --- /dev/null +++ b/drizzle/0103_public_status_hourly_rollups.sql @@ -0,0 +1,27 @@ +CREATE TABLE IF NOT EXISTS "public_status_hourly_rollups" ( + "id" serial PRIMARY KEY NOT NULL, + "bucket_start" timestamp with time zone NOT NULL, + "bucket_end" timestamp with time zone NOT NULL, + "config_version" varchar(128) NOT NULL, + "source_group_name" varchar(200) NOT NULL, + "public_group_slug" varchar(120) NOT NULL, + "public_model_key" varchar(200) NOT NULL, + "label" varchar(200) NOT NULL, + "vendor_icon_key" varchar(100) NOT NULL, + "request_type_badge" varchar(100) NOT NULL, + "state" varchar(20) NOT NULL, + "success_count" integer DEFAULT 0 NOT NULL, + "failure_count" integer DEFAULT 0 NOT NULL, + "sample_count" integer DEFAULT 0 NOT NULL, + "availability_pct" double precision, + "ttfb_ms" double precision, + "tps" double precision, + "generated_at" timestamp with time zone NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "uniq_public_status_hourly_rollup" ON "public_status_hourly_rollups" USING btree ("bucket_start","public_group_slug","public_model_key","request_type_badge");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "idx_public_status_hourly_rollups_bucket" ON "public_status_hourly_rollups" USING btree ("bucket_start");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "idx_public_status_hourly_rollups_config_bucket" ON "public_status_hourly_rollups" USING btree ("config_version","bucket_start");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "idx_public_status_hourly_rollups_group_model" ON "public_status_hourly_rollups" USING btree ("public_group_slug","public_model_key"); diff --git a/drizzle/meta/0103_snapshot.json b/drizzle/meta/0103_snapshot.json new file mode 100644 index 000000000..4d639d2a1 --- /dev/null +++ b/drizzle/meta/0103_snapshot.json @@ -0,0 +1,4726 @@ +{ + "id": "1e45c2c7-eb61-4ebe-aa97-738780ebe4d8", + "prevId": "278b6561-15fa-4eac-804d-228f8480e173", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "action_category": { + "name": "action_category", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "action_type": { + "name": "action_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "target_id": { + "name": "target_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "target_name": { + "name": "target_name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "before_value": { + "name": "before_value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "after_value": { + "name": "after_value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "operator_user_id": { + "name": "operator_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "operator_user_name": { + "name": "operator_user_name", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "operator_key_id": { + "name": "operator_key_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "operator_key_name": { + "name": "operator_key_name", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "operator_ip": { + "name": "operator_ip", + "type": "varchar(45)", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_audit_log_category_created_at": { + "name": "idx_audit_log_category_created_at", + "columns": [ + { + "expression": "action_category", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_log_operator_user_created_at": { + "name": "idx_audit_log_operator_user_created_at", + "columns": [ + { + "expression": "operator_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"audit_log\".\"operator_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_log_operator_ip_created_at": { + "name": "idx_audit_log_operator_ip_created_at", + "columns": [ + { + "expression": "operator_ip", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"audit_log\".\"operator_ip\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_log_target": { + "name": "idx_audit_log_target", + "columns": [ + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"audit_log\".\"target_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_log_created_at_id": { + "name": "idx_audit_log_created_at_id", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.error_rules": { + "name": "error_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "pattern": { + "name": "pattern", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "match_type": { + "name": "match_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'regex'" + }, + "category": { + "name": "category", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "override_response": { + "name": "override_response", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "override_status_code": { + "name": "override_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_error_rules_enabled": { + "name": "idx_error_rules_enabled", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "unique_pattern": { + "name": "unique_pattern", + "columns": [ + { + "expression": "pattern", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_category": { + "name": "idx_category", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_match_type": { + "name": "idx_match_type", + "columns": [ + { + "expression": "match_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.keys": { + "name": "keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "can_login_web_ui": { + "name": "can_login_web_ui", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "limit_5h_usd": { + "name": "limit_5h_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_5h_reset_mode": { + "name": "limit_5h_reset_mode", + "type": "daily_reset_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'rolling'" + }, + "limit_daily_usd": { + "name": "limit_daily_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "daily_reset_mode": { + "name": "daily_reset_mode", + "type": "daily_reset_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'fixed'" + }, + "daily_reset_time": { + "name": "daily_reset_time", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'00:00'" + }, + "limit_weekly_usd": { + "name": "limit_weekly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_monthly_usd": { + "name": "limit_monthly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_total_usd": { + "name": "limit_total_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "cost_reset_at": { + "name": "cost_reset_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "limit_concurrent_sessions": { + "name": "limit_concurrent_sessions", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "provider_group": { + "name": "provider_group", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false, + "default": "'default'" + }, + "cache_ttl_preference": { + "name": "cache_ttl_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_keys_user_id": { + "name": "idx_keys_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_keys_key": { + "name": "idx_keys_key", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_keys_created_at": { + "name": "idx_keys_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_keys_deleted_at": { + "name": "idx_keys_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.message_request": { + "name": "message_request", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cost_usd": { + "name": "cost_usd", + "type": "numeric(21, 15)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "cost_multiplier": { + "name": "cost_multiplier", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": false + }, + "group_cost_multiplier": { + "name": "group_cost_multiplier", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": false + }, + "cost_breakdown": { + "name": "cost_breakdown", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "request_sequence": { + "name": "request_sequence", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "provider_chain": { + "name": "provider_chain", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "api_type": { + "name": "api_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "endpoint": { + "name": "endpoint", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "original_model": { + "name": "original_model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "actual_response_model": { + "name": "actual_response_model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "ttfb_ms": { + "name": "ttfb_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_read_input_tokens": { + "name": "cache_read_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_creation_5m_input_tokens": { + "name": "cache_creation_5m_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_creation_1h_input_tokens": { + "name": "cache_creation_1h_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_ttl_applied": { + "name": "cache_ttl_applied", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "context_1m_applied": { + "name": "context_1m_applied", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "swap_cache_ttl_applied": { + "name": "swap_cache_ttl_applied", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "special_settings": { + "name": "special_settings", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_stack": { + "name": "error_stack", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_cause": { + "name": "error_cause", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "blocked_by": { + "name": "blocked_by", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "blocked_reason": { + "name": "blocked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "client_ip": { + "name": "client_ip", + "type": "varchar(45)", + "primaryKey": false, + "notNull": false + }, + "messages_count": { + "name": "messages_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_message_request_user_date_cost": { + "name": "idx_message_request_user_date_cost", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_user_created_at_cost_stats": { + "name": "idx_message_request_user_created_at_cost_stats", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_user_query": { + "name": "idx_message_request_user_query", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_provider_created_at_active": { + "name": "idx_message_request_provider_created_at_active", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_provider_created_at_finalized_active": { + "name": "idx_message_request_provider_created_at_finalized_active", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"status_code\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_session_id": { + "name": "idx_message_request_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_session_id_prefix": { + "name": "idx_message_request_session_id_prefix", + "columns": [ + { + "expression": "\"session_id\" varchar_pattern_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_session_seq": { + "name": "idx_message_request_session_seq", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_endpoint": { + "name": "idx_message_request_endpoint", + "columns": [ + { + "expression": "endpoint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_blocked_by": { + "name": "idx_message_request_blocked_by", + "columns": [ + { + "expression": "blocked_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_provider_id": { + "name": "idx_message_request_provider_id", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_user_id": { + "name": "idx_message_request_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key": { + "name": "idx_message_request_key", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key_created_at_id": { + "name": "idx_message_request_key_created_at_id", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key_model_active": { + "name": "idx_message_request_key_model_active", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"model\" IS NOT NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key_endpoint_active": { + "name": "idx_message_request_key_endpoint_active", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "endpoint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"endpoint\" IS NOT NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_created_at_id_active": { + "name": "idx_message_request_created_at_id_active", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_model_active": { + "name": "idx_message_request_model_active", + "columns": [ + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"model\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_status_code_active": { + "name": "idx_message_request_status_code_active", + "columns": [ + { + "expression": "status_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"status_code\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_created_at": { + "name": "idx_message_request_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_deleted_at": { + "name": "idx_message_request_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key_last_active": { + "name": "idx_message_request_key_last_active", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key_cost_active": { + "name": "idx_message_request_key_cost_active", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_session_user_info": { + "name": "idx_message_request_session_user_info", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_client_ip_created_at": { + "name": "idx_message_request_client_ip_created_at", + "columns": [ + { + "expression": "client_ip", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"client_ip\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_prices": { + "name": "model_prices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "model_name": { + "name": "model_name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "price_data": { + "name": "price_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'litellm'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_model_prices_latest": { + "name": "idx_model_prices_latest", + "columns": [ + { + "expression": "model_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_model_prices_model_name": { + "name": "idx_model_prices_model_name", + "columns": [ + { + "expression": "model_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_model_prices_created_at": { + "name": "idx_model_prices_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_model_prices_source": { + "name": "idx_model_prices_source", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_settings": { + "name": "notification_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "use_legacy_mode": { + "name": "use_legacy_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "circuit_breaker_enabled": { + "name": "circuit_breaker_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "circuit_breaker_webhook": { + "name": "circuit_breaker_webhook", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "daily_leaderboard_enabled": { + "name": "daily_leaderboard_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "daily_leaderboard_webhook": { + "name": "daily_leaderboard_webhook", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "daily_leaderboard_time": { + "name": "daily_leaderboard_time", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false, + "default": "'09:00'" + }, + "daily_leaderboard_top_n": { + "name": "daily_leaderboard_top_n", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5 + }, + "cost_alert_enabled": { + "name": "cost_alert_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cost_alert_webhook": { + "name": "cost_alert_webhook", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "cost_alert_threshold": { + "name": "cost_alert_threshold", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false, + "default": "'0.80'" + }, + "cost_alert_check_interval": { + "name": "cost_alert_check_interval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 60 + }, + "cache_hit_rate_alert_enabled": { + "name": "cache_hit_rate_alert_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cache_hit_rate_alert_webhook": { + "name": "cache_hit_rate_alert_webhook", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "cache_hit_rate_alert_window_mode": { + "name": "cache_hit_rate_alert_window_mode", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false, + "default": "'auto'" + }, + "cache_hit_rate_alert_check_interval": { + "name": "cache_hit_rate_alert_check_interval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5 + }, + "cache_hit_rate_alert_historical_lookback_days": { + "name": "cache_hit_rate_alert_historical_lookback_days", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 7 + }, + "cache_hit_rate_alert_min_eligible_requests": { + "name": "cache_hit_rate_alert_min_eligible_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 20 + }, + "cache_hit_rate_alert_min_eligible_tokens": { + "name": "cache_hit_rate_alert_min_eligible_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "cache_hit_rate_alert_abs_min": { + "name": "cache_hit_rate_alert_abs_min", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "cache_hit_rate_alert_drop_rel": { + "name": "cache_hit_rate_alert_drop_rel", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.3'" + }, + "cache_hit_rate_alert_drop_abs": { + "name": "cache_hit_rate_alert_drop_abs", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.1'" + }, + "cache_hit_rate_alert_cooldown_minutes": { + "name": "cache_hit_rate_alert_cooldown_minutes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30 + }, + "cache_hit_rate_alert_top_n": { + "name": "cache_hit_rate_alert_top_n", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 10 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_target_bindings": { + "name": "notification_target_bindings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "notification_type": { + "name": "notification_type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "schedule_cron": { + "name": "schedule_cron", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "schedule_timezone": { + "name": "schedule_timezone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "template_override": { + "name": "template_override", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "unique_notification_target_binding": { + "name": "unique_notification_target_binding", + "columns": [ + { + "expression": "notification_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_notification_bindings_type": { + "name": "idx_notification_bindings_type", + "columns": [ + { + "expression": "notification_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_notification_bindings_target": { + "name": "idx_notification_bindings_target", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notification_target_bindings_target_id_webhook_targets_id_fk": { + "name": "notification_target_bindings_target_id_webhook_targets_id_fk", + "tableFrom": "notification_target_bindings", + "tableTo": "webhook_targets", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_endpoint_probe_logs": { + "name": "provider_endpoint_probe_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "endpoint_id": { + "name": "endpoint_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'scheduled'" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error_type": { + "name": "error_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_provider_endpoint_probe_logs_endpoint_created_at": { + "name": "idx_provider_endpoint_probe_logs_endpoint_created_at", + "columns": [ + { + "expression": "endpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoint_probe_logs_created_at": { + "name": "idx_provider_endpoint_probe_logs_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "provider_endpoint_probe_logs_endpoint_id_provider_endpoints_id_fk": { + "name": "provider_endpoint_probe_logs_endpoint_id_provider_endpoints_id_fk", + "tableFrom": "provider_endpoint_probe_logs", + "tableTo": "provider_endpoints", + "columnsFrom": [ + "endpoint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_endpoints": { + "name": "provider_endpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provider_type": { + "name": "provider_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'claude'" + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_probed_at": { + "name": "last_probed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_probe_ok": { + "name": "last_probe_ok", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "last_probe_status_code": { + "name": "last_probe_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_probe_latency_ms": { + "name": "last_probe_latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_probe_error_type": { + "name": "last_probe_error_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "last_probe_error_message": { + "name": "last_probe_error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "uniq_provider_endpoints_vendor_type_url": { + "name": "uniq_provider_endpoints_vendor_type_url", + "columns": [ + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"provider_endpoints\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_vendor_type": { + "name": "idx_provider_endpoints_vendor_type", + "columns": [ + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"provider_endpoints\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_enabled": { + "name": "idx_provider_endpoints_enabled", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"provider_endpoints\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_pick_enabled": { + "name": "idx_provider_endpoints_pick_enabled", + "columns": [ + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"provider_endpoints\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_created_at": { + "name": "idx_provider_endpoints_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_deleted_at": { + "name": "idx_provider_endpoints_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "provider_endpoints_vendor_id_provider_vendors_id_fk": { + "name": "provider_endpoints_vendor_id_provider_vendors_id_fk", + "tableFrom": "provider_endpoints", + "tableTo": "provider_vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_groups": { + "name": "provider_groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "cost_multiplier": { + "name": "cost_multiplier", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": true, + "default": "'1.0'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "provider_groups_name_unique": { + "name": "provider_groups_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_vendors": { + "name": "provider_vendors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "website_domain": { + "name": "website_domain", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "website_url": { + "name": "website_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "favicon_url": { + "name": "favicon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "uniq_provider_vendors_website_domain": { + "name": "uniq_provider_vendors_website_domain", + "columns": [ + { + "expression": "website_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_vendors_created_at": { + "name": "idx_provider_vendors_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.providers": { + "name": "providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "provider_vendor_id": { + "name": "provider_vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "weight": { + "name": "weight", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "group_priorities": { + "name": "group_priorities", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'null'::jsonb" + }, + "cost_multiplier": { + "name": "cost_multiplier", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": false, + "default": "'1.0'" + }, + "group_tag": { + "name": "group_tag", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "provider_type": { + "name": "provider_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'claude'" + }, + "preserve_client_ip": { + "name": "preserve_client_ip", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "disable_session_reuse": { + "name": "disable_session_reuse", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "model_redirects": { + "name": "model_redirects", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "allowed_models": { + "name": "allowed_models", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'null'::jsonb" + }, + "allowed_clients": { + "name": "allowed_clients", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "blocked_clients": { + "name": "blocked_clients", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "active_time_start": { + "name": "active_time_start", + "type": "varchar(5)", + "primaryKey": false, + "notNull": false + }, + "active_time_end": { + "name": "active_time_end", + "type": "varchar(5)", + "primaryKey": false, + "notNull": false + }, + "codex_instructions_strategy": { + "name": "codex_instructions_strategy", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "default": "'auto'" + }, + "mcp_passthrough_type": { + "name": "mcp_passthrough_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "mcp_passthrough_url": { + "name": "mcp_passthrough_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "limit_5h_usd": { + "name": "limit_5h_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_5h_reset_mode": { + "name": "limit_5h_reset_mode", + "type": "daily_reset_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'rolling'" + }, + "limit_daily_usd": { + "name": "limit_daily_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "daily_reset_mode": { + "name": "daily_reset_mode", + "type": "daily_reset_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'fixed'" + }, + "daily_reset_time": { + "name": "daily_reset_time", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'00:00'" + }, + "limit_weekly_usd": { + "name": "limit_weekly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_monthly_usd": { + "name": "limit_monthly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_total_usd": { + "name": "limit_total_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "total_cost_reset_at": { + "name": "total_cost_reset_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "limit_concurrent_sessions": { + "name": "limit_concurrent_sessions", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "max_retry_attempts": { + "name": "max_retry_attempts", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "circuit_breaker_failure_threshold": { + "name": "circuit_breaker_failure_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5 + }, + "circuit_breaker_open_duration": { + "name": "circuit_breaker_open_duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1800000 + }, + "circuit_breaker_half_open_success_threshold": { + "name": "circuit_breaker_half_open_success_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 2 + }, + "proxy_url": { + "name": "proxy_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "proxy_fallback_to_direct": { + "name": "proxy_fallback_to_direct", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "custom_headers": { + "name": "custom_headers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "first_byte_timeout_streaming_ms": { + "name": "first_byte_timeout_streaming_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "streaming_idle_timeout_ms": { + "name": "streaming_idle_timeout_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "request_timeout_non_streaming_ms": { + "name": "request_timeout_non_streaming_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "website_url": { + "name": "website_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "favicon_url": { + "name": "favicon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cache_ttl_preference": { + "name": "cache_ttl_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "swap_cache_ttl_billing": { + "name": "swap_cache_ttl_billing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "context_1m_preference": { + "name": "context_1m_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "codex_reasoning_effort_preference": { + "name": "codex_reasoning_effort_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "codex_reasoning_summary_preference": { + "name": "codex_reasoning_summary_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "codex_text_verbosity_preference": { + "name": "codex_text_verbosity_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "codex_parallel_tool_calls_preference": { + "name": "codex_parallel_tool_calls_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "codex_service_tier_preference": { + "name": "codex_service_tier_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "anthropic_max_tokens_preference": { + "name": "anthropic_max_tokens_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "anthropic_thinking_budget_preference": { + "name": "anthropic_thinking_budget_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "anthropic_adaptive_thinking": { + "name": "anthropic_adaptive_thinking", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'null'::jsonb" + }, + "gemini_google_search_preference": { + "name": "gemini_google_search_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "tpm": { + "name": "tpm", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "rpm": { + "name": "rpm", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "rpd": { + "name": "rpd", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "cc": { + "name": "cc", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_providers_enabled_priority": { + "name": "idx_providers_enabled_priority", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "weight", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"providers\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_group": { + "name": "idx_providers_group", + "columns": [ + { + "expression": "group_tag", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"providers\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_vendor_type_url_active": { + "name": "idx_providers_vendor_type_url_active", + "columns": [ + { + "expression": "provider_vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"providers\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_created_at": { + "name": "idx_providers_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_deleted_at": { + "name": "idx_providers_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_vendor_type": { + "name": "idx_providers_vendor_type", + "columns": [ + { + "expression": "provider_vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"providers\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_enabled_vendor_type": { + "name": "idx_providers_enabled_vendor_type", + "columns": [ + { + "expression": "provider_vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"providers\".\"deleted_at\" IS NULL AND \"providers\".\"is_enabled\" = true AND \"providers\".\"provider_vendor_id\" IS NOT NULL AND \"providers\".\"provider_vendor_id\" > 0", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "providers_provider_vendor_id_provider_vendors_id_fk": { + "name": "providers_provider_vendor_id_provider_vendors_id_fk", + "tableFrom": "providers", + "tableTo": "provider_vendors", + "columnsFrom": [ + "provider_vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.public_status_hourly_rollups": { + "name": "public_status_hourly_rollups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "bucket_start": { + "name": "bucket_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "bucket_end": { + "name": "bucket_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "config_version": { + "name": "config_version", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "source_group_name": { + "name": "source_group_name", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "public_group_slug": { + "name": "public_group_slug", + "type": "varchar(120)", + "primaryKey": false, + "notNull": true + }, + "public_model_key": { + "name": "public_model_key", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "vendor_icon_key": { + "name": "vendor_icon_key", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "request_type_badge": { + "name": "request_type_badge", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "success_count": { + "name": "success_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failure_count": { + "name": "failure_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sample_count": { + "name": "sample_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "availability_pct": { + "name": "availability_pct", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "ttfb_ms": { + "name": "ttfb_ms", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "tps": { + "name": "tps", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "generated_at": { + "name": "generated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uniq_public_status_hourly_rollup": { + "name": "uniq_public_status_hourly_rollup", + "columns": [ + { + "expression": "bucket_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "public_group_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "public_model_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_type_badge", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_public_status_hourly_rollups_bucket": { + "name": "idx_public_status_hourly_rollups_bucket", + "columns": [ + { + "expression": "bucket_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_public_status_hourly_rollups_config_bucket": { + "name": "idx_public_status_hourly_rollups_config_bucket", + "columns": [ + { + "expression": "config_version", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "bucket_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_public_status_hourly_rollups_group_model": { + "name": "idx_public_status_hourly_rollups_group_model", + "columns": [ + { + "expression": "public_group_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "public_model_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.request_filters": { + "name": "request_filters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true + }, + "match_type": { + "name": "match_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "target": { + "name": "target", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "replacement": { + "name": "replacement", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "binding_type": { + "name": "binding_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'global'" + }, + "provider_ids": { + "name": "provider_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "group_tags": { + "name": "group_tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rule_mode": { + "name": "rule_mode", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'simple'" + }, + "execution_phase": { + "name": "execution_phase", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'guard'" + }, + "operations": { + "name": "operations", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_request_filters_enabled": { + "name": "idx_request_filters_enabled", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_request_filters_scope": { + "name": "idx_request_filters_scope", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_request_filters_action": { + "name": "idx_request_filters_action", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_request_filters_binding": { + "name": "idx_request_filters_binding", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "binding_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_request_filters_phase": { + "name": "idx_request_filters_phase", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_phase", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sensitive_words": { + "name": "sensitive_words", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "word": { + "name": "word", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "match_type": { + "name": "match_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'contains'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_sensitive_words_enabled": { + "name": "idx_sensitive_words_enabled", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "match_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sensitive_words_created_at": { + "name": "idx_sensitive_words_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.system_settings": { + "name": "system_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "site_title": { + "name": "site_title", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "default": "'Claude Code Hub'" + }, + "allow_global_usage_view": { + "name": "allow_global_usage_view", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "currency_display": { + "name": "currency_display", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "billing_model_source": { + "name": "billing_model_source", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'original'" + }, + "codex_priority_billing_source": { + "name": "codex_priority_billing_source", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'requested'" + }, + "bill_non_successful_requests": { + "name": "bill_non_successful_requests", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "timezone": { + "name": "timezone", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "enable_auto_cleanup": { + "name": "enable_auto_cleanup", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "cleanup_retention_days": { + "name": "cleanup_retention_days", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30 + }, + "cleanup_schedule": { + "name": "cleanup_schedule", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "default": "'0 2 * * *'" + }, + "cleanup_batch_size": { + "name": "cleanup_batch_size", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 10000 + }, + "enable_client_version_check": { + "name": "enable_client_version_check", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "verbose_provider_error": { + "name": "verbose_provider_error", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pass_through_upstream_error_message": { + "name": "pass_through_upstream_error_message", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_http2": { + "name": "enable_http2", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_openai_responses_websocket": { + "name": "enable_openai_responses_websocket", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_high_concurrency_mode": { + "name": "enable_high_concurrency_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "intercept_anthropic_warmup_requests": { + "name": "intercept_anthropic_warmup_requests", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_thinking_signature_rectifier": { + "name": "enable_thinking_signature_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_thinking_budget_rectifier": { + "name": "enable_thinking_budget_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_billing_header_rectifier": { + "name": "enable_billing_header_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_response_input_rectifier": { + "name": "enable_response_input_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_non_conversation_endpoint_provider_fallback": { + "name": "allow_non_conversation_endpoint_provider_fallback", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "fake_streaming_whitelist": { + "name": "fake_streaming_whitelist", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "enable_codex_session_id_completion": { + "name": "enable_codex_session_id_completion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_claude_metadata_user_id_injection": { + "name": "enable_claude_metadata_user_id_injection", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_response_fixer": { + "name": "enable_response_fixer", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "response_fixer_config": { + "name": "response_fixer_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{\"fixTruncatedJson\":true,\"fixSseFormat\":true,\"fixEncoding\":true,\"maxJsonDepth\":200,\"maxFixSize\":1048576}'::jsonb" + }, + "quota_db_refresh_interval_seconds": { + "name": "quota_db_refresh_interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 10 + }, + "quota_lease_percent_5h": { + "name": "quota_lease_percent_5h", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "quota_lease_percent_daily": { + "name": "quota_lease_percent_daily", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "quota_lease_percent_weekly": { + "name": "quota_lease_percent_weekly", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "quota_lease_percent_monthly": { + "name": "quota_lease_percent_monthly", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "quota_lease_cap_usd": { + "name": "quota_lease_cap_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "ip_extraction_config": { + "name": "ip_extraction_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ip_geo_lookup_enabled": { + "name": "ip_geo_lookup_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "public_status_window_hours": { + "name": "public_status_window_hours", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 24 + }, + "public_status_aggregation_interval_minutes": { + "name": "public_status_aggregation_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_ledger": { + "name": "usage_ledger", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "final_provider_id": { + "name": "final_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "original_model": { + "name": "original_model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "actual_response_model": { + "name": "actual_response_model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "endpoint": { + "name": "endpoint", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "api_type": { + "name": "api_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_success": { + "name": "is_success", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "success_rate_outcome": { + "name": "success_rate_outcome", + "type": "varchar(16)", + "primaryKey": false, + "notNull": false + }, + "blocked_by": { + "name": "blocked_by", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "cost_usd": { + "name": "cost_usd", + "type": "numeric(21, 15)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "cost_multiplier": { + "name": "cost_multiplier", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": false + }, + "group_cost_multiplier": { + "name": "group_cost_multiplier", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_read_input_tokens": { + "name": "cache_read_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_creation_5m_input_tokens": { + "name": "cache_creation_5m_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_creation_1h_input_tokens": { + "name": "cache_creation_1h_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_ttl_applied": { + "name": "cache_ttl_applied", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "context_1m_applied": { + "name": "context_1m_applied", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "swap_cache_ttl_applied": { + "name": "swap_cache_ttl_applied", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ttfb_ms": { + "name": "ttfb_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_ip": { + "name": "client_ip", + "type": "varchar(45)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_usage_ledger_request_id": { + "name": "idx_usage_ledger_request_id", + "columns": [ + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_user_created_at": { + "name": "idx_usage_ledger_user_created_at", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_key_created_at": { + "name": "idx_usage_ledger_key_created_at", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_provider_created_at": { + "name": "idx_usage_ledger_provider_created_at", + "columns": [ + { + "expression": "final_provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_created_at_minute": { + "name": "idx_usage_ledger_created_at_minute", + "columns": [ + { + "expression": "date_trunc('minute', \"created_at\" AT TIME ZONE 'UTC')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_created_at_desc_id": { + "name": "idx_usage_ledger_created_at_desc_id", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_session_id": { + "name": "idx_usage_ledger_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"session_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_model": { + "name": "idx_usage_ledger_model", + "columns": [ + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"model\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_key_cost": { + "name": "idx_usage_ledger_key_cost", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_user_cost_cover": { + "name": "idx_usage_ledger_user_cost_cover", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_provider_cost_cover": { + "name": "idx_usage_ledger_provider_cost_cover", + "columns": [ + { + "expression": "final_provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_key_created_at_desc_cover": { + "name": "idx_usage_ledger_key_created_at_desc_cover", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"created_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "final_provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "varchar", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "rpm_limit": { + "name": "rpm_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "daily_limit_usd": { + "name": "daily_limit_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "provider_group": { + "name": "provider_group", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false, + "default": "'default'" + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "limit_5h_usd": { + "name": "limit_5h_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_5h_reset_mode": { + "name": "limit_5h_reset_mode", + "type": "daily_reset_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'rolling'" + }, + "limit_weekly_usd": { + "name": "limit_weekly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_monthly_usd": { + "name": "limit_monthly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_total_usd": { + "name": "limit_total_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "cost_reset_at": { + "name": "cost_reset_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "limit_5h_cost_reset_at": { + "name": "limit_5h_cost_reset_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "limit_concurrent_sessions": { + "name": "limit_concurrent_sessions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "daily_reset_mode": { + "name": "daily_reset_mode", + "type": "daily_reset_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'fixed'" + }, + "daily_reset_time": { + "name": "daily_reset_time", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'00:00'" + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "allowed_clients": { + "name": "allowed_clients", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "allowed_models": { + "name": "allowed_models", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "blocked_clients": { + "name": "blocked_clients", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_users_active_role_sort": { + "name": "idx_users_active_role_sort", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"users\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_users_enabled_expires_at": { + "name": "idx_users_enabled_expires_at", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"users\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_users_tags_gin": { + "name": "idx_users_tags_gin", + "columns": [ + { + "expression": "tags", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"users\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_users_created_at": { + "name": "idx_users_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_users_deleted_at": { + "name": "idx_users_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_targets": { + "name": "webhook_targets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "provider_type": { + "name": "provider_type", + "type": "webhook_provider_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "webhook_url": { + "name": "webhook_url", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": false + }, + "telegram_bot_token": { + "name": "telegram_bot_token", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "telegram_chat_id": { + "name": "telegram_chat_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "dingtalk_secret": { + "name": "dingtalk_secret", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "custom_template": { + "name": "custom_template", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "custom_headers": { + "name": "custom_headers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "proxy_url": { + "name": "proxy_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "proxy_fallback_to_direct": { + "name": "proxy_fallback_to_direct", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_test_at": { + "name": "last_test_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_test_result": { + "name": "last_test_result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.daily_reset_mode": { + "name": "daily_reset_mode", + "schema": "public", + "values": [ + "fixed", + "rolling" + ] + }, + "public.notification_type": { + "name": "notification_type", + "schema": "public", + "values": [ + "circuit_breaker", + "daily_leaderboard", + "cost_alert", + "cache_hit_rate_alert" + ] + }, + "public.webhook_provider_type": { + "name": "webhook_provider_type", + "schema": "public", + "values": [ + "wechat", + "feishu", + "dingtalk", + "telegram", + "custom" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 35050d3d0..890b11a64 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -722,6 +722,13 @@ "when": 1777739658761, "tag": "0102_useful_lionheart", "breakpoints": true + }, + { + "idx": 103, + "version": "7", + "when": 1777968803451, + "tag": "0103_public_status_hourly_rollups", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/app/api/public-status/route.ts b/src/app/api/public-status/route.ts index 3c1122e10..ae30e4c13 100644 --- a/src/app/api/public-status/route.ts +++ b/src/app/api/public-status/route.ts @@ -1,5 +1,8 @@ import { NextResponse } from "next/server"; -import { readCurrentPublicStatusConfigSnapshot } from "@/lib/public-status/config-snapshot"; +import { + readCurrentInternalPublicStatusConfigSnapshot, + readCurrentPublicStatusConfigSnapshot, +} from "@/lib/public-status/config-snapshot"; import { buildPublicStatusRouteResponse, PublicStatusQueryValidationError, @@ -11,7 +14,10 @@ import { schedulePublicStatusRebuild } from "@/lib/public-status/rebuild-hints"; export async function GET(request: Request): Promise { try { const url = new URL(request.url); - const configSnapshot = await readCurrentPublicStatusConfigSnapshot(); + const [configSnapshot, internalConfigSnapshot] = await Promise.all([ + readCurrentPublicStatusConfigSnapshot(), + readCurrentInternalPublicStatusConfigSnapshot(), + ]); const defaults = { intervalMinutes: configSnapshot?.defaultIntervalMinutes ?? 5, rangeHours: configSnapshot?.defaultRangeHours ?? 24, @@ -23,6 +29,7 @@ export async function GET(request: Request): Promise { intervalMinutes: query.intervalMinutes, rangeHours: query.rangeHours, configVersion: configSnapshot?.configVersion, + configSnapshot: internalConfigSnapshot ?? configSnapshot, hasConfiguredGroups: configSnapshot ? configSnapshot.groups.length > 0 : undefined, nowIso: new Date().toISOString(), triggerRebuildHint: async (reason) => { diff --git a/src/drizzle/schema.ts b/src/drizzle/schema.ts index 426f4d63e..878177d98 100644 --- a/src/drizzle/schema.ts +++ b/src/drizzle/schema.ts @@ -8,6 +8,7 @@ import { integer, bigint, numeric, + doublePrecision, jsonb, index, uniqueIndex, @@ -620,6 +621,45 @@ export const messageRequest = pgTable('message_request', { .where(sql`${table.deletedAt} IS NULL AND ${table.clientIp} IS NOT NULL`), })); +export const publicStatusHourlyRollups = pgTable('public_status_hourly_rollups', { + id: serial('id').primaryKey(), + bucketStart: timestamp('bucket_start', { withTimezone: true }).notNull(), + bucketEnd: timestamp('bucket_end', { withTimezone: true }).notNull(), + configVersion: varchar('config_version', { length: 128 }).notNull(), + sourceGroupName: varchar('source_group_name', { length: 200 }).notNull(), + publicGroupSlug: varchar('public_group_slug', { length: 120 }).notNull(), + publicModelKey: varchar('public_model_key', { length: 200 }).notNull(), + label: varchar('label', { length: 200 }).notNull(), + vendorIconKey: varchar('vendor_icon_key', { length: 100 }).notNull(), + requestTypeBadge: varchar('request_type_badge', { length: 100 }).notNull(), + state: varchar('state', { length: 20 }).notNull().$type<'operational' | 'failed' | 'no_data'>(), + successCount: integer('success_count').notNull().default(0), + failureCount: integer('failure_count').notNull().default(0), + sampleCount: integer('sample_count').notNull().default(0), + availabilityPct: doublePrecision('availability_pct'), + ttfbMs: doublePrecision('ttfb_ms'), + tps: doublePrecision('tps'), + generatedAt: timestamp('generated_at', { withTimezone: true }).notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), +}, (table) => ({ + publicStatusHourlyRollupsUniq: uniqueIndex('uniq_public_status_hourly_rollup').on( + table.bucketStart, + table.publicGroupSlug, + table.publicModelKey, + table.requestTypeBadge + ), + publicStatusHourlyRollupsBucketIdx: index('idx_public_status_hourly_rollups_bucket').on( + table.bucketStart + ), + publicStatusHourlyRollupsConfigBucketIdx: index( + 'idx_public_status_hourly_rollups_config_bucket' + ).on(table.configVersion, table.bucketStart), + publicStatusHourlyRollupsGroupModelIdx: index( + 'idx_public_status_hourly_rollups_group_model' + ).on(table.publicGroupSlug, table.publicModelKey), +})); + // Model Prices table export const modelPrices = pgTable('model_prices', { id: serial('id').primaryKey(), diff --git a/src/lib/public-status/hourly-rollups.ts b/src/lib/public-status/hourly-rollups.ts new file mode 100644 index 000000000..ecb3030ea --- /dev/null +++ b/src/lib/public-status/hourly-rollups.ts @@ -0,0 +1,646 @@ +import { and, asc, gte, lt, lte, sql } from "drizzle-orm"; +import { db } from "@/drizzle/db"; +import { publicStatusHourlyRollups } from "@/drizzle/schema"; +import { + classifyProviderChainItemOutcome, + resolveSuccessRateModelKey, +} from "@/lib/request-outcome"; +import { resolveProviderGroupsWithDefault } from "@/lib/utils/provider-group"; +import type { PublicStatusConfiguredGroup, PublicStatusRequestRow } from "./aggregation"; +import { + applyBoundedGapFill, + computeTokensPerSecond, + queryPublicStatusRequests, +} from "./aggregation"; +import type { + PublicStatusGroupSnapshot, + PublicStatusPayload, + PublicStatusTimelineBucket, + PublicStatusTimelineState, +} from "./payload"; +import { buildGenerationFingerprint } from "./redis-contract"; + +export const PUBLIC_STATUS_ROLLUP_RETENTION_DAYS = 30; +export const PUBLIC_STATUS_CURRENT_HOUR_CACHE_TTL_SECONDS = 10 * 60; + +export interface PublicStatusHourlyRollupRow { + bucketStart: Date; + bucketEnd: Date; + configVersion: string; + sourceGroupName: string; + publicGroupSlug: string; + publicModelKey: string; + label: string; + vendorIconKey: string; + requestTypeBadge: string; + state: "operational" | "failed" | "no_data"; + successCount: number; + failureCount: number; + sampleCount: number; + availabilityPct: number | null; + ttfbMs: number | null; + tps: number | null; + generatedAt: Date; +} + +interface RedisCurrentHourCache { + get?(key: string): Promise | string | null; + set?(key: string, value: string, mode: "EX", seconds: number): Promise | unknown; + status?: string; +} + +function median(values: number[]): number | null { + if (values.length === 0) { + return null; + } + + const sorted = [...values].sort((left, right) => left - right); + const middle = Math.floor(sorted.length / 2); + if (sorted.length % 2 === 0) { + const left = sorted[middle - 1]; + const right = sorted[middle]; + if (typeof left !== "number" || typeof right !== "number") { + return null; + } + return Number(((left + right) / 2).toFixed(4)); + } + + return sorted[middle] ?? null; +} + +function normalizeFiniteNumber(value: unknown): number | null { + return typeof value === "number" && Number.isFinite(value) ? value : null; +} + +function getReadyRedis(redis?: RedisCurrentHourCache | null): RedisCurrentHourCache | null { + if (!redis || ("status" in redis && redis.status && redis.status !== "ready")) { + return null; + } + return redis; +} + +export function alignHourStartUtc(input: string | Date): Date { + const date = input instanceof Date ? input : new Date(input); + return new Date( + Date.UTC( + date.getUTCFullYear(), + date.getUTCMonth(), + date.getUTCDate(), + date.getUTCHours(), + 0, + 0, + 0 + ) + ); +} + +export function buildPublicStatusCurrentHourSummaryKey(input: { + configVersion: string; + hourStart: string | Date; +}): string { + return [ + "public-status:v1", + "current-hour", + encodeURIComponent(input.configVersion), + alignHourStartUtc(input.hourStart).toISOString(), + ].join(":"); +} + +export function buildPublicStatusHourlyRollupsFromRequests(input: { + configVersion: string; + hourStart: string | Date; + groups: PublicStatusConfiguredGroup[]; + requests: PublicStatusRequestRow[]; + generatedAt?: string | Date; +}): PublicStatusHourlyRollupRow[] { + const bucketStart = alignHourStartUtc(input.hourStart); + const bucketEnd = new Date(bucketStart.getTime() + 60 * 60 * 1000); + const generatedAt = + input.generatedAt instanceof Date + ? input.generatedAt + : input.generatedAt + ? new Date(input.generatedAt) + : new Date(); + + type MutableBucket = { + successCount: number; + failureCount: number; + ttfbValues: number[]; + tpsValues: number[]; + }; + + const mutable = new Map(); + const keyFor = (groupSlug: string, modelKey: string, badge: string) => + `${groupSlug}\u0000${modelKey}\u0000${badge}`; + + for (const group of input.groups) { + for (const model of group.models) { + mutable.set(keyFor(group.publicGroupSlug, model.publicModelKey, model.requestTypeBadge), { + successCount: 0, + failureCount: 0, + ttfbValues: [], + tpsValues: [], + }); + } + } + + const modelToGroups = new Map(); + for (const group of input.groups) { + for (const model of group.models) { + const existing = modelToGroups.get(model.publicModelKey) ?? []; + existing.push(group); + modelToGroups.set(model.publicModelKey, existing); + } + } + + for (const request of input.requests) { + const modelKey = resolveSuccessRateModelKey({ + originalModel: request.originalModel, + model: request.model, + }); + if (!modelKey) { + continue; + } + + const requestTime = + request.createdAt instanceof Date + ? request.createdAt.getTime() + : new Date(request.createdAt).getTime(); + if ( + !Number.isFinite(requestTime) || + requestTime < bucketStart.getTime() || + requestTime >= bucketEnd.getTime() + ) { + continue; + } + + const groups = modelToGroups.get(modelKey); + if (!groups) { + continue; + } + + const groupOutcome = new Map(); + for (const item of request.providerChain ?? []) { + const outcome = classifyProviderChainItemOutcome({ + statusCode: item.statusCode ?? undefined, + reason: item.reason ?? undefined, + errorMessage: item.errorMessage ?? undefined, + errorDetails: item.matchedRule ? { matchedRule: item.matchedRule } : undefined, + })?.outcome; + if (!outcome) { + continue; + } + + for (const sourceGroupName of new Set(resolveProviderGroupsWithDefault(item.groupTag))) { + const existing = groupOutcome.get(sourceGroupName); + if (existing === "success") { + continue; + } + + if (outcome === "success") { + groupOutcome.set(sourceGroupName, "success"); + continue; + } + + if (!existing || existing === "excluded") { + groupOutcome.set(sourceGroupName, outcome); + } + } + } + + const tps = computeTokensPerSecond({ + outputTokens: request.outputTokens, + durationMs: request.durationMs, + ttfbMs: request.ttfbMs, + }); + + for (const group of groups) { + const outcome = groupOutcome.get(group.sourceGroupName); + if (!outcome || outcome === "excluded") { + continue; + } + + const model = group.models.find((candidate) => candidate.publicModelKey === modelKey); + if (!model) { + continue; + } + const bucket = mutable.get( + keyFor(group.publicGroupSlug, model.publicModelKey, model.requestTypeBadge) + ); + if (!bucket) { + continue; + } + + if (outcome === "success") { + bucket.successCount += 1; + } else { + bucket.failureCount += 1; + } + + if (typeof request.ttfbMs === "number") { + bucket.ttfbValues.push(request.ttfbMs); + } + if (typeof tps === "number") { + bucket.tpsValues.push(tps); + } + } + } + + return input.groups.flatMap((group) => + group.models.map((model) => { + const bucket = mutable.get( + keyFor(group.publicGroupSlug, model.publicModelKey, model.requestTypeBadge) + ) ?? { + successCount: 0, + failureCount: 0, + ttfbValues: [], + tpsValues: [], + }; + const sampleCount = bucket.successCount + bucket.failureCount; + return { + bucketStart, + bucketEnd, + configVersion: input.configVersion, + sourceGroupName: group.sourceGroupName, + publicGroupSlug: group.publicGroupSlug, + publicModelKey: model.publicModelKey, + label: model.label, + vendorIconKey: model.vendorIconKey, + requestTypeBadge: model.requestTypeBadge, + state: sampleCount === 0 ? "no_data" : bucket.successCount > 0 ? "operational" : "failed", + successCount: bucket.successCount, + failureCount: bucket.failureCount, + sampleCount, + availabilityPct: + sampleCount === 0 ? null : Number(((bucket.successCount / sampleCount) * 100).toFixed(2)), + ttfbMs: median(bucket.ttfbValues), + tps: median(bucket.tpsValues), + generatedAt, + }; + }) + ); +} + +export async function buildAndPersistPublicStatusHourlyRollup(input: { + configVersion: string; + hourStart: string | Date; + groups: PublicStatusConfiguredGroup[]; +}): Promise { + const hourStart = alignHourStartUtc(input.hourStart); + const requests = await queryPublicStatusRequests({ + groups: input.groups, + coveredFrom: hourStart, + coveredTo: new Date(hourStart.getTime() + 60 * 60 * 1000), + }); + const rollups = buildPublicStatusHourlyRollupsFromRequests({ + configVersion: input.configVersion, + hourStart, + groups: input.groups, + requests, + }); + await upsertPublicStatusHourlyRollups(rollups); + return rollups; +} + +export async function upsertPublicStatusHourlyRollups( + rows: PublicStatusHourlyRollupRow[] +): Promise { + if (rows.length === 0) { + return; + } + + await db + .insert(publicStatusHourlyRollups) + .values(rows) + .onConflictDoUpdate({ + target: [ + publicStatusHourlyRollups.bucketStart, + publicStatusHourlyRollups.publicGroupSlug, + publicStatusHourlyRollups.publicModelKey, + publicStatusHourlyRollups.requestTypeBadge, + ], + set: { + bucketEnd: sql`excluded.bucket_end`, + configVersion: sql`excluded.config_version`, + sourceGroupName: sql`excluded.source_group_name`, + label: sql`excluded.label`, + vendorIconKey: sql`excluded.vendor_icon_key`, + state: sql`excluded.state`, + successCount: sql`excluded.success_count`, + failureCount: sql`excluded.failure_count`, + sampleCount: sql`excluded.sample_count`, + availabilityPct: sql`excluded.availability_pct`, + ttfbMs: sql`excluded.ttfb_ms`, + tps: sql`excluded.tps`, + generatedAt: sql`excluded.generated_at`, + updatedAt: sql`now()`, + }, + }); +} + +export async function readPublicStatusHourlyRollups(input: { + start: Date; + end: Date; + configVersion?: string; +}): Promise { + const conditions = [ + gte(publicStatusHourlyRollups.bucketStart, input.start), + lt(publicStatusHourlyRollups.bucketStart, input.end), + ]; + if (input.configVersion) { + conditions.push(sql`${publicStatusHourlyRollups.configVersion} = ${input.configVersion}`); + } + + const rows = await db + .select() + .from(publicStatusHourlyRollups) + .where(and(...conditions)) + .orderBy( + asc(publicStatusHourlyRollups.bucketStart), + asc(publicStatusHourlyRollups.publicGroupSlug), + asc(publicStatusHourlyRollups.publicModelKey) + ); + + return rows.map((row) => ({ + bucketStart: row.bucketStart, + bucketEnd: row.bucketEnd, + configVersion: row.configVersion, + sourceGroupName: row.sourceGroupName, + publicGroupSlug: row.publicGroupSlug, + publicModelKey: row.publicModelKey, + label: row.label, + vendorIconKey: row.vendorIconKey, + requestTypeBadge: row.requestTypeBadge, + state: row.state, + successCount: row.successCount, + failureCount: row.failureCount, + sampleCount: row.sampleCount, + availabilityPct: normalizeFiniteNumber(row.availabilityPct), + ttfbMs: normalizeFiniteNumber(row.ttfbMs), + tps: normalizeFiniteNumber(row.tps), + generatedAt: row.generatedAt, + })); +} + +export async function cleanupPublicStatusHourlyRollups( + input: { now?: Date; retentionDays?: number } = {} +): Promise { + const now = input.now ?? new Date(); + const retentionDays = input.retentionDays ?? PUBLIC_STATUS_ROLLUP_RETENTION_DAYS; + const cutoff = new Date(now.getTime() - retentionDays * 24 * 60 * 60 * 1000); + + await db + .delete(publicStatusHourlyRollups) + .where(lte(publicStatusHourlyRollups.bucketEnd, cutoff)); +} + +export async function writeCurrentHourPublicStatusSummary(input: { + redis?: RedisCurrentHourCache | null; + configVersion: string; + hourStart: string | Date; + rows: PublicStatusHourlyRollupRow[]; + ttlSeconds?: number; +}): Promise { + const redis = getReadyRedis(input.redis); + if (!redis || typeof redis.set !== "function") { + return false; + } + + const key = buildPublicStatusCurrentHourSummaryKey({ + configVersion: input.configVersion, + hourStart: input.hourStart, + }); + await redis.set( + key, + JSON.stringify({ + configVersion: input.configVersion, + hourStart: alignHourStartUtc(input.hourStart).toISOString(), + rows: input.rows.map(serializeRollupRow), + }), + "EX", + input.ttlSeconds ?? PUBLIC_STATUS_CURRENT_HOUR_CACHE_TTL_SECONDS + ); + return true; +} + +export async function readCurrentHourPublicStatusSummary(input: { + redis?: RedisCurrentHourCache | null; + configVersion: string; + hourStart: string | Date; +}): Promise { + const redis = getReadyRedis(input.redis); + if (!redis || typeof redis.get !== "function") { + return []; + } + + const key = buildPublicStatusCurrentHourSummaryKey({ + configVersion: input.configVersion, + hourStart: input.hourStart, + }); + let raw: string | null = null; + try { + raw = await redis.get(key); + } catch { + return []; + } + if (!raw) { + return []; + } + + try { + const parsed = JSON.parse(raw) as { rows?: unknown }; + if (!Array.isArray(parsed.rows)) { + return []; + } + return parsed.rows.flatMap(parseSerializedRollupRow); + } catch { + return []; + } +} + +export function buildPublicStatusPayloadFromHourlyRollups(input: { + groups: PublicStatusConfiguredGroup[]; + rows: PublicStatusHourlyRollupRow[]; + rangeHours: number; + now: string | Date; + configVersion?: string; + intervalMinutes?: number; +}): PublicStatusPayload { + const now = input.now instanceof Date ? input.now : new Date(input.now); + const currentHourStart = alignHourStartUtc(now); + const windowStart = new Date( + currentHourStart.getTime() - (input.rangeHours - 1) * 60 * 60 * 1000 + ); + const bucketCount = input.rangeHours; + const rowsByKey = new Map(); + for (const row of input.rows) { + rowsByKey.set( + `${row.bucketStart.toISOString()}\u0000${row.publicGroupSlug}\u0000${row.publicModelKey}\u0000${row.requestTypeBadge}`, + row + ); + } + + const generatedAt = + input.rows + .map((row) => row.generatedAt.getTime()) + .filter(Number.isFinite) + .sort((left, right) => right - left)[0] ?? now.getTime(); + const coveredFrom = windowStart.toISOString(); + const coveredTo = new Date(currentHourStart.getTime() + 60 * 60 * 1000).toISOString(); + + const groups: PublicStatusGroupSnapshot[] = input.groups.map((group) => ({ + publicGroupSlug: group.publicGroupSlug, + displayName: group.displayName, + explanatoryCopy: group.explanatoryCopy, + models: group.models.map((model) => { + const rawStates: Array<"operational" | "failed" | null> = []; + const rowSlots: Array = []; + for (let index = 0; index < bucketCount; index++) { + const bucketStart = new Date(windowStart.getTime() + index * 60 * 60 * 1000); + const row = + rowsByKey.get( + `${bucketStart.toISOString()}\u0000${group.publicGroupSlug}\u0000${model.publicModelKey}\u0000${model.requestTypeBadge}` + ) ?? null; + rowSlots.push(row); + rawStates.push( + row && row.sampleCount > 0 ? (row.successCount > 0 ? "operational" : "failed") : null + ); + } + + const filledTimeline = applyBoundedGapFill({ timeline: rawStates }); + let latestTtfbMs: number | null = null; + let latestTps: number | null = null; + let totalSuccess = 0; + let totalSamples = 0; + + const timeline: PublicStatusTimelineBucket[] = rowSlots.map((row, index) => { + const bucketStart = new Date(windowStart.getTime() + index * 60 * 60 * 1000); + const bucketEnd = new Date(bucketStart.getTime() + 60 * 60 * 1000); + if (row) { + totalSuccess += row.successCount; + totalSamples += row.sampleCount; + if (row.ttfbMs !== null) { + latestTtfbMs = row.ttfbMs; + } + if (row.tps !== null) { + latestTps = row.tps; + } + } + + const sampleCount = row?.sampleCount ?? 0; + const state: PublicStatusTimelineState = + filledTimeline[index] === "operational" + ? "operational" + : filledTimeline[index] === "failed" + ? "failed" + : "no_data"; + + return { + bucketStart: bucketStart.toISOString(), + bucketEnd: bucketEnd.toISOString(), + state, + availabilityPct: + row?.availabilityPct ?? + (sampleCount === 0 + ? filledTimeline[index] === "operational" + ? 100 + : filledTimeline[index] === "failed" + ? 0 + : null + : null), + ttfbMs: row?.ttfbMs ?? null, + tps: row?.tps ?? null, + sampleCount, + }; + }); + + const latestStateRaw = [...filledTimeline].reverse().find((state) => state !== null) ?? null; + return { + publicModelKey: model.publicModelKey, + label: model.label, + vendorIconKey: model.vendorIconKey, + requestTypeBadge: model.requestTypeBadge, + latestState: + latestStateRaw === "operational" + ? "operational" + : latestStateRaw === "failed" + ? "failed" + : "no_data", + availabilityPct: + totalSamples === 0 ? null : Number(((totalSuccess / totalSamples) * 100).toFixed(2)), + latestTtfbMs, + latestTps, + timeline, + }; + }), + })); + + return { + rebuildState: input.rows.length > 0 ? "fresh" : "no-data", + sourceGeneration: buildGenerationFingerprint({ + configVersion: input.configVersion ?? "db-rollup", + intervalMinutes: input.intervalMinutes ?? 60, + coveredFromIso: coveredFrom, + coveredToIso: coveredTo, + }), + generatedAt: new Date(generatedAt).toISOString(), + freshUntil: new Date(currentHourStart.getTime() + 60 * 60 * 1000).toISOString(), + groups, + }; +} + +function serializeRollupRow(row: PublicStatusHourlyRollupRow): Record { + return { + ...row, + bucketStart: row.bucketStart.toISOString(), + bucketEnd: row.bucketEnd.toISOString(), + generatedAt: row.generatedAt.toISOString(), + }; +} + +function parseSerializedRollupRow(input: unknown): PublicStatusHourlyRollupRow[] { + if (!input || typeof input !== "object") { + return []; + } + const value = input as Record; + if ( + typeof value.bucketStart !== "string" || + typeof value.bucketEnd !== "string" || + typeof value.configVersion !== "string" || + typeof value.sourceGroupName !== "string" || + typeof value.publicGroupSlug !== "string" || + typeof value.publicModelKey !== "string" || + typeof value.label !== "string" || + typeof value.vendorIconKey !== "string" || + typeof value.requestTypeBadge !== "string" || + (value.state !== "operational" && value.state !== "failed" && value.state !== "no_data") || + typeof value.successCount !== "number" || + typeof value.failureCount !== "number" || + typeof value.sampleCount !== "number" || + typeof value.generatedAt !== "string" + ) { + return []; + } + + return [ + { + bucketStart: new Date(value.bucketStart), + bucketEnd: new Date(value.bucketEnd), + configVersion: value.configVersion, + sourceGroupName: value.sourceGroupName, + publicGroupSlug: value.publicGroupSlug, + publicModelKey: value.publicModelKey, + label: value.label, + vendorIconKey: value.vendorIconKey, + requestTypeBadge: value.requestTypeBadge, + state: value.state, + successCount: value.successCount, + failureCount: value.failureCount, + sampleCount: value.sampleCount, + availabilityPct: normalizeFiniteNumber(value.availabilityPct), + ttfbMs: normalizeFiniteNumber(value.ttfbMs), + tps: normalizeFiniteNumber(value.tps), + generatedAt: new Date(value.generatedAt), + }, + ]; +} diff --git a/src/lib/public-status/read-store.ts b/src/lib/public-status/read-store.ts index 7aeb7dced..ca34e5c98 100644 --- a/src/lib/public-status/read-store.ts +++ b/src/lib/public-status/read-store.ts @@ -1,4 +1,15 @@ import { getRedisClient } from "@/lib/redis"; +import { getConfiguredPublicStatusGroups } from "./aggregation"; +import type { + InternalPublicStatusConfigSnapshot, + PublicStatusConfigSnapshot, +} from "./config-snapshot"; +import { + alignHourStartUtc, + buildPublicStatusPayloadFromHourlyRollups, + readCurrentHourPublicStatusSummary, + readPublicStatusHourlyRollups, +} from "./hourly-rollups"; import type { PublicStatusGroupSnapshot, PublicStatusModelSnapshot, @@ -183,6 +194,7 @@ export async function readPublicStatusPayload(input: { rangeHours: number; nowIso: string; configVersion?: string; + configSnapshot?: PublicStatusConfigSnapshot | InternalPublicStatusConfigSnapshot | null; hasConfiguredGroups?: boolean; redis?: RedisReader | null; triggerRebuildHint: (reason: string) => Promise | void; @@ -192,6 +204,48 @@ export async function readPublicStatusPayload(input: { } const redis = input.redis ?? getRedisClient({ allowWhenRateLimitDisabled: true }); + if (input.configSnapshot && input.configSnapshot.groups.length > 0) { + const groups = getConfiguredPublicStatusGroups({ + ...input.configSnapshot, + groups: input.configSnapshot.groups.map((group) => ({ + ...group, + sourceGroupName: "sourceGroupName" in group ? group.sourceGroupName : group.slug, + })), + }); + const now = new Date(input.nowIso); + const currentHourStart = alignHourStartUtc(now); + const historyStart = new Date( + currentHourStart.getTime() - (input.rangeHours - 1) * 60 * 60 * 1000 + ); + const [historyRows, currentRows] = await Promise.all([ + readPublicStatusHourlyRollups({ + start: historyStart, + end: currentHourStart, + configVersion: input.configVersion, + }), + input.configVersion + ? readCurrentHourPublicStatusSummary({ + redis, + configVersion: input.configVersion, + hourStart: currentHourStart, + }) + : Promise.resolve([]), + ]); + const rows = [...historyRows, ...currentRows]; + if (rows.length > 0) { + return buildPublicStatusPayloadFromHourlyRollups({ + groups, + rows, + rangeHours: input.rangeHours, + now, + configVersion: input.configVersion, + intervalMinutes: input.intervalMinutes, + }); + } + + await input.triggerRebuildHint("rollup-missing"); + } + if (!redis || ("status" in redis && redis.status && redis.status !== "ready")) { await input.triggerRebuildHint("redis-unavailable"); return buildRebuildingPayload(); diff --git a/src/lib/public-status/rebuild-worker.ts b/src/lib/public-status/rebuild-worker.ts index 56f09b9ae..f6f7056d2 100644 --- a/src/lib/public-status/rebuild-worker.ts +++ b/src/lib/public-status/rebuild-worker.ts @@ -1,19 +1,20 @@ import { getRedisClient } from "@/lib/redis"; -import { - buildPublicStatusPayloadFromRequests, - getConfiguredPublicStatusGroups, - queryPublicStatusRequests, -} from "./aggregation"; +import { getConfiguredPublicStatusGroups, queryPublicStatusRequests } from "./aggregation"; import { publishCurrentPublicStatusConfigProjection } from "./config-publisher"; import { readCurrentInternalPublicStatusConfigSnapshot } from "./config-snapshot"; +import { + alignHourStartUtc, + buildAndPersistPublicStatusHourlyRollup, + buildPublicStatusHourlyRollupsFromRequests, + cleanupPublicStatusHourlyRollups, + PUBLIC_STATUS_ROLLUP_RETENTION_DAYS, + writeCurrentHourPublicStatusSummary, +} from "./hourly-rollups"; import { alignBucketStartUtc, buildGenerationFingerprint, - buildPublicStatusCurrentSnapshotKey, buildPublicStatusManifestKey, buildPublicStatusRebuildLockKey, - buildPublicStatusSeriesChunkKey, - buildPublicStatusTempKey, } from "./redis-contract"; interface PublicStatusRebuildResult { @@ -23,8 +24,9 @@ interface PublicStatusRebuildResult { const inFlightRebuilds = new Map>(); const REBUILD_LOCK_TTL_MS = 60_000; -const TEMP_PROJECTION_TTL_SECONDS = 300; -const GENERATION_PROJECTION_TTL_SECONDS = 60 * 60 * 24 * 30; +const RUNTIME_MANIFEST_TTL_SECONDS = 60 * 60 * 2; +const ROLLUP_WRITE_BATCH_SIZE = 6; +const ROLLUP_HISTORY_HOURS = PUBLIC_STATUS_ROLLUP_RETENTION_DAYS * 24; interface RedisHintWriter { get?(key: string): Promise | string | null; @@ -35,15 +37,6 @@ interface RedisHintWriter { status?: string; } -async function setWithTtl( - redis: RedisHintWriter, - key: string, - value: string, - ttlSeconds: number -): Promise { - await redis.set(key, value, "EX", ttlSeconds); -} - function getReadyRedisClient(redis?: RedisHintWriter | null): RedisHintWriter | null { const client = redis ?? getRedisClient({ allowWhenRateLimitDisabled: true }); if (!client || ("status" in client && client.status && client.status !== "ready")) { @@ -84,6 +77,14 @@ function shouldPromoteCurrentManifest( return true; } +function chunkArray(items: T[], size: number): T[][] { + const chunks: T[][] = []; + for (let index = 0; index < items.length; index += size) { + chunks.push(items.slice(index, index + size)); + } + return chunks; +} + async function publishPublicStatusProjection(input: { redis: RedisHintWriter; configVersion: string; @@ -93,19 +94,7 @@ async function publishPublicStatusProjection(input: { generatedAt: string; coveredFrom: string; coveredTo: string; - groups: unknown; }): Promise { - const snapshotKey = buildPublicStatusCurrentSnapshotKey({ - intervalMinutes: input.intervalMinutes, - rangeHours: input.rangeHours, - generation: input.sourceGeneration, - }); - const seriesKey = buildPublicStatusSeriesChunkKey({ - intervalMinutes: input.intervalMinutes, - generation: input.sourceGeneration, - bucketStartIso: input.coveredFrom, - bucketEndIso: input.coveredTo, - }); const currentManifestKey = buildPublicStatusManifestKey({ configVersion: "current", intervalMinutes: input.intervalMinutes, @@ -117,26 +106,6 @@ async function publishPublicStatusProjection(input: { rangeHours: input.rangeHours, }); - const nonce = `${input.sourceGeneration}-${Date.now()}`; - const snapshotTempKey = buildPublicStatusTempKey(snapshotKey, nonce); - const seriesTempKey = buildPublicStatusTempKey(seriesKey, nonce); - - const snapshotRecord = { - rebuildState: "fresh" as const, - sourceGeneration: input.sourceGeneration, - generatedAt: input.generatedAt, - freshUntil: new Date( - Date.parse(input.generatedAt) + input.intervalMinutes * 60 * 1000 - ).toISOString(), - groups: input.groups, - }; - const seriesRecord = { - sourceGeneration: input.sourceGeneration, - generatedAt: input.generatedAt, - coveredFrom: input.coveredFrom, - coveredTo: input.coveredTo, - groups: input.groups, - }; const manifestRecord = { configVersion: input.configVersion, intervalMinutes: input.intervalMinutes, @@ -146,40 +115,18 @@ async function publishPublicStatusProjection(input: { coveredFrom: input.coveredFrom, coveredTo: input.coveredTo, generatedAt: input.generatedAt, - freshUntil: snapshotRecord.freshUntil, + freshUntil: new Date( + Date.parse(input.generatedAt) + input.intervalMinutes * 60 * 1000 + ).toISOString(), rebuildState: "idle" as const, lastCompleteGeneration: input.sourceGeneration, }; - await setWithTtl( - input.redis, - snapshotTempKey, - JSON.stringify(snapshotRecord), - TEMP_PROJECTION_TTL_SECONDS - ); - await setWithTtl( - input.redis, - seriesTempKey, - JSON.stringify(seriesRecord), - TEMP_PROJECTION_TTL_SECONDS - ); - await setWithTtl( - input.redis, - snapshotKey, - JSON.stringify(snapshotRecord), - GENERATION_PROJECTION_TTL_SECONDS - ); - await setWithTtl( - input.redis, - seriesKey, - JSON.stringify(seriesRecord), - GENERATION_PROJECTION_TTL_SECONDS - ); - await setWithTtl( - input.redis, + await input.redis.set( versionedManifestKey, JSON.stringify(manifestRecord), - GENERATION_PROJECTION_TTL_SECONDS + "EX", + RUNTIME_MANIFEST_TTL_SECONDS ); if (typeof input.redis.get === "function") { let existingCurrentManifest: { configVersion?: string; coveredTo?: string } | null = null; @@ -193,13 +140,20 @@ async function publishPublicStatusProjection(input: { } if (shouldPromoteCurrentManifest(existingCurrentManifest, manifestRecord)) { - await input.redis.set(currentManifestKey, JSON.stringify(manifestRecord)); + await input.redis.set( + currentManifestKey, + JSON.stringify(manifestRecord), + "EX", + RUNTIME_MANIFEST_TTL_SECONDS + ); } } else { - await input.redis.set(currentManifestKey, JSON.stringify(manifestRecord)); - } - if (input.redis.del) { - await input.redis.del(snapshotTempKey, seriesTempKey); + await input.redis.set( + currentManifestKey, + JSON.stringify(manifestRecord), + "EX", + RUNTIME_MANIFEST_TTL_SECONDS + ); } } @@ -319,13 +273,16 @@ export async function rebuildPublicStatusProjection(input: { const now = input.now ?? new Date(); const coveredTo = alignBucketStartUtc(now.toISOString(), input.intervalMinutes); - const coveredFrom = new Date( + const manifestCoveredFrom = new Date( Date.parse(coveredTo) - input.rangeHours * 60 * 60 * 1000 ).toISOString(); + const rollupCoveredFrom = new Date( + Date.parse(coveredTo) - ROLLUP_HISTORY_HOURS * 60 * 60 * 1000 + ).toISOString(); const sourceGeneration = buildGenerationFingerprint({ configVersion: configSnapshot.configVersion, intervalMinutes: input.intervalMinutes, - coveredFromIso: coveredFrom, + coveredFromIso: manifestCoveredFrom, coveredToIso: coveredTo, }); @@ -348,18 +305,45 @@ export async function rebuildPublicStatusProjection(input: { } try { - const requests = await queryPublicStatusRequests({ + const finalizedHourStarts: Date[] = []; + for ( + let cursorMs = Date.parse(rollupCoveredFrom); + cursorMs < Date.parse(coveredTo); + cursorMs += 60 * 60 * 1000 + ) { + finalizedHourStarts.push(new Date(cursorMs)); + } + for (const hourStartBatch of chunkArray(finalizedHourStarts, ROLLUP_WRITE_BATCH_SIZE)) { + await Promise.all( + hourStartBatch.map((hourStart) => + buildAndPersistPublicStatusHourlyRollup({ + configVersion: configSnapshot.configVersion, + hourStart, + groups, + }) + ) + ); + } + + const currentHourStart = alignHourStartUtc(now); + const currentHourRequests = await queryPublicStatusRequests({ groups, - coveredFrom: new Date(coveredFrom), - coveredTo: new Date(coveredTo), + coveredFrom: currentHourStart, + coveredTo: now, }); - const aggregation = buildPublicStatusPayloadFromRequests({ - rangeHours: input.rangeHours, - intervalMinutes: input.intervalMinutes, - now: new Date(coveredTo), + const currentHourRollups = buildPublicStatusHourlyRollupsFromRequests({ + configVersion: configSnapshot.configVersion, + hourStart: currentHourStart, groups, - requests, + requests: currentHourRequests, + }); + await writeCurrentHourPublicStatusSummary({ + redis, + configVersion: configSnapshot.configVersion, + hourStart: currentHourStart, + rows: currentHourRollups, }); + await cleanupPublicStatusHourlyRollups({ now }); await publishPublicStatusProjection({ redis, @@ -367,10 +351,9 @@ export async function rebuildPublicStatusProjection(input: { intervalMinutes: input.intervalMinutes, rangeHours: input.rangeHours, sourceGeneration, - generatedAt: aggregation.generatedAt, - coveredFrom: aggregation.coveredFrom, - coveredTo: aggregation.coveredTo, - groups: aggregation.groups, + generatedAt: now.toISOString(), + coveredFrom: manifestCoveredFrom, + coveredTo, }); return { sourceGeneration }; diff --git a/tests/integration/public-status/route-redis-only.test.ts b/tests/integration/public-status/route-redis-only.test.ts index e8257a27b..74a7442f9 100644 --- a/tests/integration/public-status/route-redis-only.test.ts +++ b/tests/integration/public-status/route-redis-only.test.ts @@ -1,10 +1,12 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const mockReadCurrentPublicStatusConfigSnapshot = vi.hoisted(() => vi.fn()); +const mockReadCurrentInternalPublicStatusConfigSnapshot = vi.hoisted(() => vi.fn()); const mockReadPublicStatusPayload = vi.hoisted(() => vi.fn()); const mockSchedulePublicStatusRebuild = vi.hoisted(() => vi.fn()); vi.mock("@/lib/public-status/config-snapshot", () => ({ + readCurrentInternalPublicStatusConfigSnapshot: mockReadCurrentInternalPublicStatusConfigSnapshot, readCurrentPublicStatusConfigSnapshot: mockReadCurrentPublicStatusConfigSnapshot, })); @@ -19,6 +21,7 @@ vi.mock("@/lib/public-status/rebuild-hints", () => ({ describe("GET /api/public-status", () => { beforeEach(() => { vi.clearAllMocks(); + mockReadCurrentInternalPublicStatusConfigSnapshot.mockResolvedValue(null); }); it("returns 200 with an explicit no-snapshot body when snapshot is missing", async () => { @@ -125,6 +128,49 @@ describe("GET /api/public-status", () => { ); }); + it("passes the internal config snapshot into the read store for DB rollups", async () => { + const publicSnapshot = { + configVersion: "cfg-public", + defaultIntervalMinutes: 5, + defaultRangeHours: 24, + groups: [{ slug: "openai" }], + }; + const internalSnapshot = { + configVersion: "cfg-internal", + defaultIntervalMinutes: 5, + defaultRangeHours: 24, + groups: [ + { + slug: "openai", + sourceGroupName: "internal-openai", + displayName: "OpenAI", + sortOrder: 1, + models: [], + }, + ], + }; + mockReadCurrentPublicStatusConfigSnapshot.mockResolvedValue(publicSnapshot); + mockReadCurrentInternalPublicStatusConfigSnapshot.mockResolvedValue(internalSnapshot); + mockReadPublicStatusPayload.mockResolvedValue({ + rebuildState: "fresh", + sourceGeneration: "gen-1", + generatedAt: "2026-04-21T10:00:00.000Z", + freshUntil: "2026-04-21T10:05:00.000Z", + groups: [], + }); + + const { GET } = await import("@/app/api/public-status/route"); + const response = await GET(new Request("http://localhost/api/public-status")); + + expect(response.status).toBe(200); + expect(mockReadPublicStatusPayload).toHaveBeenCalledWith( + expect.objectContaining({ + configVersion: "cfg-public", + configSnapshot: internalSnapshot, + }) + ); + }); + it("returns 200 with stale payload and queues rebuild for the default query", async () => { mockReadCurrentPublicStatusConfigSnapshot.mockResolvedValue({ configVersion: "cfg-1", diff --git a/tests/unit/public-status/hourly-rollups.test.ts b/tests/unit/public-status/hourly-rollups.test.ts new file mode 100644 index 000000000..fa86f93d2 --- /dev/null +++ b/tests/unit/public-status/hourly-rollups.test.ts @@ -0,0 +1,262 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mockDbInsert = vi.hoisted(() => vi.fn()); +const mockDbDelete = vi.hoisted(() => vi.fn()); + +vi.mock("@/drizzle/db", () => ({ + db: { + insert: mockDbInsert, + delete: mockDbDelete, + }, +})); + +vi.mock("@/drizzle/schema", async () => { + const actual = await vi.importActual("@/drizzle/schema"); + return { + ...actual, + publicStatusHourlyRollups: { + bucketStart: "bucket_start", + bucketEnd: "bucket_end", + configVersion: "config_version", + sourceGroupName: "source_group_name", + publicGroupSlug: "public_group_slug", + publicModelKey: "public_model_key", + label: "label", + vendorIconKey: "vendor_icon_key", + requestTypeBadge: "request_type_badge", + state: "state", + successCount: "success_count", + failureCount: "failure_count", + sampleCount: "sample_count", + availabilityPct: "availability_pct", + ttfbMs: "ttfb_ms", + tps: "tps", + generatedAt: "generated_at", + updatedAt: "updated_at", + }, + }; +}); + +function buildGroup() { + return { + sourceGroupName: "openai", + publicGroupSlug: "openai", + displayName: "OpenAI", + explanatoryCopy: "Primary", + sortOrder: 1, + models: [ + { + publicModelKey: "gpt-4.1", + label: "GPT-4.1", + vendorIconKey: "openai", + requestTypeBadge: "openaiCompatible", + }, + ], + }; +} + +describe("public-status hourly rollups", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("builds finalized hourly rollup rows from request rows", async () => { + const mod = await import("@/lib/public-status/hourly-rollups"); + + const rows = mod.buildPublicStatusHourlyRollupsFromRequests({ + configVersion: "cfg-1", + hourStart: "2026-04-21T10:23:00.000Z", + groups: [buildGroup()], + generatedAt: "2026-04-21T11:00:00.000Z", + requests: [ + { + id: 1, + createdAt: "2026-04-21T10:10:00.000Z", + originalModel: "gpt-4.1", + durationMs: 1200, + ttfbMs: 200, + outputTokens: 50, + providerChain: [ + { + id: 11, + name: "provider", + groupTag: "openai", + reason: "request_success", + statusCode: 200, + }, + ], + }, + { + id: 2, + createdAt: "2026-04-21T10:45:00.000Z", + originalModel: "gpt-4.1", + providerChain: [ + { + id: 12, + name: "provider", + groupTag: "openai", + reason: "retry_failed", + statusCode: 500, + }, + ], + }, + ], + }); + + expect(rows).toHaveLength(1); + expect(rows[0]).toMatchObject({ + bucketStart: new Date("2026-04-21T10:00:00.000Z"), + bucketEnd: new Date("2026-04-21T11:00:00.000Z"), + configVersion: "cfg-1", + publicGroupSlug: "openai", + publicModelKey: "gpt-4.1", + state: "operational", + successCount: 1, + failureCount: 1, + sampleCount: 2, + availabilityPct: 50, + ttfbMs: 200, + tps: 50, + }); + }); + + it("assembles API-compatible payload from DB history plus current hour summary", async () => { + const mod = await import("@/lib/public-status/hourly-rollups"); + const payload = mod.buildPublicStatusPayloadFromHourlyRollups({ + groups: [buildGroup()], + rangeHours: 2, + now: "2026-04-21T11:15:00.000Z", + configVersion: "cfg-1", + rows: [ + { + bucketStart: new Date("2026-04-21T10:00:00.000Z"), + bucketEnd: new Date("2026-04-21T11:00:00.000Z"), + configVersion: "cfg-1", + sourceGroupName: "openai", + publicGroupSlug: "openai", + publicModelKey: "gpt-4.1", + label: "GPT-4.1", + vendorIconKey: "openai", + requestTypeBadge: "openaiCompatible", + state: "operational", + successCount: 4, + failureCount: 0, + sampleCount: 4, + availabilityPct: 100, + ttfbMs: 120, + tps: 8, + generatedAt: new Date("2026-04-21T11:00:00.000Z"), + }, + { + bucketStart: new Date("2026-04-21T11:00:00.000Z"), + bucketEnd: new Date("2026-04-21T12:00:00.000Z"), + configVersion: "cfg-1", + sourceGroupName: "openai", + publicGroupSlug: "openai", + publicModelKey: "gpt-4.1", + label: "GPT-4.1", + vendorIconKey: "openai", + requestTypeBadge: "openaiCompatible", + state: "failed", + successCount: 0, + failureCount: 1, + sampleCount: 1, + availabilityPct: 0, + ttfbMs: null, + tps: null, + generatedAt: new Date("2026-04-21T11:15:00.000Z"), + }, + ], + }); + + expect(payload.rebuildState).toBe("fresh"); + expect(payload.groups[0]?.models[0]).toMatchObject({ + latestState: "failed", + availabilityPct: 80, + latestTtfbMs: 120, + latestTps: 8, + }); + expect(payload.groups[0]?.models[0]?.timeline).toEqual([ + expect.objectContaining({ + bucketStart: "2026-04-21T10:00:00.000Z", + sampleCount: 4, + }), + expect.objectContaining({ + bucketStart: "2026-04-21T11:00:00.000Z", + sampleCount: 1, + state: "failed", + }), + ]); + }); + + it("writes current-hour summary with short ttl and reads it back", async () => { + const mod = await import("@/lib/public-status/hourly-rollups"); + const store = new Map(); + const redis = { + status: "ready", + set: vi.fn(async (key: string, value: string) => { + store.set(key, value); + return "OK"; + }), + get: vi.fn(async (key: string) => store.get(key) ?? null), + }; + const row = mod.buildPublicStatusHourlyRollupsFromRequests({ + configVersion: "cfg-1", + hourStart: "2026-04-21T11:00:00.000Z", + groups: [buildGroup()], + requests: [], + })[0]; + if (!row) { + throw new Error("expected rollup row"); + } + + await mod.writeCurrentHourPublicStatusSummary({ + redis, + configVersion: "cfg-1", + hourStart: "2026-04-21T11:10:00.000Z", + rows: [row], + }); + const readRows = await mod.readCurrentHourPublicStatusSummary({ + redis, + configVersion: "cfg-1", + hourStart: "2026-04-21T11:30:00.000Z", + }); + + expect(redis.set).toHaveBeenCalledWith( + expect.stringContaining("public-status:v1:current-hour:cfg-1:"), + expect.any(String), + "EX", + 10 * 60 + ); + expect(readRows).toHaveLength(1); + expect(readRows[0]?.bucketStart.toISOString()).toBe("2026-04-21T11:00:00.000Z"); + }); + + it("uses batch upsert conflict handling and retention cleanup queries", async () => { + const values = vi.fn(() => ({ + onConflictDoUpdate: vi.fn(async () => undefined), + })); + mockDbInsert.mockReturnValue({ values }); + const where = vi.fn(async () => undefined); + mockDbDelete.mockReturnValue({ where }); + + const mod = await import("@/lib/public-status/hourly-rollups"); + const rows = mod.buildPublicStatusHourlyRollupsFromRequests({ + configVersion: "cfg-1", + hourStart: "2026-04-21T10:00:00.000Z", + groups: [buildGroup()], + requests: [], + }); + + await mod.upsertPublicStatusHourlyRollups(rows); + await mod.cleanupPublicStatusHourlyRollups({ + now: new Date("2026-05-21T10:00:00.000Z"), + retentionDays: 30, + }); + + expect(mockDbInsert).toHaveBeenCalled(); + expect(values).toHaveBeenCalledWith(rows); + expect(mockDbDelete).toHaveBeenCalled(); + expect(where).toHaveBeenCalled(); + }); +}); diff --git a/tests/unit/public-status/read-store.test.ts b/tests/unit/public-status/read-store.test.ts index dfbb41cdb..9c641b558 100644 --- a/tests/unit/public-status/read-store.test.ts +++ b/tests/unit/public-status/read-store.test.ts @@ -1,10 +1,24 @@ -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { buildPublicStatusCurrentSnapshotKey, buildPublicStatusManifestKey, } from "@/lib/public-status/redis-contract"; import { readPublicStatusPayload } from "@/lib/public-status/read-store"; +const mockReadPublicStatusHourlyRollups = vi.hoisted(() => vi.fn()); +const mockReadCurrentHourPublicStatusSummary = vi.hoisted(() => vi.fn()); + +vi.mock("@/lib/public-status/hourly-rollups", async () => { + const actual = await vi.importActual( + "@/lib/public-status/hourly-rollups" + ); + return { + ...actual, + readPublicStatusHourlyRollups: mockReadPublicStatusHourlyRollups, + readCurrentHourPublicStatusSummary: mockReadCurrentHourPublicStatusSummary, + }; +}); + function createRedisReader(entries: Record) { return { get: vi.fn(async (key: string) => { @@ -16,6 +30,11 @@ function createRedisReader(entries: Record) { } describe("readPublicStatusPayload", () => { + beforeEach(() => { + mockReadPublicStatusHourlyRollups.mockResolvedValue([]); + mockReadCurrentHourPublicStatusSummary.mockResolvedValue([]); + }); + it("returns no-data immediately when no public groups are configured", async () => { const triggerRebuildHint = vi.fn(); @@ -96,6 +115,115 @@ describe("readPublicStatusPayload", () => { expect(triggerRebuildHint).toHaveBeenCalledWith("stale-generation"); }); + it("serves DB hourly rollups and merges current-hour Redis summary before legacy snapshot fallback", async () => { + const triggerRebuildHint = vi.fn(); + const redis = createRedisReader({}); + mockReadPublicStatusHourlyRollups.mockResolvedValue([ + { + bucketStart: new Date("2026-04-21T08:00:00.000Z"), + bucketEnd: new Date("2026-04-21T09:00:00.000Z"), + configVersion: "cfg-1", + sourceGroupName: "openai", + publicGroupSlug: "openai", + publicModelKey: "gpt-4.1", + label: "GPT-4.1", + vendorIconKey: "openai", + requestTypeBadge: "openaiCompatible", + state: "operational", + successCount: 9, + failureCount: 1, + sampleCount: 10, + availabilityPct: 90, + ttfbMs: 120, + tps: 5, + generatedAt: new Date("2026-04-21T09:00:00.000Z"), + }, + ]); + mockReadCurrentHourPublicStatusSummary.mockResolvedValue([ + { + bucketStart: new Date("2026-04-21T10:00:00.000Z"), + bucketEnd: new Date("2026-04-21T11:00:00.000Z"), + configVersion: "cfg-1", + sourceGroupName: "openai", + publicGroupSlug: "openai", + publicModelKey: "gpt-4.1", + label: "GPT-4.1", + vendorIconKey: "openai", + requestTypeBadge: "openaiCompatible", + state: "failed", + successCount: 0, + failureCount: 2, + sampleCount: 2, + availabilityPct: 0, + ttfbMs: null, + tps: null, + generatedAt: new Date("2026-04-21T10:10:00.000Z"), + }, + ]); + + const payload = await readPublicStatusPayload({ + intervalMinutes: 5, + rangeHours: 3, + nowIso: "2026-04-21T10:15:00.000Z", + configVersion: "cfg-1", + configSnapshot: { + configVersion: "cfg-1", + generatedAt: "2026-04-21T10:00:00.000Z", + siteTitle: "Status", + siteDescription: "Status", + timeZone: null, + defaultIntervalMinutes: 5, + defaultRangeHours: 24, + groups: [ + { + slug: "openai", + sourceGroupName: "openai", + displayName: "OpenAI", + sortOrder: 1, + description: null, + models: [ + { + publicModelKey: "gpt-4.1", + label: "GPT-4.1", + vendorIconKey: "openai", + requestTypeBadge: "openaiCompatible", + }, + ], + }, + ], + }, + hasConfiguredGroups: true, + redis, + triggerRebuildHint, + }); + + expect(payload.rebuildState).toBe("fresh"); + expect(payload.groups[0]?.models[0]?.latestState).toBe("failed"); + expect(payload.groups[0]?.models[0]?.timeline).toHaveLength(3); + expect(payload.groups[0]?.models[0]?.timeline[0]).toMatchObject({ + bucketStart: "2026-04-21T08:00:00.000Z", + sampleCount: 10, + }); + expect(payload.groups[0]?.models[0]?.timeline[2]).toMatchObject({ + bucketStart: "2026-04-21T10:00:00.000Z", + sampleCount: 2, + state: "failed", + }); + expect(redis.get).not.toHaveBeenCalledWith( + buildPublicStatusManifestKey({ + configVersion: "cfg-1", + intervalMinutes: 5, + rangeHours: 3, + }) + ); + expect(mockReadPublicStatusHourlyRollups).toHaveBeenCalledWith({ + start: new Date("2026-04-21T08:00:00.000Z"), + end: new Date("2026-04-21T10:00:00.000Z"), + configVersion: "cfg-1", + }); + expect(triggerRebuildHint).not.toHaveBeenCalled(); + }); + it("returns rebuilding when the manifest exists but the snapshot payload is missing", async () => { const triggerRebuildHint = vi.fn(); const redis = createRedisReader({ diff --git a/tests/unit/public-status/rebuild-worker.test.ts b/tests/unit/public-status/rebuild-worker.test.ts index 9edc7e445..3e343abce 100644 --- a/tests/unit/public-status/rebuild-worker.test.ts +++ b/tests/unit/public-status/rebuild-worker.test.ts @@ -1,6 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { - buildPublicStatusCurrentSnapshotKey, buildPublicStatusManifestKey, buildPublicStatusRebuildHintKey, } from "@/lib/public-status/redis-contract"; @@ -13,8 +12,11 @@ const mockRedisEval = vi.hoisted(() => vi.fn()); const mockRedisPttl = vi.hoisted(() => vi.fn()); const mockReadCurrentInternalPublicStatusConfigSnapshot = vi.hoisted(() => vi.fn()); const mockQueryPublicStatusRequests = vi.hoisted(() => vi.fn()); -const mockBuildPublicStatusPayloadFromRequests = vi.hoisted(() => vi.fn()); const mockPublishCurrentPublicStatusConfigProjection = vi.hoisted(() => vi.fn()); +const mockBuildAndPersistPublicStatusHourlyRollup = vi.hoisted(() => vi.fn()); +const mockBuildPublicStatusHourlyRollupsFromRequests = vi.hoisted(() => vi.fn()); +const mockWriteCurrentHourPublicStatusSummary = vi.hoisted(() => vi.fn()); +const mockCleanupPublicStatusHourlyRollups = vi.hoisted(() => vi.fn()); async function importAggregationModule() { vi.resetModules(); @@ -96,7 +98,19 @@ async function importRebuildWorkerModule() { vi.doMock("@/lib/public-status/aggregation", () => ({ getConfiguredPublicStatusGroups: (snapshot: { groups: unknown[] }) => snapshot.groups, queryPublicStatusRequests: mockQueryPublicStatusRequests, - buildPublicStatusPayloadFromRequests: mockBuildPublicStatusPayloadFromRequests, + })); + vi.doMock("@/lib/public-status/hourly-rollups", () => ({ + PUBLIC_STATUS_ROLLUP_RETENTION_DAYS: 30, + alignHourStartUtc: (input: string | Date) => { + const date = input instanceof Date ? input : new Date(input); + return new Date( + Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours()) + ); + }, + buildAndPersistPublicStatusHourlyRollup: mockBuildAndPersistPublicStatusHourlyRollup, + buildPublicStatusHourlyRollupsFromRequests: mockBuildPublicStatusHourlyRollupsFromRequests, + cleanupPublicStatusHourlyRollups: mockCleanupPublicStatusHourlyRollups, + writeCurrentHourPublicStatusSummary: mockWriteCurrentHourPublicStatusSummary, })); return importPublicStatusModule<{ @@ -158,6 +172,10 @@ describe("public-status rebuild worker", () => { mockRedisGet.mockResolvedValue(null); mockRedisEval.mockResolvedValue(1); mockRedisPttl.mockResolvedValue(-1); + mockBuildAndPersistPublicStatusHourlyRollup.mockResolvedValue([]); + mockBuildPublicStatusHourlyRollupsFromRequests.mockReturnValue([]); + mockWriteCurrentHourPublicStatusSummary.mockResolvedValue(true); + mockCleanupPublicStatusHourlyRollups.mockResolvedValue(undefined); mockPublishCurrentPublicStatusConfigProjection.mockResolvedValue({ configVersion: "cfg-1", key: "public-status:v1:config:cfg-1", @@ -394,7 +412,7 @@ describe("public-status rebuild worker", () => { ]); }); - it("publishes snapshot and manifest records for a rebuilt generation", async () => { + it("persists finalized hourly rollups and publishes only short-lived runtime manifest records", async () => { const mod = await importRebuildWorkerModule(); mockReadCurrentInternalPublicStatusConfigSnapshot.mockResolvedValue({ @@ -423,12 +441,6 @@ describe("public-status rebuild worker", () => { ], }); mockQueryPublicStatusRequests.mockResolvedValue([]); - mockBuildPublicStatusPayloadFromRequests.mockReturnValue({ - generatedAt: "2026-04-21T10:00:00.000Z", - coveredFrom: "2026-04-20T10:00:00.000Z", - coveredTo: "2026-04-21T10:00:00.000Z", - groups: [], - }); mockRedisSet.mockReset(); mockRedisSet.mockResolvedValueOnce("OK"); @@ -459,18 +471,47 @@ describe("public-status rebuild worker", () => { expect.stringContaining("public-status:v1:rebuild-lock:"), expect.any(String) ); - expect(mockRedisDel).toHaveBeenCalled(); - - const snapshotKey = buildPublicStatusCurrentSnapshotKey({ - intervalMinutes: 5, - rangeHours: 24, - generation: manifestValue.lastCompleteGeneration, + expect(mockBuildAndPersistPublicStatusHourlyRollup).toHaveBeenCalledTimes(30 * 24); + expect(mockBuildAndPersistPublicStatusHourlyRollup).toHaveBeenCalledWith({ + configVersion: "cfg-1", + hourStart: new Date("2026-03-22T10:00:00.000Z"), + groups: expect.any(Array), + }); + expect(mockBuildAndPersistPublicStatusHourlyRollup).toHaveBeenLastCalledWith({ + configVersion: "cfg-1", + hourStart: new Date("2026-04-21T09:00:00.000Z"), + groups: expect.any(Array), }); + expect(mockQueryPublicStatusRequests).toHaveBeenCalledWith({ + groups: expect.any(Array), + coveredFrom: new Date("2026-04-21T10:00:00.000Z"), + coveredTo: new Date("2026-04-21T10:02:00.000Z"), + }); + expect(mockBuildPublicStatusHourlyRollupsFromRequests).toHaveBeenCalled(); + expect(mockWriteCurrentHourPublicStatusSummary).toHaveBeenCalledWith( + expect.objectContaining({ + configVersion: "cfg-1", + rows: [], + }) + ); + expect(mockCleanupPublicStatusHourlyRollups).toHaveBeenCalledWith({ + now: new Date("2026-04-21T10:02:00.000Z"), + }); + expect(mockRedisSet.mock.calls).not.toEqual( + expect.arrayContaining([ + expect.arrayContaining([expect.stringContaining("public-status:v1:series:")]), + ]) + ); + expect(mockRedisSet.mock.calls).not.toEqual( + expect.arrayContaining([ + expect.arrayContaining([expect.stringContaining("public-status:v1:snapshot:")]), + ]) + ); expect(mockRedisSet).toHaveBeenCalledWith( - snapshotKey, + versionedManifestKey, expect.any(String), "EX", - 60 * 60 * 24 * 30 + 60 * 60 * 2 ); }); @@ -505,12 +546,6 @@ describe("public-status rebuild worker", () => { ], }); mockQueryPublicStatusRequests.mockResolvedValue([]); - mockBuildPublicStatusPayloadFromRequests.mockReturnValue({ - generatedAt: "2026-04-21T10:00:00.000Z", - coveredFrom: "2026-04-20T10:00:00.000Z", - coveredTo: "2026-04-21T10:00:00.000Z", - groups: [], - }); mockRedisSet.mockReset(); mockRedisSet.mockResolvedValueOnce("OK"); From c71aeb3c4ace361d22bfefcdf60d39a10ec2b76b Mon Sep 17 00:00:00 2001 From: mci77777 Date: Tue, 5 May 2026 16:32:25 +0800 Subject: [PATCH 2/2] fix(public-status): address rollup review feedback --- src/drizzle/schema.ts | 2 +- src/lib/public-status/hourly-rollups.ts | 73 +++++---- src/lib/public-status/rebuild-worker.ts | 139 +++++++++++++++--- .../unit/public-status/hourly-rollups.test.ts | 62 +++++++- .../unit/public-status/rebuild-worker.test.ts | 133 ++++++++++++++++- 5 files changed, 346 insertions(+), 63 deletions(-) diff --git a/src/drizzle/schema.ts b/src/drizzle/schema.ts index 878177d98..7071627f8 100644 --- a/src/drizzle/schema.ts +++ b/src/drizzle/schema.ts @@ -632,7 +632,7 @@ export const publicStatusHourlyRollups = pgTable('public_status_hourly_rollups', label: varchar('label', { length: 200 }).notNull(), vendorIconKey: varchar('vendor_icon_key', { length: 100 }).notNull(), requestTypeBadge: varchar('request_type_badge', { length: 100 }).notNull(), - state: varchar('state', { length: 20 }).notNull().$type<'operational' | 'failed' | 'no_data'>(), + state: varchar('state', { length: 20 }).notNull().$type<'operational' | 'degraded' | 'failed' | 'no_data'>(), successCount: integer('success_count').notNull().default(0), failureCount: integer('failure_count').notNull().default(0), sampleCount: integer('sample_count').notNull().default(0), diff --git a/src/lib/public-status/hourly-rollups.ts b/src/lib/public-status/hourly-rollups.ts index ecb3030ea..79584981e 100644 --- a/src/lib/public-status/hourly-rollups.ts +++ b/src/lib/public-status/hourly-rollups.ts @@ -1,4 +1,4 @@ -import { and, asc, gte, lt, lte, sql } from "drizzle-orm"; +import { and, asc, eq, gte, lt, lte, sql } from "drizzle-orm"; import { db } from "@/drizzle/db"; import { publicStatusHourlyRollups } from "@/drizzle/schema"; import { @@ -22,6 +22,7 @@ import { buildGenerationFingerprint } from "./redis-contract"; export const PUBLIC_STATUS_ROLLUP_RETENTION_DAYS = 30; export const PUBLIC_STATUS_CURRENT_HOUR_CACHE_TTL_SECONDS = 10 * 60; +export const PUBLIC_STATUS_DEGRADED_AVAILABILITY_THRESHOLD = 99.9; export interface PublicStatusHourlyRollupRow { bucketStart: Date; @@ -33,7 +34,7 @@ export interface PublicStatusHourlyRollupRow { label: string; vendorIconKey: string; requestTypeBadge: string; - state: "operational" | "failed" | "no_data"; + state: "operational" | "degraded" | "failed" | "no_data"; successCount: number; failureCount: number; sampleCount: number; @@ -56,16 +57,12 @@ function median(values: number[]): number | null { const sorted = [...values].sort((left, right) => left - right); const middle = Math.floor(sorted.length / 2); - if (sorted.length % 2 === 0) { - const left = sorted[middle - 1]; - const right = sorted[middle]; - if (typeof left !== "number" || typeof right !== "number") { - return null; - } - return Number(((left + right) / 2).toFixed(4)); - } + const result = + sorted.length % 2 === 0 + ? ((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2 + : sorted[middle]; - return sorted[middle] ?? null; + return result === undefined ? null : Number(result.toFixed(4)); } function normalizeFiniteNumber(value: unknown): number | null { @@ -106,6 +103,21 @@ export function buildPublicStatusCurrentHourSummaryKey(input: { ].join(":"); } +function deriveRollupState(input: { + sampleCount: number; + availabilityPct: number | null; +}): PublicStatusHourlyRollupRow["state"] { + if (input.sampleCount === 0 || input.availabilityPct === null) { + return "no_data"; + } + if (input.availabilityPct === 0) { + return "failed"; + } + return input.availabilityPct >= PUBLIC_STATUS_DEGRADED_AVAILABILITY_THRESHOLD + ? "operational" + : "degraded"; +} + export function buildPublicStatusHourlyRollupsFromRequests(input: { configVersion: string; hourStart: string | Date; @@ -257,6 +269,8 @@ export function buildPublicStatusHourlyRollupsFromRequests(input: { tpsValues: [], }; const sampleCount = bucket.successCount + bucket.failureCount; + const availabilityPct = + sampleCount === 0 ? null : Number(((bucket.successCount / sampleCount) * 100).toFixed(2)); return { bucketStart, bucketEnd, @@ -267,12 +281,11 @@ export function buildPublicStatusHourlyRollupsFromRequests(input: { label: model.label, vendorIconKey: model.vendorIconKey, requestTypeBadge: model.requestTypeBadge, - state: sampleCount === 0 ? "no_data" : bucket.successCount > 0 ? "operational" : "failed", + state: deriveRollupState({ sampleCount, availabilityPct }), successCount: bucket.successCount, failureCount: bucket.failureCount, sampleCount, - availabilityPct: - sampleCount === 0 ? null : Number(((bucket.successCount / sampleCount) * 100).toFixed(2)), + availabilityPct, ttfbMs: median(bucket.ttfbValues), tps: median(bucket.tpsValues), generatedAt, @@ -348,7 +361,7 @@ export async function readPublicStatusHourlyRollups(input: { lt(publicStatusHourlyRollups.bucketStart, input.end), ]; if (input.configVersion) { - conditions.push(sql`${publicStatusHourlyRollups.configVersion} = ${input.configVersion}`); + conditions.push(eq(publicStatusHourlyRollups.configVersion, input.configVersion)); } const rows = await db @@ -387,7 +400,9 @@ export async function cleanupPublicStatusHourlyRollups( ): Promise { const now = input.now ?? new Date(); const retentionDays = input.retentionDays ?? PUBLIC_STATUS_ROLLUP_RETENTION_DAYS; - const cutoff = new Date(now.getTime() - retentionDays * 24 * 60 * 60 * 1000); + const cutoff = new Date(now.getTime()); + cutoff.setUTCDate(cutoff.getUTCDate() - retentionDays); + cutoff.setUTCHours(0, 0, 0, 0); await db .delete(publicStatusHourlyRollups) @@ -503,7 +518,7 @@ export function buildPublicStatusPayloadFromHourlyRollups(input: { ) ?? null; rowSlots.push(row); rawStates.push( - row && row.sampleCount > 0 ? (row.successCount > 0 ? "operational" : "failed") : null + row && row.sampleCount > 0 ? (row.state === "failed" ? "failed" : "operational") : null ); } @@ -529,11 +544,13 @@ export function buildPublicStatusPayloadFromHourlyRollups(input: { const sampleCount = row?.sampleCount ?? 0; const state: PublicStatusTimelineState = - filledTimeline[index] === "operational" - ? "operational" - : filledTimeline[index] === "failed" - ? "failed" - : "no_data"; + row && sampleCount > 0 + ? row.state + : filledTimeline[index] === "operational" + ? "operational" + : filledTimeline[index] === "failed" + ? "failed" + : "no_data"; return { bucketStart: bucketStart.toISOString(), @@ -555,17 +572,20 @@ export function buildPublicStatusPayloadFromHourlyRollups(input: { }); const latestStateRaw = [...filledTimeline].reverse().find((state) => state !== null) ?? null; + const latestRowState = + [...rowSlots].reverse().find((row) => row && row.sampleCount > 0)?.state ?? null; return { publicModelKey: model.publicModelKey, label: model.label, vendorIconKey: model.vendorIconKey, requestTypeBadge: model.requestTypeBadge, latestState: - latestStateRaw === "operational" + latestRowState ?? + (latestStateRaw === "operational" ? "operational" : latestStateRaw === "failed" ? "failed" - : "no_data", + : "no_data"), availabilityPct: totalSamples === 0 ? null : Number(((totalSuccess / totalSamples) * 100).toFixed(2)), latestTtfbMs, @@ -613,7 +633,10 @@ function parseSerializedRollupRow(input: unknown): PublicStatusHourlyRollupRow[] typeof value.label !== "string" || typeof value.vendorIconKey !== "string" || typeof value.requestTypeBadge !== "string" || - (value.state !== "operational" && value.state !== "failed" && value.state !== "no_data") || + (value.state !== "operational" && + value.state !== "degraded" && + value.state !== "failed" && + value.state !== "no_data") || typeof value.successCount !== "number" || typeof value.failureCount !== "number" || typeof value.sampleCount !== "number" || diff --git a/src/lib/public-status/rebuild-worker.ts b/src/lib/public-status/rebuild-worker.ts index f6f7056d2..92f8d9d6e 100644 --- a/src/lib/public-status/rebuild-worker.ts +++ b/src/lib/public-status/rebuild-worker.ts @@ -4,10 +4,11 @@ import { publishCurrentPublicStatusConfigProjection } from "./config-publisher"; import { readCurrentInternalPublicStatusConfigSnapshot } from "./config-snapshot"; import { alignHourStartUtc, - buildAndPersistPublicStatusHourlyRollup, buildPublicStatusHourlyRollupsFromRequests, cleanupPublicStatusHourlyRollups, PUBLIC_STATUS_ROLLUP_RETENTION_DAYS, + readPublicStatusHourlyRollups, + upsertPublicStatusHourlyRollups, writeCurrentHourPublicStatusSummary, } from "./hourly-rollups"; import { @@ -25,7 +26,9 @@ interface PublicStatusRebuildResult { const inFlightRebuilds = new Map>(); const REBUILD_LOCK_TTL_MS = 60_000; const RUNTIME_MANIFEST_TTL_SECONDS = 60 * 60 * 2; -const ROLLUP_WRITE_BATCH_SIZE = 6; +const ROLLUP_QUERY_CHUNK_HOURS = 24; +const ROLLUP_RECENT_FINALIZED_HOURS = 2; +const ROLLUP_WRITE_BATCH_SIZE = 500; const ROLLUP_HISTORY_HOURS = PUBLIC_STATUS_ROLLUP_RETENTION_DAYS * 24; interface RedisHintWriter { @@ -85,6 +88,86 @@ function chunkArray(items: T[], size: number): T[][] { return chunks; } +function buildHourlyStarts(input: { coveredFrom: string; coveredTo: string }): Date[] { + const starts: Date[] = []; + const coveredFromMs = alignHourStartUtc(input.coveredFrom).getTime(); + const coveredToMs = alignHourStartUtc(input.coveredTo).getTime(); + for (let cursorMs = coveredFromMs; cursorMs < coveredToMs; cursorMs += 60 * 60 * 1000) { + starts.push(new Date(cursorMs)); + } + return starts; +} + +function buildRollupIdentity(input: { + bucketStart: Date; + publicGroupSlug: string; + publicModelKey: string; + requestTypeBadge: string; +}): string { + return [ + input.bucketStart.toISOString(), + input.publicGroupSlug, + input.publicModelKey, + input.requestTypeBadge, + ].join("\u0000"); +} + +async function findHoursNeedingRollupRefresh(input: { + configVersion: string; + coveredFrom: Date; + coveredTo: Date; + groups: ReturnType; + recentFinalizedHours?: number; +}): Promise { + const allHourStarts = buildHourlyStarts({ + coveredFrom: input.coveredFrom.toISOString(), + coveredTo: input.coveredTo.toISOString(), + }); + if (allHourStarts.length === 0) { + return []; + } + + const existingRows = await readPublicStatusHourlyRollups({ + start: input.coveredFrom, + end: input.coveredTo, + configVersion: input.configVersion, + }); + const existingKeys = new Set( + existingRows.map((row) => + buildRollupIdentity({ + bucketStart: row.bucketStart, + publicGroupSlug: row.publicGroupSlug, + publicModelKey: row.publicModelKey, + requestTypeBadge: row.requestTypeBadge, + }) + ) + ); + const recentStartIndex = Math.max( + 0, + allHourStarts.length - (input.recentFinalizedHours ?? ROLLUP_RECENT_FINALIZED_HOURS) + ); + + return allHourStarts.filter((hourStart, index) => { + if (index >= recentStartIndex) { + return true; + } + + return input.groups.some((group) => + group.models.some( + (model) => + !existingKeys.has( + buildRollupIdentity({ + bucketStart: hourStart, + publicGroupSlug: group.publicGroupSlug, + publicModelKey: model.publicModelKey, + requestTypeBadge: model.requestTypeBadge, + }) + ) + ) + ); + }); +} + async function publishPublicStatusProjection(input: { redis: RedisHintWriter; configVersion: string; @@ -273,12 +356,13 @@ export async function rebuildPublicStatusProjection(input: { const now = input.now ?? new Date(); const coveredTo = alignBucketStartUtc(now.toISOString(), input.intervalMinutes); + const currentHourStart = alignHourStartUtc(now); const manifestCoveredFrom = new Date( Date.parse(coveredTo) - input.rangeHours * 60 * 60 * 1000 ).toISOString(); const rollupCoveredFrom = new Date( - Date.parse(coveredTo) - ROLLUP_HISTORY_HOURS * 60 * 60 * 1000 - ).toISOString(); + currentHourStart.getTime() - ROLLUP_HISTORY_HOURS * 60 * 60 * 1000 + ); const sourceGeneration = buildGenerationFingerprint({ configVersion: configSnapshot.configVersion, intervalMinutes: input.intervalMinutes, @@ -305,27 +389,38 @@ export async function rebuildPublicStatusProjection(input: { } try { - const finalizedHourStarts: Date[] = []; - for ( - let cursorMs = Date.parse(rollupCoveredFrom); - cursorMs < Date.parse(coveredTo); - cursorMs += 60 * 60 * 1000 - ) { - finalizedHourStarts.push(new Date(cursorMs)); - } - for (const hourStartBatch of chunkArray(finalizedHourStarts, ROLLUP_WRITE_BATCH_SIZE)) { - await Promise.all( - hourStartBatch.map((hourStart) => - buildAndPersistPublicStatusHourlyRollup({ - configVersion: configSnapshot.configVersion, - hourStart, - groups, - }) - ) + const finalizedHourStarts = await findHoursNeedingRollupRefresh({ + configVersion: configSnapshot.configVersion, + coveredFrom: rollupCoveredFrom, + coveredTo: currentHourStart, + groups, + }); + for (const hourStartBatch of chunkArray(finalizedHourStarts, ROLLUP_QUERY_CHUNK_HOURS)) { + const chunkStart = hourStartBatch[0]; + const chunkEndStart = hourStartBatch.at(-1); + if (!chunkStart || !chunkEndStart) { + continue; + } + + const chunkEnd = new Date(chunkEndStart.getTime() + 60 * 60 * 1000); + const requests = await queryPublicStatusRequests({ + groups, + coveredFrom: chunkStart, + coveredTo: chunkEnd, + }); + const rollups = hourStartBatch.flatMap((hourStart) => + buildPublicStatusHourlyRollupsFromRequests({ + configVersion: configSnapshot.configVersion, + hourStart, + groups, + requests, + }) ); + for (const rollupBatch of chunkArray(rollups, ROLLUP_WRITE_BATCH_SIZE)) { + await upsertPublicStatusHourlyRollups(rollupBatch); + } } - const currentHourStart = alignHourStartUtc(now); const currentHourRequests = await queryPublicStatusRequests({ groups, coveredFrom: currentHourStart, diff --git a/tests/unit/public-status/hourly-rollups.test.ts b/tests/unit/public-status/hourly-rollups.test.ts index fa86f93d2..842ae9e50 100644 --- a/tests/unit/public-status/hourly-rollups.test.ts +++ b/tests/unit/public-status/hourly-rollups.test.ts @@ -110,7 +110,7 @@ describe("public-status hourly rollups", () => { configVersion: "cfg-1", publicGroupSlug: "openai", publicModelKey: "gpt-4.1", - state: "operational", + state: "degraded", successCount: 1, failureCount: 1, sampleCount: 2, @@ -120,6 +120,53 @@ describe("public-status hourly rollups", () => { }); }); + it("keeps degraded state for low-success hourly buckets", async () => { + const mod = await import("@/lib/public-status/hourly-rollups"); + + const rows = mod.buildPublicStatusHourlyRollupsFromRequests({ + configVersion: "cfg-1", + hourStart: "2026-04-21T10:00:00.000Z", + groups: [buildGroup()], + requests: [ + { + id: 1, + createdAt: "2026-04-21T10:10:00.000Z", + originalModel: "gpt-4.1", + providerChain: [ + { + id: 11, + name: "provider", + groupTag: "openai", + reason: "request_success", + statusCode: 200, + }, + ], + }, + { + id: 2, + createdAt: "2026-04-21T10:20:00.000Z", + originalModel: "gpt-4.1", + providerChain: [ + { + id: 12, + name: "provider", + groupTag: "openai", + reason: "retry_failed", + statusCode: 500, + }, + ], + }, + ], + }); + + expect(rows[0]).toMatchObject({ + state: "degraded", + availabilityPct: 50, + successCount: 1, + failureCount: 1, + }); + }); + it("assembles API-compatible payload from DB history plus current hour summary", async () => { const mod = await import("@/lib/public-status/hourly-rollups"); const payload = mod.buildPublicStatusPayloadFromHourlyRollups({ @@ -138,11 +185,11 @@ describe("public-status hourly rollups", () => { label: "GPT-4.1", vendorIconKey: "openai", requestTypeBadge: "openaiCompatible", - state: "operational", + state: "degraded", successCount: 4, - failureCount: 0, - sampleCount: 4, - availabilityPct: 100, + failureCount: 1, + sampleCount: 5, + availabilityPct: 80, ttfbMs: 120, tps: 8, generatedAt: new Date("2026-04-21T11:00:00.000Z"), @@ -172,14 +219,15 @@ describe("public-status hourly rollups", () => { expect(payload.rebuildState).toBe("fresh"); expect(payload.groups[0]?.models[0]).toMatchObject({ latestState: "failed", - availabilityPct: 80, + availabilityPct: 66.67, latestTtfbMs: 120, latestTps: 8, }); expect(payload.groups[0]?.models[0]?.timeline).toEqual([ expect.objectContaining({ bucketStart: "2026-04-21T10:00:00.000Z", - sampleCount: 4, + sampleCount: 5, + state: "degraded", }), expect.objectContaining({ bucketStart: "2026-04-21T11:00:00.000Z", diff --git a/tests/unit/public-status/rebuild-worker.test.ts b/tests/unit/public-status/rebuild-worker.test.ts index 3e343abce..43dc0ba97 100644 --- a/tests/unit/public-status/rebuild-worker.test.ts +++ b/tests/unit/public-status/rebuild-worker.test.ts @@ -13,8 +13,9 @@ const mockRedisPttl = vi.hoisted(() => vi.fn()); const mockReadCurrentInternalPublicStatusConfigSnapshot = vi.hoisted(() => vi.fn()); const mockQueryPublicStatusRequests = vi.hoisted(() => vi.fn()); const mockPublishCurrentPublicStatusConfigProjection = vi.hoisted(() => vi.fn()); -const mockBuildAndPersistPublicStatusHourlyRollup = vi.hoisted(() => vi.fn()); const mockBuildPublicStatusHourlyRollupsFromRequests = vi.hoisted(() => vi.fn()); +const mockReadPublicStatusHourlyRollups = vi.hoisted(() => vi.fn()); +const mockUpsertPublicStatusHourlyRollups = vi.hoisted(() => vi.fn()); const mockWriteCurrentHourPublicStatusSummary = vi.hoisted(() => vi.fn()); const mockCleanupPublicStatusHourlyRollups = vi.hoisted(() => vi.fn()); @@ -107,9 +108,10 @@ async function importRebuildWorkerModule() { Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours()) ); }, - buildAndPersistPublicStatusHourlyRollup: mockBuildAndPersistPublicStatusHourlyRollup, buildPublicStatusHourlyRollupsFromRequests: mockBuildPublicStatusHourlyRollupsFromRequests, cleanupPublicStatusHourlyRollups: mockCleanupPublicStatusHourlyRollups, + readPublicStatusHourlyRollups: mockReadPublicStatusHourlyRollups, + upsertPublicStatusHourlyRollups: mockUpsertPublicStatusHourlyRollups, writeCurrentHourPublicStatusSummary: mockWriteCurrentHourPublicStatusSummary, })); @@ -172,8 +174,9 @@ describe("public-status rebuild worker", () => { mockRedisGet.mockResolvedValue(null); mockRedisEval.mockResolvedValue(1); mockRedisPttl.mockResolvedValue(-1); - mockBuildAndPersistPublicStatusHourlyRollup.mockResolvedValue([]); mockBuildPublicStatusHourlyRollupsFromRequests.mockReturnValue([]); + mockReadPublicStatusHourlyRollups.mockResolvedValue([]); + mockUpsertPublicStatusHourlyRollups.mockResolvedValue(undefined); mockWriteCurrentHourPublicStatusSummary.mockResolvedValue(true); mockCleanupPublicStatusHourlyRollups.mockResolvedValue(undefined); mockPublishCurrentPublicStatusConfigProjection.mockResolvedValue({ @@ -471,23 +474,41 @@ describe("public-status rebuild worker", () => { expect.stringContaining("public-status:v1:rebuild-lock:"), expect.any(String) ); - expect(mockBuildAndPersistPublicStatusHourlyRollup).toHaveBeenCalledTimes(30 * 24); - expect(mockBuildAndPersistPublicStatusHourlyRollup).toHaveBeenCalledWith({ + expect(mockReadPublicStatusHourlyRollups).toHaveBeenCalledWith({ + start: new Date("2026-03-22T10:00:00.000Z"), + end: new Date("2026-04-21T10:00:00.000Z"), + configVersion: "cfg-1", + }); + expect(mockQueryPublicStatusRequests).toHaveBeenCalledTimes(31); + expect(mockQueryPublicStatusRequests).toHaveBeenNthCalledWith(1, { + groups: expect.any(Array), + coveredFrom: new Date("2026-03-22T10:00:00.000Z"), + coveredTo: new Date("2026-03-23T10:00:00.000Z"), + }); + expect(mockQueryPublicStatusRequests).toHaveBeenNthCalledWith(30, { + groups: expect.any(Array), + coveredFrom: new Date("2026-04-20T10:00:00.000Z"), + coveredTo: new Date("2026-04-21T10:00:00.000Z"), + }); + expect(mockBuildPublicStatusHourlyRollupsFromRequests).toHaveBeenCalledTimes(30 * 24 + 1); + expect(mockBuildPublicStatusHourlyRollupsFromRequests).toHaveBeenCalledWith({ configVersion: "cfg-1", hourStart: new Date("2026-03-22T10:00:00.000Z"), groups: expect.any(Array), + requests: [], }); - expect(mockBuildAndPersistPublicStatusHourlyRollup).toHaveBeenLastCalledWith({ + expect(mockBuildPublicStatusHourlyRollupsFromRequests).toHaveBeenNthCalledWith(30 * 24, { configVersion: "cfg-1", hourStart: new Date("2026-04-21T09:00:00.000Z"), groups: expect.any(Array), + requests: [], }); - expect(mockQueryPublicStatusRequests).toHaveBeenCalledWith({ + expect(mockUpsertPublicStatusHourlyRollups).not.toHaveBeenCalled(); + expect(mockQueryPublicStatusRequests).toHaveBeenLastCalledWith({ groups: expect.any(Array), coveredFrom: new Date("2026-04-21T10:00:00.000Z"), coveredTo: new Date("2026-04-21T10:02:00.000Z"), }); - expect(mockBuildPublicStatusHourlyRollupsFromRequests).toHaveBeenCalled(); expect(mockWriteCurrentHourPublicStatusSummary).toHaveBeenCalledWith( expect.objectContaining({ configVersion: "cfg-1", @@ -561,6 +582,102 @@ describe("public-status rebuild worker", () => { }); }); + it("refreshes missing and recent finalized hours on aligned UTC hour boundaries", async () => { + const mod = await importRebuildWorkerModule(); + + mockReadCurrentInternalPublicStatusConfigSnapshot.mockResolvedValue({ + configVersion: "cfg-1", + generatedAt: "2026-04-21T10:00:00.000Z", + siteTitle: "Claude Code Hub Status", + siteDescription: "Request-derived public status", + defaultIntervalMinutes: 15, + defaultRangeHours: 24, + groups: [ + { + sourceGroupName: "openai", + publicGroupSlug: "openai", + displayName: "OpenAI", + explanatoryCopy: "Primary fleet", + sortOrder: 1, + models: [ + { + publicModelKey: "gpt-4.1", + label: "GPT-4.1", + vendorIconKey: "openai", + requestTypeBadge: "openaiCompatible", + }, + ], + }, + ], + }); + mockReadPublicStatusHourlyRollups.mockResolvedValue([ + { + bucketStart: new Date("2026-04-21T06:00:00.000Z"), + publicGroupSlug: "openai", + publicModelKey: "gpt-4.1", + requestTypeBadge: "openaiCompatible", + }, + { + bucketStart: new Date("2026-04-21T07:00:00.000Z"), + publicGroupSlug: "openai", + publicModelKey: "gpt-4.1", + requestTypeBadge: "openaiCompatible", + }, + ]); + mockQueryPublicStatusRequests.mockResolvedValue([]); + mockRedisSet.mockReset(); + mockRedisSet.mockResolvedValueOnce("OK"); + + const result = await mod.rebuildPublicStatusProjection({ + intervalMinutes: 15, + rangeHours: 24, + now: new Date("2026-04-21T10:17:00.000Z"), + }); + + expect(result.status).toBe("updated"); + expect(mockReadPublicStatusHourlyRollups).toHaveBeenCalledWith({ + start: new Date("2026-03-22T10:00:00.000Z"), + end: new Date("2026-04-21T10:00:00.000Z"), + configVersion: "cfg-1", + }); + expect(mockBuildPublicStatusHourlyRollupsFromRequests).toHaveBeenCalledWith({ + configVersion: "cfg-1", + hourStart: new Date("2026-03-22T10:00:00.000Z"), + groups: expect.any(Array), + requests: [], + }); + expect(mockBuildPublicStatusHourlyRollupsFromRequests).toHaveBeenCalledWith({ + configVersion: "cfg-1", + hourStart: new Date("2026-04-21T08:00:00.000Z"), + groups: expect.any(Array), + requests: [], + }); + expect(mockBuildPublicStatusHourlyRollupsFromRequests).toHaveBeenCalledWith({ + configVersion: "cfg-1", + hourStart: new Date("2026-04-21T09:00:00.000Z"), + groups: expect.any(Array), + requests: [], + }); + const historicalRollupCalls = mockBuildPublicStatusHourlyRollupsFromRequests.mock.calls.slice( + 0, + -1 + ); + expect(historicalRollupCalls).not.toEqual( + expect.arrayContaining([ + [ + expect.objectContaining({ + hourStart: new Date("2026-04-21T10:00:00.000Z"), + }), + ], + ]) + ); + expect(mockQueryPublicStatusRequests).toHaveBeenLastCalledWith({ + groups: expect.any(Array), + coveredFrom: new Date("2026-04-21T10:00:00.000Z"), + coveredTo: new Date("2026-04-21T10:17:00.000Z"), + }); + }); + it("writes rebuild hints with ttl and reason payload", async () => { const mod = await importRebuildHintsModule();