diff --git a/drizzle/0120_availability_projection.sql b/drizzle/0120_availability_projection.sql new file mode 100644 index 000000000..b0207db51 --- /dev/null +++ b/drizzle/0120_availability_projection.sql @@ -0,0 +1,135 @@ +-- Availability projection: outbox + 1-minute buckets (read path no longer scans message_request) +CREATE EXTENSION IF NOT EXISTS pgcrypto;--> statement-breakpoint + +CREATE TABLE IF NOT EXISTS "outbox_events" ( + "id" bigserial PRIMARY KEY, + "event_id" uuid DEFAULT gen_random_uuid() NOT NULL, + "event_type" text NOT NULL, + "aggregate_type" text NOT NULL, + "aggregate_id" bigint NOT NULL, + "occurred_at" timestamp with time zone NOT NULL, + "payload" jsonb NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "published_at" timestamp with time zone, + "attempts" integer DEFAULT 0 NOT NULL, + "last_error" text, + CONSTRAINT "outbox_events_event_id_key" UNIQUE("event_id") +);--> statement-breakpoint + +CREATE INDEX IF NOT EXISTS "idx_outbox_events_unpublished" + ON "outbox_events" USING btree ("id" ASC) + WHERE "published_at" IS NULL;--> statement-breakpoint + +CREATE TABLE IF NOT EXISTS "outbox_processed" ( + "event_id" uuid PRIMARY KEY, + "processed_at" timestamp with time zone DEFAULT now() NOT NULL +);--> statement-breakpoint + +CREATE TABLE IF NOT EXISTS "proj_applied_requests" ( + "request_id" bigint PRIMARY KEY, + "event_id" uuid NOT NULL, + "applied_at" timestamp with time zone DEFAULT now() NOT NULL +);--> statement-breakpoint + +CREATE TABLE IF NOT EXISTS "avail_bucket_1m" ( + "provider_id" integer NOT NULL, + "bucket_start" timestamp with time zone NOT NULL, + "success_cnt" integer DEFAULT 0 NOT NULL, + "failure_cnt" integer DEFAULT 0 NOT NULL, + "excluded_cnt" integer DEFAULT 0 NOT NULL, + "latency_cnt" integer DEFAULT 0 NOT NULL, + "latency_sum_ms" bigint DEFAULT 0 NOT NULL, + "last_request_at" timestamp with time zone, + CONSTRAINT "avail_bucket_1m_pkey" PRIMARY KEY("provider_id","bucket_start") +);--> statement-breakpoint + +CREATE INDEX IF NOT EXISTS "idx_avail_bucket_1m_time" + ON "avail_bucket_1m" USING btree ("bucket_start" DESC);--> statement-breakpoint + +CREATE TABLE IF NOT EXISTS "avail_current" ( + "provider_id" integer PRIMARY KEY, + "state" text DEFAULT 'unknown' NOT NULL, + "availability" double precision DEFAULT 0 NOT NULL, + "request_count" integer DEFAULT 0 NOT NULL, + "last_request_at" timestamp with time zone, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +);--> statement-breakpoint + +CREATE TABLE IF NOT EXISTS "projection_meta" ( + "key" text PRIMARY KEY, + "value" jsonb DEFAULT '{}'::jsonb NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +);--> statement-breakpoint + +INSERT INTO "projection_meta" ("key", "value") +VALUES ('bootstrap', jsonb_build_object('version', 1, 'note', 'availability outbox projections')) +ON CONFLICT ("key") DO NOTHING;--> statement-breakpoint + +CREATE OR REPLACE FUNCTION trg_message_request_outbox() +RETURNS trigger +LANGUAGE plpgsql +AS $$ +DECLARE + v_outcome text; +BEGIN + IF NEW.status_code IS NULL THEN + RETURN NEW; + END IF; + + IF TG_OP = 'UPDATE' AND OLD.status_code IS NOT NULL THEN + RETURN NEW; + END IF; + + IF COALESCE(NEW.is_replay, false) THEN + RETURN NEW; + END IF; + + BEGIN + v_outcome := fn_compute_message_request_success_rate_outcome( + NEW.blocked_by, + NEW.status_code, + NEW.error_message, + NEW.provider_chain + ); + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'trg_message_request_outbox: outcome compute failed for request %: %', + NEW.id, SQLERRM; + v_outcome := NULL; + END; + + IF v_outcome IS NULL THEN + RETURN NEW; + END IF; + + INSERT INTO outbox_events ( + event_type, aggregate_type, aggregate_id, occurred_at, payload + ) VALUES ( + 'request_finalized', + 'message_request', + NEW.id, + COALESCE(NEW.created_at, now()), + jsonb_build_object( + 'request_id', NEW.id, + 'provider_id', NEW.provider_id, + 'model', NEW.model, + 'occurred_at', COALESCE(NEW.created_at, now()), + 'status_code', NEW.status_code, + 'duration_ms', NEW.duration_ms, + 'ttfb_ms', NEW.ttfb_ms, + 'blocked_by', NEW.blocked_by, + 'outcome', v_outcome, + 'group_tag', (SELECT group_tag FROM providers p WHERE p.id = NEW.provider_id), + 'is_replay', COALESCE(NEW.is_replay, false) + ) + ); + + RETURN NEW; +END; +$$;--> statement-breakpoint + +DROP TRIGGER IF EXISTS message_request_outbox_aiud ON message_request;--> statement-breakpoint +CREATE TRIGGER message_request_outbox_aiud + AFTER INSERT OR UPDATE OF status_code, duration_ms, error_message, provider_chain, blocked_by + ON message_request + FOR EACH ROW + EXECUTE FUNCTION trg_message_request_outbox(); diff --git a/drizzle/meta/0120_snapshot.json b/drizzle/meta/0120_snapshot.json new file mode 100644 index 000000000..728e3da75 --- /dev/null +++ b/drizzle/meta/0120_snapshot.json @@ -0,0 +1,5745 @@ +{ + "id": "5a73b354-be53-4ef5-bce8-e3eb4b3c3546", + "prevId": "12d909a0-d12b-4617-b7af-5a64a50201a7", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "action_category": { + "name": "action_category", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "action_type": { + "name": "action_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "target_id": { + "name": "target_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "target_name": { + "name": "target_name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "before_value": { + "name": "before_value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "after_value": { + "name": "after_value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "operator_user_id": { + "name": "operator_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "operator_user_name": { + "name": "operator_user_name", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "operator_key_id": { + "name": "operator_key_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "operator_key_name": { + "name": "operator_key_name", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "operator_ip": { + "name": "operator_ip", + "type": "varchar(45)", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_audit_log_category_created_at": { + "name": "idx_audit_log_category_created_at", + "columns": [ + { + "expression": "action_category", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_log_operator_user_created_at": { + "name": "idx_audit_log_operator_user_created_at", + "columns": [ + { + "expression": "operator_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"audit_log\".\"operator_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_log_operator_ip_created_at": { + "name": "idx_audit_log_operator_ip_created_at", + "columns": [ + { + "expression": "operator_ip", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"audit_log\".\"operator_ip\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_log_target": { + "name": "idx_audit_log_target", + "columns": [ + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"audit_log\".\"target_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_log_created_at_id": { + "name": "idx_audit_log_created_at_id", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cloud_pricing_catalog": { + "name": "cloud_pricing_catalog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "version": { + "name": "version", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "refreshed_at": { + "name": "refreshed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "providers": { + "name": "providers", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "vendors": { + "name": "vendors", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "model_count": { + "name": "model_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "synced_at": { + "name": "synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.error_rules": { + "name": "error_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "pattern": { + "name": "pattern", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "match_type": { + "name": "match_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'regex'" + }, + "category": { + "name": "category", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "override_response": { + "name": "override_response", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "override_status_code": { + "name": "override_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_error_rules_enabled": { + "name": "idx_error_rules_enabled", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "unique_pattern": { + "name": "unique_pattern", + "columns": [ + { + "expression": "pattern", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_category": { + "name": "idx_category", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_match_type": { + "name": "idx_match_type", + "columns": [ + { + "expression": "match_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.keys": { + "name": "keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "can_login_web_ui": { + "name": "can_login_web_ui", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "limit_5h_usd": { + "name": "limit_5h_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_5h_reset_mode": { + "name": "limit_5h_reset_mode", + "type": "daily_reset_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'rolling'" + }, + "limit_daily_usd": { + "name": "limit_daily_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "daily_reset_mode": { + "name": "daily_reset_mode", + "type": "daily_reset_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'fixed'" + }, + "daily_reset_time": { + "name": "daily_reset_time", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'00:00'" + }, + "limit_weekly_usd": { + "name": "limit_weekly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_monthly_usd": { + "name": "limit_monthly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_total_usd": { + "name": "limit_total_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "cost_reset_at": { + "name": "cost_reset_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "limit_concurrent_sessions": { + "name": "limit_concurrent_sessions", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "provider_group": { + "name": "provider_group", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false, + "default": "'default'" + }, + "cache_ttl_preference": { + "name": "cache_ttl_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_keys_user_id": { + "name": "idx_keys_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_keys_key": { + "name": "idx_keys_key", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_keys_created_at": { + "name": "idx_keys_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_keys_deleted_at": { + "name": "idx_keys_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.message_request": { + "name": "message_request", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cost_usd": { + "name": "cost_usd", + "type": "numeric(21, 15)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "cost_multiplier": { + "name": "cost_multiplier", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": false + }, + "group_cost_multiplier": { + "name": "group_cost_multiplier", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": false + }, + "cost_breakdown": { + "name": "cost_breakdown", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "session_identity": { + "name": "session_identity", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "session_identity_kind": { + "name": "session_identity_kind", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "affinity_scope_tag": { + "name": "affinity_scope_tag", + "type": "varchar(16)", + "primaryKey": false, + "notNull": false + }, + "affinity_fingerprint": { + "name": "affinity_fingerprint", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "affinity_fingerprint_chain": { + "name": "affinity_fingerprint_chain", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_replay": { + "name": "is_replay", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "replay_source_request_id": { + "name": "replay_source_request_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_sequence": { + "name": "request_sequence", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "provider_chain": { + "name": "provider_chain", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "routing_trace": { + "name": "routing_trace", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "api_type": { + "name": "api_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "endpoint": { + "name": "endpoint", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "original_model": { + "name": "original_model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "actual_response_model": { + "name": "actual_response_model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "ttfb_ms": { + "name": "ttfb_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "first_byte_ms": { + "name": "first_byte_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_read_input_tokens": { + "name": "cache_read_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_creation_5m_input_tokens": { + "name": "cache_creation_5m_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_creation_1h_input_tokens": { + "name": "cache_creation_1h_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_ttl_applied": { + "name": "cache_ttl_applied", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "context_1m_applied": { + "name": "context_1m_applied", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "swap_cache_ttl_applied": { + "name": "swap_cache_ttl_applied", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "special_settings": { + "name": "special_settings", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "hedge_losers": { + "name": "hedge_losers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_stack": { + "name": "error_stack", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_cause": { + "name": "error_cause", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "blocked_by": { + "name": "blocked_by", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "blocked_reason": { + "name": "blocked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "client_ip": { + "name": "client_ip", + "type": "varchar(45)", + "primaryKey": false, + "notNull": false + }, + "messages_count": { + "name": "messages_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cache_compatibility_key": { + "name": "cache_compatibility_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "cache_score_eligible": { + "name": "cache_score_eligible", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cache_score_excluded_reason": { + "name": "cache_score_excluded_reason", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "theoretical_cache_tokens": { + "name": "theoretical_cache_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_ttl_bucket": { + "name": "cache_ttl_bucket", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_message_request_user_date_cost": { + "name": "idx_message_request_user_date_cost", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_user_created_at_cost_stats": { + "name": "idx_message_request_user_created_at_cost_stats", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_user_query": { + "name": "idx_message_request_user_query", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_provider_created_at_active": { + "name": "idx_message_request_provider_created_at_active", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_provider_created_at_finalized_active": { + "name": "idx_message_request_provider_created_at_finalized_active", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"status_code\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_proxy_status_active": { + "name": "idx_message_request_proxy_status_active", + "columns": [ + { + "expression": "\"created_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"is_replay\" = false AND \"message_request\".\"status_code\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_proxy_status_latest": { + "name": "idx_message_request_proxy_status_latest", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"updated_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"is_replay\" = false AND \"message_request\".\"status_code\" IS NOT NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_session_id": { + "name": "idx_message_request_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_session_id_prefix": { + "name": "idx_message_request_session_id_prefix", + "columns": [ + { + "expression": "\"session_id\" varchar_pattern_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_session_seq": { + "name": "idx_message_request_session_seq", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_session_identity_created_at": { + "name": "idx_message_request_session_identity_created_at", + "columns": [ + { + "expression": "COALESCE(\"session_identity\", \"session_id\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"created_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "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_endpoint": { + "name": "idx_message_request_endpoint", + "columns": [ + { + "expression": "endpoint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_blocked_by": { + "name": "idx_message_request_blocked_by", + "columns": [ + { + "expression": "blocked_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_provider_id": { + "name": "idx_message_request_provider_id", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_user_id": { + "name": "idx_message_request_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key": { + "name": "idx_message_request_key", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key_created_at_id": { + "name": "idx_message_request_key_created_at_id", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key_model_active": { + "name": "idx_message_request_key_model_active", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"model\" IS NOT NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key_endpoint_active": { + "name": "idx_message_request_key_endpoint_active", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "endpoint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"endpoint\" IS NOT NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_created_at_id_active": { + "name": "idx_message_request_created_at_id_active", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_model_active": { + "name": "idx_message_request_model_active", + "columns": [ + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"model\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_status_code_active": { + "name": "idx_message_request_status_code_active", + "columns": [ + { + "expression": "status_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"status_code\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_created_at": { + "name": "idx_message_request_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_deleted_at": { + "name": "idx_message_request_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key_last_active": { + "name": "idx_message_request_key_last_active", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key_cost_active": { + "name": "idx_message_request_key_cost_active", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_session_user_info": { + "name": "idx_message_request_session_user_info", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_client_ip_created_at": { + "name": "idx_message_request_client_ip_created_at", + "columns": [ + { + "expression": "client_ip", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"client_ip\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_prices": { + "name": "model_prices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "model_name": { + "name": "model_name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "price_data": { + "name": "price_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'cloud'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_model_prices_latest": { + "name": "idx_model_prices_latest", + "columns": [ + { + "expression": "model_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_model_prices_model_name": { + "name": "idx_model_prices_model_name", + "columns": [ + { + "expression": "model_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_model_prices_created_at": { + "name": "idx_model_prices_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_model_prices_source": { + "name": "idx_model_prices_source", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_model_prices_vendor": { + "name": "idx_model_prices_vendor", + "columns": [ + { + "expression": "((\"price_data\" ->> 'vendor'))", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_model_prices_aliases": { + "name": "idx_model_prices_aliases", + "columns": [ + { + "expression": "((\"price_data\" -> 'aliases'))", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_settings": { + "name": "notification_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "use_legacy_mode": { + "name": "use_legacy_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "circuit_breaker_enabled": { + "name": "circuit_breaker_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "circuit_breaker_webhook": { + "name": "circuit_breaker_webhook", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "daily_leaderboard_enabled": { + "name": "daily_leaderboard_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "daily_leaderboard_webhook": { + "name": "daily_leaderboard_webhook", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "daily_leaderboard_time": { + "name": "daily_leaderboard_time", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false, + "default": "'09:00'" + }, + "daily_leaderboard_top_n": { + "name": "daily_leaderboard_top_n", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5 + }, + "cost_alert_enabled": { + "name": "cost_alert_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cost_alert_webhook": { + "name": "cost_alert_webhook", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "cost_alert_threshold": { + "name": "cost_alert_threshold", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false, + "default": "'0.80'" + }, + "cost_alert_check_interval": { + "name": "cost_alert_check_interval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 60 + }, + "cache_hit_rate_alert_enabled": { + "name": "cache_hit_rate_alert_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cache_hit_rate_alert_webhook": { + "name": "cache_hit_rate_alert_webhook", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "cache_hit_rate_alert_window_mode": { + "name": "cache_hit_rate_alert_window_mode", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false, + "default": "'auto'" + }, + "cache_hit_rate_alert_check_interval": { + "name": "cache_hit_rate_alert_check_interval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5 + }, + "cache_hit_rate_alert_historical_lookback_days": { + "name": "cache_hit_rate_alert_historical_lookback_days", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 7 + }, + "cache_hit_rate_alert_min_eligible_requests": { + "name": "cache_hit_rate_alert_min_eligible_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 20 + }, + "cache_hit_rate_alert_min_eligible_tokens": { + "name": "cache_hit_rate_alert_min_eligible_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "cache_hit_rate_alert_abs_min": { + "name": "cache_hit_rate_alert_abs_min", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "cache_hit_rate_alert_drop_rel": { + "name": "cache_hit_rate_alert_drop_rel", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.3'" + }, + "cache_hit_rate_alert_drop_abs": { + "name": "cache_hit_rate_alert_drop_abs", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.1'" + }, + "cache_hit_rate_alert_cooldown_minutes": { + "name": "cache_hit_rate_alert_cooldown_minutes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30 + }, + "cache_hit_rate_alert_top_n": { + "name": "cache_hit_rate_alert_top_n", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 10 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_target_bindings": { + "name": "notification_target_bindings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "notification_type": { + "name": "notification_type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "schedule_cron": { + "name": "schedule_cron", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "schedule_timezone": { + "name": "schedule_timezone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "template_override": { + "name": "template_override", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "unique_notification_target_binding": { + "name": "unique_notification_target_binding", + "columns": [ + { + "expression": "notification_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_notification_bindings_type": { + "name": "idx_notification_bindings_type", + "columns": [ + { + "expression": "notification_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_notification_bindings_target": { + "name": "idx_notification_bindings_target", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notification_target_bindings_target_id_webhook_targets_id_fk": { + "name": "notification_target_bindings_target_id_webhook_targets_id_fk", + "tableFrom": "notification_target_bindings", + "tableTo": "webhook_targets", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_batch_apply_operations": { + "name": "provider_batch_apply_operations", + "schema": "", + "columns": { + "claim_key": { + "name": "claim_key", + "type": "varchar(256)", + "primaryKey": true, + "notNull": true + }, + "preview_token": { + "name": "preview_token", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "payload_fingerprint": { + "name": "payload_fingerprint", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "operation_id": { + "name": "operation_id", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "undo_token": { + "name": "undo_token", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "undo_expires_at": { + "name": "undo_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "undo_consumed_at": { + "name": "undo_consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uniq_provider_batch_apply_operations_preview_token": { + "name": "uniq_provider_batch_apply_operations_preview_token", + "columns": [ + { + "expression": "preview_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "uniq_provider_batch_apply_operations_operation_id": { + "name": "uniq_provider_batch_apply_operations_operation_id", + "columns": [ + { + "expression": "operation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "uniq_provider_batch_apply_operations_undo_token": { + "name": "uniq_provider_batch_apply_operations_undo_token", + "columns": [ + { + "expression": "undo_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_batch_apply_operations_expires_at": { + "name": "idx_provider_batch_apply_operations_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_cache_effectiveness": { + "name": "provider_cache_effectiveness", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "cache_ttl_bucket": { + "name": "cache_ttl_bucket", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true + }, + "window_start": { + "name": "window_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "window_end": { + "name": "window_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "sample_count": { + "name": "sample_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "eligible_count": { + "name": "eligible_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "theoretical_cache_tokens": { + "name": "theoretical_cache_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "observed_cache_read_tokens": { + "name": "observed_cache_read_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "raw_effectiveness_bp": { + "name": "raw_effectiveness_bp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "confidence_bp": { + "name": "confidence_bp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "effectiveness_bp": { + "name": "effectiveness_bp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_provider_cache_effectiveness_window": { + "name": "idx_provider_cache_effectiveness_window", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_start", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_endpoint_probe_logs": { + "name": "provider_endpoint_probe_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "endpoint_id": { + "name": "endpoint_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'scheduled'" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error_type": { + "name": "error_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_provider_endpoint_probe_logs_endpoint_created_at": { + "name": "idx_provider_endpoint_probe_logs_endpoint_created_at", + "columns": [ + { + "expression": "endpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoint_probe_logs_created_at": { + "name": "idx_provider_endpoint_probe_logs_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "provider_endpoint_probe_logs_endpoint_id_provider_endpoints_id_fk": { + "name": "provider_endpoint_probe_logs_endpoint_id_provider_endpoints_id_fk", + "tableFrom": "provider_endpoint_probe_logs", + "tableTo": "provider_endpoints", + "columnsFrom": [ + "endpoint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_endpoints": { + "name": "provider_endpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provider_type": { + "name": "provider_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'claude'" + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_probed_at": { + "name": "last_probed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_probe_ok": { + "name": "last_probe_ok", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "last_probe_status_code": { + "name": "last_probe_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_probe_latency_ms": { + "name": "last_probe_latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_probe_error_type": { + "name": "last_probe_error_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "last_probe_error_message": { + "name": "last_probe_error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "uniq_provider_endpoints_vendor_type_url": { + "name": "uniq_provider_endpoints_vendor_type_url", + "columns": [ + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"provider_endpoints\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_vendor_type": { + "name": "idx_provider_endpoints_vendor_type", + "columns": [ + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"provider_endpoints\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_enabled": { + "name": "idx_provider_endpoints_enabled", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"provider_endpoints\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_pick_enabled": { + "name": "idx_provider_endpoints_pick_enabled", + "columns": [ + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"provider_endpoints\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_created_at": { + "name": "idx_provider_endpoints_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_deleted_at": { + "name": "idx_provider_endpoints_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "provider_endpoints_vendor_id_provider_vendors_id_fk": { + "name": "provider_endpoints_vendor_id_provider_vendors_id_fk", + "tableFrom": "provider_endpoints", + "tableTo": "provider_vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_groups": { + "name": "provider_groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "cost_multiplier": { + "name": "cost_multiplier", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": true, + "default": "'1.0'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "provider_groups_name_unique": { + "name": "provider_groups_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_vendors": { + "name": "provider_vendors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "website_domain": { + "name": "website_domain", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "website_url": { + "name": "website_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "favicon_url": { + "name": "favicon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "uniq_provider_vendors_website_domain": { + "name": "uniq_provider_vendors_website_domain", + "columns": [ + { + "expression": "website_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_vendors_created_at": { + "name": "idx_provider_vendors_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.providers": { + "name": "providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "provider_vendor_id": { + "name": "provider_vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "weight": { + "name": "weight", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "group_priorities": { + "name": "group_priorities", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'null'::jsonb" + }, + "cost_multiplier": { + "name": "cost_multiplier", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": false, + "default": "'1.0'" + }, + "group_tag": { + "name": "group_tag", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "provider_type": { + "name": "provider_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'claude'" + }, + "preserve_client_ip": { + "name": "preserve_client_ip", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "disable_session_reuse": { + "name": "disable_session_reuse", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "model_redirects": { + "name": "model_redirects", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "allowed_models": { + "name": "allowed_models", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'null'::jsonb" + }, + "allowed_clients": { + "name": "allowed_clients", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "blocked_clients": { + "name": "blocked_clients", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "active_time_start": { + "name": "active_time_start", + "type": "varchar(5)", + "primaryKey": false, + "notNull": false + }, + "active_time_end": { + "name": "active_time_end", + "type": "varchar(5)", + "primaryKey": false, + "notNull": false + }, + "codex_instructions_strategy": { + "name": "codex_instructions_strategy", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "default": "'auto'" + }, + "mcp_passthrough_type": { + "name": "mcp_passthrough_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "mcp_passthrough_url": { + "name": "mcp_passthrough_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "limit_5h_usd": { + "name": "limit_5h_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_5h_reset_mode": { + "name": "limit_5h_reset_mode", + "type": "daily_reset_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'rolling'" + }, + "limit_daily_usd": { + "name": "limit_daily_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "daily_reset_mode": { + "name": "daily_reset_mode", + "type": "daily_reset_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'fixed'" + }, + "daily_reset_time": { + "name": "daily_reset_time", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'00:00'" + }, + "limit_weekly_usd": { + "name": "limit_weekly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_monthly_usd": { + "name": "limit_monthly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_total_usd": { + "name": "limit_total_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "total_cost_reset_at": { + "name": "total_cost_reset_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "limit_concurrent_sessions": { + "name": "limit_concurrent_sessions", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "max_retry_attempts": { + "name": "max_retry_attempts", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "circuit_breaker_failure_threshold": { + "name": "circuit_breaker_failure_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5 + }, + "circuit_breaker_open_duration": { + "name": "circuit_breaker_open_duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1800000 + }, + "circuit_breaker_half_open_success_threshold": { + "name": "circuit_breaker_half_open_success_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 2 + }, + "proxy_url": { + "name": "proxy_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "proxy_fallback_to_direct": { + "name": "proxy_fallback_to_direct", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "custom_headers": { + "name": "custom_headers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "first_byte_timeout_streaming_ms": { + "name": "first_byte_timeout_streaming_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "streaming_idle_timeout_ms": { + "name": "streaming_idle_timeout_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "request_timeout_non_streaming_ms": { + "name": "request_timeout_non_streaming_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "website_url": { + "name": "website_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "favicon_url": { + "name": "favicon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cache_ttl_preference": { + "name": "cache_ttl_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "swap_cache_ttl_billing": { + "name": "swap_cache_ttl_billing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "context_1m_preference": { + "name": "context_1m_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "codex_reasoning_effort_preference": { + "name": "codex_reasoning_effort_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "codex_reasoning_summary_preference": { + "name": "codex_reasoning_summary_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "codex_text_verbosity_preference": { + "name": "codex_text_verbosity_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "codex_parallel_tool_calls_preference": { + "name": "codex_parallel_tool_calls_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "codex_image_generation_preference": { + "name": "codex_image_generation_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "codex_service_tier_preference": { + "name": "codex_service_tier_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "anthropic_max_tokens_preference": { + "name": "anthropic_max_tokens_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "anthropic_thinking_budget_preference": { + "name": "anthropic_thinking_budget_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "anthropic_adaptive_thinking": { + "name": "anthropic_adaptive_thinking", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'null'::jsonb" + }, + "gemini_google_search_preference": { + "name": "gemini_google_search_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "tpm": { + "name": "tpm", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "rpm": { + "name": "rpm", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "rpd": { + "name": "rpd", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "cc": { + "name": "cc", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_providers_enabled_priority": { + "name": "idx_providers_enabled_priority", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "weight", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"providers\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_group": { + "name": "idx_providers_group", + "columns": [ + { + "expression": "group_tag", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"providers\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_vendor_type_url_active": { + "name": "idx_providers_vendor_type_url_active", + "columns": [ + { + "expression": "provider_vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"providers\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_created_at": { + "name": "idx_providers_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_deleted_at": { + "name": "idx_providers_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_vendor_type": { + "name": "idx_providers_vendor_type", + "columns": [ + { + "expression": "provider_vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"providers\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_enabled_vendor_type": { + "name": "idx_providers_enabled_vendor_type", + "columns": [ + { + "expression": "provider_vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"providers\".\"deleted_at\" IS NULL AND \"providers\".\"is_enabled\" = true AND \"providers\".\"provider_vendor_id\" IS NOT NULL AND \"providers\".\"provider_vendor_id\" > 0", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "providers_provider_vendor_id_provider_vendors_id_fk": { + "name": "providers_provider_vendor_id_provider_vendors_id_fk", + "tableFrom": "providers", + "tableTo": "provider_vendors", + "columnsFrom": [ + "provider_vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.replay_payloads": { + "name": "replay_payloads", + "schema": "", + "columns": { + "replay_id": { + "name": "replay_id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "verifier": { + "name": "verifier", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "scope_tag": { + "name": "scope_tag", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "format": { + "name": "format", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "headers_json": { + "name": "headers_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "source_message_request_id": { + "name": "source_message_request_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_replay_payloads_key_id": { + "name": "idx_replay_payloads_key_id", + "columns": [ + { + "expression": "key_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_replay_payloads_expires_at": { + "name": "idx_replay_payloads_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.request_filters": { + "name": "request_filters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true + }, + "match_type": { + "name": "match_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "target": { + "name": "target", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "replacement": { + "name": "replacement", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "binding_type": { + "name": "binding_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'global'" + }, + "provider_ids": { + "name": "provider_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "group_tags": { + "name": "group_tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rule_mode": { + "name": "rule_mode", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'simple'" + }, + "execution_phase": { + "name": "execution_phase", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'guard'" + }, + "operations": { + "name": "operations", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_request_filters_enabled": { + "name": "idx_request_filters_enabled", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_request_filters_scope": { + "name": "idx_request_filters_scope", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_request_filters_action": { + "name": "idx_request_filters_action", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_request_filters_binding": { + "name": "idx_request_filters_binding", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "binding_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_request_filters_phase": { + "name": "idx_request_filters_phase", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_phase", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sensitive_words": { + "name": "sensitive_words", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "word": { + "name": "word", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "match_type": { + "name": "match_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'contains'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_sensitive_words_enabled": { + "name": "idx_sensitive_words_enabled", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "match_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sensitive_words_created_at": { + "name": "idx_sensitive_words_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.system_settings": { + "name": "system_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "site_title": { + "name": "site_title", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "default": "'CC Hub'" + }, + "allow_global_usage_view": { + "name": "allow_global_usage_view", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "currency_display": { + "name": "currency_display", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "billing_model_source": { + "name": "billing_model_source", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'original'" + }, + "codex_priority_billing_source": { + "name": "codex_priority_billing_source", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'requested'" + }, + "bill_non_successful_requests": { + "name": "bill_non_successful_requests", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "bill_hedge_losers": { + "name": "bill_hedge_losers", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "discovery_enabled": { + "name": "discovery_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "discovery_concurrency": { + "name": "discovery_concurrency", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 2 + }, + "max_discovery_rounds": { + "name": "max_discovery_rounds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 2 + }, + "discovery_sla_ms": { + "name": "discovery_sla_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10000 + }, + "sticky_sla_ms": { + "name": "sticky_sla_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 20000 + }, + "racing_total_timeout_ms": { + "name": "racing_total_timeout_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 60000 + }, + "sticky_timeout_cooldown_ms": { + "name": "sticky_timeout_cooldown_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 300000 + }, + "timezone": { + "name": "timezone", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "enable_auto_cleanup": { + "name": "enable_auto_cleanup", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "cleanup_retention_days": { + "name": "cleanup_retention_days", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30 + }, + "cleanup_schedule": { + "name": "cleanup_schedule", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "default": "'0 2 * * *'" + }, + "cleanup_batch_size": { + "name": "cleanup_batch_size", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 10000 + }, + "enable_client_version_check": { + "name": "enable_client_version_check", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "verbose_provider_error": { + "name": "verbose_provider_error", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pass_through_upstream_error_message": { + "name": "pass_through_upstream_error_message", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_http2": { + "name": "enable_http2", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_openai_responses_websocket": { + "name": "enable_openai_responses_websocket", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_high_concurrency_mode": { + "name": "enable_high_concurrency_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "intercept_anthropic_warmup_requests": { + "name": "intercept_anthropic_warmup_requests", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_thinking_signature_rectifier": { + "name": "enable_thinking_signature_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_thinking_budget_rectifier": { + "name": "enable_thinking_budget_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_thinking_effort_conflict_rectifier": { + "name": "enable_thinking_effort_conflict_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_gemini_function_id_rectifier": { + "name": "enable_gemini_function_id_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_billing_header_rectifier": { + "name": "enable_billing_header_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_response_input_rectifier": { + "name": "enable_response_input_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_non_conversation_endpoint_provider_fallback": { + "name": "allow_non_conversation_endpoint_provider_fallback", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "fake_streaming_whitelist": { + "name": "fake_streaming_whitelist", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "enable_codex_session_id_completion": { + "name": "enable_codex_session_id_completion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_claude_metadata_user_id_injection": { + "name": "enable_claude_metadata_user_id_injection", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_response_fixer": { + "name": "enable_response_fixer", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "response_fixer_config": { + "name": "response_fixer_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{\"fixTruncatedJson\":true,\"fixSseFormat\":true,\"fixEncoding\":true,\"maxJsonDepth\":200,\"maxFixSize\":1048576}'::jsonb" + }, + "quota_db_refresh_interval_seconds": { + "name": "quota_db_refresh_interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 10 + }, + "quota_lease_percent_5h": { + "name": "quota_lease_percent_5h", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "quota_lease_percent_daily": { + "name": "quota_lease_percent_daily", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "quota_lease_percent_weekly": { + "name": "quota_lease_percent_weekly", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "quota_lease_percent_monthly": { + "name": "quota_lease_percent_monthly", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "quota_lease_cap_usd": { + "name": "quota_lease_cap_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "ip_extraction_config": { + "name": "ip_extraction_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ip_geo_lookup_enabled": { + "name": "ip_geo_lookup_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "public_status_window_hours": { + "name": "public_status_window_hours", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 24 + }, + "public_status_aggregation_interval_minutes": { + "name": "public_status_aggregation_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "stream_gate_mode": { + "name": "stream_gate_mode", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true, + "default": "'enforce'" + }, + "affinity_ignore_client_session_id": { + "name": "affinity_ignore_client_session_id", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "replay_enabled": { + "name": "replay_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "replay_cache_ttl_minutes": { + "name": "replay_cache_ttl_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 30 + }, + "cache_effectiveness_enabled": { + "name": "cache_effectiveness_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_ledger": { + "name": "usage_ledger", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "final_provider_id": { + "name": "final_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "original_model": { + "name": "original_model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "actual_response_model": { + "name": "actual_response_model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "endpoint": { + "name": "endpoint", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "api_type": { + "name": "api_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "session_identity": { + "name": "session_identity", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "session_identity_kind": { + "name": "session_identity_kind", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "affinity_scope_tag": { + "name": "affinity_scope_tag", + "type": "varchar(16)", + "primaryKey": false, + "notNull": false + }, + "affinity_fingerprint": { + "name": "affinity_fingerprint", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "affinity_fingerprint_chain": { + "name": "affinity_fingerprint_chain", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_replay": { + "name": "is_replay", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "replay_source_request_id": { + "name": "replay_source_request_id", + "type": "integer", + "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 + }, + "first_byte_ms": { + "name": "first_byte_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 AND \"usage_ledger\".\"is_replay\" = false", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_user_id_reset": { + "name": "idx_usage_ledger_user_id_reset", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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 AND \"usage_ledger\".\"is_replay\" = false", + "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 AND \"usage_ledger\".\"is_replay\" = false", + "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_session_identity_created_at": { + "name": "idx_usage_ledger_session_identity_created_at", + "columns": [ + { + "expression": "COALESCE(\"session_identity\", \"session_id\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"created_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL AND \"usage_ledger\".\"is_replay\" = false", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_session_identity": { + "name": "idx_usage_ledger_session_identity", + "columns": [ + { + "expression": "COALESCE(\"session_identity\", \"session_id\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_model": { + "name": "idx_usage_ledger_model", + "columns": [ + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"model\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_key_cost": { + "name": "idx_usage_ledger_key_cost", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "endpoint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL AND \"usage_ledger\".\"is_replay\" = false", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_user_cost_cover": { + "name": "idx_usage_ledger_user_cost_cover", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "endpoint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL AND \"usage_ledger\".\"is_replay\" = false", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_provider_cost_cover": { + "name": "idx_usage_ledger_provider_cost_cover", + "columns": [ + { + "expression": "final_provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "endpoint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL AND \"usage_ledger\".\"is_replay\" = false", + "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 AND \"usage_ledger\".\"is_replay\" = false", + "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 + }, + "public.outbox_events": { + "name": "outbox_events", + "schema": "public", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "aggregate_type": { + "name": "aggregate_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "aggregate_id": { + "name": "aggregate_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_outbox_events_unpublished": { + "name": "idx_outbox_events_unpublished", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {}, + "where": "\"outbox_events\".\"published_at\" IS NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "outbox_events_event_id_key": { + "name": "outbox_events_event_id_key", + "nullsNotDistinct": false, + "columns": [ + "event_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outbox_processed": { + "name": "outbox_processed", + "schema": "public", + "columns": { + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.proj_applied_requests": { + "name": "proj_applied_requests", + "schema": "public", + "columns": { + "request_id": { + "name": "request_id", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.avail_bucket_1m": { + "name": "avail_bucket_1m", + "schema": "public", + "columns": { + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "bucket_start": { + "name": "bucket_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "success_cnt": { + "name": "success_cnt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "failure_cnt": { + "name": "failure_cnt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "excluded_cnt": { + "name": "excluded_cnt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "latency_cnt": { + "name": "latency_cnt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "latency_sum_ms": { + "name": "latency_sum_ms", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "last_request_at": { + "name": "last_request_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_avail_bucket_1m_time": { + "name": "idx_avail_bucket_1m_time", + "columns": [ + { + "expression": "bucket_start", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "avail_bucket_1m_pkey": { + "name": "avail_bucket_1m_pkey", + "columns": [ + "provider_id", + "bucket_start" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.avail_current": { + "name": "avail_current", + "schema": "public", + "columns": { + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "availability": { + "name": "availability", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "last_request_at": { + "name": "last_request_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projection_meta": { + "name": "projection_meta", + "schema": "public", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "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": {} + } +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index f6b450d14..6e10f0a36 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -841,6 +841,13 @@ "when": 1786038550610, "tag": "0119_tiresome_banshee", "breakpoints": true + }, + { + "idx": 120, + "version": "7", + "when": 1786500000000, + "tag": "0120_availability_projection", + "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/src/drizzle/schema.ts b/src/drizzle/schema.ts index fea464b10..08088d73d 100644 --- a/src/drizzle/schema.ts +++ b/src/drizzle/schema.ts @@ -1425,3 +1425,13 @@ export const messageRequestRelations = relations(messageRequest, ({ one }) => ({ references: [providers.id], }), })); + +// Availability projection tables (outbox + 1m buckets). Source of truth for drizzle-kit. +export { + availBucket1m, + availCurrent, + outboxEvents, + outboxProcessed, + projAppliedRequests, + projectionMeta, +} from "@/lib/availability/projection-tables"; diff --git a/src/instrumentation.ts b/src/instrumentation.ts index a14f0003d..5696092a8 100644 --- a/src/instrumentation.ts +++ b/src/instrumentation.ts @@ -639,6 +639,17 @@ export async function register() { }); } + try { + const { startAvailabilityProjectionWorker } = await import( + "@/lib/availability/projection-worker" + ); + startAvailabilityProjectionWorker(); + } catch (error) { + logger.warn("[Instrumentation] Failed to start availability projection worker", { + error: error instanceof Error ? error.message : String(error), + }); + } + // 初始化端点熔断器(禁用时清理残留状态) try { const { initEndpointCircuitBreaker } = await import("@/lib/endpoint-circuit-breaker"); @@ -808,6 +819,17 @@ export async function register() { }); } + try { + const { startAvailabilityProjectionWorker } = await import( + "@/lib/availability/projection-worker" + ); + startAvailabilityProjectionWorker(); + } catch (error) { + logger.warn("[Instrumentation] Failed to start availability projection worker", { + error: error instanceof Error ? error.message : String(error), + }); + } + // 初始化端点熔断器(禁用时清理残留状态) try { const { initEndpointCircuitBreaker } = await import("@/lib/endpoint-circuit-breaker"); diff --git a/src/lib/availability/availability-service.ts b/src/lib/availability/availability-service.ts index d465cb31d..28cee31e8 100644 --- a/src/lib/availability/availability-service.ts +++ b/src/lib/availability/availability-service.ts @@ -2,11 +2,14 @@ * Provider Availability Aggregation Service * Calculates availability metrics from request logs * Simple two-tier status: success (green) or failure (red) + * + * Read path uses incremental 1-minute projection buckets (avail_bucket_1m). + * message_request is no longer scanned here. */ -import { and, eq, inArray, isNotNull, isNull, type SQLWrapper, sql } from "drizzle-orm"; +import { and, eq, inArray, isNull, sql } from "drizzle-orm"; import { db } from "@/drizzle/db"; -import { messageRequest, providers } from "@/drizzle/schema"; +import { providers } from "@/drizzle/schema"; import type { AvailabilityQueryOptions, AvailabilityQueryResult, @@ -30,31 +33,34 @@ type AggregatedAvailabilityBucketRow = { lastRequestAt: Date | null; }; -type AggregatedCurrentProviderStatusRow = { - providerId: number; - greenCount: number; - redCount: number; - lastRequestAt: Date | null; -}; - export const MIN_BUCKET_SIZE_MINUTES = 0.25; export const MAX_BUCKET_SIZE_MINUTES = 1440; const DEFAULT_MAX_BUCKETS = 100; const AVAILABILITY_SUCCESS_STATUS_CODE_MIN = 200; const AVAILABILITY_SUCCESS_STATUS_CODE_MAX_EXCLUSIVE = 400; -const FINALIZED_REQUEST_OUTCOME_ALIAS = "successRateOutcome" as const; -const FINALIZED_REQUEST_OUTCOME_SQL = sql.raw(`"${FINALIZED_REQUEST_OUTCOME_ALIAS}"`); -const COUNTABLE_REQUEST_OUTCOME_SQL = sql`${FINALIZED_REQUEST_OUTCOME_SQL} IN ('success', 'failure')`; // Keep the hard cap independent from the UI/API default so future default tuning does not silently relax/tighten the guardrail. // It intentionally equals the default today; the separation preserves distinct semantic roles for future tuning. export const MAX_BUCKETS_HARD_LIMIT = 100; -const CURRENT_PROVIDER_STATUS_WINDOW_MINUTES = 15; +/** Shared window for avail_current freshness and getCurrentProviderStatus fallback. */ +export const CURRENT_PROVIDER_STATUS_WINDOW_MINUTES = 15; export const MAX_AVAILABILITY_QUERY_RANGE_DAYS = (MAX_BUCKETS_HARD_LIMIT * MAX_BUCKET_SIZE_MINUTES) / (24 * 60); const MAX_AVAILABILITY_QUERY_RANGE_MS = MAX_BUCKETS_HARD_LIMIT * MAX_BUCKET_SIZE_MINUTES * 60 * 1000; +function floorToUtcMinute(date: Date): Date { + return new Date(Date.UTC( + date.getUTCFullYear(), + date.getUTCMonth(), + date.getUTCDate(), + date.getUTCHours(), + date.getUTCMinutes(), + 0, + 0 + )); +} + export class AvailabilityQueryValidationError extends Error { constructor(message: string) { super(message); @@ -62,27 +68,6 @@ export class AvailabilityQueryValidationError extends Error { } } -/** - * 可用性监控的"已终态"边界收敛为 `status_code IS NOT NULL`。 - * - * 这与部分索引 `idx_message_request_provider_created_at_finalized_active` - * 的谓词 `deleted_at IS NULL AND status_code IS NOT NULL` 对齐,让 - * provider + 时间范围聚合可以直接命中索引,而不是退化为大范围扫描。 - * - * 不复刻 `fn_is_message_request_finalized` 的语义(即使内联)也是有意为之: - * 该函数会把仅有 providerChain / errorMessage 片段但 statusCode 仍为 NULL - * 的"请求中"记录判为终态;放到可用性统计里会被分类函数误算成 failure。 - * 终态记录的成功/失败/排除分类继续由 - * `fn_compute_message_request_success_rate_outcome(...)` 处理。 - * - * 已知限制:若未来出现 status_code 长时间未落库但请求已稳定结束的写路径, - * 这些记录会被排除;届时应引入独立的、SARGable 的 finalized 谓词, - * 而不是放回 PL/pgSQL 函数调用。 - */ -function buildAvailabilityFinalizedCondition() { - return isNotNull(messageRequest.statusCode); -} - function assertValidDate(date: Date, fieldName: string): Date { if (!Number.isFinite(date.getTime())) { throw new AvailabilityQueryValidationError( @@ -97,50 +82,6 @@ function parseAvailabilityDate(value: Date | string, fieldName: string): Date { return assertValidDate(typeof value === "string" ? new Date(value) : value, fieldName); } -function buildTimestampLowerBound( - column: typeof messageRequest.createdAt, - date: Date, - fieldName: string -) { - return sql`${column} >= CAST(${assertValidDate(date, fieldName).toISOString()} AS timestamptz)`; -} - -function buildTimestampUpperBound( - column: typeof messageRequest.createdAt, - date: Date, - fieldName: string -) { - return sql`${column} <= CAST(${assertValidDate(date, fieldName).toISOString()} AS timestamptz)`; -} - -function buildRelativeNowLowerBound(column: typeof messageRequest.createdAt, minutes: number) { - return sql`${column} >= NOW() - (${sql.raw(String(minutes))} * INTERVAL '1 minute')`; -} - -function buildNowUpperBound(column: typeof messageRequest.createdAt) { - return sql`${column} <= NOW()`; -} - -function buildAvailabilityRequestConditions(input: { - providerIds: number[]; - startDate: Date; - endDate?: Date; -}) { - const conditions = [ - inArray(messageRequest.providerId, input.providerIds), - buildTimestampLowerBound(messageRequest.createdAt, input.startDate, "startTime"), - isNull(messageRequest.deletedAt), - eq(messageRequest.isReplay, false), - buildAvailabilityFinalizedCondition(), - ]; - - if (input.endDate) { - conditions.push(buildTimestampUpperBound(messageRequest.createdAt, input.endDate, "endTime")); - } - - return and(...conditions); -} - function toFiniteNumber(value: number | string | null | undefined): number { const parsed = Number(value ?? 0); return Number.isFinite(parsed) ? parsed : 0; @@ -168,28 +109,6 @@ function isAvailabilitySuccessStatusCode(statusCode: number): boolean { ); } -function buildRequestOutcomeSql( - blockedByExpression: SQLWrapper, - statusCodeExpression: SQLWrapper, - errorMessageExpression: SQLWrapper, - providerChainExpression: SQLWrapper -) { - return sql`fn_compute_message_request_success_rate_outcome( - ${blockedByExpression}, - ${statusCodeExpression}, - ${errorMessageExpression}, - ${providerChainExpression} - )`; -} - -function buildAvailabilitySuccessOutcomeCondition(outcomeExpression: SQLWrapper) { - return sql`${outcomeExpression} = 'success'`; -} - -function buildAvailabilityFailureOutcomeCondition(outcomeExpression: SQLWrapper) { - return sql`${outcomeExpression} = 'failure'`; -} - /** * Classify a single finalized request's status * Simple: success (2xx/3xx) = green, failure = red @@ -306,7 +225,9 @@ function validateAvailabilityTimeRange(startDate: Date, endDate: Date): void { } /** - * Query availability data for providers + * Query availability data for providers. + * Read path uses incremental 1-minute projection buckets (avail_bucket_1m). + * message_request is no longer scanned here. */ export async function queryProviderAvailability( options: AvailabilityQueryOptions = {} @@ -333,7 +254,6 @@ export async function queryProviderAvailability( sanitizedMaxBuckets ); const bucketSizeMs = bucketSizeMinutes * 60 * 1000; - const bucketSizeSeconds = bucketSizeMinutes * 60; // Get provider list const providerConditions = [isNull(providers.deletedAt)]; @@ -366,75 +286,59 @@ export async function queryProviderAvailability( } const providerIdList = providerList.map((provider) => provider.id); - const requestConditions = buildAvailabilityRequestConditions({ - providerIds: providerIdList, - startDate, - endDate, - }); - const availabilityAggregationCtes = sql` - finalized_requests AS ( + // Include every 1m bucket that overlaps [startDate, endDate]: floor start, keep end exclusive upper via end. + const rangeStartBucket = floorToUtcMinute(startDate); + const rangeEndBucket = floorToUtcMinute(endDate); + + // Aggregate pre-projected 1-minute buckets into the requested display bucket size. + // p50/p95/p99 currently equal avg (sum/count) until sketch-based percentiles land — + // field names stay for API compatibility; treat them as mean approximations. + const bucketQuery = sql` + WITH provider_bucket_stats AS ( SELECT - ${messageRequest.providerId} AS "providerId", - ${messageRequest.createdAt} AS "createdAt", - ${buildRequestOutcomeSql( - messageRequest.blockedBy, - messageRequest.statusCode, - messageRequest.errorMessage, - messageRequest.providerChain - )} AS ${FINALIZED_REQUEST_OUTCOME_SQL}, - ${messageRequest.durationMs} AS "durationMs", - to_timestamp( - floor(extract(epoch from ${messageRequest.createdAt}) / ${bucketSizeSeconds}) * ${bucketSizeSeconds} - ) AS "bucketStart" - FROM ${messageRequest} - WHERE ${requestConditions} + provider_id AS "providerId", + date_bin( + (${bucketSizeMinutes} * INTERVAL '1 minute'), + bucket_start, + TIMESTAMPTZ '1970-01-01T00:00:00Z' + ) AS "bucketStart", + SUM(success_cnt)::int AS "greenCount", + SUM(failure_cnt)::int AS "redCount", + SUM(latency_cnt)::int AS "latencyCount", + COALESCE(SUM(latency_sum_ms), 0)::double precision AS "latencySumMs", + CASE + WHEN SUM(latency_cnt) > 0 THEN (SUM(latency_sum_ms)::double precision / SUM(latency_cnt)) + ELSE 0 + END AS "avgLatencyMs", + CASE + WHEN SUM(latency_cnt) > 0 THEN (SUM(latency_sum_ms)::double precision / SUM(latency_cnt)) + ELSE 0 + END AS "p50LatencyMs", + CASE + WHEN SUM(latency_cnt) > 0 THEN (SUM(latency_sum_ms)::double precision / SUM(latency_cnt)) + ELSE 0 + END AS "p95LatencyMs", + CASE + WHEN SUM(latency_cnt) > 0 THEN (SUM(latency_sum_ms)::double precision / SUM(latency_cnt)) + ELSE 0 + END AS "p99LatencyMs", + MAX(last_request_at) AS "lastRequestAt" + FROM avail_bucket_1m + WHERE provider_id IN (${sql.join( + providerIdList.map((id) => sql`${id}`), + sql`, ` + )}) + AND bucket_start >= CAST(${rangeStartBucket.toISOString()} AS timestamptz) + AND bucket_start <= CAST(${rangeEndBucket.toISOString()} AS timestamptz) + GROUP BY provider_id, 2 ), - provider_bucket_stats AS ( + limited_provider_bucket_stats AS ( SELECT - "providerId", - "bucketStart", - COUNT(*) FILTER (WHERE ${buildAvailabilitySuccessOutcomeCondition(FINALIZED_REQUEST_OUTCOME_SQL)})::int AS "greenCount", - COUNT(*) FILTER (WHERE ${buildAvailabilityFailureOutcomeCondition(FINALIZED_REQUEST_OUTCOME_SQL)})::int AS "redCount", - COUNT("durationMs") FILTER (WHERE ${COUNTABLE_REQUEST_OUTCOME_SQL})::int AS "latencyCount", - COALESCE( - SUM("durationMs") FILTER (WHERE ${COUNTABLE_REQUEST_OUTCOME_SQL})::double precision, - 0 - ) AS "latencySumMs", - COALESCE( - AVG("durationMs") FILTER (WHERE ${COUNTABLE_REQUEST_OUTCOME_SQL})::double precision, - 0 - ) AS "avgLatencyMs", - COALESCE( - percentile_cont(0.5) WITHIN GROUP (ORDER BY "durationMs"::double precision) - FILTER (WHERE "durationMs" IS NOT NULL AND ${COUNTABLE_REQUEST_OUTCOME_SQL}), - 0 - )::double precision AS "p50LatencyMs", - COALESCE( - percentile_cont(0.95) WITHIN GROUP (ORDER BY "durationMs"::double precision) - FILTER (WHERE "durationMs" IS NOT NULL AND ${COUNTABLE_REQUEST_OUTCOME_SQL}), - 0 - )::double precision AS "p95LatencyMs", - COALESCE( - percentile_cont(0.99) WITHIN GROUP (ORDER BY "durationMs"::double precision) - FILTER (WHERE "durationMs" IS NOT NULL AND ${COUNTABLE_REQUEST_OUTCOME_SQL}), - 0 - )::double precision AS "p99LatencyMs", - MAX("createdAt") AS "lastRequestAt" - FROM finalized_requests - GROUP BY "providerId", "bucketStart" + *, + ROW_NUMBER() OVER (PARTITION BY "providerId" ORDER BY "bucketStart" DESC) AS rn + FROM provider_bucket_stats ) - `; - - const bucketQuery = sql` - WITH - ${availabilityAggregationCtes}, - limited_provider_bucket_stats AS ( - SELECT - *, - ROW_NUMBER() OVER (PARTITION BY "providerId" ORDER BY "bucketStart" DESC) AS rn - FROM provider_bucket_stats - ) SELECT "providerId", "bucketStart", @@ -565,7 +469,7 @@ export async function queryProviderAvailability( } /** - * Get current availability status for all providers (lightweight query) + * Get current availability status for all providers (lightweight, projection table). */ export async function getCurrentProviderStatus(): Promise< Array<{ @@ -590,90 +494,116 @@ export async function getCurrentProviderStatus(): Promise< return []; } - const providerIdList = providerList.map((provider) => provider.id); - const requestConditions = and( - inArray(messageRequest.providerId, providerIdList), - buildRelativeNowLowerBound(messageRequest.createdAt, CURRENT_PROVIDER_STATUS_WINDOW_MINUTES), - buildNowUpperBound(messageRequest.createdAt), - isNull(messageRequest.deletedAt), - eq(messageRequest.isReplay, false), - buildAvailabilityFinalizedCondition() - ); + type CurrentRow = { + providerId: number; + state: string; + availability: number; + requestCount: number; + lastRequestAt: Date | string | null; + updatedAt: Date | string | null; + }; - const aggregateQuery = sql` - SELECT - ${messageRequest.providerId} AS "providerId", - COUNT(*) FILTER (WHERE ${buildAvailabilitySuccessOutcomeCondition( - buildRequestOutcomeSql( - messageRequest.blockedBy, - messageRequest.statusCode, - messageRequest.errorMessage, - messageRequest.providerChain - ) - )})::int AS "greenCount", - COUNT(*) FILTER (WHERE ${buildAvailabilityFailureOutcomeCondition( - buildRequestOutcomeSql( - messageRequest.blockedBy, - messageRequest.statusCode, - messageRequest.errorMessage, - messageRequest.providerChain - ) - )})::int AS "redCount", - MAX(${messageRequest.createdAt}) AS "lastRequestAt" - FROM ${messageRequest} - WHERE ${requestConditions} - GROUP BY ${messageRequest.providerId} - `; + const windowMs = CURRENT_PROVIDER_STATUS_WINDOW_MINUTES * 60 * 1000; + const nowMs = Date.now(); - const aggregateRows = Array.from( - await db.execute(aggregateQuery) - ) as AggregatedCurrentProviderStatusRow[]; - const providerStats = new Map< - number, - { - greenCount: number; - redCount: number; - lastRequestAt: string | null; + const currentRows = await db.execute(sql` + SELECT + c.provider_id AS "providerId", + c.state AS "state", + c.availability AS "availability", + c.request_count AS "requestCount", + c.last_request_at AS "lastRequestAt", + c.updated_at AS "updatedAt" + FROM avail_current c + WHERE c.provider_id IN (${sql.join( + providerList.map((p) => sql`${p.id}`), + sql`, ` + )}) + `); + + const byId = new Map(); + for (const row of Array.from(currentRows as Iterable)) { + const updatedAtMs = getTimeValue(row.updatedAt); + const lastRequestAtMs = getTimeValue(row.lastRequestAt); + const freshAt = Math.max(updatedAtMs, lastRequestAtMs); + // Idle providers must not keep a frozen green/red forever. + if (freshAt <= 0 || nowMs - freshAt > windowMs) { + continue; } - >(); - - for (const provider of providerList) { - providerStats.set(provider.id, { - greenCount: 0, - redCount: 0, - lastRequestAt: null, - }); + byId.set(Number(row.providerId), row); } - for (const row of aggregateRows) { - providerStats.set(row.providerId, { - greenCount: toFiniteNumber(row.greenCount), - redCount: toFiniteNumber(row.redCount), - lastRequestAt: toIsoString(row.lastRequestAt), - }); + const missing = providerList.filter((p) => !byId.has(p.id)).map((p) => p.id); + if (missing.length > 0) { + const fallback = await db.execute(sql` + SELECT + provider_id AS "providerId", + SUM(success_cnt)::int AS "greenCount", + SUM(failure_cnt)::int AS "redCount", + MAX(last_request_at) AS "lastRequestAt" + FROM avail_bucket_1m + WHERE provider_id IN (${sql.join( + missing.map((id) => sql`${id}`), + sql`, ` + )}) + AND bucket_start >= NOW() - (${sql.raw(String(CURRENT_PROVIDER_STATUS_WINDOW_MINUTES))} * INTERVAL '1 minute') + GROUP BY provider_id + `); + + for (const row of Array.from( + fallback as Iterable<{ + providerId: number; + greenCount: number; + redCount: number; + lastRequestAt: Date | string | null; + }> + )) { + const g = toFiniteNumber(row.greenCount); + const r = toFiniteNumber(row.redCount); + const total = g + r; + if (total <= 0) continue; + const availability = calculateAvailabilityScore(g, r); + byId.set(Number(row.providerId), { + providerId: Number(row.providerId), + state: availability >= 0.5 ? "green" : "red", + availability, + requestCount: total, + lastRequestAt: row.lastRequestAt, + updatedAt: row.lastRequestAt, + }); + } } return providerList.map((provider) => { - const stats = providerStats.get(provider.id)!; - const total = stats.greenCount + stats.redCount; - const availability = calculateAvailabilityScore(stats.greenCount, stats.redCount); + const stats = byId.get(provider.id); + if (!stats || toFiniteNumber(stats.requestCount) <= 0) { + return { + providerId: provider.id, + providerName: provider.name, + status: "unknown" as AvailabilityStatus, + availability: 0, + requestCount: 0, + lastRequestAt: null, + }; + } - // IMPORTANT: No data = 'unknown', NOT 'green'! Must be honest. + const rawState = String(stats.state || "unknown"); let status: AvailabilityStatus = "unknown"; - if (total === 0) { - status = "unknown"; // No data - must be honest, don't assume healthy! + if (rawState === "green" || rawState === "red" || rawState === "unknown") { + status = rawState; + } else if (rawState === "yellow") { + status = toFiniteNumber(stats.availability) >= 0.5 ? "green" : "red"; } else { - // Simple: >= 50% success = green, otherwise red - status = availability >= 0.5 ? "green" : "red"; + status = toFiniteNumber(stats.availability) >= 0.5 ? "green" : "red"; } return { providerId: provider.id, providerName: provider.name, status, - availability, - requestCount: total, - lastRequestAt: stats.lastRequestAt, + availability: toFiniteNumber(stats.availability), + requestCount: toFiniteNumber(stats.requestCount), + lastRequestAt: toIsoString(stats.lastRequestAt), }; }); } diff --git a/src/lib/availability/index.ts b/src/lib/availability/index.ts index b795312ee..369dc5bbf 100644 --- a/src/lib/availability/index.ts +++ b/src/lib/availability/index.ts @@ -1,9 +1,10 @@ /** * Provider Availability Module * - * This module provides availability monitoring based on request log data. - * Availability is calculated only from finalized requests that already have a persisted - * `statusCode`. In-flight / intermediate records are excluded upstream. + * Read path aggregates pre-projected 1-minute buckets (avail_bucket_1m / avail_current). + * Write path finalization still relies on message_request.statusCode: a DB trigger enqueues + * outbox events, and the in-process projection worker increments the buckets. + * In-flight / intermediate records never enter the projection. * * 1. HTTP Status Check: 2xx/3xx = success (green), other finalized HTTP status codes = failure (red) * @@ -17,6 +18,7 @@ export { AvailabilityQueryValidationError, calculateAvailabilityScore, classifyRequestStatus, + CURRENT_PROVIDER_STATUS_WINDOW_MINUTES, determineOptimalBucketSize, getCurrentProviderStatus, MAX_AVAILABILITY_QUERY_RANGE_DAYS, diff --git a/src/lib/availability/projection-tables.ts b/src/lib/availability/projection-tables.ts new file mode 100644 index 000000000..f945bb7d1 --- /dev/null +++ b/src/lib/availability/projection-tables.ts @@ -0,0 +1,85 @@ +/** + * Availability projection table definitions (outbox + 1m buckets). + * Kept in sync with drizzle/0120_availability_projection.sql and re-exported from schema.ts + * so drizzle-kit generate sees the same shape. + */ +import { sql } from "drizzle-orm"; +import { + bigint, + bigserial, + doublePrecision, + index, + integer, + jsonb, + pgTable, + primaryKey, + text, + timestamp, + unique, + uuid, +} from "drizzle-orm/pg-core"; + +export const outboxEvents = pgTable( + "outbox_events", + { + id: bigserial("id", { mode: "number" }).primaryKey(), + eventId: uuid("event_id").notNull().defaultRandom(), + eventType: text("event_type").notNull(), + aggregateType: text("aggregate_type").notNull(), + aggregateId: bigint("aggregate_id", { mode: "number" }).notNull(), + occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull(), + payload: jsonb("payload").notNull(), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + publishedAt: timestamp("published_at", { withTimezone: true }), + attempts: integer("attempts").notNull().default(0), + lastError: text("last_error"), + }, + (t) => [ + unique("outbox_events_event_id_key").on(t.eventId), + index("idx_outbox_events_unpublished").on(t.id.asc()).where(sql`${t.publishedAt} IS NULL`), + ] +); + +export const outboxProcessed = pgTable("outbox_processed", { + eventId: uuid("event_id").primaryKey(), + processedAt: timestamp("processed_at", { withTimezone: true }).notNull().defaultNow(), +}); + +export const projAppliedRequests = pgTable("proj_applied_requests", { + requestId: bigint("request_id", { mode: "number" }).primaryKey(), + eventId: uuid("event_id").notNull(), + appliedAt: timestamp("applied_at", { withTimezone: true }).notNull().defaultNow(), +}); + +export const availBucket1m = pgTable( + "avail_bucket_1m", + { + providerId: integer("provider_id").notNull(), + bucketStart: timestamp("bucket_start", { withTimezone: true }).notNull(), + successCnt: integer("success_cnt").notNull().default(0), + failureCnt: integer("failure_cnt").notNull().default(0), + excludedCnt: integer("excluded_cnt").notNull().default(0), + latencyCnt: integer("latency_cnt").notNull().default(0), + latencySumMs: bigint("latency_sum_ms", { mode: "number" }).notNull().default(0), + lastRequestAt: timestamp("last_request_at", { withTimezone: true }), + }, + (t) => [ + primaryKey({ columns: [t.providerId, t.bucketStart], name: "avail_bucket_1m_pkey" }), + index("idx_avail_bucket_1m_time").on(t.bucketStart.desc()), + ] +); + +export const availCurrent = pgTable("avail_current", { + providerId: integer("provider_id").primaryKey(), + state: text("state").notNull().default("unknown"), + availability: doublePrecision("availability").notNull().default(0), + requestCount: integer("request_count").notNull().default(0), + lastRequestAt: timestamp("last_request_at", { withTimezone: true }), + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), +}); + +export const projectionMeta = pgTable("projection_meta", { + key: text("key").primaryKey(), + value: jsonb("value").notNull().default({}), + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), +}); diff --git a/src/lib/availability/projection-worker.ts b/src/lib/availability/projection-worker.ts new file mode 100644 index 000000000..a3e4d1cc7 --- /dev/null +++ b/src/lib/availability/projection-worker.ts @@ -0,0 +1,540 @@ +/** + * In-process outbox consumer for availability projection buckets. + * DB trigger on message_request writes outbox_events; this loop increments avail_bucket_1m. + */ +import "server-only"; + +import { sql } from "drizzle-orm"; +import { db } from "@/drizzle/db"; +import { CURRENT_PROVIDER_STATUS_WINDOW_MINUTES } from "@/lib/availability/availability-service"; +import { logger } from "@/lib/logger"; +import { withAdvisoryLock } from "@/lib/migrate"; + +const BATCH = 300; +const BUSY_MS = 10; +const TICK_MS = 200; +const BACKFILL_LOCK = "claude-code-hub:availability-projection-backfill"; +/** Match MAX_AVAILABILITY_QUERY_RANGE_DAYS so historical ranges are not silently empty after upgrade. */ +const BACKFILL_RANGE_DAYS = 100; +const BACKFILL_CHUNK_HOURS = 6; + +type SchedulerState = { + started?: boolean; + stopRequested?: boolean; + intervalId?: ReturnType; + currentPromise?: Promise; + bootstrapPromise?: Promise; +}; + +const schedulerState = globalThis as typeof globalThis & { + __CCH_AVAIL_PROJ_WORKER__?: SchedulerState; +}; + +function state(): SchedulerState { + if (!schedulerState.__CCH_AVAIL_PROJ_WORKER__) { + schedulerState.__CCH_AVAIL_PROJ_WORKER__ = {}; + } + return schedulerState.__CCH_AVAIL_PROJ_WORKER__; +} + +type ClaimedEvent = { + id: number; + event_id: string; + payload: { + request_id?: number | string; + provider_id?: number | string; + outcome?: string; + occurred_at?: string; + duration_ms?: number | string | null; + }; +}; + +export function asPayload(raw: unknown): ClaimedEvent["payload"] { + if (!raw) return {}; + if (typeof raw === "string") { + try { + return JSON.parse(raw) as ClaimedEvent["payload"]; + } catch { + return {}; + } + } + if (typeof raw === "object") { + return raw as ClaimedEvent["payload"]; + } + return {}; +} + +async function enqueueBackfillChunk(fromIso: string, toIso: string): Promise { + const result = await db.execute(sql` + WITH inserted AS ( + INSERT INTO outbox_events (event_type, aggregate_type, aggregate_id, occurred_at, payload) + SELECT + 'request_finalized', + 'message_request', + mr.id, + mr.created_at, + jsonb_build_object( + 'request_id', mr.id, + 'provider_id', mr.provider_id, + 'model', mr.model, + 'occurred_at', mr.created_at, + 'status_code', mr.status_code, + 'duration_ms', mr.duration_ms, + 'ttfb_ms', mr.ttfb_ms, + 'blocked_by', mr.blocked_by, + 'outcome', fn_compute_message_request_success_rate_outcome( + mr.blocked_by, mr.status_code, mr.error_message, mr.provider_chain + ), + 'group_tag', p.group_tag, + 'is_replay', COALESCE(mr.is_replay, false) + ) + FROM message_request mr + LEFT JOIN providers p ON p.id = mr.provider_id + WHERE mr.status_code IS NOT NULL + AND mr.created_at >= ${fromIso}::timestamptz + AND mr.created_at < ${toIso}::timestamptz + AND COALESCE(mr.is_replay, false) = false + AND NOT EXISTS (SELECT 1 FROM proj_applied_requests a WHERE a.request_id = mr.id) + AND fn_compute_message_request_success_rate_outcome( + mr.blocked_by, mr.status_code, mr.error_message, mr.provider_chain + ) IS NOT NULL + RETURNING 1 + ) + SELECT count(*)::int AS n FROM inserted + `); + const row = Array.from(result as Iterable<{ n?: number }>)[0]; + return Number(row?.n ?? 0); +} + +async function bootstrapBackfill(): Promise { + const existing = await db.execute(sql` + SELECT key FROM projection_meta WHERE key = 'backfill_done' LIMIT 1 + `); + if (Array.from(existing as Iterable).length > 0) { + return; + } + + const lockResult = await withAdvisoryLock( + BACKFILL_LOCK, + async () => { + // Re-check under lock so concurrent instances do not double-enqueue. + const again = await db.execute(sql` + SELECT key FROM projection_meta WHERE key = 'backfill_done' LIMIT 1 + `); + if (Array.from(again as Iterable).length > 0) { + return { skipped: true as const, inserted: 0 }; + } + + logger.info("[AvailProjection] starting backfill into outbox", { + rangeDays: BACKFILL_RANGE_DAYS, + chunkHours: BACKFILL_CHUNK_HOURS, + }); + + const endMs = Date.now(); + const startMs = endMs - BACKFILL_RANGE_DAYS * 24 * 60 * 60 * 1000; + const chunkMs = BACKFILL_CHUNK_HOURS * 60 * 60 * 1000; + let inserted = 0; + + for (let cursor = startMs; cursor < endMs; cursor += chunkMs) { + if (state().stopRequested) { + logger.warn("[AvailProjection] backfill interrupted by stop"); + break; + } + const fromIso = new Date(cursor).toISOString(); + const toIso = new Date(Math.min(cursor + chunkMs, endMs)).toISOString(); + inserted += await enqueueBackfillChunk(fromIso, toIso); + } + + if (!state().stopRequested) { + await db.execute(sql` + INSERT INTO projection_meta (key, value, updated_at) + VALUES ( + 'backfill_done', + jsonb_build_object( + 'at', now(), + 'note', 'availability backfill', + 'rangeDays', ${BACKFILL_RANGE_DAYS}, + 'inserted', ${inserted} + ), + now() + ) + ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now() + `); + logger.info("[AvailProjection] backfill enqueue finished", { inserted }); + } + + return { skipped: false as const, inserted }; + }, + { skipIfLocked: true } + ); + + if (!lockResult.ran) { + logger.info("[AvailProjection] backfill skipped; another instance holds the lock"); + } +} + +async function recomputeAvailCurrent(tx: typeof db, providerIds: number[]): Promise { + if (providerIds.length === 0) return; + + // Stable ascending lock order across concurrent worker instances (avoids deadlocks). + const sortedProviderIds = [...new Set(providerIds)].sort((a, b) => a - b); + const providerIdList = sql.join( + sortedProviderIds.map((id) => sql`${id}`), + sql`, ` + ); + + await tx.execute(sql` + INSERT INTO avail_current AS c ( + provider_id, state, availability, request_count, last_request_at, updated_at + ) + SELECT + s.provider_id, + s.state, + s.availability, + s.request_count, + s.last_request_at, + s.updated_at + FROM ( + SELECT + b.provider_id, + CASE + WHEN COALESCE(SUM(b.success_cnt), 0) + COALESCE(SUM(b.failure_cnt), 0) <= 0 THEN 'unknown' + WHEN (COALESCE(SUM(b.success_cnt), 0)::float + / (COALESCE(SUM(b.success_cnt), 0) + COALESCE(SUM(b.failure_cnt), 0))) >= 0.8 THEN 'green' + WHEN (COALESCE(SUM(b.success_cnt), 0)::float + / (COALESCE(SUM(b.success_cnt), 0) + COALESCE(SUM(b.failure_cnt), 0))) >= 0.5 THEN 'yellow' + ELSE 'red' + END AS state, + CASE + WHEN COALESCE(SUM(b.success_cnt), 0) + COALESCE(SUM(b.failure_cnt), 0) <= 0 THEN 0 + ELSE COALESCE(SUM(b.success_cnt), 0)::float + / (COALESCE(SUM(b.success_cnt), 0) + COALESCE(SUM(b.failure_cnt), 0)) + END AS availability, + (COALESCE(SUM(b.success_cnt), 0) + COALESCE(SUM(b.failure_cnt), 0))::int AS request_count, + MAX(b.last_request_at) AS last_request_at, + now() AS updated_at + FROM avail_bucket_1m b + WHERE b.provider_id IN (${providerIdList}) + AND b.bucket_start >= now() - (${sql.raw(String(CURRENT_PROVIDER_STATUS_WINDOW_MINUTES))} * INTERVAL '1 minute') + GROUP BY b.provider_id + ORDER BY b.provider_id ASC + ) s + ON CONFLICT (provider_id) DO UPDATE SET + state = EXCLUDED.state, + availability = EXCLUDED.availability, + request_count = EXCLUDED.request_count, + last_request_at = EXCLUDED.last_request_at, + updated_at = now() + `); + + // Providers with no traffic in the window become unknown (honest empty state). + // Lock target rows in provider_id order before updating. + await tx.execute(sql` + WITH targets AS ( + SELECT c.provider_id + FROM avail_current c + WHERE c.provider_id IN (${providerIdList}) + AND NOT EXISTS ( + SELECT 1 + FROM avail_bucket_1m b + WHERE b.provider_id = c.provider_id + AND b.bucket_start >= now() - (${sql.raw(String(CURRENT_PROVIDER_STATUS_WINDOW_MINUTES))} * INTERVAL '1 minute') + AND (b.success_cnt + b.failure_cnt) > 0 + ) + ORDER BY c.provider_id ASC + FOR UPDATE OF c + ) + UPDATE avail_current c + SET + state = 'unknown', + availability = 0, + request_count = 0, + updated_at = now() + FROM targets t + WHERE c.provider_id = t.provider_id + `); +} + +export async function processBatch(): Promise { + return await db.transaction(async (tx) => { + const claimedRows = await tx.execute(sql` + SELECT id, event_id, payload + FROM outbox_events + WHERE published_at IS NULL + ORDER BY id + FOR UPDATE SKIP LOCKED + LIMIT ${BATCH} + `); + + const claimed = Array.from(claimedRows as Iterable); + if (claimed.length === 0) { + return 0; + } + + let applied = 0; + const touchedProviders = new Set(); + const publishedIds: number[] = []; + const invalidIds: number[] = []; + + // Bucket deltas aggregated in JS, then one upsert per distinct (provider, minute). + type BucketKey = string; + const bucketDeltas = new Map< + BucketKey, + { + providerId: number; + bucketStartIso: string; + successCnt: number; + failureCnt: number; + excludedCnt: number; + latencyCnt: number; + latencySum: number; + lastRequestAtIso: string; + } + >(); + + for (const row of claimed) { + const payload = asPayload(row.payload); + const requestId = Number(payload.request_id); + const providerId = Number(payload.provider_id); + const outcome = String(payload.outcome || "excluded"); + const occurredAt = payload.occurred_at; + if (!Number.isFinite(requestId) || !Number.isFinite(providerId) || !occurredAt) { + invalidIds.push(row.id); + continue; + } + + const inserted = await tx.execute(sql` + INSERT INTO proj_applied_requests (request_id, event_id) + VALUES (${requestId}, ${row.event_id}::uuid) + ON CONFLICT (request_id) DO NOTHING + RETURNING request_id + `); + const isFresh = Array.from(inserted as Iterable).length > 0; + + if (isFresh) { + const durationMs = + payload.duration_ms === null || payload.duration_ms === undefined + ? null + : Number(payload.duration_ms); + const successCnt = outcome === "success" ? 1 : 0; + const failureCnt = outcome === "failure" ? 1 : 0; + const excludedCnt = outcome === "excluded" ? 1 : 0; + const latencyCnt = + (outcome === "success" || outcome === "failure") && + durationMs !== null && + Number.isFinite(durationMs) + ? 1 + : 0; + const latencySum = + latencyCnt === 1 && durationMs !== null && Number.isFinite(durationMs) + ? Math.trunc(durationMs) + : 0; + + const occurred = new Date(occurredAt); + const bucketStart = new Date( + Date.UTC( + occurred.getUTCFullYear(), + occurred.getUTCMonth(), + occurred.getUTCDate(), + occurred.getUTCHours(), + occurred.getUTCMinutes(), + 0, + 0 + ) + ); + const bucketStartIso = bucketStart.toISOString(); + const key = `${providerId}|${bucketStartIso}`; + const prev = bucketDeltas.get(key); + if (prev) { + prev.successCnt += successCnt; + prev.failureCnt += failureCnt; + prev.excludedCnt += excludedCnt; + prev.latencyCnt += latencyCnt; + prev.latencySum += latencySum; + if (occurredAt > prev.lastRequestAtIso) { + prev.lastRequestAtIso = occurredAt; + } + } else { + bucketDeltas.set(key, { + providerId, + bucketStartIso, + successCnt, + failureCnt, + excludedCnt, + latencyCnt, + latencySum, + lastRequestAtIso: occurredAt, + }); + } + touchedProviders.add(providerId); + applied += 1; + } + + publishedIds.push(row.id); + } + + // Upsert buckets in (provider_id, bucket_start) order so concurrent workers take locks consistently. + const sortedDeltas = Array.from(bucketDeltas.values()).sort((a, b) => { + if (a.providerId !== b.providerId) return a.providerId - b.providerId; + return a.bucketStartIso < b.bucketStartIso ? -1 : a.bucketStartIso > b.bucketStartIso ? 1 : 0; + }); + for (const delta of sortedDeltas) { + await tx.execute(sql` + INSERT INTO avail_bucket_1m AS b ( + provider_id, + bucket_start, + success_cnt, + failure_cnt, + excluded_cnt, + latency_cnt, + latency_sum_ms, + last_request_at + ) VALUES ( + ${delta.providerId}, + ${delta.bucketStartIso}::timestamptz, + ${delta.successCnt}, + ${delta.failureCnt}, + ${delta.excludedCnt}, + ${delta.latencyCnt}, + ${delta.latencySum}, + ${delta.lastRequestAtIso}::timestamptz + ) + ON CONFLICT (provider_id, bucket_start) DO UPDATE SET + success_cnt = b.success_cnt + EXCLUDED.success_cnt, + failure_cnt = b.failure_cnt + EXCLUDED.failure_cnt, + excluded_cnt = b.excluded_cnt + EXCLUDED.excluded_cnt, + latency_cnt = b.latency_cnt + EXCLUDED.latency_cnt, + latency_sum_ms = b.latency_sum_ms + EXCLUDED.latency_sum_ms, + last_request_at = GREATEST( + COALESCE(b.last_request_at, EXCLUDED.last_request_at), + EXCLUDED.last_request_at + ) + `); + } + + if (publishedIds.length > 0) { + const sortedPublishedIds = [...publishedIds].sort((a, b) => a - b); + await tx.execute(sql` + UPDATE outbox_events + SET published_at = now(), + attempts = attempts + 1, + last_error = NULL + WHERE id IN (${sql.join( + sortedPublishedIds.map((id) => sql`${id}`), + sql`, ` + )}) + `); + } + + if (invalidIds.length > 0) { + const sortedInvalidIds = [...invalidIds].sort((a, b) => a - b); + await tx.execute(sql` + UPDATE outbox_events + SET published_at = now(), + attempts = attempts + 1, + last_error = 'invalid payload' + WHERE id IN (${sql.join( + sortedInvalidIds.map((id) => sql`${id}`), + sql`, ` + )}) + `); + } + + await recomputeAvailCurrent(tx as unknown as typeof db, Array.from(touchedProviders)); + + return applied; + }); +} + +async function runCycle(): Promise { + const s = state(); + if (s.stopRequested) return; + if (s.currentPromise) return; + + let current!: Promise; + current = (async () => { + try { + let total = 0; + for (let i = 0; i < 20; i++) { + if (s.stopRequested) break; + const n = await processBatch(); + total += n; + if (n === 0) break; + if (n < BATCH) break; + await new Promise((r) => setTimeout(r, BUSY_MS)); + } + if (total > 0) { + logger.info("[AvailProjection] projected events", { count: total }); + } + } catch (error) { + logger.warn("[AvailProjection] cycle failed", { + error: error instanceof Error ? error.message : String(error), + }); + } finally { + if (s.currentPromise === current) { + s.currentPromise = undefined; + } + } + })(); + + s.currentPromise = current; + await current; +} + +export function startAvailabilityProjectionWorker(): void { + const s = state(); + if (s.started) return; + + s.stopRequested = false; + s.started = true; + + s.bootstrapPromise = (async () => { + try { + await bootstrapBackfill(); + } catch (error) { + logger.warn("[AvailProjection] backfill failed", { + error: error instanceof Error ? error.message : String(error), + }); + } + void runCycle(); + })(); + + s.intervalId = setInterval(() => { + void runCycle(); + }, TICK_MS); + (s.intervalId as { unref?: () => void } | undefined)?.unref?.(); + + logger.info("[AvailProjection] worker started"); +} + +export async function stopAvailabilityProjectionWorker(): Promise { + const s = state(); + s.stopRequested = true; + if (s.intervalId) { + clearInterval(s.intervalId); + s.intervalId = undefined; + } + await s.bootstrapPromise; + await s.currentPromise; + s.started = false; + s.bootstrapPromise = undefined; + logger.info("[AvailProjection] worker stopped"); +} + +export function getAvailabilityProjectionWorkerStatus() { + const s = state(); + return { + started: s.started === true, + running: Boolean(s.currentPromise), + bootstrapping: Boolean(s.bootstrapPromise), + tickMs: TICK_MS, + }; +} + +/** Test-only helpers */ +export const __test__ = { + bootstrapBackfill, + recomputeAvailCurrent, + BACKFILL_RANGE_DAYS, + BATCH, +}; diff --git a/src/lib/availability/types.ts b/src/lib/availability/types.ts index 281aa9e36..e78ca466b 100644 --- a/src/lib/availability/types.ts +++ b/src/lib/availability/types.ts @@ -63,11 +63,15 @@ export interface TimeBucketMetrics { availabilityScore: number; /** Average latency in ms */ avgLatencyMs: number; - /** P50 latency in ms */ + /** + * Latency percentile fields kept for API compatibility. + * With 1m sum/count projection buckets these currently equal avgLatencyMs + * (mean approximation) until sketch/histogram-based percentiles land. + */ p50LatencyMs: number; - /** P95 latency in ms */ + /** @see p50LatencyMs */ p95LatencyMs: number; - /** P99 latency in ms */ + /** @see p50LatencyMs */ p99LatencyMs: number; } diff --git a/src/lib/lifecycle/shutdown.ts b/src/lib/lifecycle/shutdown.ts index 23443ee95..79e053c4c 100644 --- a/src/lib/lifecycle/shutdown.ts +++ b/src/lib/lifecycle/shutdown.ts @@ -191,7 +191,26 @@ export async function runApplicationCleanup( clearTimeout(asyncTasksWarningTimer); } - // 7. 刷写 message_request 异步写缓冲。这里不能用可脱离的单步 timeout: + // 7a. 可用性投影 worker 在 closeDbPools 前必须真正停住(含 backfill / 在飞 batch)。 + // 超时只告警,不能 detach;否则会在投影事务进行中关掉 DB。 + const availProjWarningTimer = setTimeout(() => { + logger.warn("[Shutdown] stopAvailabilityProjectionWorker still pending", { ms: stepMs }); + }, stepMs); + try { + const { stopAvailabilityProjectionWorker } = await import( + "@/lib/availability/projection-worker" + ); + await stopAvailabilityProjectionWorker(); + } catch (error) { + logger.error("[Shutdown] availability projection worker failed to stop", { + error: error instanceof Error ? error.message : String(error), + }); + throw error; + } finally { + clearTimeout(availProjWarningTimer); + } + + // 7b. 刷写 message_request 异步写缓冲。这里不能用可脱离的单步 timeout: // closeDbPools 必须等 writer 真正 settled,否则会关闭仍在执行终态 SQL 的连接。 writerQuiescencePending = true; const writerWarningTimer = setTimeout(() => { diff --git a/tests/unit/lib/availability-service.test.ts b/tests/unit/lib/availability-service.test.ts index fcae47fde..8c25249ad 100644 --- a/tests/unit/lib/availability-service.test.ts +++ b/tests/unit/lib/availability-service.test.ts @@ -39,26 +39,14 @@ function normalizeSql(sqlObject: unknown): string { return sqlToString(sqlObject).replace(/\s+/g, " ").trim().toLowerCase(); } -function extractFinalizedRequestsSql(queryText: string): string { - const start = queryText.indexOf("finalized_requests as"); - const end = queryText.indexOf("provider_bucket_stats as"); - - if (start === -1 || end === -1 || end <= start) { - throw new Error("Could not locate finalized_requests CTE in query text"); - } - - return queryText.slice(start, end); -} - -// 终态边界必须仅由 status_code 收敛:不能回退到包含 blocked_by / -// error_message / provider_chain 任一非空的旧语义,否则会重新把"请求中" -// 记录纳入可用性统计。每一处断言都重复这套规则,防止个别用例漏检导致回归。 -function expectStatusCodeOnlyFinalizedBoundary(sqlText: string) { +function expectProjectionBucketReadPath(sqlText: string) { + expect(sqlText).toContain("from avail_bucket_1m"); + expect(sqlText).toContain("date_bin"); + expect(sqlText).toContain("row_number() over"); + expect(sqlText).not.toContain("from message_request"); + expect(sqlText).not.toContain("fn_compute_message_request_success_rate_outcome"); + expect(sqlText).not.toContain("percentile_cont"); expect(sqlText).not.toContain("fn_is_message_request_finalized"); - expect(sqlText).toContain(`"status_code" is not null`); - expect(sqlText).not.toContain(`"blocked_by" is not null`); - expect(sqlText).not.toContain(`"error_message" is not null`); - expect(sqlText).not.toContain(`"provider_chain" -> -1 ->> 'reason'`); } describe("availability-service", () => { @@ -253,7 +241,41 @@ describe("availability-service", () => { expect(executeMock).not.toHaveBeenCalled(); }); - it("queryProviderAvailability 改为数据库聚合后仍只统计终态请求", async () => { + it("queryProviderAvailability 用 floor 到分钟的边界包含部分分钟桶", async () => { + const selectMock = vi.fn(() => + createThenableQuery([ + { + id: 1, + name: "Provider A", + providerType: "claude", + enabled: true, + }, + ]) + ); + const executeMock = vi.fn(async () => []); + + vi.doMock("@/drizzle/db", () => ({ + db: { + select: selectMock, + execute: executeMock, + }, + })); + + const { queryProviderAvailability } = await import("@/lib/availability/availability-service"); + await queryProviderAvailability({ + startTime: new Date("2026-04-13T07:00:30.000Z"), + endTime: new Date("2026-04-13T09:00:45.000Z"), + bucketSizeMinutes: 60, + }); + + const query = sqlToQuery(executeMock.mock.calls[0]?.[0]); + // floor start -> 07:00:00, floor end -> 09:00:00 + expect(query.params).toContain("2026-04-13T07:00:00.000Z"); + expect(query.params).toContain("2026-04-13T09:00:00.000Z"); + expect(query.params).not.toContain("2026-04-13T07:00:30.000Z"); + }); + + it("queryProviderAvailability 从 avail_bucket_1m 投影表聚合,不再扫描 message_request", async () => { const selectMock = vi.fn(() => createThenableQuery([ { @@ -319,21 +341,9 @@ describe("availability-service", () => { }); const queryText = normalizeSql(executeMock.mock.calls[0]?.[0]); - const finalizedRequestsSql = extractFinalizedRequestsSql(queryText); - // 可用性监控的终态边界收敛为 status_code IS NOT NULL, - // 这样才能命中部分索引 idx_message_request_provider_created_at_finalized_active; - // 同时不会把 providerChain / errorMessage 已写入但 statusCode 仍为空的"请求中" - // 记录纳入聚合 —— 它们会在分类阶段被误判成 failure。 - expectStatusCodeOnlyFinalizedBoundary(finalizedRequestsSql); - expect(queryText).toContain("group by"); - expect(queryText).toContain("percentile_cont(0.95)"); - expect(queryText).toContain("row_number() over"); - expect(queryText).toContain(`"successrateoutcome" in ('success', 'failure')`); - // 终态记录的 success/failure/excluded 分类仍由 outcome 函数完成。 - expect(queryText).toContain("fn_compute_message_request_success_rate_outcome"); - expect(queryText).toContain('avg("durationms") filter'); - expect(queryText).toContain('"message_request"."is_replay" ='); - expect(sqlToQuery(executeMock.mock.calls[0]?.[0]).params).toContain(false); + expectProjectionBucketReadPath(queryText); + expect(queryText).toContain("where rn <="); + expect(sqlToQuery(executeMock.mock.calls[0]?.[0]).params).toContain(60); }); it("queryProviderAvailability 计算 currentStatus 时会按最近 buckets 的请求量加权", async () => { @@ -431,8 +441,9 @@ describe("availability-service", () => { expect(selectMock).toHaveBeenCalledTimes(1); expect(executeMock).toHaveBeenCalledTimes(1); expect(result.bucketSizeMinutes).toBe(5); - expect(query.params).toContain(300); + expect(query.params).toContain(5); expect(query.params).not.toContain(Number.POSITIVE_INFINITY); + expectProjectionBucketReadPath(normalizeSql(executeMock.mock.calls[0]?.[0])); }); it("queryProviderAvailability 在 bucketSizeMinutes 为超大有限值时钳制到 1440 分钟", async () => { @@ -467,117 +478,8 @@ describe("availability-service", () => { expect(selectMock).toHaveBeenCalledTimes(1); expect(executeMock).toHaveBeenCalledTimes(1); expect(result.bucketSizeMinutes).toBe(1440); - expect(query.params).toContain(86400); - expect(query.params).not.toContain(Number.MAX_SAFE_INTEGER * 60); - }); - - it("queryProviderAvailability 会排除进行中请求(statusCode=null 且 durationMs=null)", async () => { - const selectMock = vi.fn(() => - createThenableQuery([ - { - id: 1, - name: "Provider A", - providerType: "claude", - enabled: true, - }, - ]) - ); - const executeMock = vi.fn(async () => []); - - vi.doMock("@/drizzle/db", () => ({ - db: { - select: selectMock, - execute: executeMock, - }, - })); - - const { queryProviderAvailability } = await import("@/lib/availability/availability-service"); - await queryProviderAvailability({ - startTime: new Date("2026-04-13T07:00:00.000Z"), - endTime: new Date("2026-04-13T09:00:00.000Z"), - bucketSizeMinutes: 60, - }); - - const finalizedRequestsSql = extractFinalizedRequestsSql( - normalizeSql(executeMock.mock.calls[0]?.[0]) - ); - // 终态判定只看 status_code IS NOT NULL:要么命中部分索引,要么直接排除"请求中" - // 的记录,不再依据 providerChain / errorMessage 片段把它们判为终态。 - expectStatusCodeOnlyFinalizedBoundary(finalizedRequestsSql); - }); - - it("queryProviderAvailability 会保留 Gemini passthrough 终态(statusCode!=null 且 durationMs=null)", async () => { - const selectMock = vi.fn(() => - createThenableQuery([ - { - id: 1, - name: "Provider A", - providerType: "claude", - enabled: true, - }, - ]) - ); - const executeMock = vi.fn(async () => []); - - vi.doMock("@/drizzle/db", () => ({ - db: { - select: selectMock, - execute: executeMock, - }, - })); - - const { queryProviderAvailability } = await import("@/lib/availability/availability-service"); - await queryProviderAvailability({ - startTime: new Date("2026-04-13T07:00:00.000Z"), - endTime: new Date("2026-04-13T09:00:00.000Z"), - bucketSizeMinutes: 60, - }); - - const finalizedRequestsSql = extractFinalizedRequestsSql( - normalizeSql(executeMock.mock.calls[0]?.[0]) - ); - expect(finalizedRequestsSql).not.toMatch(/where .*duration_?ms.*is not null/); - // Gemini passthrough 写入了 statusCode(即使 durationMs 仍为 null), - // 因此会被 status_code IS NOT NULL 的终态过滤保留下来;同时保持终态边界 - // 不被其他字段放宽。 - expectStatusCodeOnlyFinalizedBoundary(finalizedRequestsSql); - }); - - it("queryProviderAvailability 当前不会把中间持久化状态(statusCode=null 且 durationMs!=null)误算为 red", async () => { - const selectMock = vi.fn(() => - createThenableQuery([ - { - id: 1, - name: "Provider A", - providerType: "claude", - enabled: true, - }, - ]) - ); - const executeMock = vi.fn(async () => []); - - vi.doMock("@/drizzle/db", () => ({ - db: { - select: selectMock, - execute: executeMock, - }, - })); - - const { queryProviderAvailability } = await import("@/lib/availability/availability-service"); - await queryProviderAvailability({ - startTime: new Date("2026-04-13T07:00:00.000Z"), - endTime: new Date("2026-04-13T09:00:00.000Z"), - bucketSizeMinutes: 60, - }); - - const queryText = normalizeSql(executeMock.mock.calls[0]?.[0]); - const finalizedRequestsSql = extractFinalizedRequestsSql(queryText); - - // status_code IS NOT NULL 把 statusCode=null 的中间持久化记录直接排除在聚合外, - // 它们根本不会进入 outcome 分类阶段,所以不会被算成 failure。 - expectStatusCodeOnlyFinalizedBoundary(finalizedRequestsSql); - expect(queryText).toContain("fn_compute_message_request_success_rate_outcome"); - expect(queryText).toContain(`"successrateoutcome" = 'failure'`); + expect(query.params).toContain(1440); + expect(query.params).not.toContain(Number.MAX_SAFE_INTEGER); }); it("queryProviderAvailability 在 maxBuckets 为 Infinity 时仍使用默认桶上限", async () => { @@ -702,7 +604,7 @@ describe("availability-service", () => { ]); }); - it("getCurrentProviderStatus 改为数据库聚合后仍只统计终态请求", async () => { + it("getCurrentProviderStatus 优先读取 avail_current 投影表", async () => { const selectMock = vi.fn(() => createThenableQuery([ { @@ -711,12 +613,15 @@ describe("availability-service", () => { }, ]) ); + const fresh = new Date(); const executeMock = vi.fn(async () => [ { providerId: 1, - greenCount: 1, - redCount: 1, - lastRequestAt: new Date("2026-04-13T08:02:00.000Z"), + state: "green", + availability: 0.5, + requestCount: 2, + lastRequestAt: fresh, + updatedAt: fresh, }, ]); @@ -739,21 +644,112 @@ describe("availability-service", () => { status: "green", availability: 0.5, requestCount: 2, - lastRequestAt: "2026-04-13T08:02:00.000Z", + lastRequestAt: fresh.toISOString(), }, ]); const queryText = normalizeSql(executeMock.mock.calls[0]?.[0]); - // getCurrentProviderStatus 同样使用 status_code IS NOT NULL 终态边界, - // 让短窗口查询也能直接命中部分索引并避免误判"请求中"。 - expectStatusCodeOnlyFinalizedBoundary(queryText); - expect(queryText).toContain("fn_compute_message_request_success_rate_outcome"); - expect(queryText).toContain(">= now() - (15 * interval '1 minute')"); - expect(queryText).toContain("<= now()"); - expect(queryText).toContain("count(*) filter"); - expect(queryText).toContain("max("); - expect(queryText).toContain('"message_request"."is_replay" ='); - expect(sqlToQuery(executeMock.mock.calls[0]?.[0]).params).toContain(false); + expect(queryText).toContain("from avail_current"); + expect(queryText).toContain("updated_at"); + expect(queryText).not.toContain("from message_request"); + expect(queryText).not.toContain("fn_compute_message_request_success_rate_outcome"); + }); + + it("getCurrentProviderStatus 对过期 avail_current 行返回 unknown(或走桶回退)", async () => { + const selectMock = vi.fn(() => + createThenableQuery([ + { + id: 1, + name: "Provider A", + }, + ]) + ); + const stale = new Date(Date.now() - 60 * 60 * 1000); + const executeMock = vi + .fn() + .mockResolvedValueOnce([ + { + providerId: 1, + state: "green", + availability: 1, + requestCount: 9, + lastRequestAt: stale, + updatedAt: stale, + }, + ]) + .mockResolvedValueOnce([]); + + vi.doMock("@/drizzle/db", () => ({ + db: { + select: selectMock, + execute: executeMock, + }, + })); + + const { getCurrentProviderStatus } = await import("@/lib/availability/availability-service"); + const result = await getCurrentProviderStatus(); + + expect(executeMock).toHaveBeenCalledTimes(2); + expect(result).toEqual([ + { + providerId: 1, + providerName: "Provider A", + status: "unknown", + availability: 0, + requestCount: 0, + lastRequestAt: null, + }, + ]); + }); + + it("getCurrentProviderStatus 在 avail_current 缺失时回退到 avail_bucket_1m", async () => { + const selectMock = vi.fn(() => + createThenableQuery([ + { + id: 1, + name: "Provider A", + }, + ]) + ); + const executeMock = vi + .fn() + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([ + { + providerId: 1, + greenCount: 3, + redCount: 1, + lastRequestAt: new Date("2026-04-13T08:05:00.000Z"), + }, + ]); + + vi.doMock("@/drizzle/db", () => ({ + db: { + select: selectMock, + execute: executeMock, + }, + })); + + const { getCurrentProviderStatus } = await import("@/lib/availability/availability-service"); + const result = await getCurrentProviderStatus(); + + expect(executeMock).toHaveBeenCalledTimes(2); + expect(result).toEqual([ + { + providerId: 1, + providerName: "Provider A", + status: "green", + availability: 0.75, + requestCount: 4, + lastRequestAt: "2026-04-13T08:05:00.000Z", + }, + ]); + + expect(normalizeSql(executeMock.mock.calls[0]?.[0])).toContain("from avail_current"); + expect(normalizeSql(executeMock.mock.calls[1]?.[0])).toContain("from avail_bucket_1m"); + expect(normalizeSql(executeMock.mock.calls[1]?.[0])).toContain( + ">= now() - (15 * interval '1 minute')" + ); }); it("getCurrentProviderStatus 在提供商无聚合数据时返回 unknown", async () => { @@ -765,7 +761,8 @@ describe("availability-service", () => { }, ]) ); - const executeMock = vi.fn(async () => []); + // first avail_current empty, then fallback empty + const executeMock = vi.fn().mockResolvedValueOnce([]).mockResolvedValueOnce([]); vi.doMock("@/drizzle/db", () => ({ db: { @@ -778,7 +775,7 @@ describe("availability-service", () => { const result = await getCurrentProviderStatus(); expect(selectMock).toHaveBeenCalledTimes(1); - expect(executeMock).toHaveBeenCalledTimes(1); + expect(executeMock).toHaveBeenCalledTimes(2); expect(result).toEqual([ { providerId: 1, diff --git a/tests/unit/lib/availability/projection-worker.test.ts b/tests/unit/lib/availability/projection-worker.test.ts new file mode 100644 index 000000000..baa653709 --- /dev/null +++ b/tests/unit/lib/availability/projection-worker.test.ts @@ -0,0 +1,210 @@ +import type { SQL } from "drizzle-orm"; +import { CasingCache } from "drizzle-orm/casing"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +function sqlToString(sqlObject: unknown): string { + return (sqlObject as SQL) + .toQuery({ + escapeName: (name: string) => `"${name}"`, + escapeParam: (num: number, _value: unknown) => `$${num}`, + escapeString: (value: string) => `'${value}'`, + casing: new CasingCache(), + paramStartIndex: { value: 1 }, + }) + .sql.replace(/\s+/g, " ") + .trim() + .toLowerCase(); +} + +describe("availability projection-worker", () => { + beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + delete (globalThis as { __CCH_AVAIL_PROJ_WORKER__?: unknown }).__CCH_AVAIL_PROJ_WORKER__; + }); + + it("asPayload 解析 object / JSON 字符串 / 非法输入", async () => { + vi.doMock("@/drizzle/db", () => ({ + db: { execute: vi.fn(), transaction: vi.fn() }, + })); + vi.doMock("@/lib/migrate", () => ({ + withAdvisoryLock: vi.fn(async (_n: string, fn: () => Promise) => ({ + ran: true, + result: await fn(), + })), + })); + vi.doMock("@/lib/logger", () => ({ + logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, + })); + + const { asPayload } = await import("@/lib/availability/projection-worker"); + + expect(asPayload({ request_id: 1, provider_id: 2 })).toEqual({ + request_id: 1, + provider_id: 2, + }); + expect(asPayload('{"request_id":3}')).toEqual({ request_id: 3 }); + expect(asPayload("{not-json")).toEqual({}); + expect(asPayload(null)).toEqual({}); + expect(asPayload(42)).toEqual({}); + }); + + it("processBatch 对新鲜事件写入 1m 桶并重算 avail_current", async () => { + const executeMock = vi.fn(async (query: unknown) => { + const text = sqlToString(query); + if (text.includes("for update skip locked")) { + return [ + { + id: 10, + event_id: "11111111-1111-1111-1111-111111111111", + payload: { + request_id: 100, + provider_id: 7, + outcome: "success", + occurred_at: "2026-04-13T08:03:12.000Z", + duration_ms: 120, + }, + }, + ]; + } + if (text.includes("insert into proj_applied_requests")) { + return [{ request_id: 100 }]; + } + return []; + }); + const transactionMock = vi.fn(async (fn: (tx: { execute: typeof executeMock }) => Promise) => + fn({ execute: executeMock }) + ); + + vi.doMock("@/drizzle/db", () => ({ + db: { execute: executeMock, transaction: transactionMock }, + })); + vi.doMock("@/lib/migrate", () => ({ + withAdvisoryLock: vi.fn(async (_n: string, fn: () => Promise) => ({ + ran: true, + result: await fn(), + })), + })); + vi.doMock("@/lib/logger", () => ({ + logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, + })); + + const { processBatch } = await import("@/lib/availability/projection-worker"); + const applied = await processBatch(); + expect(applied).toBe(1); + + const texts = executeMock.mock.calls.map((c) => sqlToString(c[0])); + expect(texts.some((t) => t.includes("insert into avail_bucket_1m"))).toBe(true); + expect(texts.some((t) => t.includes("insert into avail_current"))).toBe(true); + expect(texts.some((t) => t.includes("15 * interval '1 minute'"))).toBe(true); + expect(texts.some((t) => t.includes("update outbox_events") && t.includes("published_at"))).toBe( + true + ); + }); + + it("processBatch 对重复 request 不重复计数", async () => { + const executeMock = vi.fn(async (query: unknown) => { + const text = sqlToString(query); + if (text.includes("for update skip locked")) { + return [ + { + id: 11, + event_id: "22222222-2222-2222-2222-222222222222", + payload: { + request_id: 100, + provider_id: 7, + outcome: "success", + occurred_at: "2026-04-13T08:03:12.000Z", + duration_ms: 120, + }, + }, + ]; + } + if (text.includes("insert into proj_applied_requests")) { + return []; // ON CONFLICT DO NOTHING + } + return []; + }); + const transactionMock = vi.fn(async (fn: (tx: { execute: typeof executeMock }) => Promise) => + fn({ execute: executeMock }) + ); + + vi.doMock("@/drizzle/db", () => ({ + db: { execute: executeMock, transaction: transactionMock }, + })); + vi.doMock("@/lib/migrate", () => ({ + withAdvisoryLock: vi.fn(async (_n: string, fn: () => Promise) => ({ + ran: true, + result: await fn(), + })), + })); + vi.doMock("@/lib/logger", () => ({ + logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, + })); + + const { processBatch } = await import("@/lib/availability/projection-worker"); + const applied = await processBatch(); + expect(applied).toBe(0); + + const texts = executeMock.mock.calls.map((c) => sqlToString(c[0])); + expect(texts.some((t) => t.includes("insert into avail_bucket_1m"))).toBe(false); + expect(texts.some((t) => t.includes("update outbox_events"))).toBe(true); + }); + + it("processBatch 将非法 payload 标记 published + last_error", async () => { + const executeMock = vi.fn(async (query: unknown) => { + const text = sqlToString(query); + if (text.includes("for update skip locked")) { + return [ + { + id: 12, + event_id: "33333333-3333-3333-3333-333333333333", + payload: { outcome: "success" }, + }, + ]; + } + return []; + }); + const transactionMock = vi.fn(async (fn: (tx: { execute: typeof executeMock }) => Promise) => + fn({ execute: executeMock }) + ); + + vi.doMock("@/drizzle/db", () => ({ + db: { execute: executeMock, transaction: transactionMock }, + })); + vi.doMock("@/lib/migrate", () => ({ + withAdvisoryLock: vi.fn(async (_n: string, fn: () => Promise) => ({ + ran: true, + result: await fn(), + })), + })); + vi.doMock("@/lib/logger", () => ({ + logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, + })); + + const { processBatch } = await import("@/lib/availability/projection-worker"); + expect(await processBatch()).toBe(0); + + const texts = executeMock.mock.calls.map((c) => sqlToString(c[0])); + expect(texts.some((t) => t.includes("last_error") && t.includes("invalid payload"))).toBe(true); + }); + + it("bootstrapBackfill 在 backfill_done 已存在时为 no-op", async () => { + const executeMock = vi.fn(async () => [{ key: "backfill_done" }]); + const withAdvisoryLock = vi.fn(); + + vi.doMock("@/drizzle/db", () => ({ + db: { execute: executeMock, transaction: vi.fn() }, + })); + vi.doMock("@/lib/migrate", () => ({ withAdvisoryLock })); + vi.doMock("@/lib/logger", () => ({ + logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, + })); + + const { __test__ } = await import("@/lib/availability/projection-worker"); + await __test__.bootstrapBackfill(); + + expect(executeMock).toHaveBeenCalledTimes(1); + expect(withAdvisoryLock).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/unit/lib/shutdown.test.ts b/tests/unit/lib/shutdown.test.ts index 70b80e612..96b415327 100644 --- a/tests/unit/lib/shutdown.test.ts +++ b/tests/unit/lib/shutdown.test.ts @@ -66,6 +66,7 @@ describe.sequential("lifecycle/shutdown", () => { throw new Error("simulated probe scheduler shutdown failure"); }); const stopPublicStatus = vi.fn(async () => {}); + const stopAvailProj = vi.fn(async () => {}); const stopProbeLog = vi.fn(); const shutdownTasks = vi.fn(async () => {}); const stopWriteBuffer = vi.fn(async () => {}); @@ -79,6 +80,9 @@ describe.sequential("lifecycle/shutdown", () => { vi.doMock("@/lib/public-status/scheduler", () => ({ stopPublicStatusRebuildScheduler: stopPublicStatus, })); + vi.doMock("@/lib/availability/projection-worker", () => ({ + stopAvailabilityProjectionWorker: stopAvailProj, + })); vi.doMock("@/lib/provider-endpoints/probe-log-cleanup", () => ({ stopEndpointProbeLogCleanup: stopProbeLog, })); @@ -109,6 +113,7 @@ describe.sequential("lifecycle/shutdown", () => { expect(stopCache).toHaveBeenCalled(); expect(stopProbe).toHaveBeenCalled(); expect(stopPublicStatus).toHaveBeenCalled(); + expect(stopAvailProj).toHaveBeenCalled(); expect(stopProbeLog).toHaveBeenCalled(); expect(shutdownTasks).toHaveBeenCalled(); expect(stopWriteBuffer).toHaveBeenCalled(); @@ -133,6 +138,9 @@ describe.sequential("lifecycle/shutdown", () => { vi.doMock("@/lib/public-status/scheduler", () => ({ stopPublicStatusRebuildScheduler: async () => {}, })); + vi.doMock("@/lib/availability/projection-worker", () => ({ + stopAvailabilityProjectionWorker: async () => {}, + })); vi.doMock("@/lib/provider-endpoints/probe-log-cleanup", () => ({ stopEndpointProbeLogCleanup: async () => {}, })); @@ -174,6 +182,9 @@ describe.sequential("lifecycle/shutdown", () => { vi.doMock("@/lib/public-status/scheduler", () => ({ stopPublicStatusRebuildScheduler: async () => {}, })); + vi.doMock("@/lib/availability/projection-worker", () => ({ + stopAvailabilityProjectionWorker: async () => {}, + })); vi.doMock("@/lib/provider-endpoints/probe-log-cleanup", () => ({ stopEndpointProbeLogCleanup: () => {}, })); @@ -206,6 +217,9 @@ describe.sequential("lifecycle/shutdown", () => { vi.doMock("@/lib/public-status/scheduler", () => ({ stopPublicStatusRebuildScheduler: async () => {}, })); + vi.doMock("@/lib/availability/projection-worker", () => ({ + stopAvailabilityProjectionWorker: async () => {}, + })); vi.doMock("@/lib/provider-endpoints/probe-log-cleanup", () => ({ stopEndpointProbeLogCleanup: () => {}, })); @@ -289,6 +303,9 @@ describe.sequential("lifecycle/shutdown", () => { vi.doMock("@/lib/public-status/scheduler", () => ({ stopPublicStatusRebuildScheduler: async () => {}, })); + vi.doMock("@/lib/availability/projection-worker", () => ({ + stopAvailabilityProjectionWorker: async () => {}, + })); vi.doMock("@/lib/provider-endpoints/probe-log-cleanup", () => ({ stopEndpointProbeLogCleanup: async () => {}, })); @@ -331,6 +348,9 @@ describe.sequential("lifecycle/shutdown", () => { vi.doMock("@/lib/public-status/scheduler", () => ({ stopPublicStatusRebuildScheduler: async () => {}, })); + vi.doMock("@/lib/availability/projection-worker", () => ({ + stopAvailabilityProjectionWorker: async () => {}, + })); vi.doMock("@/lib/provider-endpoints/probe-log-cleanup", () => ({ stopEndpointProbeLogCleanup: () => {}, })); diff --git a/tests/unit/server-shutdown.test.ts b/tests/unit/server-shutdown.test.ts index caa628d97..f2aae86b3 100644 --- a/tests/unit/server-shutdown.test.ts +++ b/tests/unit/server-shutdown.test.ts @@ -258,6 +258,9 @@ describe.sequential("registerOrchestratedShutdown", () => { vi.doMock("@/lib/public-status/scheduler", () => ({ stopPublicStatusRebuildScheduler: async () => {}, })); + vi.doMock("@/lib/availability/projection-worker", () => ({ + stopAvailabilityProjectionWorker: async () => {}, + })); vi.doMock("@/lib/provider-endpoints/probe-log-cleanup", () => ({ stopEndpointProbeLogCleanup: () => {}, }));