diff --git a/drizzle/0121_legacy_hedge_abort_health.sql b/drizzle/0121_legacy_hedge_abort_health.sql new file mode 100644 index 000000000..6d80a41b2 --- /dev/null +++ b/drizzle/0121_legacy_hedge_abort_health.sql @@ -0,0 +1,137 @@ +ALTER TABLE "system_settings" + ADD COLUMN IF NOT EXISTS "legacy_hedge_max_in_flight" integer DEFAULT 2 NOT NULL;--> statement-breakpoint + +UPDATE "system_settings" +SET "legacy_hedge_max_in_flight" = 2 +WHERE "legacy_hedge_max_in_flight" IS NULL;--> statement-breakpoint + +ALTER TABLE "system_settings" + ALTER COLUMN "legacy_hedge_max_in_flight" SET DEFAULT 2, + ALTER COLUMN "legacy_hedge_max_in_flight" SET NOT NULL;--> statement-breakpoint + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'system_settings_legacy_hedge_max_in_flight_range' + ) THEN + ALTER TABLE "system_settings" + ADD CONSTRAINT "system_settings_legacy_hedge_max_in_flight_range" + CHECK ("legacy_hedge_max_in_flight" >= 1 AND "legacy_hedge_max_in_flight" <= 4); + END IF; +END $$;--> statement-breakpoint + +CREATE OR REPLACE FUNCTION fn_is_message_request_finalized( + blocked_by varchar, + status_code integer, + provider_chain jsonb, + error_message text +) +RETURNS boolean AS $$ +DECLARE + last_reason text; + last_status_code integer; + last_error_message text; +BEGIN + IF blocked_by IS NOT NULL OR status_code IS NOT NULL THEN + RETURN TRUE; + END IF; + IF error_message IS NOT NULL AND error_message <> '' THEN + RETURN TRUE; + END IF; + IF provider_chain IS NOT NULL + AND jsonb_typeof(provider_chain) = 'array' + AND jsonb_array_length(provider_chain) > 0 + AND jsonb_typeof(provider_chain -> -1) = 'object' THEN + last_reason := provider_chain -> -1 ->> 'reason'; + IF (provider_chain -> -1 ? 'statusCode') + AND jsonb_typeof(provider_chain -> -1 -> 'statusCode') = 'number' THEN + last_status_code := (provider_chain -> -1 ->> 'statusCode')::integer; + END IF; + last_error_message := provider_chain -> -1 ->> 'errorMessage'; + IF last_reason IN ( + 'request_success', 'retry_success', 'retry_failed', 'system_error', + 'resource_not_found', 'client_error_non_retryable', 'concurrent_limit_failed', + 'hedge_winner', 'hedge_loser_cancelled', 'hedge_loser_billed', + 'client_abort', 'client_abort_no_first_byte' + ) OR last_status_code IS NOT NULL OR COALESCE(last_error_message, '') <> '' THEN + RETURN TRUE; + END IF; + END IF; + RETURN FALSE; +END; +$$ LANGUAGE plpgsql IMMUTABLE;--> statement-breakpoint + +CREATE OR REPLACE FUNCTION fn_compute_message_request_success_rate_outcome( + blocked_by varchar, + status_code integer, + error_message text, + provider_chain jsonb +) +RETURNS varchar AS $$ +DECLARE + last_reason text; + last_status_code integer; + last_error_message text; + normalized_error text; + has_matched_rule boolean := false; +BEGIN + IF NOT fn_is_message_request_finalized(blocked_by, status_code, provider_chain, error_message) THEN + RETURN NULL; + END IF; + IF blocked_by IS NOT NULL THEN + RETURN 'excluded'; + END IF; + IF provider_chain IS NOT NULL + AND jsonb_typeof(provider_chain) = 'array' + AND jsonb_array_length(provider_chain) > 0 + AND jsonb_typeof(provider_chain -> -1) = 'object' THEN + last_reason := provider_chain -> -1 ->> 'reason'; + IF (provider_chain -> -1 ? 'statusCode') + AND jsonb_typeof(provider_chain -> -1 -> 'statusCode') = 'number' THEN + last_status_code := (provider_chain -> -1 ->> 'statusCode')::integer; + END IF; + last_error_message := provider_chain -> -1 ->> 'errorMessage'; + has_matched_rule := jsonb_typeof(provider_chain -> -1 -> 'errorDetails') = 'object' + AND (provider_chain -> -1 -> 'errorDetails' ? 'matchedRule'); + END IF; + IF has_matched_rule THEN + RETURN 'excluded'; + END IF; + IF COALESCE(last_status_code, status_code) = 404 + OR (COALESCE(last_status_code, status_code) = 499 + AND last_reason IS DISTINCT FROM 'client_abort_no_first_byte') THEN + RETURN 'excluded'; + END IF; + IF last_reason IN ( + 'resource_not_found', 'concurrent_limit_failed', 'hedge_loser_cancelled', + 'hedge_loser_billed', 'client_error_non_retryable', 'client_abort' + ) THEN + RETURN 'excluded'; + END IF; + normalized_error := lower(COALESCE(last_error_message, error_message, '')); + IF normalized_error LIKE '%no available provider%' + OR normalized_error LIKE '%insufficient quota%' + OR normalized_error LIKE '%quota exceeded%' + OR normalized_error LIKE '%rate limit%' + OR normalized_error LIKE '%rate_limit%' + OR normalized_error LIKE '%concurrency limit%' + OR normalized_error LIKE '%concurrent limit%' + OR normalized_error LIKE '%limit exceeded%' THEN + RETURN 'excluded'; + END IF; + IF last_reason IN ('request_success', 'retry_success', 'hedge_winner') + OR COALESCE(last_status_code, status_code) BETWEEN 200 AND 399 THEN + RETURN 'success'; + END IF; + IF last_reason IN ( + 'session_reuse', 'initial_selection', 'hedge_triggered', 'hedge_launched', + 'client_restriction_filtered', 'http2_fallback' + ) AND last_status_code IS NULL + AND COALESCE(last_error_message, error_message, '') = '' THEN + RETURN NULL; + END IF; + RETURN 'failure'; +END; +$$ LANGUAGE plpgsql IMMUTABLE; diff --git a/drizzle/meta/0121_snapshot.json b/drizzle/meta/0121_snapshot.json new file mode 100644 index 000000000..e1834fb57 --- /dev/null +++ b/drizzle/meta/0121_snapshot.json @@ -0,0 +1,5757 @@ +{ + "id": "57129cf2-37e4-47e1-8f1e-b1c955914f9a", + "prevId": "5a73b354-be53-4ef5-bce8-e3eb4b3c3546", + "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 + }, + "legacy_hedge_max_in_flight": { + "name": "legacy_hedge_max_in_flight", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 2 + }, + "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": { + "system_settings_legacy_hedge_max_in_flight_range": { + "name": "system_settings_legacy_hedge_max_in_flight_range", + "value": "\"legacy_hedge_max_in_flight\" >= 1 AND \"legacy_hedge_max_in_flight\" <= 4" + } + }, + "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 6e10f0a36..9ca79f910 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -848,6 +848,13 @@ "when": 1786500000000, "tag": "0120_availability_projection", "breakpoints": true + }, + { + "idx": 121, + "version": "7", + "when": 1788182990280, + "tag": "0121_legacy_hedge_abort_health", + "breakpoints": true } ] } diff --git a/messages/en/dashboard.json b/messages/en/dashboard.json index 62e86d8e2..7e229685d 100644 --- a/messages/en/dashboard.json +++ b/messages/en/dashboard.json @@ -515,12 +515,15 @@ "summary": "{rounds} rounds · {attempts} attempts · max {maxActive} active", "config": "Configuration snapshot", "configConcurrency": "Concurrency", + "configLegacyHedgeMaxInFlight": "Legacy hedge cap", "configMaxRounds": "Maximum rounds", "configDiscoverySla": "Round SLA", "configStickySla": "Sticky SLA", "configTotalTimeout": "Total timeout", "configStickyCooldown": "Sticky timeout cooldown", "configStickyBindingTtl": "Sticky binding validity (SESSION_TTL)", + "slotSaturation": "Hedge slots saturated ({count})", + "slotSaturationEvent": "{provider} · {active}/{cap} active · {elapsed}ms elapsed", "attemptDetails": { "providerId": "Provider ID", "attempt": "Attempt", diff --git a/messages/en/provider-chain.json b/messages/en/provider-chain.json index a5ccf1501..c7e1b1038 100644 --- a/messages/en/provider-chain.json +++ b/messages/en/provider-chain.json @@ -71,7 +71,8 @@ "hedge_winner": "Hedge Winner", "hedge_loser_cancelled": "Hedge Loser (Cancelled)", "hedge_loser_billed": "Hedge Loser (Billed)", - "client_abort": "Client Aborted" + "client_abort": "Client Aborted", + "client_abort_no_first_byte": "Client Aborted Before First Byte" }, "filterReasons": { "rate_limited": "Rate Limited", diff --git a/messages/en/settings/config.json b/messages/en/settings/config.json index c6f6a1832..74b7af8da 100644 --- a/messages/en/settings/config.json +++ b/messages/en/settings/config.json @@ -118,6 +118,10 @@ "billHedgeLosers": "Bill Provider-Racing Losers by Token Usage", "billHedgeLosersDesc": "When provider racing (streaming hedge or bounded Discovery) is on, losers that have already produced a valid response prefix may be kept connected in the background, drained for usage, and billed. Their cost is added to this request's total. Other losers are cancelled. Default on.", "billHedgeLosersTooltip": "Only losers with a readable valid response are drained. SLA timeouts, explicit failures, and attempts without a valid prefix are cancelled; their unknown upstream cost is not added.", + "legacyHedgeMaxInFlight": "Legacy hedge maximum in-flight requests", + "legacyHedgeMaxInFlightDesc": "Default is 2. Changes apply to newly admitted requests without restarting Pods. Higher values may reduce tail latency when providers hang, but increase concurrent upstream traffic, token consumption, and potential hedge-loser cost.", + "legacyHedgeMaxInFlightTooltip": "Applies whenever routing falls back to legacy hedge, including requests ineligible for bounded Discovery. The value includes the primary request and must be between 1 and 4.", + "legacyHedgeMaxInFlightInvalid": "Legacy hedge concurrency must be an integer from 1 to 4.", "discoveryEnabled": "Enable bounded provider Discovery", "discoveryEnabledDesc": "When enabled, cold-start streaming requests probe multiple providers within a bounded window and keep at most one fallback. It is disabled by default.", "discoveryConcurrency": "Discovery initial concurrency", diff --git a/messages/ja/dashboard.json b/messages/ja/dashboard.json index 49c5bbea4..f821d2955 100644 --- a/messages/ja/dashboard.json +++ b/messages/ja/dashboard.json @@ -515,12 +515,15 @@ "summary": "{rounds} ラウンド · {attempts} 試行 · 最大同時実行 {maxActive}", "config": "設定スナップショット", "configConcurrency": "同時実行数", + "configLegacyHedgeMaxInFlight": "Legacy hedge 上限", "configMaxRounds": "最大ラウンド数", "configDiscoverySla": "ラウンド SLA", "configStickySla": "Sticky SLA", "configTotalTimeout": "全体タイムアウト", "configStickyCooldown": "Sticky タイムアウトのクールダウン", "configStickyBindingTtl": "Sticky バインドの有効期間 (SESSION_TTL)", + "slotSaturation": "Hedge スロット飽和({count} 件)", + "slotSaturationEvent": "{provider} · {active}/{cap} 件がアクティブ · 経過 {elapsed}ms", "attemptDetails": { "providerId": "Provider ID", "attempt": "試行回数", diff --git a/messages/ja/provider-chain.json b/messages/ja/provider-chain.json index 2b6447c1a..eaa31b575 100644 --- a/messages/ja/provider-chain.json +++ b/messages/ja/provider-chain.json @@ -71,7 +71,8 @@ "hedge_winner": "Hedge 競争勝者", "hedge_loser_cancelled": "Hedge 競争敗者(キャンセル)", "hedge_loser_billed": "競争の敗者(課金済み)", - "client_abort": "クライアント中断" + "client_abort": "クライアント中断", + "client_abort_no_first_byte": "最初のバイト前にクライアント中断" }, "filterReasons": { "rate_limited": "レート制限", diff --git a/messages/ja/settings/config.json b/messages/ja/settings/config.json index 27cb07459..687ae51e7 100644 --- a/messages/ja/settings/config.json +++ b/messages/ja/settings/config.json @@ -118,6 +118,10 @@ "billHedgeLosers": "プロバイダー競争(hedge)の敗者を Token 使用量で課金", "billHedgeLosersDesc": "プロバイダー競争(ストリーミング hedge または制限付き Discovery)が有効な場合、有効な応答プレフィックスを受信済みの敗者だけをバックグラウンドで読み切って課金します。費用はリクエスト合計に加算され、それ以外の敗者はキャンセルされます。既定はオン。", "billHedgeLosersTooltip": "読み取り可能な有効応答がある敗者だけを読み切ります。SLA タイムアウト、明示的な失敗、有効なプレフィックスがない試行はキャンセルされ、確認できない上流費用は加算されません。", + "legacyHedgeMaxInFlight": "Legacy hedge 最大同時実行リクエスト数(主リクエストを含む)", + "legacyHedgeMaxInFlightDesc": "既定値は 2 です。変更は Pod の再起動なしで新しく受け付けたリクエストに適用されます。値を増やすとプロバイダーが停止した場合のテールレイテンシを下げられる可能性がありますが、同時上流トラフィック、Token 消費、競争敗者のコストも増加します。", + "legacyHedgeMaxInFlightTooltip": "制限付き Discovery の対象外リクエストを含め、ルーティングが legacy hedge にフォールバックするときに適用されます。主リクエストを含む 1 から 4 の整数を指定してください。", + "legacyHedgeMaxInFlightInvalid": "Legacy hedge の同時実行数は 1 から 4 の整数で指定してください。", "discoveryEnabled": "制限付き Provider Discovery を有効化", "discoveryEnabledDesc": "有効にすると、コールドスタートのストリーミングリクエストで複数 Provider を制限時間内に探索し、フォールバックを最大 1 つ保持します。既定はオフです。", "discoveryConcurrency": "Discovery 初期並列数", diff --git a/messages/ru/dashboard.json b/messages/ru/dashboard.json index 298d72f85..0182f11bb 100644 --- a/messages/ru/dashboard.json +++ b/messages/ru/dashboard.json @@ -515,12 +515,15 @@ "summary": "Раундов: {rounds} · попыток: {attempts} · максимум активных: {maxActive}", "config": "Снимок настроек", "configConcurrency": "Параллельность", + "configLegacyHedgeMaxInFlight": "Лимит legacy hedge", "configMaxRounds": "Максимум раундов", "configDiscoverySla": "SLA раунда", "configStickySla": "SLA Sticky", "configTotalTimeout": "Общий тайм-аут", "configStickyCooldown": "Пауза после тайм-аута Sticky", "configStickyBindingTtl": "Срок действия привязки Sticky (SESSION_TTL)", + "slotSaturation": "Слоты hedge заполнены ({count})", + "slotSaturationEvent": "{provider} · активно {active}/{cap} · прошло {elapsed} мс", "attemptDetails": { "providerId": "ID провайдера", "attempt": "Попытка", diff --git a/messages/ru/provider-chain.json b/messages/ru/provider-chain.json index 5d97be6de..08e432461 100644 --- a/messages/ru/provider-chain.json +++ b/messages/ru/provider-chain.json @@ -71,7 +71,8 @@ "hedge_winner": "Победитель Hedge-гонки", "hedge_loser_cancelled": "Проигравший Hedge-гонки (отменён)", "hedge_loser_billed": "Проигравший (тарифицирован)", - "client_abort": "Клиент прервал запрос" + "client_abort": "Клиент прервал запрос", + "client_abort_no_first_byte": "Клиент прервал запрос до первого байта" }, "filterReasons": { "rate_limited": "Ограничение скорости", diff --git a/messages/ru/settings/config.json b/messages/ru/settings/config.json index 3171cce69..fe4ebc3c4 100644 --- a/messages/ru/settings/config.json +++ b/messages/ru/settings/config.json @@ -118,6 +118,10 @@ "billHedgeLosers": "Тарифицировать проигравших в гонке провайдеров по токенам", "billHedgeLosersDesc": "Если включена гонка провайдеров (streaming hedge или ограниченный Discovery), в фоне дочитываются и тарифицируются только проигравшие, уже отдавшие корректный префикс ответа. Их стоимость добавляется к общей стоимости запроса. Остальные проигравшие отменяются. По умолчанию включено.", "billHedgeLosersTooltip": "Дочитываются только проигравшие с доступным корректным ответом. Тайм-ауты SLA, явные ошибки и попытки без корректного префикса отменяются; неизвестная стоимость апстрима не добавляется.", + "legacyHedgeMaxInFlight": "Максимум одновременных запросов legacy hedge (включая основной)", + "legacyHedgeMaxInFlightDesc": "По умолчанию 2. Изменение применяется к новым запросам без перезапуска Pod. Большее значение может уменьшить хвостовую задержку при зависании провайдеров, но увеличивает параллельный трафик к апстриму, расход токенов и потенциальную стоимость проигравших в гонке.", + "legacyHedgeMaxInFlightTooltip": "Применяется при откате маршрутизации к legacy hedge, включая запросы, не подходящие для ограниченного Discovery. Значение включает основной запрос и должно быть целым числом от 1 до 4.", + "legacyHedgeMaxInFlightInvalid": "Параллельность legacy hedge должна быть целым числом от 1 до 4.", "discoveryEnabled": "Включить ограниченное обнаружение провайдеров", "discoveryEnabledDesc": "При включении потоковые запросы холодного старта проверяют несколько провайдеров в ограниченном окне и сохраняют не более одного резервного. По умолчанию выключено.", "discoveryConcurrency": "Начальная параллельность Discovery", diff --git a/messages/zh-CN/dashboard.json b/messages/zh-CN/dashboard.json index 3c9bfda04..6912945a0 100644 --- a/messages/zh-CN/dashboard.json +++ b/messages/zh-CN/dashboard.json @@ -515,12 +515,15 @@ "summary": "{rounds} 轮 · {attempts} 次尝试 · 最大并发 {maxActive}", "config": "配置快照", "configConcurrency": "并发数", + "configLegacyHedgeMaxInFlight": "Legacy hedge 并发上限", "configMaxRounds": "最大轮数", "configDiscoverySla": "每轮 SLA", "configStickySla": "Sticky SLA", "configTotalTimeout": "总超时", "configStickyCooldown": "Sticky 超时冷却", "configStickyBindingTtl": "Sticky 绑定有效期(SESSION_TTL)", + "slotSaturation": "Hedge 槽位已饱和({count} 次)", + "slotSaturationEvent": "{provider} · {active}/{cap} 个活跃 · 已耗时 {elapsed}ms", "attemptDetails": { "providerId": "供应商 ID", "attempt": "尝试次数", diff --git a/messages/zh-CN/provider-chain.json b/messages/zh-CN/provider-chain.json index 81e3b21ea..aceea0a50 100644 --- a/messages/zh-CN/provider-chain.json +++ b/messages/zh-CN/provider-chain.json @@ -71,7 +71,8 @@ "hedge_winner": "Hedge 竞速赢家", "hedge_loser_cancelled": "Hedge 竞速输家(已取消)", "hedge_loser_billed": "竞速输家(已计费)", - "client_abort": "客户端中断" + "client_abort": "客户端中断", + "client_abort_no_first_byte": "首字节前客户端中断" }, "filterReasons": { "rate_limited": "速率限制", diff --git a/messages/zh-CN/settings/config.json b/messages/zh-CN/settings/config.json index 6b3e4ed08..32332b7be 100644 --- a/messages/zh-CN/settings/config.json +++ b/messages/zh-CN/settings/config.json @@ -47,6 +47,10 @@ "billHedgeLosers": "对供应商竞速输家计费", "billHedgeLosersDesc": "开启供应商竞速(流式 Hedge 或有界 Discovery)后,只有已经收到有效响应前缀的输家会在后台继续读取并按用量计费,费用会累加到本条请求总花费;其他输家会取消。默认开启。", "billHedgeLosersTooltip": "只保活已经收到可读取有效响应的输家;SLA 超时、明确失败或未取得有效首字的请求会取消,无法确认的上游费用不会计入。", + "legacyHedgeMaxInFlight": "Legacy hedge 最大并发请求数(包含主请求)", + "legacyHedgeMaxInFlightDesc": "默认值为 2。修改仅对新接入的请求生效,无需重启 Pod。提高数值可能降低供应商挂起时的尾延迟,但会增加并发上游流量、Token 消耗和潜在的竞速输家成本。", + "legacyHedgeMaxInFlightTooltip": "当路由回退到 legacy hedge 时生效,包括不符合有界 Discovery 条件的请求。数值包含主请求,必须在 1 到 4 之间。", + "legacyHedgeMaxInFlightInvalid": "Legacy hedge 并发数必须是 1 到 4 之间的整数。", "discoveryEnabled": "启用有界供应商 Discovery", "discoveryEnabledDesc": "启用后,冷启动流式请求会在限定窗口内探测多个供应商,并且最多保留一个保底请求。默认关闭。", "discoveryConcurrency": "Discovery 首轮并发数", diff --git a/messages/zh-TW/dashboard.json b/messages/zh-TW/dashboard.json index ec100e4e0..ef1558fbc 100644 --- a/messages/zh-TW/dashboard.json +++ b/messages/zh-TW/dashboard.json @@ -515,12 +515,15 @@ "summary": "{rounds} 輪 · {attempts} 次嘗試 · 最大並發 {maxActive}", "config": "設定快照", "configConcurrency": "並發數", + "configLegacyHedgeMaxInFlight": "Legacy hedge 並發上限", "configMaxRounds": "最大輪數", "configDiscoverySla": "每輪 SLA", "configStickySla": "Sticky SLA", "configTotalTimeout": "總逾時", "configStickyCooldown": "Sticky 逾時冷卻", "configStickyBindingTtl": "Sticky 綁定有效期(SESSION_TTL)", + "slotSaturation": "Hedge 槽位已飽和({count} 次)", + "slotSaturationEvent": "{provider} · {active}/{cap} 個活躍 · 已耗時 {elapsed}ms", "attemptDetails": { "providerId": "供應商 ID", "attempt": "嘗試次數", diff --git a/messages/zh-TW/provider-chain.json b/messages/zh-TW/provider-chain.json index 565bb793e..038df8b95 100644 --- a/messages/zh-TW/provider-chain.json +++ b/messages/zh-TW/provider-chain.json @@ -71,7 +71,8 @@ "hedge_winner": "Hedge 競速贏家", "hedge_loser_cancelled": "Hedge 競速輸家(已取消)", "hedge_loser_billed": "競速輸家(已計費)", - "client_abort": "客戶端中斷" + "client_abort": "客戶端中斷", + "client_abort_no_first_byte": "首字節前客戶端中斷" }, "filterReasons": { "rate_limited": "速率限制", diff --git a/messages/zh-TW/settings/config.json b/messages/zh-TW/settings/config.json index 45b9debf2..63bf05755 100644 --- a/messages/zh-TW/settings/config.json +++ b/messages/zh-TW/settings/config.json @@ -118,6 +118,10 @@ "billHedgeLosers": "對供應商競速輸家計費", "billHedgeLosersDesc": "開啟供應商競速(串流 Hedge 或有界 Discovery)後,只有已收到有效回應前綴的輸家會在後台繼續讀取並按用量計費,費用會累加到本條請求總花費;其他輸家會取消。預設開啟。", "billHedgeLosersTooltip": "只保活已收到可讀取有效回應的輸家;SLA 逾時、明確失敗或未取得有效首字的請求會取消,無法確認的上游費用不會計入。", + "legacyHedgeMaxInFlight": "Legacy hedge 最大並發請求數(包含主請求)", + "legacyHedgeMaxInFlightDesc": "預設值為 2。變更只套用到新接入的請求,無需重新啟動 Pod。提高數值可能降低供應商掛起時的尾延遲,但會增加並發上游流量、Token 消耗和潛在的競速輸家成本。", + "legacyHedgeMaxInFlightTooltip": "當路由回退到 legacy hedge 時生效,包括不符合有界 Discovery 條件的請求。數值包含主請求,必須介於 1 到 4。", + "legacyHedgeMaxInFlightInvalid": "Legacy hedge 並發數必須是 1 到 4 之間的整數。", "discoveryEnabled": "啟用有界供應商 Discovery", "discoveryEnabledDesc": "啟用後,冷啟動串流請求會在限定視窗內探測多個供應商,並且最多保留一個保底請求。預設關閉。", "discoveryConcurrency": "Discovery 首輪並發數", diff --git a/src/actions/system-config.ts b/src/actions/system-config.ts index bf4fb222a..b17954044 100644 --- a/src/actions/system-config.ts +++ b/src/actions/system-config.ts @@ -84,6 +84,7 @@ export async function saveSystemSettings(formData: { codexPriorityBillingSource?: CodexPriorityBillingSource; billNonSuccessfulRequests?: boolean; billHedgeLosers?: boolean; + legacyHedgeMaxInFlight?: number; discoveryEnabled?: boolean; discoveryConcurrency?: number; maxDiscoveryRounds?: number; @@ -174,6 +175,7 @@ export async function saveSystemSettings(formData: { codexPriorityBillingSource: validated.codexPriorityBillingSource, billNonSuccessfulRequests: validated.billNonSuccessfulRequests, billHedgeLosers: validated.billHedgeLosers, + legacyHedgeMaxInFlight: validated.legacyHedgeMaxInFlight, discoveryEnabled: validated.discoveryEnabled, discoveryConcurrency: validated.discoveryConcurrency, maxDiscoveryRounds: validated.maxDiscoveryRounds, diff --git a/src/app/[locale]/dashboard/logs/_components/error-details-dialog.test.tsx b/src/app/[locale]/dashboard/logs/_components/error-details-dialog.test.tsx index 1acb562a4..606f1d2d4 100644 --- a/src/app/[locale]/dashboard/logs/_components/error-details-dialog.test.tsx +++ b/src/app/[locale]/dashboard/logs/_components/error-details-dialog.test.tsx @@ -2342,6 +2342,55 @@ describe("error-details-dialog routing trace", () => { expect(html).not.toContain("Discovery rounds"); }); + test("surfaces legacy hedge slot saturation and its configured cap", () => { + const legacyHedgeTrace: RoutingTraceV1 = { + version: 1, + mode: "legacy_hedge", + startedAt: 1_000, + updatedAt: 2_000, + discoveryEnabled: false, + eligible: false, + bypassReason: "disabled", + config: { + discoveryConcurrency: 2, + maxDiscoveryRounds: 1, + discoverySlaMs: 10_000, + stickySlaMs: 20_000, + racingTotalTimeoutMs: 60_000, + stickyTimeoutCooldownMs: 300_000, + legacyHedgeMaxInFlight: 3, + }, + events: [ + { + type: "hedge_slot_saturated", + at: 2_000, + elapsedMs: 1_000, + attemptId: "legacy-hedge-1-1", + provider: { id: 1, name: "slow-provider" }, + activeAttemptCount: 3, + configuredCap: 3, + durationMs: 1_000, + }, + ], + }; + const html = renderWithIntl( + + ); + + expect(html).toContain("Legacy Hedge"); + expect(html).toContain("Legacy hedge cap"); + expect(html).toContain("Hedge slots saturated (1)"); + expect(html).toContain("slow-provider"); + expect(html).toContain("3/3 active"); + }); + test("shows late terminal failure and Sticky binding result after a first-byte winner", () => { const failedTrace: RoutingTraceV1 = { ...discoveryTrace, diff --git a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/DiscoveryTraceView.tsx b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/DiscoveryTraceView.tsx index 48fd063bf..7d70c0dd9 100644 --- a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/DiscoveryTraceView.tsx +++ b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/DiscoveryTraceView.tsx @@ -614,6 +614,7 @@ export function DiscoveryTraceView({ numberFrom(summary, "attemptsPerRequest", "attempts", "attemptsStarted") ?? Math.max(runtimeStats.attemptCount, attempts.length); const maxActive = numberFrom(summary, "maxActive", "maxActiveAttempts") ?? runtimeStats.maxActive; + const saturationEvents = trace.events.filter((event) => event.type === "hedge_slot_saturated"); const terminalEvent = trace.events.findLast((event) => event.type === "request_finished"); const bindingEvent = trace.events.findLast((event) => event.type === "binding_finalized"); const terminalOutcome = normalizeTerminalOutcome(terminalEvent?.outcome); @@ -699,6 +700,12 @@ export function DiscoveryTraceView({ value={numberFrom(config, "discoveryConcurrency", "concurrency")} /> )} + {numberFrom(config, "legacyHedgeMaxInFlight") != null && ( + + )} {numberFrom(config, "maxDiscoveryRounds", "maxRounds") != null && ( )} + {saturationEvents.length > 0 && ( +
+
+ {t("slotSaturation", { count: saturationEvents.length })} +
+
+ {saturationEvents.map((event, index) => { + const provider = asRecord(event.provider); + return ( +
+ {t("slotSaturationEvent", { + provider: asString(provider.name) ?? "-", + active: asNumber(event.activeAttemptCount) ?? 0, + cap: asNumber(event.configuredCap) ?? 0, + elapsed: Math.round( + asNumber(event.elapsedMs) ?? asNumber(event.durationMs) ?? 0 + ), + })} +
+ ); + })} +
+
+ )} + {grouped.size === 0 ? (
diff --git a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/LogicTraceTab.tsx b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/LogicTraceTab.tsx index 807190702..c0b6ab95b 100644 --- a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/LogicTraceTab.tsx +++ b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/LogicTraceTab.tsx @@ -64,7 +64,8 @@ function getRequestStatus(item: ProviderChainItem): StepStatus { item.reason === "concurrent_limit_failed" || item.reason === "hedge_loser_cancelled" || item.reason === "hedge_loser_billed" || - item.reason === "client_abort" + item.reason === "client_abort" || + item.reason === "client_abort_no_first_byte" ) { return "failure"; } @@ -222,7 +223,10 @@ export function LogicTraceTab({ // Calculate step offset for session reuse flow const sessionReuseStepOffset = isSessionReuseFlow ? 1 : 0; - if (normalizedRoutingTrace?.mode === "discovery") { + if ( + normalizedRoutingTrace?.mode === "discovery" || + normalizedRoutingTrace?.mode === "legacy_hedge" + ) { return (
@@ -927,7 +931,8 @@ export function LogicTraceTab({ const isHedgeWinner = item.reason === "hedge_winner"; const isHedgeLoser = item.reason === "hedge_loser_cancelled"; const isHedgeLoserBilled = item.reason === "hedge_loser_billed"; - const isClientAbort = item.reason === "client_abort"; + const isClientAbort = + item.reason === "client_abort" || item.reason === "client_abort_no_first_byte"; // Resolved hedge losers (cancelled or billed) carry billing detail when // their reclaimed upstream response was charged to the request total. const hedgeLoserBilling = @@ -963,7 +968,9 @@ export function LogicTraceTab({ : isHedgeLoserBilled ? tChain("timeline.hedgeLoserBilled") : isClientAbort - ? tChain("timeline.clientAbort") + ? item.reason === "client_abort_no_first_byte" + ? tChain("reasons.client_abort_no_first_byte") + : tChain("timeline.clientAbort") : isRetry ? t("logicTrace.retryAttempt", { number: item.attemptNumber ?? 1 }) : item.reason === "hedge_winner" diff --git a/src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx b/src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx index 676edde53..3c16f6c94 100644 --- a/src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx +++ b/src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx @@ -281,7 +281,7 @@ function getItemStatus(item: ProviderChainItem): { bgColor: "bg-slate-50 dark:bg-slate-800/50", }; } - if (item.reason === "client_abort") { + if (item.reason === "client_abort" || item.reason === "client_abort_no_first_byte") { return { icon: MinusCircle, color: "text-amber-600", diff --git a/src/app/[locale]/settings/config/_components/system-settings-form.tsx b/src/app/[locale]/settings/config/_components/system-settings-form.tsx index a749d211e..09212da22 100644 --- a/src/app/[locale]/settings/config/_components/system-settings-form.tsx +++ b/src/app/[locale]/settings/config/_components/system-settings-form.tsx @@ -80,6 +80,7 @@ interface SystemSettingsFormProps { | "codexPriorityBillingSource" | "billNonSuccessfulRequests" | "billHedgeLosers" + | "legacyHedgeMaxInFlight" | "discoveryEnabled" | "discoveryConcurrency" | "maxDiscoveryRounds" @@ -163,6 +164,9 @@ export function SystemSettingsForm({ initialSettings.billNonSuccessfulRequests ); const [billHedgeLosers, setBillHedgeLosers] = useState(initialSettings.billHedgeLosers); + const [legacyHedgeMaxInFlight, setLegacyHedgeMaxInFlight] = useState( + initialSettings.legacyHedgeMaxInFlight ?? 2 + ); const [discoveryEnabled, setDiscoveryEnabled] = useState(initialSettings.discoveryEnabled); const [discoveryConcurrency, setDiscoveryConcurrency] = useState( initialSettings.discoveryConcurrency @@ -295,6 +299,16 @@ export function SystemSettingsForm({ return; } + const legacyHedgeMaxInFlightValue = Number(legacyHedgeMaxInFlight); + if ( + !Number.isSafeInteger(legacyHedgeMaxInFlightValue) || + legacyHedgeMaxInFlightValue < 1 || + legacyHedgeMaxInFlightValue > 4 + ) { + toast.error(t("legacyHedgeMaxInFlightInvalid")); + return; + } + const discoveryConfig = { discoveryConcurrency: Number(discoveryConcurrency), maxDiscoveryRounds: Number(maxDiscoveryRounds), @@ -402,6 +416,7 @@ export function SystemSettingsForm({ codexPriorityBillingSource, billNonSuccessfulRequests, billHedgeLosers, + legacyHedgeMaxInFlight: legacyHedgeMaxInFlightValue, discoveryEnabled, ...(discoveryEnabled ? discoveryConfig : {}), timezone, @@ -459,6 +474,7 @@ export function SystemSettingsForm({ setCodexPriorityBillingSource(result.data.codexPriorityBillingSource); setBillNonSuccessfulRequests(result.data.billNonSuccessfulRequests); setBillHedgeLosers(result.data.billHedgeLosers); + setLegacyHedgeMaxInFlight(result.data.legacyHedgeMaxInFlight); setDiscoveryEnabled(result.data.discoveryEnabled); setDiscoveryConcurrency(result.data.discoveryConcurrency); setMaxDiscoveryRounds(result.data.maxDiscoveryRounds); @@ -754,7 +770,51 @@ export function SystemSettingsForm({ />
- {/* Bounded Streaming Discovery */} + {/* Legacy streaming hedge concurrency */} +
+
+
+
+ + + + + + + {t("legacyHedgeMaxInFlightTooltip")} + + +
+

+ {t("legacyHedgeMaxInFlightDesc")} +

+
+ + setLegacyHedgeMaxInFlight( + event.target.value === "" ? "" : Number(event.target.value) + ) + } + disabled={isPending} + className={`${inputClassName} w-24 shrink-0`} + /> +
+
+
diff --git a/src/app/[locale]/settings/config/page.tsx b/src/app/[locale]/settings/config/page.tsx index c32f4e579..37a02e503 100644 --- a/src/app/[locale]/settings/config/page.tsx +++ b/src/app/[locale]/settings/config/page.tsx @@ -57,6 +57,7 @@ async function SettingsConfigContent({ locale }: { locale: string }) { codexPriorityBillingSource: settings.codexPriorityBillingSource, billNonSuccessfulRequests: settings.billNonSuccessfulRequests, billHedgeLosers: settings.billHedgeLosers, + legacyHedgeMaxInFlight: settings.legacyHedgeMaxInFlight, discoveryEnabled: settings.discoveryEnabled, discoveryConcurrency: settings.discoveryConcurrency, maxDiscoveryRounds: settings.maxDiscoveryRounds, diff --git a/src/app/api/admin/system-config/route.ts b/src/app/api/admin/system-config/route.ts index b98c02390..a5f56127a 100644 --- a/src/app/api/admin/system-config/route.ts +++ b/src/app/api/admin/system-config/route.ts @@ -83,6 +83,7 @@ export async function POST(req: Request) { currencyDisplay: validated.currencyDisplay, billingModelSource: validated.billingModelSource, codexPriorityBillingSource: validated.codexPriorityBillingSource, + legacyHedgeMaxInFlight: validated.legacyHedgeMaxInFlight, discoveryEnabled: validated.discoveryEnabled, discoveryConcurrency: validated.discoveryConcurrency, maxDiscoveryRounds: validated.maxDiscoveryRounds, diff --git a/src/app/api/v1/resources/system/handlers.ts b/src/app/api/v1/resources/system/handlers.ts index f238060b8..3ce448569 100644 --- a/src/app/api/v1/resources/system/handlers.ts +++ b/src/app/api/v1/resources/system/handlers.ts @@ -10,6 +10,7 @@ import { jsonResponse } from "@/lib/api/v1/_shared/response-helpers"; import { SystemSettingsUpdateSchema } from "@/lib/api/v1/schemas/system-config"; import { getDiscoveryValidationErrorCode } from "@/lib/validation/discovery-settings"; import { getReplayCacheTtlValidationErrorCode } from "@/lib/validation/replay-settings"; +import { getLegacyHedgeMaxInFlightValidationErrorCode } from "@/lib/validation/schemas"; export async function getSystemSettings(c: Context): Promise { const actions = await import("@/actions/system-config"); @@ -32,7 +33,8 @@ export async function updateSystemSettings(c: Context): Promise { const body = await parseHonoJsonBody(c, SystemSettingsUpdateSchema, { validationErrorCode: (error) => getDiscoveryValidationErrorCode(error.issues) ?? - getReplayCacheTtlValidationErrorCode(error.issues), + getReplayCacheTtlValidationErrorCode(error.issues) ?? + getLegacyHedgeMaxInFlightValidationErrorCode(error.issues), }); if (!body.ok) return body.response; const actions = await import("@/actions/system-config"); diff --git a/src/app/api/v1/resources/system/router.ts b/src/app/api/v1/resources/system/router.ts index b3962aa50..a90048090 100644 --- a/src/app/api/v1/resources/system/router.ts +++ b/src/app/api/v1/resources/system/router.ts @@ -11,6 +11,7 @@ import { } from "@/lib/api/v1/schemas/system-config"; import { getDiscoveryValidationErrorCode } from "@/lib/validation/discovery-settings"; import { getReplayCacheTtlValidationErrorCode } from "@/lib/validation/replay-settings"; +import { getLegacyHedgeMaxInFlightValidationErrorCode } from "@/lib/validation/schemas"; import { getSystemDisplaySettings, getSystemSettings, @@ -25,7 +26,8 @@ export const systemRouter = new OpenAPIHono({ result.error, new URL(c.req.url).pathname, getDiscoveryValidationErrorCode(result.error.issues) ?? - getReplayCacheTtlValidationErrorCode(result.error.issues) + getReplayCacheTtlValidationErrorCode(result.error.issues) ?? + getLegacyHedgeMaxInFlightValidationErrorCode(result.error.issues) ); } }, diff --git a/src/app/v1/_lib/proxy/forwarder.ts b/src/app/v1/_lib/proxy/forwarder.ts index fbe719d21..f70155d35 100644 --- a/src/app/v1/_lib/proxy/forwarder.ts +++ b/src/app/v1/_lib/proxy/forwarder.ts @@ -164,7 +164,19 @@ import { export const DEFAULT_CODEX_USER_AGENT = "codex_cli_rs/0.93.0 (Windows 10.0.26200; x86_64) vscode/1.108.1"; const EMPTY_PREFIX_CHUNK = new Uint8Array(0); -const LEGACY_STREAMING_HEDGE_MAX_CONCURRENCY = 2; +const LEGACY_STREAMING_HEDGE_DEFAULT_MAX_IN_FLIGHT = 2; +const LEGACY_STREAMING_HEDGE_MIN_MAX_IN_FLIGHT = 1; +const LEGACY_STREAMING_HEDGE_MAX_MAX_IN_FLIGHT = 4; +const CLIENT_ABORT_HEALTH_FALLBACK_THRESHOLD_MS = 30_000; + +function clampLegacyHedgeMaxInFlight(value: unknown): number { + const numeric = typeof value === "number" ? value : Number(value); + if (!Number.isFinite(numeric)) return LEGACY_STREAMING_HEDGE_DEFAULT_MAX_IN_FLIGHT; + return Math.min( + LEGACY_STREAMING_HEDGE_MAX_MAX_IN_FLIGHT, + Math.max(LEGACY_STREAMING_HEDGE_MIN_MAX_IN_FLIGHT, Math.floor(numeric)) + ); +} async function runStreamContentGateWithAbortSignals( reader: ReadableStreamDefaultReader, @@ -586,6 +598,23 @@ type StreamingHedgeAttempt = { gateAudit?: ProviderChainItem["streamGate"]; /** 该 attempt 首字节到达时刻(epoch ms);只有赢家的值会被记为 session TTFB。 */ firstByteAt?: number | null; + /** Stable identity for routing trace and per-attempt health attribution. */ + attemptId: string; + /** Monotonic dispatch timestamp used for client-abort threshold comparisons. */ + startedAtMonotonic: number; + /** Monotonic timestamp for the current hedge threshold window, including pre-dispatch setup. */ + thresholdStartedAtMonotonic: number; + /** Effective health-attribution threshold; independent from request timeout behavior. */ + healthAttributionThresholdMs: number; + /** Set immediately when the upstream dispatch starts. */ + dispatched: boolean; + /** Exactly-once guard for provider health/circuit settlement. */ + healthSettlementClaimed: boolean; + healthOutcome: "client_abort_no_first_byte" | "provider_failure" | "other_failure" | null; + healthPausedAtMonotonic: number | null; + healthPausedDurationMs: number; + /** Avoid duplicate saturation events for a threshold trigger. */ + hedgeSaturationRecorded: boolean; /** * Billing context snapshot for the INITIAL provider's losing attempt, captured BEFORE * commitWinner overwrites the shared session's model/context with the winner's. Null for @@ -670,7 +699,8 @@ const NON_STREAM_BODY_INSPECTION_MAX_BYTES = 32 * 1024; // 32 KiB */ async function readResponseTextUpTo( response: Response, - maxBytes: number + maxBytes: number, + onChunk?: (value: Uint8Array) => void ): Promise<{ text: string; truncated: boolean }> { const reader = response.body?.getReader(); if (!reader) { @@ -687,6 +717,7 @@ async function readResponseTextUpTo( const { done, value } = await reader.read(); if (done) break; if (!value || value.byteLength === 0) continue; + onChunk?.(value); const remaining = maxBytes - bytesRead; // 注意:remaining<=0 发生在“已经读到下一块 chunk”之后。 @@ -1648,6 +1679,9 @@ export class ProxyForwarder { 1, discoverySettings.stickyTimeoutCooldownMs ?? 300_000 ), + legacyHedgeMaxInFlight: clampLegacyHedgeMaxInFlight( + discoverySettings.legacyHedgeMaxInFlight + ), sessionTtlSeconds, }, }); @@ -1660,6 +1694,9 @@ export class ProxyForwarder { } const useStreamingHedge = ProxyForwarder.shouldUseStreamingHedge(session); + const legacyHedgeMaxInFlight = clampLegacyHedgeMaxInFlight( + discoverySettings.legacyHedgeMaxInFlight + ); const singleUpstream = discoveryPreparation.reason === "binding_conflict" || discoveryPreparation.reason === "lease_conflict" || @@ -1676,10 +1713,24 @@ export class ProxyForwarder { eligible: false, bypassReason: discoveryPreparation.reason, startedAt: requestStartedAt, + config: { + discoveryConcurrency: Math.max(2, Math.floor(discoverySettings.discoveryConcurrency ?? 2)), + maxDiscoveryRounds: Math.max(1, Math.floor(discoverySettings.maxDiscoveryRounds ?? 2)), + discoverySlaMs: Math.max(1, discoverySettings.discoverySlaMs ?? 10_000), + stickySlaMs: Math.max(1, discoverySettings.stickySlaMs ?? 20_000), + racingTotalTimeoutMs: Math.max(1, discoverySettings.racingTotalTimeoutMs ?? 60_000), + stickyTimeoutCooldownMs: Math.max(1, discoverySettings.stickyTimeoutCooldownMs ?? 300_000), + legacyHedgeMaxInFlight, + sessionTtlSeconds, + }, }); if (useStreamingHedge) { - const hedgePromise = ProxyForwarder.sendStreamingWithHedge(session); + const hedgePromise = ProxyForwarder.sendStreamingWithHedge( + session, + discoverySettings, + legacyHedgeMaxInFlight + ); void hedgePromise.catch(() => undefined); return await hedgePromise; } @@ -1908,6 +1959,11 @@ export class ProxyForwarder { // ========== 内层循环:重试当前供应商(根据配置最多尝试 maxAttemptsPerProvider 次)========== while (attemptCount < maxAttemptsPerProvider) { attemptCount++; + let attemptStartedAtMonotonic = 0; + let attemptFirstByteSeen = false; + let attemptDispatched = false; + let healthPausedAtMonotonic: number | null = null; + let healthPausedDurationMs = 0; // Use currentEndpointIndex for endpoint selection (sticky behavior) // - currentEndpointIndex is advanced only on SYSTEM_ERROR (network errors) @@ -1929,7 +1985,14 @@ export class ProxyForwarder { currentProvider, activeEndpoint.baseUrl, endpointAudit, - attemptCount + attemptCount, + false, + undefined, + () => { + attemptDispatched = true; + attemptStartedAtMonotonic = performance.now(); + attemptFirstByteSeen = false; + } ); // ========== 空响应检测(仅非流式)========== @@ -2001,6 +2064,7 @@ export class ProxyForwarder { // 首字节到达即清除首字节计时器,保持「首字节超时」的原始语义—— // 思考型模型可在首个内容帧前长时间输出中性帧,不应触发该计时器 onFirstByte: () => { + attemptFirstByteSeen = true; gateFirstByteAt ??= Date.now(); runtime.clearResponseTimeout?.(); }, @@ -2008,8 +2072,20 @@ export class ProxyForwarder { idleTimeoutMs: currentProvider.streamingIdleTimeoutMs, captureCommitMarker: !session.isHighConcurrencyModeEnabled(), prebufferBudget: getStreamGatePrebufferBudget(), - onBudgetWaitStart: runtime.pauseResponseTimeout, - onBudgetWaitEnd: runtime.resumeResponseTimeout, + onBudgetWaitStart: () => { + runtime.pauseResponseTimeout?.(); + healthPausedAtMonotonic ??= performance.now(); + }, + onBudgetWaitEnd: () => { + runtime.resumeResponseTimeout?.(); + if (healthPausedAtMonotonic !== null) { + healthPausedDurationMs += Math.max( + 0, + performance.now() - healthPausedAtMonotonic + ); + healthPausedAtMonotonic = null; + } + }, }, [runtime.responseController?.signal, session.clientAbortSignal] ); @@ -2107,6 +2183,15 @@ export class ProxyForwarder { endpointUrl: endpointAudit.endpointUrl, upstreamStatusCode: response.status, bindingIntent: session.isSessionBindingAllowed() ? undefined : "none", + healthAttemptId: `legacy-serial-${totalProvidersAttempted}-${attemptCount}`, + healthAttemptStartedAtMonotonic: attemptStartedAtMonotonic, + healthAttributionThresholdMs: + currentProvider.firstByteTimeoutStreamingMs > 0 + ? currentProvider.firstByteTimeoutStreamingMs + : CLIENT_ABORT_HEALTH_FALLBACK_THRESHOLD_MS, + healthFirstByteSeen: attemptFirstByteSeen, + healthPausedDurationMs, + healthOutcomeSettled: false, }); logger.info("ProxyForwarder: Streaming response received, deferring finalization", { @@ -2185,7 +2270,10 @@ export class ProxyForwarder { const clonedResponse = response.clone(); const inspected = await readResponseTextUpTo( clonedResponse, - NON_STREAM_BODY_INSPECTION_MAX_BYTES + NON_STREAM_BODY_INSPECTION_MAX_BYTES, + (value) => { + if (value.byteLength > 0) attemptFirstByteSeen = true; + } ); inspectedText = inspected.text; inspectedTruncated = inspected.truncated; @@ -2453,12 +2541,61 @@ export class ProxyForwarder { totalProvidersAttempted, }); + const now = performance.now(); + const elapsedMs = Math.max( + 0, + now - + attemptStartedAtMonotonic - + healthPausedDurationMs - + (healthPausedAtMonotonic === null ? 0 : now - healthPausedAtMonotonic) + ); + const thresholdMs = + currentProvider.firstByteTimeoutStreamingMs > 0 + ? currentProvider.firstByteTimeoutStreamingMs + : CLIENT_ABORT_HEALTH_FALLBACK_THRESHOLD_MS; + const qualifiesForHealth = + attemptDispatched && + !attemptFirstByteSeen && + elapsedMs >= thresholdMs && + endpointPolicy.allowCircuitBreakerAccounting; + + if (qualifiesForHealth) { + const abortFailure = new ProxyError( + "Client aborted while provider was waiting for the first byte", + 499, + undefined, + true + ); + await recordFailure(currentProvider.id, abortFailure).catch((healthError) => { + logger.warn("ProxyForwarder: Failed to account serial client abort health", { + providerId: currentProvider.id, + error: healthError instanceof Error ? healthError.message : String(healthError), + }); + }); + session.appendRoutingTraceEvent({ + type: "client_abort_no_first_byte", + attemptId: `legacy-serial-${totalProvidersAttempted}-${attemptCount}`, + provider: { + id: currentProvider.id, + name: currentProvider.name, + priority: currentProvider.priority || 0, + }, + outcome: "provider_failure", + cancellationKind: "client_abort", + reason: "external_client_abort", + effectiveThresholdMs: thresholdMs, + circuitAccountingApplied: true, + availabilityAccountingApplied: true, + durationMs: Math.round(elapsedMs), + }); + } + await ProxyForwarder.clearSessionProviderBinding(session, currentProvider.id); // 记录到决策链(标记为客户端中断) session.addProviderToChain(currentProvider, { ...endpointAudit, - reason: "client_abort", + reason: qualifiesForHealth ? "client_abort_no_first_byte" : "client_abort", circuitState: getCircuitState(currentProvider.id), attemptNumber: attemptCount, errorMessage: "Client aborted request", @@ -3085,7 +3222,8 @@ export class ProxyForwarder { endpointAudit?: { endpointId: number | null; endpointUrl: string }, attemptNumber?: number, deferDetailSnapshotPersistence: boolean = false, - externalAbortSignal?: AbortSignal + externalAbortSignal?: AbortSignal, + onUpstreamDispatch?: () => void ): Promise { if (!provider) { throw new Error("Provider is required"); @@ -3661,6 +3799,10 @@ export class ProxyForwarder { interface UndiciFetchOptions extends RequestInit { dispatcher?: Dispatcher; } + const fetchWithDispatch = async (url: string, requestInit: UndiciFetchOptions) => { + onUpstreamDispatch?.(); + return await fetch(url, requestInit); + }; // ⭐ 双路超时控制(first-byte / total) // 注意:由于 undici fetch API 的限制,无法精确分离 DNS/TCP/TLS 连接阶段和响应头接收阶段 @@ -3878,6 +4020,7 @@ export class ProxyForwarder { const requestBodyJson = decodeRequestBodyAsJson(requestBody); if (requestBodyJson) { + onUpstreamDispatch?.(); const wsResult = await tryResponsesWebsocketUpstream({ provider, upstreamUrl: proxyUrl, @@ -3965,9 +4108,10 @@ export class ProxyForwarder { provider.id, provider.name, session, - deferDetailSnapshotPersistence + deferDetailSnapshotPersistence, + onUpstreamDispatch ) - : await fetch(proxyUrl, init); + : await fetchWithDispatch(proxyUrl, init); // ⭐ fetch 成功:收到 HTTP 响应头,保留响应超时继续监控 // 注意:undici 的 fetch 在收到 HTTP 响应头后就 resolve,但实际数据(SSE 首字节 / 完整 JSON) // 还没到达。responseTimeoutId 需要延续到 response-handler 中才能真正控制"首字节"或"总耗时" @@ -4227,9 +4371,10 @@ export class ProxyForwarder { provider.id, provider.name, session, - deferDetailSnapshotPersistence + deferDetailSnapshotPersistence, + onUpstreamDispatch ) - : await fetch(proxyUrl, http1FallbackInit); + : await fetchWithDispatch(proxyUrl, http1FallbackInit); logger.info("ProxyForwarder: HTTP/1.1 fallback succeeded", { providerId: provider.id, @@ -4304,9 +4449,10 @@ export class ProxyForwarder { provider.id, provider.name, session, - deferDetailSnapshotPersistence + deferDetailSnapshotPersistence, + onUpstreamDispatch ) - : await fetch(proxyUrl, fallbackInit); + : await fetchWithDispatch(proxyUrl, fallbackInit); logger.info("ProxyForwarder: Direct connection succeeded after proxy failure", { providerId: provider.id, providerName: provider.name, @@ -4682,7 +4828,11 @@ export class ProxyForwarder { return resolveEndpointPolicy(policySession.requestUrl?.pathname ?? "/"); } - private static async sendStreamingWithHedge(session: ProxySession): Promise { + private static async sendStreamingWithHedge( + session: ProxySession, + settings: SystemSettings, + maxInFlight: number + ): Promise { const initialProvider = session.provider; if (!initialProvider) { throw new Error("代理上下文缺少供应商"); @@ -4690,7 +4840,7 @@ export class ProxyForwarder { const rawCrossProviderFallbackEnabled = session.isRawCrossProviderFallbackEnabled(); // 竞速输家计费开关:开启时落败供应商不被直接掐断,而是后台 drain 并计费。 - const billHedgeLosers = (await getCachedSystemSettings()).billHedgeLosers === true; + const billHedgeLosers = settings.billHedgeLosers === true; const launchedProviderIds = new Set(); let launchedProviderCount = 0; let settled = false; @@ -4895,6 +5045,38 @@ export class ProxyForwarder { attempt.thresholdRemainingMs = 0; if (settled || attempt.settled || attempt.thresholdTriggered) return; attempt.thresholdTriggered = true; + if (attempts.size >= maxInFlight && !attempt.hedgeSaturationRecorded) { + attempt.hedgeSaturationRecorded = true; + const now = performance.now(); + const thresholdStartedAt = + attempt.startedAtMonotonic > 0 + ? attempt.startedAtMonotonic + : attempt.thresholdStartedAtMonotonic; + const elapsedMs = Math.max( + 0, + Math.round( + now - + thresholdStartedAt - + attempt.healthPausedDurationMs - + (attempt.healthPausedAtMonotonic === null ? 0 : now - attempt.healthPausedAtMonotonic) + ) + ); + session.appendRoutingTraceEvent({ + type: "hedge_slot_saturated", + attemptId: attempt.attemptId, + provider: { + id: attempt.provider.id, + name: attempt.provider.name, + priority: attempt.provider.priority || 0, + }, + outcome: "slot_saturated", + reason: "hedge_threshold", + activeAttemptCount: attempts.size, + configuredCap: maxInFlight, + durationMs: elapsedMs, + elapsedMs, + }); + } session.addProviderToChain(attempt.provider, { ...attempt.endpointAudit, reason: "hedge_triggered", @@ -4929,6 +5111,9 @@ export class ProxyForwarder { attempt.thresholdPaused = false; attempt.thresholdDeadlineAt = null; attempt.thresholdRemainingMs = attempt.firstByteTimeoutMs; + attempt.thresholdStartedAtMonotonic = performance.now(); + attempt.healthPausedAtMonotonic = null; + attempt.healthPausedDurationMs = 0; scheduleAttemptThreshold(attempt); }; @@ -4944,6 +5129,9 @@ export class ProxyForwarder { if (attempt.thresholdDeadlineAt !== null) { attempt.thresholdRemainingMs = Math.max(1, attempt.thresholdDeadlineAt - Date.now()); } + if (attempt.healthPausedAtMonotonic === null) { + attempt.healthPausedAtMonotonic = performance.now(); + } clearTimeout(attempt.thresholdTimer); attempt.thresholdTimer = null; attempt.thresholdDeadlineAt = null; @@ -4952,6 +5140,13 @@ export class ProxyForwarder { const resumeAttemptThreshold = (attempt: StreamingHedgeAttempt) => { if (!attempt.thresholdPaused) return; + if (attempt.healthPausedAtMonotonic !== null) { + attempt.healthPausedDurationMs += Math.max( + 0, + performance.now() - attempt.healthPausedAtMonotonic + ); + attempt.healthPausedAtMonotonic = null; + } attempt.thresholdPaused = false; scheduleAttemptThreshold(attempt); }; @@ -4971,7 +5166,7 @@ export class ProxyForwarder { const launchAlternative = async () => { if (settled || winnerCommitted || noMoreProviders) return; - if (attempts.size >= LEGACY_STREAMING_HEDGE_MAX_CONCURRENCY) return; + if (attempts.size >= maxInFlight) return; if (launchingAlternative) { await launchingAlternative; return; @@ -5022,17 +5217,34 @@ export class ProxyForwarder { const runAttempt = (attempt: StreamingHedgeAttempt) => { const providerForRequest = - attempt.firstByteTimeoutMs > 0 + attempt.firstByteTimeoutMs > 0 && maxInFlight > 1 ? { ...attempt.provider, firstByteTimeoutStreamingMs: 0 } : attempt.provider; + let dispatchMarked = false; + + const markUpstreamDispatch = () => { + if (dispatchMarked) return; + dispatchMarked = true; + attempt.dispatched = true; + attempt.startedAtMonotonic = performance.now(); + attempt.healthPausedAtMonotonic = null; + attempt.healthPausedDurationMs = 0; + armAttemptThreshold(attempt); + }; + // Arm the hedge threshold when the attempt enters the transport call. The health clock + // remains gated by `attempt.dispatched` and is reset by the transport callback below, so + // setup time can trigger a hedge without being eligible for provider-failure attribution. + armAttemptThreshold(attempt); void ProxyForwarder.doForward( attempt.session, providerForRequest, attempt.baseUrl, attempt.endpointAudit, attempt.requestAttemptCount, - true + true, + undefined, + markUpstreamDispatch ) .then(async (response) => { if (settled || winnerCommitted || attempt.settled) { @@ -5171,6 +5383,8 @@ export class ProxyForwarder { return; } + attempt.firstByteAt ??= Date.now(); + // 保留首块:若本 attempt 落败且需要计费,drain 时需要补回首块的 usage。 attempt.billingPrefixChunks = [firstChunk.value]; acceptedAsWinner = await commitWinner(attempt, [firstChunk.value], false); @@ -5236,6 +5450,12 @@ export class ProxyForwarder { } if (settled || winnerCommitted || attempt.settled) return; + // Claim the attempt's terminal race before awaiting asynchronous error classification. If + // the downstream abort arrives while classification is in flight, the upstream error that + // reached this handler first remains authoritative. A rectifier retry below reopens this + // claim for the same logical attempt. + attempt.healthSettlementClaimed = true; + attempt.healthOutcome = "other_failure"; lastError = error; let errorCategory = await categorizeErrorAsync(error); @@ -5387,7 +5607,13 @@ export class ProxyForwarder { attempt.thresholdTimer = null; } attempt.requestAttemptCount += 1; - armAttemptThreshold(attempt); + attempt.dispatched = false; + attempt.startedAtMonotonic = 0; + attempt.firstByteAt = null; + attempt.attemptId = `legacy-hedge-${attempt.sequence}-${attempt.requestAttemptCount}`; + attempt.healthSettlementClaimed = false; + attempt.healthOutcome = null; + attempt.hedgeSaturationRecorded = false; runAttempt(attempt); return; } @@ -5408,6 +5634,8 @@ export class ProxyForwarder { }); } + attempt.healthSettlementClaimed = true; + attempt.healthOutcome = "other_failure"; attempt.settled = true; if (attempt.thresholdTimer) { clearTimeout(attempt.thresholdTimer); @@ -5421,6 +5649,7 @@ export class ProxyForwarder { statusCode !== 404 && !isRequestScopedGateFailure(error) ) { + attempt.healthOutcome = "provider_failure"; await recordFailure(attempt.provider.id, error); } @@ -5700,6 +5929,19 @@ export class ProxyForwarder { clearResponseTimeout: null, firstByteTimeoutMs: provider.firstByteTimeoutStreamingMs > 0 ? provider.firstByteTimeoutStreamingMs : 0, + attemptId: `legacy-hedge-${launchedProviderCount}-1`, + startedAtMonotonic: 0, + thresholdStartedAtMonotonic: 0, + healthAttributionThresholdMs: + provider.firstByteTimeoutStreamingMs > 0 + ? provider.firstByteTimeoutStreamingMs + : CLIENT_ABORT_HEALTH_FALLBACK_THRESHOLD_MS, + dispatched: false, + healthSettlementClaimed: false, + healthOutcome: null, + healthPausedAtMonotonic: null, + healthPausedDurationMs: 0, + hedgeSaturationRecorded: false, sequence: launchedProviderCount, requestAttemptCount: 1, reactiveRectifierRetryState: { @@ -5742,28 +5984,103 @@ export class ProxyForwarder { }); } - armAttemptThreshold(attempt); - runAttempt(attempt); return true; }; + const settleClientAbortHealth = (attempt: StreamingHedgeAttempt): boolean => { + if ( + !attempt.dispatched || + attempt.settled || + attempt.firstByteAt != null || + winnerCommitted || + attempt.healthSettlementClaimed + ) { + return false; + } + + const now = performance.now(); + const elapsedMs = Math.max( + 0, + now - + attempt.startedAtMonotonic - + attempt.healthPausedDurationMs - + (attempt.healthPausedAtMonotonic === null ? 0 : now - attempt.healthPausedAtMonotonic) + ); + if (elapsedMs < attempt.healthAttributionThresholdMs) return false; + + attempt.healthSettlementClaimed = true; + attempt.healthOutcome = "client_abort_no_first_byte"; + const roundedElapsedMs = Math.round(elapsedMs); + const failure = new ProxyError( + "Client aborted while provider was waiting for the first byte", + 499, + undefined, + true + ); + + session.appendRoutingTraceEvent({ + type: "client_abort_no_first_byte", + attemptId: attempt.attemptId, + provider: { + id: attempt.provider.id, + name: attempt.provider.name, + priority: attempt.provider.priority || 0, + }, + outcome: "provider_failure", + cancellationKind: "client_abort", + reason: "external_client_abort", + effectiveThresholdMs: attempt.healthAttributionThresholdMs, + circuitAccountingApplied: true, + availabilityAccountingApplied: true, + durationMs: roundedElapsedMs, + elapsedMs: roundedElapsedMs, + }); + + // Do not inherit the downstream abort signal: health and trace side effects must finish + // independently after the client-facing response has become HTTP 499. + void recordFailure(attempt.provider.id, failure).catch((healthError) => { + logger.warn("ProxyForwarder: Failed to account client abort provider health", { + error: healthError instanceof Error ? healthError.message : String(healthError), + attemptId: attempt.attemptId, + providerId: attempt.provider.id, + }); + }); + return true; + }; + const cleanupClientAbortListener = bindClientAbortListener(session.clientAbortSignal, () => { if (settled || winnerCommitted) return; noMoreProviders = true; lastError = new ProxyError("Request aborted by client", 499, undefined, true); lastErrorCategory = ErrorCategory.CLIENT_ABORT; + const attributedAttempts: StreamingHedgeAttempt[] = []; for (const attempt of Array.from(attempts)) { if (!attempt.settled) { - session.addProviderToChain(attempt.provider, { - ...attempt.endpointAudit, - reason: "client_abort", - attemptNumber: attempt.sequence, - errorMessage: "Client aborted request", - modelRedirect: getAttemptModelRedirect(attempt), - }); + const attributed = settleClientAbortHealth(attempt); + if (!attributed) { + session.addProviderToChain(attempt.provider, { + ...attempt.endpointAudit, + reason: "client_abort", + attemptNumber: attempt.sequence, + errorMessage: "Client aborted request", + modelRedirect: getAttemptModelRedirect(attempt), + }); + } else { + attributedAttempts.push(attempt); + } } } + for (const attempt of attributedAttempts) { + session.addProviderToChain(attempt.provider, { + ...attempt.endpointAudit, + reason: "client_abort_no_first_byte", + attemptNumber: attempt.sequence, + errorMessage: "Client aborted before provider first byte threshold", + circuitState: getCircuitState(attempt.provider.id), + modelRedirect: getAttemptModelRedirect(attempt), + }); + } abortAllAttempts(undefined, "client_abort"); void finishIfExhausted(); }); @@ -8598,7 +8915,8 @@ export class ProxyForwarder { providerId: number, providerName: string, session?: ProxySession, - deferDetailSnapshotPersistence: boolean = false + deferDetailSnapshotPersistence: boolean = false, + onUpstreamDispatch?: () => void ): Promise { const { FETCH_HEADERS_TIMEOUT: headersTimeout, FETCH_BODY_TIMEOUT: bodyTimeout } = getEnvConfig(); @@ -8636,6 +8954,7 @@ export class ProxyForwarder { return undefined; }; + onUpstreamDispatch?.(); const undiciRes = await undiciRequest(url, { method: init.method as string, headers: headersObj, diff --git a/src/app/v1/_lib/proxy/response-handler.ts b/src/app/v1/_lib/proxy/response-handler.ts index 26d34a17f..d2a545bae 100644 --- a/src/app/v1/_lib/proxy/response-handler.ts +++ b/src/app/v1/_lib/proxy/response-handler.ts @@ -1794,6 +1794,8 @@ type FinalizeDeferredStreamingResult = { * @param streamEndedNormally - 必须是 reader 读到 done=true 的“自然结束”;超时/中断等异常结束由其它逻辑处理。 * @param clientAborted - 标记是否为客户端主动中断(用于内部状态码映射,避免把中断记为 200 completed) * @param abortReason - 非自然结束时的原因码(用于内部记录/熔断归因;不会影响客户端响应) + * @param firstByteSeen - Authoritative body-byte observation. Undefined means the caller cannot + * report first-byte state and therefore cannot qualify a no-first-byte attribution. */ function finalizeDeferredStreamingFinalizationIfNeeded( session: ProxySession, @@ -1803,7 +1805,8 @@ function finalizeDeferredStreamingFinalizationIfNeeded( clientAborted: boolean, discoveryLeaseLifecycle: DiscoveryLeaseLifecycle, protocolObservation: StreamProtocolObservation | null, - abortReason?: string + abortReason?: string, + firstByteSeen?: boolean ): FinalizeDeferredStreamingResult { const meta = consumeDeferredStreamingFinalization(session); const provider = session.provider; @@ -2149,6 +2152,48 @@ function finalizeDeferredStreamingFinalizationIfNeeded( return true; })(); + const healthAttributionElapsedMs = + meta?.healthAttemptStartedAtMonotonic == null + ? null + : Math.max( + 0, + (meta.healthAbortAtMonotonic ?? performance.now()) - + meta.healthAttemptStartedAtMonotonic - + (meta.healthPausedDurationMs ?? 0) + ); + const clientAbortNoFirstByte = + !clientAbortCompleteSuccess && + clientAborted && + meta?.healthAttemptId != null && + meta.healthFirstByteSeen !== true && + firstByteSeen === false && + meta.healthAttributionThresholdMs != null && + healthAttributionElapsedMs != null && + healthAttributionElapsedMs >= meta.healthAttributionThresholdMs && + meta.healthOutcomeSettled !== true && + session.getEndpointPolicy().allowCircuitBreakerAccounting; + if (clientAbortNoFirstByte && meta) { + meta.healthOutcomeSettled = true; + const elapsedMs = Math.round(healthAttributionElapsedMs ?? 0); + session.appendRoutingTraceEvent({ + type: "client_abort_no_first_byte", + attemptId: meta.healthAttemptId, + provider: { + id: meta.providerId, + name: meta.providerName, + priority: meta.providerPriority, + }, + outcome: "provider_failure", + cancellationKind: "client_abort", + reason: "external_client_abort", + effectiveThresholdMs: meta.healthAttributionThresholdMs, + circuitAccountingApplied: true, + availabilityAccountingApplied: true, + durationMs: elapsedMs, + elapsedMs, + }); + } + // “内部结算用”的状态码(不会改变客户端实际 HTTP 状态码)。 // - 假 200:优先映射为“推断得到的 4xx/5xx”(未命中则回退 502),确保内部统计/熔断/会话绑定把它当作失败。 // - 未自然结束:也应映射为失败(避免把中断/部分流误记为 200 completed)。 @@ -2324,13 +2369,13 @@ function finalizeDeferredStreamingFinalizationIfNeeded( // 未自然结束:不更新 session 绑定(避免把会话粘到不稳定 provider),但要避免把它误记为 200 completed。 // // 同时,为了让故障转移/熔断能正确工作: - // - 客户端主动中断:不计入熔断器(这通常不是供应商问题) + // - 客户端主动中断:默认不计入熔断器;仅 legacy serial 的静默首字节阈值命中会归因供应商 // - 非客户端中断:计入 provider/endpoint 熔断失败(与 timeout 路径保持一致) if ((clientAborted || !streamEndedNormally) && !clientAbortCompleteSuccess) { session.addProviderToChain(providerForChain, { endpointId: meta.endpointId, endpointUrl: meta.endpointUrl, - reason: "system_error", + reason: clientAbortNoFirstByte ? "client_abort_no_first_byte" : "system_error", attemptNumber: meta.attemptNumber, statusCode: effectiveStatusCode, errorMessage: errorMessage ?? undefined, @@ -2340,7 +2385,10 @@ function finalizeDeferredStreamingFinalizationIfNeeded( try { await clearSessionBinding(); - if (!clientAborted && session.getEndpointPolicy().allowCircuitBreakerAccounting) { + if ( + (!clientAborted || clientAbortNoFirstByte) && + session.getEndpointPolicy().allowCircuitBreakerAccounting + ) { try { const { recordFailure } = await import("@/lib/circuit-breaker"); await recordFailure(meta.providerId, new Error(errorMessage ?? "STREAM_ABORTED")); @@ -3889,6 +3937,7 @@ export class ProxyResponseHandler { const streamTextAccumulator = new BoundedStreamTextAccumulator(); let lastStreamTextSnapshot: BoundedStreamTextSnapshot | null = null; + let passthroughFirstByteSeen = false; let observePassthroughChunk = (_value: Uint8Array) => {}; let observePassthroughReadStart = () => {}; let observePassthroughDrainStart = () => {}; @@ -3910,6 +3959,11 @@ export class ProxyResponseHandler { return; } passthroughClientDetached = true; + const deferredMeta = peekDeferredStreamingFinalization(session); + if (deferredMeta) { + deferredMeta.healthAbortAtMonotonic = performance.now(); + deferredMeta.healthFirstByteSeen = passthroughFirstByteSeen; + } clientAbortMeter?.switchToDetachedMode(); if (!clientAbortMeter) { const rejection = new Error("client_detached_without_metering"); @@ -3963,6 +4017,7 @@ export class ProxyResponseHandler { source: response.body, onReadStart: () => observePassthroughReadStart(), onChunk: (value) => { + if (value.byteLength > 0) passthroughFirstByteSeen = true; const metering = clientAbortMeter?.observe(value); passthroughShadowObserver?.observe(value); streamProtocolObserver?.observe(value); @@ -4251,7 +4306,8 @@ export class ProxyResponseHandler { discoveryLeaseLifecycle, streamProtocolObserver?.finish() ?? (meteringSnapshot ? protocolObservationFromMetering(meteringSnapshot) : null), - abortReason + abortReason, + passthroughFirstByteSeen ); latestCommitSideEffects = finalized.commitSideEffects; latestFinalizeAttemptResources = finalized.finalizeAttemptResources; @@ -4326,7 +4382,8 @@ export class ProxyResponseHandler { clientAborted, discoveryLeaseLifecycle, meteringSnapshot ? protocolObservationFromMetering(meteringSnapshot) : null, - abortReason + abortReason, + passthroughFirstByteSeen ); latestCommitSideEffects = finalized.commitSideEffects; latestFinalizeAttemptResources = finalized.finalizeAttemptResources; @@ -4704,6 +4761,9 @@ export class ProxyResponseHandler { responsePump?.startDrain(reason ?? "client_detached"); return; } + upstreamFirstByteSeenAtAbort = upstreamFirstByteSeen; + const deferredMeta = peekDeferredStreamingFinalization(session); + if (deferredMeta) deferredMeta.healthAbortAtMonotonic = performance.now(); clientDetachHandled = true; clientAbortMeter?.switchToDetachedMode(); const activeReplaySpool = replaySpool && !replaySpool.isTerminal ? replaySpool : null; @@ -4767,6 +4827,8 @@ export class ProxyResponseHandler { // 统计/结算只保留有界的“头 + 尾”文本快照,避免长流式响应把进程堆撑满。 let usageForCost: UsageMetrics | null = null; let isFirstChunk = true; // 标记是否为第一块数据 + let upstreamFirstByteSeen = false; + let upstreamFirstByteSeenAtAbort: boolean | null = null; // 不在首次读取前启动 idle timer(避免与首字节超时职责重叠) // idle timer 仅在首块数据到达后启动,用于检测流中途静默。 @@ -4894,7 +4956,10 @@ export class ProxyResponseHandler { clientAborted, discoveryLeaseLifecycle, streamProtocolObserver?.finish() ?? compactProtocolObservation, - abortReason + abortReason, + clientAborted && upstreamFirstByteSeenAtAbort !== null + ? upstreamFirstByteSeenAtAbort + : upstreamFirstByteSeen ); latestStreamCommitSideEffects = finalized.commitSideEffects ? [finalized.commitSideEffects] @@ -5333,6 +5398,7 @@ export class ProxyResponseHandler { const observeChunk = (value: Uint8Array) => { const chunkSize = value.length; + if (chunkSize > 0) upstreamFirstByteSeen = true; clearIdleTimer(); const metering = clientAbortMeter?.observe(value); AsyncTaskManager.touch(taskId); diff --git a/src/app/v1/_lib/proxy/session.ts b/src/app/v1/_lib/proxy/session.ts index f4062eee7..babac2496 100644 --- a/src/app/v1/_lib/proxy/session.ts +++ b/src/app/v1/_lib/proxy/session.ts @@ -829,6 +829,7 @@ export class ProxySession { | "hedge_loser_cancelled" // 该供应商输掉 Hedge 竞速,请求被取消(未计费) | "hedge_loser_billed" // 该供应商输掉 Hedge 竞速,但其响应被后台拿回并计费 | "client_abort" // 客户端在响应完成前断开连接 + | "client_abort_no_first_byte" // 客户端阈值后断开且供应商未返回首字节 | "affinity_hit"; // 最长前缀亲和命中(软提名,已通过全套硬校验) selectionMethod?: | "session_reuse" @@ -1067,7 +1068,9 @@ export class ProxySession { const resolvedOutcome = outcome ?? (statusCode === 499 - ? "client_abort" + ? this.providerChain.at(-1)?.reason === "client_abort_no_first_byte" + ? "failed" + : "client_abort" : this.routingTraceSummaryDraft?.outcome === "deadline" || this.routingTrace.summary?.outcome === "deadline" ? "deadline" diff --git a/src/app/v1/_lib/proxy/stream-finalization.ts b/src/app/v1/_lib/proxy/stream-finalization.ts index 6f34eb6db..746d5b9b4 100644 --- a/src/app/v1/_lib/proxy/stream-finalization.ts +++ b/src/app/v1/_lib/proxy/stream-finalization.ts @@ -73,6 +73,14 @@ export type DeferredStreamingFinalization = { hedgeBindingHeartbeat?: DeferredStreamingBindingHeartbeat; /** F1 门控提交标记:随成功链条目落库(高并发模式下为空)。 */ streamGate?: ProviderChainItem["streamGate"]; + /** Optional attempt-scoped health attribution metadata for serial streaming requests. */ + healthAttemptId?: string; + healthAttemptStartedAtMonotonic?: number; + healthAttributionThresholdMs?: number; + healthFirstByteSeen?: boolean; + healthAbortAtMonotonic?: number; + healthPausedDurationMs?: number; + healthOutcomeSettled?: boolean; }; const deferredMeta = new WeakMap(); diff --git a/src/drizzle/schema.ts b/src/drizzle/schema.ts index 08088d73d..3e9282ee2 100644 --- a/src/drizzle/schema.ts +++ b/src/drizzle/schema.ts @@ -11,6 +11,7 @@ import { jsonb, index, uniqueIndex, + check, pgEnum, } from 'drizzle-orm/pg-core'; import { relations, sql } from 'drizzle-orm'; @@ -899,6 +900,9 @@ export const systemSettings = pgTable('system_settings', { // 关闭:竞速输家直接取消连接,不计费(旧行为) billHedgeLosers: boolean('bill_hedge_losers').notNull().default(true), + // Maximum number of simultaneously active attempts in the legacy streaming hedge. + legacyHedgeMaxInFlight: integer('legacy_hedge_max_in_flight').notNull().default(2), + // Bounded streaming Discovery (disabled by default until explicitly enabled). discoveryEnabled: boolean('discovery_enabled').notNull().default(false), discoveryConcurrency: integer('discovery_concurrency').notNull().default(2), @@ -1067,7 +1071,12 @@ export const systemSettings = pgTable('system_settings', { createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(), -}); +}, (table) => ({ + legacyHedgeMaxInFlightRange: check( + 'system_settings_legacy_hedge_max_in_flight_range', + sql`${table.legacyHedgeMaxInFlight} >= 1 AND ${table.legacyHedgeMaxInFlight} <= 4` + ), +})); // Notification Settings table - Webhook 通知配置 export const notificationSettings = pgTable('notification_settings', { diff --git a/src/lib/api-client/v1/openapi-types.gen.ts b/src/lib/api-client/v1/openapi-types.gen.ts index 01539443a..a2d7ccee0 100644 --- a/src/lib/api-client/v1/openapi-types.gen.ts +++ b/src/lib/api-client/v1/openapi-types.gen.ts @@ -12139,6 +12139,8 @@ export interface operations { billNonSuccessfulRequests: boolean; /** @description Whether streaming-hedge (provider racing) losers are kept alive, drained, and billed (their cost accumulates into the request total). */ billHedgeLosers: boolean; + /** @description Maximum simultaneously active provider attempts for one legacy streaming hedge request (including the primary attempt). */ + legacyHedgeMaxInFlight: number; /** @description Whether bounded streaming Discovery is enabled. */ discoveryEnabled: boolean; /** @description Maximum number of normal Discovery attempts in the initial batch. */ @@ -12428,6 +12430,8 @@ export interface operations { billNonSuccessfulRequests?: boolean; /** @description Whether streaming-hedge (provider racing) losers are kept alive, drained, and billed (their cost accumulates into the request total). */ billHedgeLosers?: boolean; + /** @description Maximum simultaneously active provider attempts for one legacy streaming hedge request (including the primary attempt). */ + legacyHedgeMaxInFlight?: number; /** @description Whether bounded streaming Discovery is enabled. */ discoveryEnabled?: boolean; /** @description Maximum number of normal Discovery attempts in the initial batch. */ @@ -12590,6 +12594,8 @@ export interface operations { billNonSuccessfulRequests: boolean; /** @description Whether streaming-hedge (provider racing) losers are kept alive, drained, and billed (their cost accumulates into the request total). */ billHedgeLosers: boolean; + /** @description Maximum simultaneously active provider attempts for one legacy streaming hedge request (including the primary attempt). */ + legacyHedgeMaxInFlight: number; /** @description Whether bounded streaming Discovery is enabled. */ discoveryEnabled: boolean; /** @description Maximum number of normal Discovery attempts in the initial batch. */ diff --git a/src/lib/api/v1/schemas/system-config.ts b/src/lib/api/v1/schemas/system-config.ts index 944e4cab2..3c38a215c 100644 --- a/src/lib/api/v1/schemas/system-config.ts +++ b/src/lib/api/v1/schemas/system-config.ts @@ -106,6 +106,14 @@ export const SystemSettingsSchema = z .describe( "Whether streaming-hedge (provider racing) losers are kept alive, drained, and billed (their cost accumulates into the request total)." ), + legacyHedgeMaxInFlight: z + .number() + .int() + .min(1) + .max(4) + .describe( + "Maximum simultaneously active provider attempts for one legacy streaming hedge request (including the primary attempt)." + ), discoveryEnabled: z.boolean().describe("Whether bounded streaming Discovery is enabled."), discoveryConcurrency: z .number() diff --git a/src/lib/config/system-settings-cache.ts b/src/lib/config/system-settings-cache.ts index 770877194..15d814c03 100644 --- a/src/lib/config/system-settings-cache.ts +++ b/src/lib/config/system-settings-cache.ts @@ -127,6 +127,7 @@ export const DEFAULT_SETTINGS: Pick< | "stickySlaMs" | "racingTotalTimeoutMs" | "stickyTimeoutCooldownMs" + | "legacyHedgeMaxInFlight" > = { enableHttp2: false, enableOpenaiResponsesWebsocket: true, @@ -167,6 +168,7 @@ export const DEFAULT_SETTINGS: Pick< stickySlaMs: 20_000, racingTotalTimeoutMs: 60_000, stickyTimeoutCooldownMs: 300_000, + legacyHedgeMaxInFlight: 2, }; /** @@ -227,6 +229,7 @@ export async function getCachedSystemSettings(): Promise { codexPriorityBillingSource: DEFAULT_SETTINGS.codexPriorityBillingSource, billNonSuccessfulRequests: false, billHedgeLosers: true, + legacyHedgeMaxInFlight: DEFAULT_SETTINGS.legacyHedgeMaxInFlight, timezone: null, verboseProviderError: false, passThroughUpstreamErrorMessage: DEFAULT_SETTINGS.passThroughUpstreamErrorMessage, diff --git a/src/lib/langfuse/trace-proxy-request.test.ts b/src/lib/langfuse/trace-proxy-request.test.ts index 43a842c0b..60ce55c06 100644 --- a/src/lib/langfuse/trace-proxy-request.test.ts +++ b/src/lib/langfuse/trace-proxy-request.test.ts @@ -26,6 +26,7 @@ const ERROR_REASONS = new Set([ "vendor_type_all_timeout", "endpoint_pool_exhausted", "client_abort", + "client_abort_no_first_byte", ]); function isSuccessReason(reason: string | undefined): boolean { @@ -71,6 +72,10 @@ describe("isErrorReason", () => { expect(isErrorReason("client_abort")).toBe(true); }); + test("client_abort_no_first_byte is an error reason", () => { + expect(isErrorReason("client_abort_no_first_byte")).toBe(true); + }); + test("system_error is an error reason", () => { expect(isErrorReason("system_error")).toBe(true); }); diff --git a/src/lib/langfuse/trace-proxy-request.ts b/src/lib/langfuse/trace-proxy-request.ts index 3fb1aa237..4d63c84e1 100644 --- a/src/lib/langfuse/trace-proxy-request.ts +++ b/src/lib/langfuse/trace-proxy-request.ts @@ -85,6 +85,7 @@ const ERROR_REASONS = new Set([ "vendor_type_all_timeout", "endpoint_pool_exhausted", "client_abort", + "client_abort_no_first_byte", ]); function isErrorReason(reason: string | undefined): boolean { diff --git a/src/lib/ledger-backfill/trigger.sql b/src/lib/ledger-backfill/trigger.sql index bbe051782..876ee55e7 100644 --- a/src/lib/ledger-backfill/trigger.sql +++ b/src/lib/ledger-backfill/trigger.sql @@ -44,7 +44,8 @@ BEGIN 'hedge_winner', 'hedge_loser_cancelled', 'hedge_loser_billed', - 'client_abort' + 'client_abort', + 'client_abort_no_first_byte' ) OR last_status_code IS NOT NULL OR COALESCE(last_error_message, '') <> '' THEN @@ -96,7 +97,9 @@ BEGIN RETURN 'excluded'; END IF; - IF COALESCE(last_status_code, status_code) IN (404, 499) THEN + IF COALESCE(last_status_code, status_code) = 404 + OR (COALESCE(last_status_code, status_code) = 499 + AND last_reason IS DISTINCT FROM 'client_abort_no_first_byte') THEN RETURN 'excluded'; END IF; diff --git a/src/lib/redis/live-chain-store.ts b/src/lib/redis/live-chain-store.ts index 1a3f89a75..a58fcd123 100644 --- a/src/lib/redis/live-chain-store.ts +++ b/src/lib/redis/live-chain-store.ts @@ -151,6 +151,7 @@ function deriveLegacyActiveProviders(chain: ProviderChainItem[]): LiveProviderSn case "hedge_loser_cancelled": case "hedge_loser_billed": case "client_abort": + case "client_abort_no_first_byte": activeProviders.delete(item.id); break; } @@ -192,6 +193,8 @@ export function inferPhase( return "streaming"; case "client_abort": return "aborted"; + case "client_abort_no_first_byte": + return "failed"; default: return "forwarding"; } diff --git a/src/lib/request-outcome.ts b/src/lib/request-outcome.ts index ae44378d2..e23106923 100644 --- a/src/lib/request-outcome.ts +++ b/src/lib/request-outcome.ts @@ -135,7 +135,10 @@ export function classifyRequestOutcomeSignal( return buildExcludedTaxonomy("matched_rule"); } - if (signal.statusCode === 499 || signal.reason === "client_abort") { + if ( + (signal.statusCode === 499 && signal.reason !== "client_abort_no_first_byte") || + signal.reason === "client_abort" + ) { return buildExcludedTaxonomy("client_abort"); } diff --git a/src/lib/utils/provider-chain-formatter.ts b/src/lib/utils/provider-chain-formatter.ts index af8240013..c41191084 100644 --- a/src/lib/utils/provider-chain-formatter.ts +++ b/src/lib/utils/provider-chain-formatter.ts @@ -110,7 +110,8 @@ function getProviderStatus(item: ProviderChainItem): "✓" | "✗" | "⚡" | " item.reason === "client_error_non_retryable" || item.reason === "endpoint_pool_exhausted" || item.reason === "vendor_type_all_timeout" || - item.reason === "client_abort" + item.reason === "client_abort" || + item.reason === "client_abort_no_first_byte" ) { return "✗"; } @@ -153,7 +154,8 @@ export function isActualRequest(item: ProviderChainItem): boolean { item.reason === "client_error_non_retryable" || item.reason === "endpoint_pool_exhausted" || item.reason === "vendor_type_all_timeout" || - item.reason === "client_abort" + item.reason === "client_abort" || + item.reason === "client_abort_no_first_byte" ) { return true; } diff --git a/src/lib/validation/schemas.ts b/src/lib/validation/schemas.ts index 80ecdb9dd..e06dde3e2 100644 --- a/src/lib/validation/schemas.ts +++ b/src/lib/validation/schemas.ts @@ -32,6 +32,20 @@ export { DISCOVERY_WINDOW_INVALID_ERROR_CODE, } from "@/lib/validation/discovery-settings"; +export const LEGACY_HEDGE_MAX_IN_FLIGHT_INVALID_ERROR_CODE = "LEGACY_HEDGE_MAX_IN_FLIGHT_INVALID"; + +export function getLegacyHedgeMaxInFlightValidationErrorCode( + issues: ReadonlyArray<{ message: string; path: readonly PropertyKey[] }> +): string | undefined { + return issues.some( + (issue) => + issue.path[0] === "legacyHedgeMaxInFlight" || + issue.message === LEGACY_HEDGE_MAX_IN_FLIGHT_INVALID_ERROR_CODE + ) + ? LEGACY_HEDGE_MAX_IN_FLIGHT_INVALID_ERROR_CODE + : undefined; +} + const CACHE_TTL_PREFERENCE = z.enum(["inherit", "5m", "1h"]); const CONTEXT_1M_PREFERENCE = z.enum(["inherit", "force_enable", "disabled"]); @@ -1022,6 +1036,16 @@ export const UpdateSystemSettingsSchema = z billNonSuccessfulRequests: z.boolean().optional(), // 供应商竞速输家计费(可选;默认开启) billHedgeLosers: z.boolean().optional(), + // Legacy streaming hedge concurrency cap (inclusive of the primary attempt). + legacyHedgeMaxInFlight: z.preprocess( + (value) => (typeof value === "string" && value.trim() !== "" ? Number(value.trim()) : value), + z + .number(LEGACY_HEDGE_MAX_IN_FLIGHT_INVALID_ERROR_CODE) + .int(LEGACY_HEDGE_MAX_IN_FLIGHT_INVALID_ERROR_CODE) + .min(1, LEGACY_HEDGE_MAX_IN_FLIGHT_INVALID_ERROR_CODE) + .max(4, LEGACY_HEDGE_MAX_IN_FLIGHT_INVALID_ERROR_CODE) + .optional() + ), // Bounded streaming Discovery(默认关闭;启用前需满足总窗口约束) discoveryEnabled: z.boolean().optional(), discoveryConcurrency: z.coerce diff --git a/src/repository/_shared/transformers.test.ts b/src/repository/_shared/transformers.test.ts index 460917b8e..935efc24c 100644 --- a/src/repository/_shared/transformers.test.ts +++ b/src/repository/_shared/transformers.test.ts @@ -300,6 +300,16 @@ describe("src/repository/_shared/transformers.ts", () => { expect(toSystemSettings({ replayCacheTtlMinutes: 45 }).replayCacheTtlMinutes).toBe(45); }); + it.each([1, 2, 4])("应保留有效 legacy hedge 并发上限 %s", (value) => { + expect(toSystemSettings({ legacyHedgeMaxInFlight: value }).legacyHedgeMaxInFlight).toBe( + value + ); + }); + + it.each([0, 5, 1.5, "3", null])("应将无效 legacy hedge 并发上限 %s 回退为 2", (value) => { + expect(toSystemSettings({ legacyHedgeMaxInFlight: value }).legacyHedgeMaxInFlight).toBe(2); + }); + it.each([5, 120])("应保留 Replay 缓存时间的有效边界 %s", (value) => { expect(toSystemSettings({ replayCacheTtlMinutes: value }).replayCacheTtlMinutes).toBe(value); }); diff --git a/src/repository/_shared/transformers.ts b/src/repository/_shared/transformers.ts index b49454fd8..5a53d2c0a 100644 --- a/src/repository/_shared/transformers.ts +++ b/src/repository/_shared/transformers.ts @@ -257,6 +257,13 @@ export function toSystemSettings(dbSettings: any): SystemSettings { replayCacheTtlMinutes <= REPLAY_CACHE_TTL_MINUTES_MAX ? replayCacheTtlMinutes : REPLAY_CACHE_TTL_MINUTES_DEFAULT; + const legacyHedgeMaxInFlight = + typeof dbSettings?.legacyHedgeMaxInFlight === "number" && + Number.isInteger(dbSettings.legacyHedgeMaxInFlight) && + dbSettings.legacyHedgeMaxInFlight >= 1 && + dbSettings.legacyHedgeMaxInFlight <= 4 + ? dbSettings.legacyHedgeMaxInFlight + : 2; return { id: dbSettings?.id ?? 0, @@ -271,6 +278,7 @@ export function toSystemSettings(dbSettings: any): SystemSettings { : "requested", billNonSuccessfulRequests: dbSettings?.billNonSuccessfulRequests ?? false, billHedgeLosers: dbSettings?.billHedgeLosers ?? true, + legacyHedgeMaxInFlight, timezone: dbSettings?.timezone ?? null, enableAutoCleanup: dbSettings?.enableAutoCleanup ?? false, cleanupRetentionDays: dbSettings?.cleanupRetentionDays ?? 30, diff --git a/src/repository/_shared/usage-log-filters.ts b/src/repository/_shared/usage-log-filters.ts index 2ae3e9c8c..6f5089052 100644 --- a/src/repository/_shared/usage-log-filters.ts +++ b/src/repository/_shared/usage-log-filters.ts @@ -122,6 +122,7 @@ export const RETRY_COUNT_EXPR: SQL = sql`( 'endpoint_pool_exhausted', 'vendor_type_all_timeout', 'client_abort', + 'client_abort_no_first_byte', 'http2_fallback' ) OR ( diff --git a/src/repository/system-config.ts b/src/repository/system-config.ts index c26d8fcba..22e7a9f98 100644 --- a/src/repository/system-config.ts +++ b/src/repository/system-config.ts @@ -151,6 +151,7 @@ function createFallbackSettings(): SystemSettings { codexPriorityBillingSource: "requested", billNonSuccessfulRequests: false, billHedgeLosers: true, + legacyHedgeMaxInFlight: 2, timezone: null, enableAutoCleanup: false, cleanupRetentionDays: 30, @@ -284,6 +285,12 @@ const RECENT_COLUMN_LADDER: ReadonlyArray<{ // 本层更新失败(仍有列缺失)时记录的告警 updateWarn: string; }> = [ + { + key: "legacyHedgeMaxInFlight", + column: systemSettings.legacyHedgeMaxInFlight, + selectWarn: "system_settings 缺少 legacyHedgeMaxInFlight,回退到上一代字段集。", + updateWarn: "system_settings 缺少 legacyHedgeMaxInFlight,继续降级更新。", + }, { key: "replayCacheTtlMinutes", column: systemSettings.replayCacheTtlMinutes, @@ -414,6 +421,7 @@ const RECENT_COLUMN_LADDER: ReadonlyArray<{ // 历史世代字段集(冻结):passThrough 世代之前的 schema 没有以下五列。 // 注意:世代字段集相对近代阶梯末层会重新选取更晚引入的列(与历史实现一致)。 const PASS_THROUGH_ERA_OMIT: readonly string[] = [ + "legacyHedgeMaxInFlight", "billHedgeLosers", "billNonSuccessfulRequests", "passThroughUpstreamErrorMessage", @@ -715,6 +723,10 @@ export async function updateSystemSettings( updates.billHedgeLosers = payload.billHedgeLosers; } + if (payload.legacyHedgeMaxInFlight !== undefined) { + updates.legacyHedgeMaxInFlight = payload.legacyHedgeMaxInFlight; + } + if (payload.discoveryEnabled !== undefined) { updates.discoveryEnabled = payload.discoveryEnabled; } diff --git a/src/types/message.ts b/src/types/message.ts index ea932916c..4f0d11018 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -50,6 +50,7 @@ export interface ProviderChainItem { | "hedge_loser_cancelled" // 该供应商输掉 Hedge 竞速,请求被取消(未对输家计费) | "hedge_loser_billed" // 该供应商输掉 Hedge 竞速,但其上游响应被后台拿回并计费 | "client_abort" // 客户端在响应完成前断开连接 + | "client_abort_no_first_byte" // 客户端在阈值后断开且该供应商未返回首字节 | "affinity_hit"; // 最长前缀亲和命中(软提名,已通过全套硬校验) // === 选择方法(细化) === diff --git a/src/types/routing-trace.ts b/src/types/routing-trace.ts index 665b020f1..9d68f6fa4 100644 --- a/src/types/routing-trace.ts +++ b/src/types/routing-trace.ts @@ -15,6 +15,8 @@ export type RoutingTraceEventType = | "fallback_promoted" | "winner_committed" | "binding_finalized" + | "hedge_slot_saturated" + | "client_abort_no_first_byte" | "request_finished"; export type RoutingTraceAttemptKind = "sticky" | "normal" | "fallback"; @@ -30,6 +32,7 @@ export interface RoutingTraceConfigV1 { stickyTimeoutCooldownMs: number; /** The binding/session TTL in seconds; optional for traces written before this field existed. */ sessionTtlSeconds?: number; + legacyHedgeMaxInFlight?: number; } export interface RoutingTraceProviderV1 { @@ -52,6 +55,11 @@ export interface RoutingTraceEventV1 { provider?: RoutingTraceProviderV1; outcome?: string; cancellationKind?: string; + effectiveThresholdMs?: number; + activeAttemptCount?: number; + configuredCap?: number; + circuitAccountingApplied?: boolean; + availabilityAccountingApplied?: boolean; statusCode?: number; reason?: string; bindingAction?: "create" | "renew" | "clear" | "none"; @@ -106,6 +114,8 @@ const ROUTING_TRACE_EVENT_TYPES = new Set([ "fallback_promoted", "winner_committed", "binding_finalized", + "hedge_slot_saturated", + "client_abort_no_first_byte", "request_finished", ]); @@ -166,6 +176,21 @@ function normalizeRoutingTraceEvent(value: unknown): RoutingTraceEventV1 | null ...(nonEmptyString(event.cancellationKind) ? { cancellationKind: event.cancellationKind as string } : {}), + ...(finiteNumber(event.effectiveThresholdMs) !== undefined + ? { effectiveThresholdMs: event.effectiveThresholdMs as number } + : {}), + ...(finiteNumber(event.activeAttemptCount) !== undefined + ? { activeAttemptCount: event.activeAttemptCount as number } + : {}), + ...(finiteNumber(event.configuredCap) !== undefined + ? { configuredCap: event.configuredCap as number } + : {}), + ...(typeof event.circuitAccountingApplied === "boolean" + ? { circuitAccountingApplied: event.circuitAccountingApplied } + : {}), + ...(typeof event.availabilityAccountingApplied === "boolean" + ? { availabilityAccountingApplied: event.availabilityAccountingApplied } + : {}), ...(finiteNumber(event.statusCode) !== undefined ? { statusCode: event.statusCode as number } : {}), @@ -191,6 +216,7 @@ function normalizeRoutingTraceConfig(value: unknown): RoutingTraceConfigV1 | und const racingTotalTimeoutMs = finiteNumber(config.racingTotalTimeoutMs); const stickyTimeoutCooldownMs = finiteNumber(config.stickyTimeoutCooldownMs); const sessionTtlSeconds = finiteNumber(config.sessionTtlSeconds); + const legacyHedgeMaxInFlight = finiteNumber(config.legacyHedgeMaxInFlight); if ( discoveryConcurrency === undefined || maxDiscoveryRounds === undefined || @@ -209,6 +235,7 @@ function normalizeRoutingTraceConfig(value: unknown): RoutingTraceConfigV1 | und racingTotalTimeoutMs, stickyTimeoutCooldownMs, ...(sessionTtlSeconds !== undefined ? { sessionTtlSeconds } : {}), + ...(legacyHedgeMaxInFlight !== undefined ? { legacyHedgeMaxInFlight } : {}), }; } diff --git a/src/types/system-config.ts b/src/types/system-config.ts index a1eea2b3a..693845799 100644 --- a/src/types/system-config.ts +++ b/src/types/system-config.ts @@ -51,6 +51,9 @@ export interface SystemSettings { // 其费用异步累加进该请求的总花费(与上游对多个供应商分别计费保持一致)。 billHedgeLosers: boolean; + // Legacy streaming hedge concurrency cap (includes the primary attempt). + legacyHedgeMaxInFlight: number; + // 系统时区配置 (IANA timezone identifier) // 用于统一后端时间边界计算和前端日期/时间显示 // null 表示使用环境变量 TZ 或默认 UTC @@ -201,6 +204,9 @@ export interface UpdateSystemSettingsInput { // 供应商竞速输家计费(可选) billHedgeLosers?: boolean; + // Legacy streaming hedge concurrency cap (includes the primary attempt). + legacyHedgeMaxInFlight?: number; + discoveryEnabled?: boolean; discoveryConcurrency?: number; maxDiscoveryRounds?: number; diff --git a/tests/api/v1/system/system-config.test.ts b/tests/api/v1/system/system-config.test.ts index 440c23239..487139755 100644 --- a/tests/api/v1/system/system-config.test.ts +++ b/tests/api/v1/system/system-config.test.ts @@ -42,6 +42,7 @@ const settings: SystemSettings = { currencyDisplay: "USD", billingModelSource: "original", codexPriorityBillingSource: "requested", + legacyHedgeMaxInFlight: 2, timezone: "Asia/Shanghai", enableAutoCleanup: false, cleanupRetentionDays: 30, @@ -225,6 +226,23 @@ describe("v1 system config endpoints", () => { expect(saveSystemSettingsMock).not.toHaveBeenCalled(); }); + test("returns a stable error code for invalid legacy hedge concurrency", async () => { + for (const value of [0, 5, true, [2]]) { + const invalid = await callV1Route({ + method: "PUT", + pathname: "/api/v1/system/settings", + headers: { Authorization: "Bearer admin-token" }, + body: { legacyHedgeMaxInFlight: value }, + }); + + expect(invalid.response.status).toBe(400); + expect(invalid.json).toMatchObject({ + errorCode: "LEGACY_HEDGE_MAX_IN_FLIGHT_INVALID", + }); + } + expect(saveSystemSettingsMock).not.toHaveBeenCalled(); + }); + test("rejects malformed and non-json system settings update bodies", async () => { const handlers = await import("@/app/api/v1/resources/system/handlers"); const malformed = await handlers.updateSystemSettings({ diff --git a/tests/integration/billing-model-source.test.ts b/tests/integration/billing-model-source.test.ts index f8576f970..e8b1db5cd 100644 --- a/tests/integration/billing-model-source.test.ts +++ b/tests/integration/billing-model-source.test.ts @@ -146,6 +146,7 @@ function makeSystemSettings( currencyDisplay: "USD", billingModelSource, codexPriorityBillingSource, + legacyHedgeMaxInFlight: 2, timezone: null, enableAutoCleanup: false, cleanupRetentionDays: 30, diff --git a/tests/unit/api/admin-system-config-route.test.ts b/tests/unit/api/admin-system-config-route.test.ts index d566c8699..873d19dcf 100644 --- a/tests/unit/api/admin-system-config-route.test.ts +++ b/tests/unit/api/admin-system-config-route.test.ts @@ -103,4 +103,24 @@ describe("POST /api/admin/system-config", () => { }); expect(mocks.updateSystemSettings).not.toHaveBeenCalled(); }); + + it.each([[0], [5], [true], [[2]]])( + "returns a stable error for invalid legacy hedge concurrency %s", + async (value) => { + const { POST } = await import("@/app/api/admin/system-config/route"); + const response = await POST( + new Request("http://localhost/api/admin/system-config", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ legacyHedgeMaxInFlight: value }), + }) + ); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toEqual({ + error: "LEGACY_HEDGE_MAX_IN_FLIGHT_INVALID", + }); + expect(mocks.updateSystemSettings).not.toHaveBeenCalled(); + } + ); }); diff --git a/tests/unit/lib/config/system-settings-cache.test.ts b/tests/unit/lib/config/system-settings-cache.test.ts index 2fc12998c..ce852000a 100644 --- a/tests/unit/lib/config/system-settings-cache.test.ts +++ b/tests/unit/lib/config/system-settings-cache.test.ts @@ -47,6 +47,7 @@ function createSettings(overrides: Partial = {}): SystemSettings currencyDisplay: "USD", billingModelSource: "original", codexPriorityBillingSource: "requested", + legacyHedgeMaxInFlight: 2, timezone: null, enableAutoCleanup: false, cleanupRetentionDays: 30, diff --git a/tests/unit/lib/request-outcome.test.ts b/tests/unit/lib/request-outcome.test.ts index a1c1afb2e..735751c08 100644 --- a/tests/unit/lib/request-outcome.test.ts +++ b/tests/unit/lib/request-outcome.test.ts @@ -22,6 +22,20 @@ describe("request outcome taxonomy", () => { }); }); + it("counts thresholded no-first-byte client aborts as provider failures", () => { + expect( + classifyRequestOutcomeSignal({ + reason: "client_abort_no_first_byte", + statusCode: 499, + errorMessage: "Client aborted before provider first byte threshold", + }) + ).toMatchObject({ + outcome: "failure", + locus: "upstream", + countability: "countable", + }); + }); + it("excludes both cancelled and billed hedge losers from success-rate", () => { expect( classifyRequestOutcomeSignal({ diff --git a/tests/unit/proxy/proxy-forwarder-hedge-first-byte.test.ts b/tests/unit/proxy/proxy-forwarder-hedge-first-byte.test.ts index 5301c56ce..36f4a5733 100644 --- a/tests/unit/proxy/proxy-forwarder-hedge-first-byte.test.ts +++ b/tests/unit/proxy/proxy-forwarder-hedge-first-byte.test.ts @@ -1823,6 +1823,202 @@ describe("ProxyForwarder - first-byte hedge scheduling", () => { } }); + test("legacy serial client abort after the health threshold records one provider failure", async () => { + vi.useFakeTimers(); + + try { + const provider = createProvider({ id: 1, name: "p1", firstByteTimeoutStreamingMs: 0 }); + const clientAbortController = new AbortController(); + const session = createSession(clientAbortController.signal); + setProviderWithSessionRef(session, provider); + mocks.categorizeErrorAsync.mockResolvedValueOnce(ProxyErrorCategory.CLIENT_ABORT); + + const doForward = vi.spyOn( + ProxyForwarder as unknown as { + doForward: (...args: unknown[]) => Promise; + }, + "doForward" + ); + doForward.mockImplementationOnce(async (...args) => { + const runtime = args[0] as ProxySession & AttemptRuntime; + runtime.clearResponseTimeout = vi.fn(); + (args[7] as () => void)(); + return await new Promise((_, reject) => { + setTimeout(() => { + clientAbortController.abort(new Error("client_cancelled")); + reject(new UpstreamProxyError("Request aborted by client", 499, undefined, true)); + }, 30_000); + }); + }); + + const responsePromise = ProxyForwarder.send(session); + const rejection = expect(responsePromise).rejects.toMatchObject({ statusCode: 499 }); + await vi.advanceTimersByTimeAsync(30_000); + await rejection; + expect(mocks.recordFailure).toHaveBeenCalledTimes(1); + expect(mocks.recordFailure).toHaveBeenCalledWith(provider.id, expect.any(Error)); + expect( + session + .getRoutingTrace() + ?.events.some((event) => event.type === "client_abort_no_first_byte") + ).toBe(true); + expect( + session.getProviderChain().some((item) => item.reason === "client_abort_no_first_byte") + ).toBe(true); + } finally { + vi.useRealTimers(); + } + }); + + test("legacy hedge cap one preserves serial timeout fallback and records saturation", async () => { + vi.useFakeTimers(); + + try { + const provider1 = createProvider({ id: 1, name: "p1", firstByteTimeoutStreamingMs: 100 }); + const provider2 = createProvider({ id: 2, name: "p2", firstByteTimeoutStreamingMs: 100 }); + const session = createSession(); + setProviderWithSessionRef(session, provider1); + mocks.getCachedSystemSettings.mockResolvedValue({ + billHedgeLosers: false, + legacyHedgeMaxInFlight: 1, + enableThinkingSignatureRectifier: true, + enableThinkingBudgetRectifier: true, + }); + mocks.pickRandomProviderWithExclusion.mockResolvedValueOnce(provider2); + + const doForward = vi.spyOn( + ProxyForwarder as unknown as { + doForward: (...args: unknown[]) => Promise; + }, + "doForward" + ); + const controller1 = new AbortController(); + const controller2 = new AbortController(); + doForward.mockImplementationOnce(async (attemptSession, providerForRequest, ...args) => { + const runtime = attemptSession as ProxySession & AttemptRuntime; + runtime.responseController = controller1; + runtime.clearResponseTimeout = vi.fn(); + (args[5] as (() => void) | undefined)?.(); + expect((providerForRequest as Provider).firstByteTimeoutStreamingMs).toBe(100); + return createDelayedFailure({ + delayMs: 100, + error: new Error("p1 timed out"), + controller: controller1, + }); + }); + doForward.mockImplementationOnce(async (attemptSession) => { + const runtime = attemptSession as ProxySession & AttemptRuntime; + runtime.responseController = controller2; + runtime.clearResponseTimeout = vi.fn(); + return createStreamingResponse({ + label: "p2", + firstChunkDelayMs: 10, + controller: controller2, + }); + }); + + const responsePromise = ProxyForwarder.send(session); + await vi.advanceTimersByTimeAsync(100); + expect(doForward).toHaveBeenCalledTimes(2); + await vi.advanceTimersByTimeAsync(10); + const response = await responsePromise; + expect(await response.text()).toContain('"provider":"p2"'); + expect(session.getRoutingTrace()?.mode).toBe("legacy_hedge"); + expect(session.getRoutingTrace()?.config?.legacyHedgeMaxInFlight).toBe(1); + expect( + session.getRoutingTrace()?.events.filter((event) => event.type === "hedge_slot_saturated") + ).toEqual([ + expect.objectContaining({ + activeAttemptCount: 1, + configuredCap: 1, + }), + ]); + expect(mocks.recordFailure).toHaveBeenCalledWith(1, expect.any(Error)); + } finally { + vi.useRealTimers(); + } + }); + + test("legacy hedge cap three launches a third candidate on the next threshold", async () => { + vi.useFakeTimers(); + + try { + const provider1 = createProvider({ id: 1, name: "p1", firstByteTimeoutStreamingMs: 100 }); + const provider2 = createProvider({ id: 2, name: "p2", firstByteTimeoutStreamingMs: 100 }); + const provider3 = createProvider({ id: 3, name: "p3", firstByteTimeoutStreamingMs: 100 }); + const session = createSession(); + setProviderWithSessionRef(session, provider1); + mocks.getCachedSystemSettings.mockResolvedValue({ + billHedgeLosers: false, + legacyHedgeMaxInFlight: 3, + enableThinkingSignatureRectifier: true, + enableThinkingBudgetRectifier: true, + }); + mocks.pickRandomProviderWithExclusion + .mockResolvedValueOnce(provider2) + .mockResolvedValueOnce(provider3); + + const doForward = vi.spyOn( + ProxyForwarder as unknown as { + doForward: (...args: unknown[]) => Promise; + }, + "doForward" + ); + const controller1 = new AbortController(); + const controller2 = new AbortController(); + const controller3 = new AbortController(); + doForward + .mockImplementationOnce(async (attemptSession) => { + const runtime = attemptSession as ProxySession & AttemptRuntime; + runtime.responseController = controller1; + runtime.clearResponseTimeout = vi.fn(); + return createStreamingResponse({ + label: "p1", + firstChunkDelayMs: 1000, + controller: controller1, + }); + }) + .mockImplementationOnce(async (attemptSession) => { + const runtime = attemptSession as ProxySession & AttemptRuntime; + runtime.responseController = controller2; + runtime.clearResponseTimeout = vi.fn(); + return createStreamingResponse({ + label: "p2", + firstChunkDelayMs: 1000, + controller: controller2, + }); + }) + .mockImplementationOnce(async (attemptSession) => { + const runtime = attemptSession as ProxySession & AttemptRuntime; + runtime.responseController = controller3; + runtime.clearResponseTimeout = vi.fn(); + return createStreamingResponse({ + label: "p3", + firstChunkDelayMs: 10, + controller: controller3, + }); + }); + + const responsePromise = ProxyForwarder.send(session); + await vi.advanceTimersByTimeAsync(100); + expect(doForward).toHaveBeenCalledTimes(2); + await vi.advanceTimersByTimeAsync(100); + expect(doForward).toHaveBeenCalledTimes(3); + await vi.advanceTimersByTimeAsync(10); + const response = await responsePromise; + expect(await response.text()).toContain('"provider":"p3"'); + expect(controller1.signal.aborted).toBe(true); + expect(controller2.signal.aborted).toBe(true); + expect(controller3.signal.aborted).toBe(false); + expect(session.getRoutingTrace()?.config?.legacyHedgeMaxInFlight).toBe(3); + expect( + session.getRoutingTrace()?.events.some((event) => event.type === "hedge_slot_saturated") + ).toBe(false); + } finally { + vi.useRealTimers(); + } + }); + test("client abort before any winner should abort all in-flight attempts, return 499, and clear sticky provider binding", async () => { vi.useFakeTimers(); @@ -1870,10 +2066,11 @@ describe("ProxyForwarder - first-byte hedge scheduling", () => { const controller1 = new AbortController(); const controller2 = new AbortController(); - doForward.mockImplementationOnce(async (attemptSession, providerForRequest) => { + doForward.mockImplementationOnce(async (attemptSession, providerForRequest, ...args) => { const runtime = attemptSession as ProxySession & AttemptRuntime; runtime.responseController = controller1; runtime.clearResponseTimeout = vi.fn(); + (args[5] as (() => void) | undefined)?.(); expect( ModelRedirector.apply(attemptSession as ProxySession, providerForRequest as Provider) ).toBe(true); @@ -1884,10 +2081,11 @@ describe("ProxyForwarder - first-byte hedge scheduling", () => { }); }); - doForward.mockImplementationOnce(async (attemptSession, providerForRequest) => { + doForward.mockImplementationOnce(async (attemptSession, providerForRequest, ...args) => { const runtime = attemptSession as ProxySession & AttemptRuntime; runtime.responseController = controller2; runtime.clearResponseTimeout = vi.fn(); + (args[5] as (() => void) | undefined)?.(); expect( ModelRedirector.apply(attemptSession as ProxySession, providerForRequest as Provider) ).toBe(true); @@ -1913,13 +2111,14 @@ describe("ProxyForwarder - first-byte hedge scheduling", () => { expect(controller1.signal.aborted).toBe(true); expect(controller2.signal.aborted).toBe(true); expect(mocks.clearSessionProviders).toHaveBeenCalledWith("sess-hedge", new Set([1, 2]), null); - expect(mocks.recordFailure).not.toHaveBeenCalled(); + expect(mocks.recordFailure).toHaveBeenCalledTimes(1); expect(mocks.recordSuccess).not.toHaveBeenCalled(); const chain = session.getProviderChain(); expect( - chain.find((item) => item.id === provider1.id && item.reason === "client_abort") - ?.modelRedirect + chain.find( + (item) => item.id === provider1.id && item.reason === "client_abort_no_first_byte" + )?.modelRedirect ).toMatchObject({ originalModel: requestedModel, redirectedModel: "accounts/fireworks/routers/kimi-k2p5-turbo", diff --git a/tests/unit/proxy/response-handler-client-abort-drain.test.ts b/tests/unit/proxy/response-handler-client-abort-drain.test.ts index 779d18c9a..e3a6b9064 100644 --- a/tests/unit/proxy/response-handler-client-abort-drain.test.ts +++ b/tests/unit/proxy/response-handler-client-abort-drain.test.ts @@ -2199,6 +2199,39 @@ describe("ProxyResponseHandler stream client abort finalization", () => { ); }); + it("attributes an old no-first-byte client abort once", async () => { + vi.mocked(recordFailure).mockClear(); + const clientController = new AbortController(); + const session = createSession(clientController.signal); + setDeferredStreamingFinalization(session, { + providerId: 1, + providerName: "avemujica-responses", + providerPriority: 1, + attemptNumber: 1, + totalProvidersAttempted: 1, + isFirstAttempt: true, + isFailoverSuccess: false, + endpointId: 42, + endpointUrl: "https://api.test.invalid/v1", + upstreamStatusCode: 200, + healthAttemptId: "legacy-serial-1-1", + healthAttemptStartedAtMonotonic: performance.now() - 1_000, + healthAttributionThresholdMs: 1, + healthFirstByteSeen: false, + }); + const upstream = createControllableEmptyResponsesSse(); + + await ProxyResponseHandler.dispatch(session, upstream.response); + clientController.abort(new Error("client detached")); + upstream.close(); + await drainAsyncTasks(); + + expect(recordFailure).toHaveBeenCalledTimes(1); + expect(session.getProviderChain()).toEqual( + expect.arrayContaining([expect.objectContaining({ reason: "client_abort_no_first_byte" })]) + ); + }); + it("stops a detached source as soon as compact terminal usage is captured", async () => { const clientController = new AbortController(); const session = createSession(clientController.signal); diff --git a/tests/unit/proxy/response-handler-gemini-stream-passthrough-timeouts.test.ts b/tests/unit/proxy/response-handler-gemini-stream-passthrough-timeouts.test.ts index c1a0df38c..d51cb3014 100644 --- a/tests/unit/proxy/response-handler-gemini-stream-passthrough-timeouts.test.ts +++ b/tests/unit/proxy/response-handler-gemini-stream-passthrough-timeouts.test.ts @@ -8,6 +8,7 @@ import { ProxySession } from "@/app/v1/_lib/proxy/session"; import { setDeferredStreamingFinalization } from "@/app/v1/_lib/proxy/stream-finalization"; import { AsyncTaskManager } from "@/lib/async-task-manager"; import { SessionManager } from "@/lib/session-manager"; +import { recordFailure } from "@/lib/circuit-breaker"; import { updateMessageRequestDetails, updateMessageRequestDetailsDurably, @@ -774,6 +775,66 @@ describe("ProxyResponseHandler - Gemini stream passthrough timeouts", () => { } }); + test("Gemini passthrough does not attribute a client abort after the first byte", async () => { + asyncTasks.length = 0; + vi.mocked(recordFailure).mockClear(); + const clientAbortController = new AbortController(); + const provider = createProvider({ firstByteTimeoutStreamingMs: 1 }); + const session = createSession({ + clientAbortSignal: clientAbortController.signal, + messageId: 5, + userId: 1, + }); + session.setProvider(provider); + setDeferredStreamingFinalization(session, { + providerId: provider.id, + providerName: provider.name, + providerPriority: provider.priority, + attemptNumber: 1, + totalProvidersAttempted: 1, + isFirstAttempt: true, + isFailoverSuccess: false, + endpointId: null, + endpointUrl: provider.url, + upstreamStatusCode: 200, + healthAttemptId: "legacy-serial-1-1", + healthAttemptStartedAtMonotonic: performance.now() - 1_000, + healthAttributionThresholdMs: 1, + healthFirstByteSeen: false, + }); + const encoder = new TextEncoder(); + let upstreamController: ReadableStreamDefaultController | null = null; + const upstream = new ReadableStream({ + start(controller) { + upstreamController = controller; + controller.enqueue( + encoder.encode('{"candidates":[{"content":{"parts":[{"text":"x"}]}}]}\n') + ); + }, + }); + + const downstream = await ( + ProxyResponseHandler as unknown as { + handleStream: (session: ProxySession, response: Response) => Promise; + } + ).handleStream( + session, + new Response(upstream, { status: 200, headers: { "content-type": "text/event-stream" } }) + ); + const reader = downstream.body?.getReader(); + expect(reader).toBeTruthy(); + if (!reader) throw new Error("Missing body reader"); + await reader.read(); + clientAbortController.abort(new Error("client_cancelled")); + upstreamController?.close(); + await expectAllFulfilled(asyncTasks); + + expect(recordFailure).not.toHaveBeenCalled(); + expect(session.getProviderChain()).not.toEqual( + expect.arrayContaining([expect.objectContaining({ reason: "client_abort_no_first_byte" })]) + ); + }); + test("Gemini 流式透传超大单 chunk 应保留尾部 usage 且不把截断快照作为完整正文存储", async () => { asyncTasks.length = 0; vi.mocked(SessionManager.storeSessionResponse).mockClear(); diff --git a/tests/unit/proxy/routing-trace.test.ts b/tests/unit/proxy/routing-trace.test.ts index 9317fd80e..74f07506d 100644 --- a/tests/unit/proxy/routing-trace.test.ts +++ b/tests/unit/proxy/routing-trace.test.ts @@ -61,6 +61,7 @@ function makeTraceSession(startTime = 1_000): ProxySession { liveObservabilityFlushPromise: null, liveObservabilityClosePromise: null, liveObservabilityClosed: false, + liveActiveProviders: new Map(), routingTraceTerminalLogged: false, providerChain: [], ttftMs: null, @@ -190,6 +191,47 @@ describe("ProxySession routing trace recorder", () => { nowSpy.mockRestore(); }); + it("marks a thresholded client abort as a failed routing outcome", () => { + const session = makeTraceSession(); + session.initializeRoutingTrace({ + mode: "legacy_hedge", + discoveryEnabled: false, + eligible: false, + startedAt: 1_000, + }); + session.addProviderToChain( + { id: 1, name: "slow", providerType: "openai", priority: 1 } as never, + { reason: "client_abort_no_first_byte", attemptNumber: 1 } + ); + session.setRoutingTraceSummary({ + outcome: "client_abort", + statusCode: 499, + durationMs: 1_000, + ttftMs: null, + attemptsPerRequest: 1, + maxActiveAttempts: 1, + rounds: 0, + providerMs: 1_000, + fallbackPromotions: 0, + cancelFailures: 0, + winnerOrigin: "none", + winnerProviderId: null, + winnerRound: null, + }); + + session.finalizeRoutingTrace(499); + + expect(session.getRoutingTrace()?.summary).toMatchObject({ + outcome: "failed", + statusCode: 499, + }); + expect(session.getRoutingTrace()?.events.at(-1)).toMatchObject({ + type: "request_finished", + outcome: "failed", + statusCode: 499, + }); + }); + it("caps the trace at 512 events and persists the truncated snapshot independently", async () => { const session = makeTraceSession(); session.initializeRoutingTrace({ diff --git a/tests/unit/repository/system-config-degradation-ladder.test.ts b/tests/unit/repository/system-config-degradation-ladder.test.ts index 5469eaf3c..08b0ce198 100644 --- a/tests/unit/repository/system-config-degradation-ladder.test.ts +++ b/tests/unit/repository/system-config-degradation-ladder.test.ts @@ -7,6 +7,7 @@ import type { UpdateSystemSettingsInput } from "@/types/system-config"; // 近代新增列(最新在前),降级链按引入顺序逐层累计剥离。 const RECENT_COLUMNS = [ + "legacyHedgeMaxInFlight", "replayCacheTtlMinutes", "cacheEffectivenessEnabled", "replayEnabled", @@ -44,6 +45,7 @@ const FULL_COLUMNS = [ "stickyTimeoutCooldownMs", "enableGeminiFunctionIdRectifier", "billHedgeLosers", + "legacyHedgeMaxInFlight", "billNonSuccessfulRequests", "passThroughUpstreamErrorMessage", "fakeStreamingWhitelist", @@ -91,6 +93,7 @@ const FULL_COLUMNS = [ // 历史世代字段集(冻结):passThrough 世代之前的 schema 没有以下五列, // 但仍包含 enableThinkingEffortConflictRectifier / allowNonConversationEndpointProviderFallback。 const PASS_THROUGH_ERA_OMIT = [ + "legacyHedgeMaxInFlight", "billHedgeLosers", "billNonSuccessfulRequests", "passThroughUpstreamErrorMessage", diff --git a/tests/unit/repository/system-config-update-missing-columns.test.ts b/tests/unit/repository/system-config-update-missing-columns.test.ts index b968e07e4..fb0596f82 100644 --- a/tests/unit/repository/system-config-update-missing-columns.test.ts +++ b/tests/unit/repository/system-config-update-missing-columns.test.ts @@ -299,12 +299,12 @@ describe("SystemSettings:数据库缺列时的保存兜底", () => { vi.setSystemTime(now); // 第一次 select(fullSelection) 因新列缺失而抛 42703; - // 第二次仅去掉最新的 replayCacheTtlMinutes 后仍失败; - // 第三次累计去掉 cacheEffectivenessEnabled 后命中. + // The new legacy hedge column is the newest rung, so it is stripped before replay columns. const selectMock = vi .fn() .mockReturnValueOnce(createRejectedThenableQuery({ code: "42703" })) .mockReturnValueOnce(createRejectedThenableQuery({ code: "42703" })) + .mockReturnValueOnce(createRejectedThenableQuery({ code: "42703" })) .mockReturnValueOnce( createThenableQuery([ { @@ -337,22 +337,23 @@ describe("SystemSettings:数据库缺列时的保存兜底", () => { const result = await getSystemSettings(); // 降级读取成功(未抛错),缺失列由 transformer 落默认值。 - expect(selectMock).toHaveBeenCalledTimes(3); + expect(selectMock).toHaveBeenCalledTimes(4); expect(result.siteTitle).toBe("CC Hub"); expect(result.enableHttp2).toBe(true); expect(result.affinityIgnoreClientSessionId).toBe(true); expect(result.streamGateMode).toBe("enforce"); - const thirdSelection = selectMock.mock.calls[2]?.[0] as Record; - expect(thirdSelection).not.toHaveProperty("replayCacheTtlMinutes"); - expect(thirdSelection).not.toHaveProperty("cacheEffectivenessEnabled"); - expect(thirdSelection).toHaveProperty("replayEnabled"); - expect(thirdSelection).toHaveProperty("affinityIgnoreClientSessionId"); - expect(thirdSelection).toHaveProperty("streamGateMode"); - expect(thirdSelection).toHaveProperty("stickyTimeoutCooldownMs"); - expect(thirdSelection).toHaveProperty("racingTotalTimeoutMs"); - expect(thirdSelection).toHaveProperty("enableGeminiFunctionIdRectifier"); - expect(thirdSelection).toHaveProperty("enableThinkingEffortConflictRectifier"); + const fourthSelection = selectMock.mock.calls[3]?.[0] as Record; + expect(fourthSelection).not.toHaveProperty("legacyHedgeMaxInFlight"); + expect(fourthSelection).not.toHaveProperty("replayCacheTtlMinutes"); + expect(fourthSelection).not.toHaveProperty("cacheEffectivenessEnabled"); + expect(fourthSelection).toHaveProperty("replayEnabled"); + expect(fourthSelection).toHaveProperty("affinityIgnoreClientSessionId"); + expect(fourthSelection).toHaveProperty("streamGateMode"); + expect(fourthSelection).toHaveProperty("stickyTimeoutCooldownMs"); + expect(fourthSelection).toHaveProperty("racingTotalTimeoutMs"); + expect(fourthSelection).toHaveProperty("enableGeminiFunctionIdRectifier"); + expect(fourthSelection).toHaveProperty("enableThinkingEffortConflictRectifier"); vi.useRealTimers(); }); diff --git a/tests/unit/repository/usage-logs-min-retry-count-filter.test.ts b/tests/unit/repository/usage-logs-min-retry-count-filter.test.ts index 149447521..e5d6d4f4b 100644 --- a/tests/unit/repository/usage-logs-min-retry-count-filter.test.ts +++ b/tests/unit/repository/usage-logs-min-retry-count-filter.test.ts @@ -58,6 +58,7 @@ describe("Usage logs minRetryCount filter", () => { expect(whereSql).toContain("request_success"); expect(whereSql).toContain("retry_success"); expect(whereSql).toContain("retry_failed"); + expect(whereSql).toContain("client_abort_no_first_byte"); expect(whereSql).toContain("statuscode"); expect(whereSql).toContain("hedge_triggered"); expect(whereSql).not.toContain("jsonb_array_length"); diff --git a/tests/unit/validation/system-settings-discovery.test.ts b/tests/unit/validation/system-settings-discovery.test.ts index 3039b1fea..2b30ebe96 100644 --- a/tests/unit/validation/system-settings-discovery.test.ts +++ b/tests/unit/validation/system-settings-discovery.test.ts @@ -70,6 +70,26 @@ describe("UpdateSystemSettingsSchema Discovery settings", () => { }); }); + it.each([1, 2, 4])("accepts legacy hedge concurrency %s", (value) => { + expect(UpdateSystemSettingsSchema.parse({ legacyHedgeMaxInFlight: value })).toEqual({ + legacyHedgeMaxInFlight: value, + }); + }); + + it.each([0, 5, 1.5, null, true, [2]])("rejects invalid legacy hedge concurrency %s", (value) => { + expect(UpdateSystemSettingsSchema.safeParse({ legacyHedgeMaxInFlight: value }).success).toBe( + false + ); + }); + + it("rejects invalid legacy hedge concurrency with a stable error code", () => { + const result = UpdateSystemSettingsSchema.safeParse({ legacyHedgeMaxInFlight: true }); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0]?.message).toBe("LEGACY_HEDGE_MAX_IN_FLIGHT_INVALID"); + } + }); + it("rejects inherited Object prototype names as Discovery fields", () => { expect(isDiscoverySettingField("toString")).toBe(false); expect(isDiscoverySettingField("valueOf")).toBe(false);