diff --git a/drizzle/0111_happy_mauler.sql b/drizzle/0111_happy_mauler.sql new file mode 100644 index 000000000..b0a595652 --- /dev/null +++ b/drizzle/0111_happy_mauler.sql @@ -0,0 +1,41 @@ +ALTER TABLE "message_request" ADD COLUMN "routing_trace" jsonb; + +-- Routing trace finalization is observability-only. Restrict the ledger trigger +-- to columns that can actually change its projection so trace-only patches do +-- not rewrite accounting rows. +DROP TRIGGER IF EXISTS trg_upsert_usage_ledger ON message_request; + +CREATE TRIGGER trg_upsert_usage_ledger +AFTER INSERT OR UPDATE OF + blocked_by, + status_code, + error_message, + provider_chain, + actual_response_model, + endpoint, + provider_id, + user_id, + "key", + model, + original_model, + api_type, + session_id, + cost_usd, + cost_multiplier, + group_cost_multiplier, + input_tokens, + output_tokens, + cache_creation_input_tokens, + cache_read_input_tokens, + cache_creation_5m_input_tokens, + cache_creation_1h_input_tokens, + cache_ttl_applied, + context_1m_applied, + swap_cache_ttl_applied, + duration_ms, + ttfb_ms, + client_ip, + created_at +ON message_request +FOR EACH ROW +EXECUTE FUNCTION fn_upsert_usage_ledger(); diff --git a/drizzle/meta/0111_snapshot.json b/drizzle/meta/0111_snapshot.json new file mode 100644 index 000000000..2c6d18c62 --- /dev/null +++ b/drizzle/meta/0111_snapshot.json @@ -0,0 +1,4697 @@ +{ + "id": "b59029b9-eae7-4d23-97b8-323ee78c8e99", + "prevId": "b6b7996d-5b70-4a31-a1c7-3a318b3398a0", + "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 + }, + "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 + }, + "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 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_message_request_user_date_cost": { + "name": "idx_message_request_user_date_cost", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_user_created_at_cost_stats": { + "name": "idx_message_request_user_created_at_cost_stats", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_user_query": { + "name": "idx_message_request_user_query", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_provider_created_at_active": { + "name": "idx_message_request_provider_created_at_active", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_provider_created_at_finalized_active": { + "name": "idx_message_request_provider_created_at_finalized_active", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"status_code\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_session_id": { + "name": "idx_message_request_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_session_id_prefix": { + "name": "idx_message_request_session_id_prefix", + "columns": [ + { + "expression": "\"session_id\" varchar_pattern_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_session_seq": { + "name": "idx_message_request_session_seq", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_endpoint": { + "name": "idx_message_request_endpoint", + "columns": [ + { + "expression": "endpoint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_blocked_by": { + "name": "idx_message_request_blocked_by", + "columns": [ + { + "expression": "blocked_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_provider_id": { + "name": "idx_message_request_provider_id", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_user_id": { + "name": "idx_message_request_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key": { + "name": "idx_message_request_key", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key_created_at_id": { + "name": "idx_message_request_key_created_at_id", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key_model_active": { + "name": "idx_message_request_key_model_active", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"model\" IS NOT NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key_endpoint_active": { + "name": "idx_message_request_key_endpoint_active", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "endpoint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"endpoint\" IS NOT NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_created_at_id_active": { + "name": "idx_message_request_created_at_id_active", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_model_active": { + "name": "idx_message_request_model_active", + "columns": [ + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"model\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_status_code_active": { + "name": "idx_message_request_status_code_active", + "columns": [ + { + "expression": "status_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"status_code\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_created_at": { + "name": "idx_message_request_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_deleted_at": { + "name": "idx_message_request_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key_last_active": { + "name": "idx_message_request_key_last_active", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key_cost_active": { + "name": "idx_message_request_key_cost_active", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_session_user_info": { + "name": "idx_message_request_session_user_info", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_client_ip_created_at": { + "name": "idx_message_request_client_ip_created_at", + "columns": [ + { + "expression": "client_ip", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"client_ip\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_prices": { + "name": "model_prices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "model_name": { + "name": "model_name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "price_data": { + "name": "price_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'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_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.request_filters": { + "name": "request_filters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true + }, + "match_type": { + "name": "match_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "target": { + "name": "target", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "replacement": { + "name": "replacement", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "binding_type": { + "name": "binding_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'global'" + }, + "provider_ids": { + "name": "provider_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "group_tags": { + "name": "group_tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rule_mode": { + "name": "rule_mode", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'simple'" + }, + "execution_phase": { + "name": "execution_phase", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'guard'" + }, + "operations": { + "name": "operations", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_request_filters_enabled": { + "name": "idx_request_filters_enabled", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_request_filters_scope": { + "name": "idx_request_filters_scope", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_request_filters_action": { + "name": "idx_request_filters_action", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_request_filters_binding": { + "name": "idx_request_filters_binding", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "binding_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_request_filters_phase": { + "name": "idx_request_filters_phase", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_phase", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sensitive_words": { + "name": "sensitive_words", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "word": { + "name": "word", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "match_type": { + "name": "match_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'contains'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_sensitive_words_enabled": { + "name": "idx_sensitive_words_enabled", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "match_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sensitive_words_created_at": { + "name": "idx_sensitive_words_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.system_settings": { + "name": "system_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "site_title": { + "name": "site_title", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "default": "'Claude Code Hub'" + }, + "allow_global_usage_view": { + "name": "allow_global_usage_view", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "currency_display": { + "name": "currency_display", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "billing_model_source": { + "name": "billing_model_source", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'original'" + }, + "codex_priority_billing_source": { + "name": "codex_priority_billing_source", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'requested'" + }, + "bill_non_successful_requests": { + "name": "bill_non_successful_requests", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "bill_hedge_losers": { + "name": "bill_hedge_losers", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "discovery_enabled": { + "name": "discovery_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "discovery_concurrency": { + "name": "discovery_concurrency", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 2 + }, + "max_discovery_rounds": { + "name": "max_discovery_rounds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 2 + }, + "discovery_sla_ms": { + "name": "discovery_sla_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10000 + }, + "sticky_sla_ms": { + "name": "sticky_sla_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 20000 + }, + "racing_total_timeout_ms": { + "name": "racing_total_timeout_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 60000 + }, + "sticky_timeout_cooldown_ms": { + "name": "sticky_timeout_cooldown_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 300000 + }, + "timezone": { + "name": "timezone", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "enable_auto_cleanup": { + "name": "enable_auto_cleanup", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "cleanup_retention_days": { + "name": "cleanup_retention_days", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30 + }, + "cleanup_schedule": { + "name": "cleanup_schedule", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "default": "'0 2 * * *'" + }, + "cleanup_batch_size": { + "name": "cleanup_batch_size", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 10000 + }, + "enable_client_version_check": { + "name": "enable_client_version_check", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "verbose_provider_error": { + "name": "verbose_provider_error", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pass_through_upstream_error_message": { + "name": "pass_through_upstream_error_message", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_http2": { + "name": "enable_http2", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_openai_responses_websocket": { + "name": "enable_openai_responses_websocket", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_high_concurrency_mode": { + "name": "enable_high_concurrency_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "intercept_anthropic_warmup_requests": { + "name": "intercept_anthropic_warmup_requests", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_thinking_signature_rectifier": { + "name": "enable_thinking_signature_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_thinking_budget_rectifier": { + "name": "enable_thinking_budget_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_thinking_effort_conflict_rectifier": { + "name": "enable_thinking_effort_conflict_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_gemini_function_id_rectifier": { + "name": "enable_gemini_function_id_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_billing_header_rectifier": { + "name": "enable_billing_header_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_response_input_rectifier": { + "name": "enable_response_input_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_non_conversation_endpoint_provider_fallback": { + "name": "allow_non_conversation_endpoint_provider_fallback", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "fake_streaming_whitelist": { + "name": "fake_streaming_whitelist", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "enable_codex_session_id_completion": { + "name": "enable_codex_session_id_completion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_claude_metadata_user_id_injection": { + "name": "enable_claude_metadata_user_id_injection", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_response_fixer": { + "name": "enable_response_fixer", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "response_fixer_config": { + "name": "response_fixer_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{\"fixTruncatedJson\":true,\"fixSseFormat\":true,\"fixEncoding\":true,\"maxJsonDepth\":200,\"maxFixSize\":1048576}'::jsonb" + }, + "quota_db_refresh_interval_seconds": { + "name": "quota_db_refresh_interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 10 + }, + "quota_lease_percent_5h": { + "name": "quota_lease_percent_5h", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "quota_lease_percent_daily": { + "name": "quota_lease_percent_daily", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "quota_lease_percent_weekly": { + "name": "quota_lease_percent_weekly", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "quota_lease_percent_monthly": { + "name": "quota_lease_percent_monthly", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "quota_lease_cap_usd": { + "name": "quota_lease_cap_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "ip_extraction_config": { + "name": "ip_extraction_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ip_geo_lookup_enabled": { + "name": "ip_geo_lookup_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "public_status_window_hours": { + "name": "public_status_window_hours", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 24 + }, + "public_status_aggregation_interval_minutes": { + "name": "public_status_aggregation_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_ledger": { + "name": "usage_ledger", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "final_provider_id": { + "name": "final_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "original_model": { + "name": "original_model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "actual_response_model": { + "name": "actual_response_model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "endpoint": { + "name": "endpoint", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "api_type": { + "name": "api_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_success": { + "name": "is_success", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "success_rate_outcome": { + "name": "success_rate_outcome", + "type": "varchar(16)", + "primaryKey": false, + "notNull": false + }, + "blocked_by": { + "name": "blocked_by", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "cost_usd": { + "name": "cost_usd", + "type": "numeric(21, 15)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "cost_multiplier": { + "name": "cost_multiplier", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": false + }, + "group_cost_multiplier": { + "name": "group_cost_multiplier", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_read_input_tokens": { + "name": "cache_read_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_creation_5m_input_tokens": { + "name": "cache_creation_5m_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_creation_1h_input_tokens": { + "name": "cache_creation_1h_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_ttl_applied": { + "name": "cache_ttl_applied", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "context_1m_applied": { + "name": "context_1m_applied", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "swap_cache_ttl_applied": { + "name": "swap_cache_ttl_applied", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ttfb_ms": { + "name": "ttfb_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_ip": { + "name": "client_ip", + "type": "varchar(45)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_usage_ledger_request_id": { + "name": "idx_usage_ledger_request_id", + "columns": [ + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_user_created_at": { + "name": "idx_usage_ledger_user_created_at", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_key_created_at": { + "name": "idx_usage_ledger_key_created_at", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_provider_created_at": { + "name": "idx_usage_ledger_provider_created_at", + "columns": [ + { + "expression": "final_provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_created_at_minute": { + "name": "idx_usage_ledger_created_at_minute", + "columns": [ + { + "expression": "date_trunc('minute', \"created_at\" AT TIME ZONE 'UTC')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_created_at_desc_id": { + "name": "idx_usage_ledger_created_at_desc_id", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_session_id": { + "name": "idx_usage_ledger_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"session_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_model": { + "name": "idx_usage_ledger_model", + "columns": [ + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"model\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_key_cost": { + "name": "idx_usage_ledger_key_cost", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "endpoint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_user_cost_cover": { + "name": "idx_usage_ledger_user_cost_cover", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "endpoint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_provider_cost_cover": { + "name": "idx_usage_ledger_provider_cost_cover", + "columns": [ + { + "expression": "final_provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "endpoint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_key_created_at_desc_cover": { + "name": "idx_usage_ledger_key_created_at_desc_cover", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"created_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "final_provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "varchar", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "rpm_limit": { + "name": "rpm_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "daily_limit_usd": { + "name": "daily_limit_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "provider_group": { + "name": "provider_group", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false, + "default": "'default'" + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "limit_5h_usd": { + "name": "limit_5h_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_5h_reset_mode": { + "name": "limit_5h_reset_mode", + "type": "daily_reset_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'rolling'" + }, + "limit_weekly_usd": { + "name": "limit_weekly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_monthly_usd": { + "name": "limit_monthly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_total_usd": { + "name": "limit_total_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "cost_reset_at": { + "name": "cost_reset_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "limit_5h_cost_reset_at": { + "name": "limit_5h_cost_reset_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "limit_concurrent_sessions": { + "name": "limit_concurrent_sessions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "daily_reset_mode": { + "name": "daily_reset_mode", + "type": "daily_reset_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'fixed'" + }, + "daily_reset_time": { + "name": "daily_reset_time", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'00:00'" + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "allowed_clients": { + "name": "allowed_clients", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "allowed_models": { + "name": "allowed_models", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "blocked_clients": { + "name": "blocked_clients", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_users_active_role_sort": { + "name": "idx_users_active_role_sort", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"users\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_users_enabled_expires_at": { + "name": "idx_users_enabled_expires_at", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"users\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_users_tags_gin": { + "name": "idx_users_tags_gin", + "columns": [ + { + "expression": "tags", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"users\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_users_created_at": { + "name": "idx_users_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_users_deleted_at": { + "name": "idx_users_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_targets": { + "name": "webhook_targets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "provider_type": { + "name": "provider_type", + "type": "webhook_provider_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "webhook_url": { + "name": "webhook_url", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": false + }, + "telegram_bot_token": { + "name": "telegram_bot_token", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "telegram_chat_id": { + "name": "telegram_chat_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "dingtalk_secret": { + "name": "dingtalk_secret", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "custom_template": { + "name": "custom_template", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "custom_headers": { + "name": "custom_headers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "proxy_url": { + "name": "proxy_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "proxy_fallback_to_direct": { + "name": "proxy_fallback_to_direct", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_test_at": { + "name": "last_test_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_test_result": { + "name": "last_test_result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.daily_reset_mode": { + "name": "daily_reset_mode", + "schema": "public", + "values": [ + "fixed", + "rolling" + ] + }, + "public.notification_type": { + "name": "notification_type", + "schema": "public", + "values": [ + "circuit_breaker", + "daily_leaderboard", + "cost_alert", + "cache_hit_rate_alert" + ] + }, + "public.webhook_provider_type": { + "name": "webhook_provider_type", + "schema": "public", + "values": [ + "wechat", + "feishu", + "dingtalk", + "telegram", + "custom" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index cdc138e1e..1f3a7d711 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -778,6 +778,13 @@ "when": 1784571513591, "tag": "0110_daffy_rawhide_kid", "breakpoints": true + }, + { + "idx": 111, + "version": "7", + "when": 1784622924311, + "tag": "0111_happy_mauler", + "breakpoints": true } ] } diff --git a/messages/en/dashboard.json b/messages/en/dashboard.json index 085467bc3..a15a6ea28 100644 --- a/messages/en/dashboard.json +++ b/messages/en/dashboard.json @@ -380,6 +380,135 @@ "overridden": "Overridden by provider", "tooltip": "Thinking effort requested by the client (output_config.effort), shown verbatim. The proxy does not rename or convert levels." }, + "routingTrace": { + "title": "Routing mode", + "modes": { + "discovery": "Bounded Discovery", + "legacy_hedge": "Legacy Hedge", + "legacy_serial": "Legacy serial fallback", + "single_upstream": "Single upstream", + "lease_conflict": "Single-route protection" + }, + "bypassed": "Discovery was not used: {reason}", + "bypassReasons": { + "disabled": "Discovery is disabled", + "non_streaming": "Non-streaming request", + "retry_not_allowed": "This endpoint does not allow retries", + "provider_switch_not_allowed": "This endpoint does not allow provider switching", + "raw_passthrough": "Raw passthrough request", + "unsupported_protocol": "Protocol is not supported by Discovery", + "websocket": "WebSocket request", + "streaming_hedge_disabled": "Streaming racing is disabled for this request", + "raw_cross_provider_fallback": "Raw cross-provider fallback is enabled", + "missing_session": "Session identity is missing", + "missing_key": "API key identity is missing", + "rollout_ineligible": "Request is outside the current rollout", + "redis_capability_unavailable": "Redis binding capability is unavailable", + "binding_conflict": "Session binding conflict", + "lease_conflict": "Another request owns the Discovery lease; this request uses one upstream", + "lease_unavailable": "Discovery lease is unavailable", + "unknown": "Eligibility requirement was not met" + }, + "discoveryTitle": "Discovery rounds", + "discoveryCompact": "{rounds}R · {attempts} tries", + "routeMode": "Route mode", + "routeModes": { + "sticky": "Sticky", + "cold_start": "Cold start", + "rediscovery": "Rediscovery" + }, + "finalProvider": "Final provider", + "roundsLabel": "Rounds", + "attemptsLabel": "Attempts", + "winnerOrigin": "Winner origin", + "viewDetails": "View Discovery details", + "stickyPhase": "Sticky probe", + "round": "Round {round}", + "attempts": "{count} attempts", + "noAttempts": "No provider attempts were recorded", + "providerFallback": "Provider {id}", + "elapsed": "+{elapsed}ms", + "priority": "Priority {priority}", + "roles": { + "sticky": "Sticky", + "normal": "Candidate", + "fallback": "Fallback" + }, + "winnerSources": { + "sticky": "Sticky", + "normal": "Normal candidate", + "fallback": "Fallback takeover" + }, + "roleTransition": "{from} → {to}", + "outcomes": { + "success": "Success", + "pending": "Pending", + "ready": "Ready", + "held": "Ready, held", + "winner": "Winner", + "failed": "Failed", + "cancelled": "Cancelled", + "timeout": "SLA timeout", + "client_abort": "Client aborted", + "deadline": "Total deadline" + }, + "terminalOutcome": "Request result", + "bindingResult": "Sticky binding", + "bindingActions": { + "create": "Create", + "renew": "Renew", + "clear": "Clear", + "none": "No change" + }, + "bindingFallbackWinner": "Not written · Fallback winner", + "bindingOutcomes": { + "updated": "Updated", + "cleared": "Cleared", + "skipped": "Skipped", + "failed": "Failed", + "unknown": "Unknown" + }, + "fallbackPromoted": "Promoted to fallback", + "winnerCommitted": "Winner committed", + "roundStarted": "Round started", + "events": { + "started": "Request started", + "ready": "Valid first content received", + "held": "Delivery held by routing policy", + "fallbackPromoted": "Promoted to fallback", + "winnerCommitted": "Committed as winner", + "finished": "Finished: {outcome}" + }, + "traceTruncated": "Some events were omitted because this trace reached its storage limit.", + "summary": "{rounds} rounds · {attempts} attempts · max {maxActive} active", + "config": "Configuration snapshot", + "configConcurrency": "Concurrency", + "configMaxRounds": "Maximum rounds", + "configDiscoverySla": "Round SLA", + "configStickySla": "Sticky SLA", + "configTotalTimeout": "Total timeout", + "configStickyCooldown": "Sticky timeout cooldown", + "configStickyBindingTtl": "Sticky binding validity (SESSION_TTL)", + "attemptDetails": { + "providerId": "Provider ID", + "attempt": "Attempt", + "status": "HTTP status", + "inferred": "inferred", + "endpoint": "Endpoint", + "error": "Upstream error", + "cancellation": "Cancellation reason", + "timeline": "Attempt timeline" + }, + "cancellationKinds": { + "discovery_loser": "Another Discovery attempt won", + "discovery_sla_timeout": "Discovery SLA expired", + "round_timeout": "Round SLA expired", + "sticky_timeout": "Sticky SLA expired", + "request_deadline": "Total Discovery deadline reached", + "client_abort": "Client disconnected", + "winner_committed": "Another provider was committed" + } + }, "reasoningEffort": { "label": "Reasoning effort", "overridden": "Overridden by provider", @@ -387,6 +516,7 @@ }, "logicTrace": { "title": "Decision Chain", + "singleRouteSelectionTitle": "Provider selection under single-route protection", "noDecisionData": "No decision data available", "providersCount": "{count} providers", "healthyCount": "{count} healthy", diff --git a/messages/en/settings/config.json b/messages/en/settings/config.json index 149408b5e..50a311026 100644 --- a/messages/en/settings/config.json +++ b/messages/en/settings/config.json @@ -126,6 +126,8 @@ "stickySlaMs": "Sticky SLA (milliseconds)", "racingTotalTimeoutMs": "Discovery total timeout (milliseconds)", "stickyTimeoutCooldownMs": "Sticky timeout cooldown (milliseconds)", + "stickyBindingTtl": "Sticky binding validity", + "stickyBindingTtlDesc": "Read-only. This follows the SESSION_TTL environment variable and is shared with other session snapshots.", "discoveryWindowDesc": "The total timeout must be at least Sticky SLA + maximum rounds × Discovery SLA. Discovery losers are cancelled and are not drained or billed by the legacy Hedge path.", "discoveryWindowInvalid": "Discovery total timeout is shorter than the configured Sticky and Discovery windows.", "discoverySettingsInvalid": "One or more Discovery values are outside the allowed range.", diff --git a/messages/ja/dashboard.json b/messages/ja/dashboard.json index dcbfbe427..b06d97f46 100644 --- a/messages/ja/dashboard.json +++ b/messages/ja/dashboard.json @@ -380,6 +380,135 @@ "overridden": "プロバイダーにより上書き", "tooltip": "クライアントがリクエストボディで指定した思考強度 (output_config.effort)。プロキシは値をそのまま表示し、レベル名を変換しません。" }, + "routingTrace": { + "title": "ルーティングモード", + "modes": { + "discovery": "限定 Discovery", + "legacy_hedge": "従来の Hedge 競争", + "legacy_serial": "従来の直列フェイルオーバー", + "single_upstream": "単一アップストリーム", + "lease_conflict": "単一経路保護" + }, + "bypassed": "このリクエストでは Discovery を使用していません:{reason}", + "bypassReasons": { + "disabled": "Discovery が無効です", + "non_streaming": "非ストリーミングリクエストです", + "retry_not_allowed": "このエンドポイントでは再試行できません", + "provider_switch_not_allowed": "このエンドポイントではプロバイダーを切り替えられません", + "raw_passthrough": "Raw パススルーリクエストです", + "unsupported_protocol": "Discovery が対応していないプロトコルです", + "websocket": "WebSocket リクエストです", + "streaming_hedge_disabled": "このリクエストではストリーミング競争が無効です", + "raw_cross_provider_fallback": "Raw クロスプロバイダーフォールバックが有効です", + "missing_session": "Session 識別子がありません", + "missing_key": "API Key 識別子がありません", + "rollout_ineligible": "現在のロールアウト対象外です", + "redis_capability_unavailable": "Redis バインディング機能を利用できません", + "binding_conflict": "Session バインディングが競合しています", + "lease_conflict": "別のリクエストが Discovery リースを保持しているため、このリクエストは単一アップストリームを使用します", + "lease_unavailable": "Discovery リースを利用できません", + "unknown": "Discovery の適格条件を満たしていません" + }, + "discoveryTitle": "Discovery ラウンド", + "discoveryCompact": "{rounds}R · {attempts} 回", + "routeMode": "ルーティング方式", + "routeModes": { + "sticky": "Sticky", + "cold_start": "コールドスタート", + "rediscovery": "再探索" + }, + "finalProvider": "最終プロバイダー", + "roundsLabel": "ラウンド", + "attemptsLabel": "試行", + "winnerOrigin": "勝者の経路", + "viewDetails": "Discovery の詳細を表示", + "stickyPhase": "Sticky プローブ", + "round": "ラウンド {round}", + "attempts": "{count} 回の試行", + "noAttempts": "プロバイダー試行の記録がありません", + "providerFallback": "プロバイダー {id}", + "elapsed": "+{elapsed}ms", + "priority": "優先度 {priority}", + "roles": { + "sticky": "Sticky", + "normal": "候補", + "fallback": "フォールバック" + }, + "winnerSources": { + "sticky": "Sticky", + "normal": "通常候補", + "fallback": "フォールバック引き継ぎ" + }, + "roleTransition": "{from} → {to}", + "outcomes": { + "success": "正常終了", + "pending": "待機中", + "ready": "準備完了", + "held": "準備完了、保留中", + "winner": "勝者", + "failed": "失敗", + "cancelled": "キャンセル済み", + "timeout": "SLA タイムアウト", + "client_abort": "クライアント切断", + "deadline": "全体期限" + }, + "terminalOutcome": "リクエスト結果", + "bindingResult": "Sticky バインド", + "bindingActions": { + "create": "作成", + "renew": "更新", + "clear": "解除", + "none": "変更なし" + }, + "bindingFallbackWinner": "書き込みなし · フォールバック勝者", + "bindingOutcomes": { + "updated": "更新済み", + "cleared": "解除済み", + "skipped": "スキップ", + "failed": "失敗", + "unknown": "不明" + }, + "fallbackPromoted": "フォールバックに昇格", + "winnerCommitted": "勝者を確定", + "roundStarted": "ラウンド開始", + "events": { + "started": "リクエスト開始", + "ready": "有効な先頭データを受信", + "held": "ルーティング規則により配信を保留", + "fallbackPromoted": "フォールバックに昇格", + "winnerCommitted": "勝者として確定", + "finished": "完了:{outcome}" + }, + "traceTruncated": "保存上限に達したため、一部のイベントは省略されました。", + "summary": "{rounds} ラウンド · {attempts} 試行 · 最大同時実行 {maxActive}", + "config": "設定スナップショット", + "configConcurrency": "同時実行数", + "configMaxRounds": "最大ラウンド数", + "configDiscoverySla": "ラウンド SLA", + "configStickySla": "Sticky SLA", + "configTotalTimeout": "全体タイムアウト", + "configStickyCooldown": "Sticky タイムアウトのクールダウン", + "configStickyBindingTtl": "Sticky バインドの有効期間 (SESSION_TTL)", + "attemptDetails": { + "providerId": "Provider ID", + "attempt": "試行回数", + "status": "HTTP ステータス", + "inferred": "推定", + "endpoint": "エンドポイント", + "error": "上流エラー", + "cancellation": "キャンセル理由", + "timeline": "試行タイムライン" + }, + "cancellationKinds": { + "discovery_loser": "別の Discovery 試行が勝利しました", + "discovery_sla_timeout": "Discovery SLA の期限に達しました", + "round_timeout": "ラウンド SLA の期限に達しました", + "sticky_timeout": "Sticky SLA の期限に達しました", + "request_deadline": "Discovery の合計期限に達しました", + "client_abort": "クライアントが切断しました", + "winner_committed": "別の Provider が勝者として確定しました" + } + }, "reasoningEffort": { "label": "推論強度", "overridden": "プロバイダーにより上書き", @@ -387,6 +516,7 @@ }, "logicTrace": { "title": "決定チェーン", + "singleRouteSelectionTitle": "単一経路保護での Provider 選択", "noDecisionData": "決定データがありません", "providersCount": "{count} プロバイダー", "healthyCount": "{count} 健全", diff --git a/messages/ja/settings/config.json b/messages/ja/settings/config.json index e49d28c1b..85a3b9e5b 100644 --- a/messages/ja/settings/config.json +++ b/messages/ja/settings/config.json @@ -128,6 +128,8 @@ "stickySlaMs": "Sticky SLA(ミリ秒)", "racingTotalTimeoutMs": "Discovery 合計タイムアウト(ミリ秒)", "stickyTimeoutCooldownMs": "Sticky タイムアウト後のクールダウン(ミリ秒)", + "stickyBindingTtl": "Sticky バインドの有効期間", + "stickyBindingTtlDesc": "読み取り専用です。この値は環境変数 SESSION_TTL に従い、他の Session スナップショットと共有されます。", "discoveryWindowDesc": "合計タイムアウトは Sticky SLA + 最大ラウンド数 × Discovery SLA 以上にしてください。", "discoveryWindowInvalid": "Discovery 合計タイムアウトが設定された Sticky/Discovery ウィンドウより短くなっています。", "discoverySettingsInvalid": "1 つ以上の Discovery 設定値が許容範囲外です。", diff --git a/messages/ru/dashboard.json b/messages/ru/dashboard.json index ea05d66a8..dac0c0e8a 100644 --- a/messages/ru/dashboard.json +++ b/messages/ru/dashboard.json @@ -380,6 +380,135 @@ "overridden": "Переопределено провайдером", "tooltip": "Уровень усилий на размышления, запрошенный клиентом (output_config.effort); показан как есть — прокси не переименовывает и не преобразует уровни." }, + "routingTrace": { + "title": "Режим маршрутизации", + "modes": { + "discovery": "Ограниченный Discovery", + "legacy_hedge": "Прежняя гонка Hedge", + "legacy_serial": "Прежнее последовательное переключение", + "single_upstream": "Один upstream", + "lease_conflict": "Защита одним маршрутом" + }, + "bypassed": "Discovery не использован: {reason}", + "bypassReasons": { + "disabled": "Discovery отключён", + "non_streaming": "Непотоковый запрос", + "retry_not_allowed": "Эта конечная точка не разрешает повторы", + "provider_switch_not_allowed": "Эта конечная точка не разрешает смену провайдера", + "raw_passthrough": "Запрос с прямой передачей данных", + "unsupported_protocol": "Протокол не поддерживается Discovery", + "websocket": "Запрос WebSocket", + "streaming_hedge_disabled": "Потоковая гонка отключена для этого запроса", + "raw_cross_provider_fallback": "Включено прямое переключение между провайдерами", + "missing_session": "Нет идентификатора Session", + "missing_key": "Нет идентификатора API Key", + "rollout_ineligible": "Запрос не входит в текущий rollout", + "redis_capability_unavailable": "Возможность привязки Redis недоступна", + "binding_conflict": "Конфликт привязки Session", + "lease_conflict": "Lease Discovery занят другим запросом; этот запрос использует один upstream", + "lease_unavailable": "Lease Discovery недоступен", + "unknown": "Условия запуска Discovery не выполнены" + }, + "discoveryTitle": "Раунды Discovery", + "discoveryCompact": "{rounds} р. · {attempts} поп.", + "routeMode": "Способ маршрутизации", + "routeModes": { + "sticky": "Sticky", + "cold_start": "Холодный старт", + "rediscovery": "Повторное обнаружение" + }, + "finalProvider": "Итоговый провайдер", + "roundsLabel": "Раунды", + "attemptsLabel": "Попытки", + "winnerOrigin": "Источник победителя", + "viewDetails": "Открыть детали Discovery", + "stickyPhase": "Проверка Sticky", + "round": "Раунд {round}", + "attempts": "Попыток: {count}", + "noAttempts": "Попытки провайдеров не записаны", + "providerFallback": "Провайдер {id}", + "elapsed": "+{elapsed} мс", + "priority": "Приоритет {priority}", + "roles": { + "sticky": "Sticky", + "normal": "Кандидат", + "fallback": "Резерв" + }, + "winnerSources": { + "sticky": "Sticky", + "normal": "Обычный кандидат", + "fallback": "Перехват резервным провайдером" + }, + "roleTransition": "{from} → {to}", + "outcomes": { + "success": "Успешно", + "pending": "Ожидание", + "ready": "Готов", + "held": "Готов, удерживается", + "winner": "Победитель", + "failed": "Ошибка", + "cancelled": "Отменён", + "timeout": "Тайм-аут SLA", + "client_abort": "Клиент отключился", + "deadline": "Общий срок" + }, + "terminalOutcome": "Результат запроса", + "bindingResult": "Sticky-привязка", + "bindingActions": { + "create": "Создать", + "renew": "Продлить", + "clear": "Очистить", + "none": "Без изменений" + }, + "bindingFallbackWinner": "Не записано · Победитель резерва", + "bindingOutcomes": { + "updated": "Обновлено", + "cleared": "Очищено", + "skipped": "Пропущено", + "failed": "Ошибка", + "unknown": "Неизвестно" + }, + "fallbackPromoted": "Повышен до резерва", + "winnerCommitted": "Победитель подтверждён", + "roundStarted": "Раунд начат", + "events": { + "started": "Запрос запущен", + "ready": "Получены первые допустимые данные", + "held": "Выдача удержана правилами маршрутизации", + "fallbackPromoted": "Повышен до резерва", + "winnerCommitted": "Подтверждён как победитель", + "finished": "Завершено: {outcome}" + }, + "traceTruncated": "Часть событий опущена из-за ограничения хранилища.", + "summary": "Раундов: {rounds} · попыток: {attempts} · максимум активных: {maxActive}", + "config": "Снимок настроек", + "configConcurrency": "Параллельность", + "configMaxRounds": "Максимум раундов", + "configDiscoverySla": "SLA раунда", + "configStickySla": "SLA Sticky", + "configTotalTimeout": "Общий тайм-аут", + "configStickyCooldown": "Пауза после тайм-аута Sticky", + "configStickyBindingTtl": "Срок действия привязки Sticky (SESSION_TTL)", + "attemptDetails": { + "providerId": "ID провайдера", + "attempt": "Попытка", + "status": "Статус HTTP", + "inferred": "выведен", + "endpoint": "Upstream", + "error": "Ошибка upstream", + "cancellation": "Причина отмены", + "timeline": "Хронология попытки" + }, + "cancellationKinds": { + "discovery_loser": "Победила другая попытка Discovery", + "discovery_sla_timeout": "SLA Discovery истёк", + "round_timeout": "SLA раунда истёк", + "sticky_timeout": "SLA Sticky истёк", + "request_deadline": "Общий срок Discovery истёк", + "client_abort": "Клиент отключился", + "winner_committed": "Другой провайдер был выбран победителем" + } + }, "reasoningEffort": { "label": "Интенсивность рассуждений", "overridden": "Переопределено провайдером", @@ -387,6 +516,7 @@ }, "logicTrace": { "title": "Цепочка решений", + "singleRouteSelectionTitle": "Выбор провайдера в режиме защиты одним маршрутом", "noDecisionData": "Нет данных о решениях", "providersCount": "{count} поставщиков", "healthyCount": "{count} исправных", diff --git a/messages/ru/settings/config.json b/messages/ru/settings/config.json index 9624cf448..31aed5319 100644 --- a/messages/ru/settings/config.json +++ b/messages/ru/settings/config.json @@ -128,6 +128,8 @@ "stickySlaMs": "SLA Sticky (миллисекунды)", "racingTotalTimeoutMs": "Общий тайм-аут Discovery (миллисекунды)", "stickyTimeoutCooldownMs": "Пауза после тайм-аута Sticky (миллисекунды)", + "stickyBindingTtl": "Срок действия привязки Sticky", + "stickyBindingTtlDesc": "Только для чтения. Значение следует переменной окружения SESSION_TTL и используется другими снимками Session.", "discoveryWindowDesc": "Общий тайм-аут должен быть не меньше SLA Sticky + максимальное число раундов × SLA Discovery.", "discoveryWindowInvalid": "Общий тайм-аут Discovery меньше настроенного окна Sticky и Discovery.", "discoverySettingsInvalid": "Одно или несколько значений Discovery находятся вне допустимого диапазона.", diff --git a/messages/zh-CN/dashboard.json b/messages/zh-CN/dashboard.json index d707df578..b349765c7 100644 --- a/messages/zh-CN/dashboard.json +++ b/messages/zh-CN/dashboard.json @@ -380,6 +380,135 @@ "overridden": "已被供应商覆写", "tooltip": "客户端在请求体中声明的思考强度(output_config.effort),按原值显示,代理不会重命名或转换等级。" }, + "routingTrace": { + "title": "路由模式", + "modes": { + "discovery": "有界供应商 Discovery", + "legacy_hedge": "旧版 Hedge 竞速", + "legacy_serial": "旧版串行故障转移", + "single_upstream": "单上游模式", + "lease_conflict": "单路保护" + }, + "bypassed": "本请求未使用 Discovery:{reason}", + "bypassReasons": { + "disabled": "Discovery 未启用", + "non_streaming": "非流式请求", + "retry_not_allowed": "该端点不允许重试", + "provider_switch_not_allowed": "该端点不允许切换供应商", + "raw_passthrough": "原始透传请求", + "unsupported_protocol": "Discovery 不支持该协议", + "websocket": "WebSocket 请求", + "streaming_hedge_disabled": "本请求已禁用流式竞速", + "raw_cross_provider_fallback": "已启用原始跨供应商故障转移", + "missing_session": "缺少 Session 标识", + "missing_key": "缺少 API Key 标识", + "rollout_ineligible": "本请求不在当前灰度范围内", + "redis_capability_unavailable": "Redis 绑定能力不可用", + "binding_conflict": "Session 绑定状态冲突", + "lease_conflict": "另一个请求持有 Discovery 租约,本请求只使用一个上游", + "lease_unavailable": "Discovery 租约不可用", + "unknown": "未满足 Discovery 准入条件" + }, + "discoveryTitle": "Discovery 轮次", + "discoveryCompact": "{rounds} 轮 · {attempts} 次", + "routeMode": "路由方式", + "routeModes": { + "sticky": "Sticky", + "cold_start": "冷启动", + "rediscovery": "重新发现" + }, + "finalProvider": "最终供应商", + "roundsLabel": "轮次", + "attemptsLabel": "尝试", + "winnerOrigin": "赢家来源", + "viewDetails": "查看 Discovery 详情", + "stickyPhase": "Sticky 探测", + "round": "第 {round} 轮", + "attempts": "{count} 次尝试", + "noAttempts": "没有记录到供应商尝试", + "providerFallback": "供应商 {id}", + "elapsed": "+{elapsed}ms", + "priority": "优先级 {priority}", + "roles": { + "sticky": "Sticky", + "normal": "候选", + "fallback": "保底" + }, + "winnerSources": { + "sticky": "Sticky", + "normal": "正常候选", + "fallback": "保底接管" + }, + "roleTransition": "{from} → {to}", + "outcomes": { + "success": "成功", + "pending": "等待中", + "ready": "已就绪", + "held": "已就绪,暂缓交付", + "winner": "胜出", + "failed": "失败", + "cancelled": "已取消", + "timeout": "SLA 超时", + "client_abort": "客户端已断开", + "deadline": "总时限到期" + }, + "terminalOutcome": "请求结果", + "bindingResult": "Sticky 绑定", + "bindingActions": { + "create": "创建", + "renew": "续期", + "clear": "清除", + "none": "不变更" + }, + "bindingFallbackWinner": "未写入 · 保底赢家", + "bindingOutcomes": { + "updated": "已更新", + "cleared": "已清除", + "skipped": "已跳过", + "failed": "失败", + "unknown": "未知" + }, + "fallbackPromoted": "提升为保底请求", + "winnerCommitted": "已提交赢家", + "roundStarted": "轮次开始", + "events": { + "started": "请求已发出", + "ready": "已收到有效首字", + "held": "按路由规则暂缓交付", + "fallbackPromoted": "已提升为保底", + "winnerCommitted": "已提交为赢家", + "finished": "结束:{outcome}" + }, + "traceTruncated": "追踪事件达到存储上限,部分事件已省略。", + "summary": "{rounds} 轮 · {attempts} 次尝试 · 最大并发 {maxActive}", + "config": "配置快照", + "configConcurrency": "并发数", + "configMaxRounds": "最大轮数", + "configDiscoverySla": "每轮 SLA", + "configStickySla": "Sticky SLA", + "configTotalTimeout": "总超时", + "configStickyCooldown": "Sticky 超时冷却", + "configStickyBindingTtl": "Sticky 绑定有效期(SESSION_TTL)", + "attemptDetails": { + "providerId": "供应商 ID", + "attempt": "尝试次数", + "status": "HTTP 状态", + "inferred": "推断", + "endpoint": "端点", + "error": "上游错误", + "cancellation": "取消原因", + "timeline": "尝试时间线" + }, + "cancellationKinds": { + "discovery_loser": "其他 Discovery 尝试已胜出", + "discovery_sla_timeout": "Discovery SLA 已到期", + "round_timeout": "本轮 SLA 已到期", + "sticky_timeout": "Sticky SLA 已到期", + "request_deadline": "Discovery 总时限已到期", + "client_abort": "客户端已断开", + "winner_committed": "其他供应商已提交为赢家" + } + }, "reasoningEffort": { "label": "思考强度", "overridden": "已被供应商覆写", @@ -387,6 +516,7 @@ }, "logicTrace": { "title": "决策链", + "singleRouteSelectionTitle": "单路保护下的供应商选择", "noDecisionData": "暂无决策数据", "providersCount": "{count} 个供应商", "healthyCount": "{count} 个健康", diff --git a/messages/zh-CN/settings/config.json b/messages/zh-CN/settings/config.json index c4a94ea93..acc8fc05d 100644 --- a/messages/zh-CN/settings/config.json +++ b/messages/zh-CN/settings/config.json @@ -55,6 +55,8 @@ "stickySlaMs": "Sticky SLA(毫秒)", "racingTotalTimeoutMs": "Discovery 总超时(毫秒)", "stickyTimeoutCooldownMs": "Sticky 超时冷却(毫秒)", + "stickyBindingTtl": "Sticky 绑定有效期", + "stickyBindingTtlDesc": "只读;该值跟随环境变量 SESSION_TTL,并与其他 Session 快照共用。", "discoveryWindowDesc": "总超时必须不小于 Sticky SLA + 最大轮数 × Discovery SLA。Discovery 输家会取消,不走旧 Hedge 的 drain 或输家计费。", "discoveryWindowInvalid": "Discovery 总超时短于已配置的 Sticky 与 Discovery 窗口。", "discoverySettingsInvalid": "一个或多个 Discovery 配置值超出允许范围。", diff --git a/messages/zh-TW/dashboard.json b/messages/zh-TW/dashboard.json index c38f603d6..7cd29dc38 100644 --- a/messages/zh-TW/dashboard.json +++ b/messages/zh-TW/dashboard.json @@ -380,6 +380,135 @@ "overridden": "已被供應商覆寫", "tooltip": "用戶端在請求體中宣告的思考強度(output_config.effort),按原值顯示,代理不會重新命名或轉換等級。" }, + "routingTrace": { + "title": "路由方式", + "modes": { + "discovery": "有界供應商 Discovery", + "legacy_hedge": "舊版 Hedge 競速", + "legacy_serial": "舊版串行故障轉移", + "single_upstream": "單上游模式", + "lease_conflict": "單路保護" + }, + "bypassed": "本請求未使用 Discovery:{reason}", + "bypassReasons": { + "disabled": "Discovery 未啟用", + "non_streaming": "非串流請求", + "retry_not_allowed": "該端點不允許重試", + "provider_switch_not_allowed": "該端點不允許切換供應商", + "raw_passthrough": "原始透傳請求", + "unsupported_protocol": "Discovery 不支援該協議", + "websocket": "WebSocket 請求", + "streaming_hedge_disabled": "本請求已停用串流競速", + "raw_cross_provider_fallback": "已啟用原始跨供應商故障轉移", + "missing_session": "缺少 Session 識別", + "missing_key": "缺少 API Key 識別", + "rollout_ineligible": "本請求不在目前灰度範圍內", + "redis_capability_unavailable": "Redis 綁定能力不可用", + "binding_conflict": "Session 綁定狀態衝突", + "lease_conflict": "另一個請求持有 Discovery 租約,本請求只使用一個上游", + "lease_unavailable": "Discovery 租約不可用", + "unknown": "未滿足 Discovery 准入條件" + }, + "discoveryTitle": "Discovery 輪次", + "discoveryCompact": "{rounds} 輪 · {attempts} 次", + "routeMode": "路由類型", + "routeModes": { + "sticky": "Sticky", + "cold_start": "冷啟動", + "rediscovery": "重新發現" + }, + "finalProvider": "最終供應商", + "roundsLabel": "輪次", + "attemptsLabel": "嘗試", + "winnerOrigin": "贏家來源", + "viewDetails": "查看 Discovery 詳情", + "stickyPhase": "Sticky 探測", + "round": "第 {round} 輪", + "attempts": "{count} 次嘗試", + "noAttempts": "沒有記錄到供應商嘗試", + "providerFallback": "供應商 {id}", + "elapsed": "+{elapsed}ms", + "priority": "優先級 {priority}", + "roles": { + "sticky": "Sticky", + "normal": "候選", + "fallback": "備援" + }, + "winnerSources": { + "sticky": "Sticky", + "normal": "正常候選", + "fallback": "備援接管" + }, + "roleTransition": "{from} → {to}", + "outcomes": { + "success": "成功完成", + "pending": "處理中", + "ready": "已就緒", + "held": "已就緒,暫緩交付", + "winner": "勝出", + "failed": "失敗", + "cancelled": "已中止", + "timeout": "SLA 逾時", + "client_abort": "用戶端已斷開", + "deadline": "總時限到期" + }, + "terminalOutcome": "請求結果", + "bindingResult": "Sticky 綁定", + "bindingActions": { + "create": "建立", + "renew": "續期", + "clear": "解除綁定", + "none": "不變更" + }, + "bindingFallbackWinner": "未寫入 · 備援贏家", + "bindingOutcomes": { + "updated": "綁定已更新", + "cleared": "綁定已解除", + "skipped": "已略過", + "failed": "失敗", + "unknown": "未知狀態" + }, + "fallbackPromoted": "提升為保底請求", + "winnerCommitted": "已提交贏家", + "roundStarted": "輪次開始", + "events": { + "started": "請求已送出", + "ready": "已收到有效首個字元", + "held": "依路由規則暫緩交付", + "fallbackPromoted": "已提升為備援", + "winnerCommitted": "已提交為贏家", + "finished": "結束:{outcome}" + }, + "traceTruncated": "追蹤事件達到儲存上限,部分事件已省略。", + "summary": "{rounds} 輪 · {attempts} 次嘗試 · 最大並發 {maxActive}", + "config": "設定快照", + "configConcurrency": "並發數", + "configMaxRounds": "最大輪數", + "configDiscoverySla": "每輪 SLA", + "configStickySla": "Sticky SLA", + "configTotalTimeout": "總逾時", + "configStickyCooldown": "Sticky 逾時冷卻", + "configStickyBindingTtl": "Sticky 綁定有效期(SESSION_TTL)", + "attemptDetails": { + "providerId": "供應商 ID", + "attempt": "嘗試次數", + "status": "HTTP 狀態", + "inferred": "推斷", + "endpoint": "端點", + "error": "上游錯誤", + "cancellation": "取消緣由", + "timeline": "嘗試時間線" + }, + "cancellationKinds": { + "discovery_loser": "其他 Discovery 嘗試已勝出", + "discovery_sla_timeout": "Discovery SLA 已逾期", + "round_timeout": "本輪 SLA 已到期", + "sticky_timeout": "Sticky SLA 已逾期", + "request_deadline": "Discovery 總時限已到期", + "client_abort": "客戶端已斷開", + "winner_committed": "其他供應商已提交為贏家" + } + }, "reasoningEffort": { "label": "思考強度", "overridden": "已被供應商覆寫", @@ -387,6 +516,7 @@ }, "logicTrace": { "title": "決策鏈", + "singleRouteSelectionTitle": "單路保護下的供應商選擇", "noDecisionData": "暫無決策資料", "providersCount": "{count} 個供應商", "healthyCount": "{count} 個健康", diff --git a/messages/zh-TW/settings/config.json b/messages/zh-TW/settings/config.json index 613494c92..7a166f96c 100644 --- a/messages/zh-TW/settings/config.json +++ b/messages/zh-TW/settings/config.json @@ -128,6 +128,8 @@ "stickySlaMs": "Sticky 等待 SLA(毫秒)", "racingTotalTimeoutMs": "Discovery 總逾時(毫秒)", "stickyTimeoutCooldownMs": "Sticky 逾時冷卻(毫秒)", + "stickyBindingTtl": "Sticky 綁定有效期", + "stickyBindingTtlDesc": "唯讀;此值跟隨環境變數 SESSION_TTL,並與其他 Session 快照共用。", "discoveryWindowDesc": "總逾時必須不小於 Sticky SLA + 最大輪數 × Discovery SLA。Discovery 輸家會取消,不走舊 Hedge 的 drain 或輸家計費。", "discoveryWindowInvalid": "Discovery 總逾時短於已設定的 Sticky 與 Discovery 視窗。", "discoverySettingsInvalid": "一個或多個 Discovery 設定值超出允許範圍。", diff --git a/src/actions/usage-logs.ts b/src/actions/usage-logs.ts index e75f71fe3..026412b97 100644 --- a/src/actions/usage-logs.ts +++ b/src/actions/usage-logs.ts @@ -701,6 +701,7 @@ export async function getUsageLogsBatch( const snapshot = liveData.get(key); if (snapshot) { row._liveChain = snapshot; + row.routingTrace = snapshot.routingTrace ?? row.routingTrace; } } } 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 4926a53e5..6e12bdc8a 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 @@ -5,8 +5,10 @@ import { act } from "react"; import { NextIntlClientProvider } from "next-intl"; import { Window } from "happy-dom"; import { beforeEach, describe, expect, test, vi } from "vitest"; +import dashboardMessages from "../../../../../../messages/en/dashboard.json"; import ipDetailsMessages from "../../../../../../messages/en/ipDetails.json"; import providerChainMessages from "../../../../../../messages/en/provider-chain.json"; +import type { RoutingTraceV1 } from "@/types/routing-trace"; const hasSessionMessagesMock = vi.fn(); @@ -206,6 +208,8 @@ const messages = { endpoint: "Endpoint", }, details: { + routingTrace: dashboardMessages.logs.details.routingTrace, + modelAudit: dashboardMessages.logs.details.modelAudit, title: "Request Details", inProgress: "In progress", statusTitle: "Status: {status}", @@ -287,6 +291,7 @@ const messages = { }, logicTrace: { title: "Decision Chain", + singleRouteSelectionTitle: "Provider selection under single-route protection", noDecisionData: "No decision data", providersCount: "{count} providers", healthyCount: "{count} healthy", @@ -1216,6 +1221,628 @@ describe("error-details-dialog tabs", () => { }); }); +describe("error-details-dialog routing trace", () => { + const discoveryTrace: RoutingTraceV1 = { + version: 1, + mode: "discovery", + startedAt: 1_000, + updatedAt: 13_000, + discoveryEnabled: true, + eligible: true, + config: { + discoveryConcurrency: 2, + maxDiscoveryRounds: 2, + discoverySlaMs: 10_000, + stickySlaMs: 20_000, + racingTotalTimeoutMs: 60_000, + stickyTimeoutCooldownMs: 300_000, + sessionTtlSeconds: 300, + }, + events: [ + { + type: "attempt_started", + at: 1_000, + elapsedMs: 0, + round: 0, + attemptId: "sticky:1", + attemptKind: "sticky", + provider: { id: 10, name: "sticky-provider", priority: 1 }, + }, + { + type: "attempt_finished", + at: 3_000, + elapsedMs: 2_000, + round: 0, + attemptId: "sticky:1", + attemptKind: "sticky", + provider: { id: 10, name: "sticky-provider", priority: 1 }, + outcome: "cancelled", + cancellationKind: "sticky_timeout", + }, + { + type: "round_started", + at: 3_000, + elapsedMs: 2_000, + round: 1, + }, + { + type: "attempt_started", + at: 3_010, + elapsedMs: 2_010, + round: 1, + attemptId: "normal:1", + attemptKind: "normal", + provider: { id: 11, name: "candidate-a", priority: 1 }, + }, + { + type: "attempt_held", + at: 3_100, + elapsedMs: 2_100, + round: 1, + attemptId: "normal:1", + attemptKind: "normal", + provider: { id: 11, name: "candidate-a", priority: 1 }, + }, + { + type: "attempt_started", + at: 3_020, + elapsedMs: 2_020, + round: 1, + attemptId: "normal:2", + attemptKind: "normal", + provider: { id: 12, name: "candidate-b", priority: 2 }, + }, + { + type: "winner_committed", + at: 4_000, + elapsedMs: 3_000, + round: 1, + attemptId: "normal:2", + attemptKind: "normal", + provider: { id: 12, name: "candidate-b", priority: 2 }, + statusCode: 200, + }, + ], + summary: { + outcome: "success", + statusCode: 200, + durationMs: 12_000, + ttfbMs: 3_000, + attemptsPerRequest: 3, + maxActiveAttempts: 2, + rounds: 1, + providerMs: 5_000, + fallbackPromotions: 0, + cancelFailures: 0, + winnerOrigin: "normal", + winnerProviderId: 12, + winnerRound: 1, + }, + }; + + test("renders Sticky and same-round attempts in the Discovery branch", () => { + const html = renderWithIntl( + + ); + const document = parseHtml(html); + + expect(document.querySelector("[data-testid='routing-mode-banner']")?.textContent).toContain( + "Bounded Discovery" + ); + expect(document.querySelector("[data-testid='discovery-round-0']")?.textContent).toContain( + "sticky-provider" + ); + const roundOne = document.querySelector("[data-testid='discovery-round-1']"); + expect(roundOne?.textContent).toContain("candidate-a"); + expect(roundOne?.textContent).toContain("candidate-b"); + expect(roundOne?.textContent).toContain("Candidate"); + expect(roundOne?.textContent).not.toContain("Candidate → Fallback"); + expect(roundOne?.textContent).toContain("Ready, held"); + expect(roundOne?.textContent).toContain("Winner"); + expect(roundOne?.querySelector(".sm\\:grid-cols-2")).not.toBeNull(); + expect(html).toContain("60000ms"); + expect(html).toContain("300000ms"); + expect(html).toContain("300s"); + }); + + test("shows fallback promotion and humanizes the fallback winner binding result", () => { + const fallbackTrace: RoutingTraceV1 = { + ...discoveryTrace, + events: [ + { + type: "attempt_started", + at: 1_000, + elapsedMs: 0, + round: 1, + attemptId: "normal:1", + attemptKind: "normal", + provider: { id: 80, name: "slow-provider", priority: 1 }, + }, + { + type: "fallback_promoted", + at: 11_000, + elapsedMs: 10_000, + round: 1, + attemptId: "normal:1", + attemptKind: "fallback", + provider: { id: 80, name: "slow-provider", priority: 1 }, + outcome: "timeout", + }, + { + type: "winner_committed", + at: 20_000, + elapsedMs: 19_000, + round: 1, + attemptId: "normal:1", + attemptKind: "fallback", + provider: { id: 80, name: "slow-provider", priority: 1 }, + statusCode: 200, + }, + { + type: "request_finished", + at: 21_000, + elapsedMs: 20_000, + outcome: "success", + statusCode: 200, + }, + { + type: "binding_finalized", + at: 21_100, + elapsedMs: 20_100, + provider: { id: 80, name: "slow-provider" }, + bindingAction: "none", + outcome: "skipped", + reason: "fallback_winner", + }, + ], + summary: { + ...discoveryTrace.summary!, + durationMs: 20_000, + ttfbMs: 19_000, + attemptsPerRequest: 1, + rounds: 1, + fallbackPromotions: 1, + winnerOrigin: "fallback", + winnerProviderId: 80, + winnerRound: 1, + }, + }; + + const html = renderWithIntl( + + ); + const document = parseHtml(html); + const fallbackCard = document.querySelector("[data-testid='discovery-attempt']"); + const terminal = document.querySelector("[data-testid='discovery-terminal-status']"); + const bindingSummary = document.querySelector("[data-testid='discovery-binding-summary']"); + + expect(fallbackCard?.textContent).toContain("Candidate → Fallback"); + expect(fallbackCard?.textContent).toContain("Winner"); + expect(terminal?.textContent).toContain("Not written · Fallback winner"); + expect(terminal?.textContent).not.toContain("fallback_winner"); + expect(bindingSummary?.getAttribute("title")).toBe("fallback_winner"); + expect(terminal?.textContent).not.toContain("No change · Skipped"); + + const normalWinnerHtml = renderWithIntl( + + ); + const normalWinnerTerminal = parseHtml(normalWinnerHtml).querySelector( + "[data-testid='discovery-terminal-status']" + ); + expect(normalWinnerTerminal?.textContent).toContain("No change · Skipped"); + expect(normalWinnerTerminal?.textContent).toContain("fallback_winner"); + expect(normalWinnerTerminal?.textContent).not.toContain("Not written · Fallback winner"); + }); + + test("shows Sticky promotion without inventing a source role for truncated traces", () => { + const { summary: _summary, ...traceWithoutSummary } = discoveryTrace; + const renderRole = (events: RoutingTraceV1["events"], truncated = false) => { + const html = renderWithIntl( + + ); + return parseHtml(html).querySelector("[data-testid='discovery-attempt']")?.textContent ?? ""; + }; + + const stickyRole = renderRole([ + { + type: "attempt_started", + at: 1_000, + elapsedMs: 0, + round: 0, + attemptId: "sticky:1", + attemptKind: "sticky", + provider: { id: 80, name: "sticky-provider", priority: 1 }, + }, + { + type: "fallback_promoted", + at: 21_000, + elapsedMs: 20_000, + round: 0, + attemptId: "sticky:1", + attemptKind: "fallback", + provider: { id: 80, name: "sticky-provider", priority: 1 }, + }, + ]); + const truncatedFallbackRole = renderRole( + [ + { + type: "fallback_promoted", + at: 21_000, + elapsedMs: 20_000, + round: 1, + attemptId: "fallback:1", + attemptKind: "fallback", + provider: { id: 80, name: "fallback-provider", priority: 1 }, + }, + ], + true + ); + + expect(stickyRole).toContain("Sticky → Fallback"); + expect(truncatedFallbackRole).toContain("Fallback"); + expect(truncatedFallbackRole).not.toContain("Candidate → Fallback"); + expect(truncatedFallbackRole).not.toContain("Sticky → Fallback"); + }); + + test("expands exact Discovery attempts with sanitized provider details and cancellation reasons", () => { + const longSecondError = + " second-attempt-429 " + "x".repeat(8_200) + "TAIL_NOT_RENDERED"; + const detailedTrace: RoutingTraceV1 = { + version: 1, + mode: "discovery", + startedAt: 1_000, + updatedAt: 5_000, + discoveryEnabled: true, + eligible: true, + events: [ + { + type: "attempt_started", + at: 1_000, + elapsedMs: 0, + round: 1, + attemptId: "80:1", + attemptKind: "normal", + provider: { id: 80, name: "same-provider", priority: 1 }, + }, + { + type: "attempt_finished", + at: 1_500, + elapsedMs: 500, + round: 1, + attemptId: "80:1", + attemptKind: "normal", + provider: { id: 80, name: "same-provider", priority: 1 }, + outcome: "failed", + statusCode: 403, + }, + { + type: "attempt_started", + at: 1_600, + elapsedMs: 600, + round: 1, + attemptId: "80:2", + attemptKind: "normal", + provider: { id: 80, name: "same-provider", priority: 1 }, + }, + { + type: "attempt_finished", + at: 2_000, + elapsedMs: 1_000, + round: 1, + attemptId: "80:2", + attemptKind: "normal", + provider: { id: 80, name: "same-provider", priority: 1 }, + outcome: "failed", + statusCode: 429, + }, + { + type: "attempt_started", + at: 2_100, + elapsedMs: 1_100, + round: 1, + attemptId: "91:3", + attemptKind: "normal", + provider: { id: 91, name: "legacy-provider", priority: 2 }, + }, + { + type: "attempt_finished", + at: 2_500, + elapsedMs: 1_500, + round: 1, + attemptId: "91:3", + attemptKind: "normal", + provider: { id: 91, name: "legacy-provider", priority: 2 }, + outcome: "failed", + statusCode: 503, + }, + { + type: "attempt_started", + at: 2_600, + elapsedMs: 1_600, + round: 1, + attemptId: "56:4", + attemptKind: "normal", + provider: { id: 56, name: "cancelled-provider", priority: 3 }, + }, + { + type: "attempt_finished", + at: 5_000, + elapsedMs: 4_000, + round: 1, + attemptId: "56:4", + attemptKind: "normal", + provider: { id: 56, name: "cancelled-provider", priority: 3 }, + outcome: "cancelled", + cancellationKind: "discovery_sla_timeout", + }, + ], + }; + const { container, unmount } = renderClientWithIntl( + + ); + + const attempts = Array.from( + container.querySelectorAll("[data-testid='discovery-attempt']") + ); + const secondAttempt = attempts.find((card) => card.textContent?.includes("HTTP 429")); + const secondToggle = + secondAttempt?.querySelector("[data-testid='discovery-attempt-toggle']") ?? null; + expect(secondToggle?.getAttribute("aria-expanded")).toBe("false"); + click(secondToggle); + expect(secondToggle?.getAttribute("aria-expanded")).toBe("true"); + expect(secondAttempt?.textContent).toContain("second-attempt-429"); + expect(secondAttempt?.textContent).not.toContain("first-attempt-403"); + expect(secondAttempt?.textContent).toContain("https://api.example.com/v2"); + expect(secondAttempt?.textContent).not.toContain("secret-two"); + expect(secondAttempt?.textContent).not.toContain("TAIL_NOT_RENDERED"); + expect(secondAttempt?.querySelector("script")).toBeNull(); + + const legacyAttempt = attempts.find((card) => card.textContent?.includes("HTTP 503")); + click(legacyAttempt?.querySelector("[data-testid='discovery-attempt-toggle']") ?? null); + expect(legacyAttempt?.textContent).toContain("third-attempt-503"); + expect(legacyAttempt?.textContent).toContain("https://api.example.com/v3"); + expect(legacyAttempt?.textContent).not.toContain("endpoint-user"); + expect(legacyAttempt?.textContent).not.toContain("endpoint-secret"); + expect(legacyAttempt?.textContent).not.toContain("abcdefghijklmnopqrstuvwxyz123456"); + expect(legacyAttempt?.textContent).not.toContain("bearer-secret-value"); + + const cancelledAttempt = attempts.find((card) => + card.textContent?.includes("cancelled-provider") + ); + click(cancelledAttempt?.querySelector("[data-testid='discovery-attempt-toggle']") ?? null); + expect(cancelledAttempt?.textContent).toContain("Discovery SLA expired"); + expect(cancelledAttempt?.textContent).not.toContain("Upstream error"); + unmount(); + }); + + test("labels a lease conflict as single-route protection while retaining provider selection", () => { + const protectedTrace: RoutingTraceV1 = { + version: 1, + mode: "single_upstream", + startedAt: 1_000, + updatedAt: 2_000, + discoveryEnabled: true, + eligible: false, + bypassReason: "lease_conflict", + events: [], + }; + const html = renderWithIntl( + + ); + + expect(html).toContain("Single-route protection"); + expect(html).toContain("Provider selection under single-route protection"); + expect(html).toContain("protected-provider"); + }); + + test("shows a legacy mode and bypass reason while retaining the old chain", () => { + const legacyTrace: RoutingTraceV1 = { + version: 1, + mode: "legacy_serial", + startedAt: 1_000, + updatedAt: 2_000, + discoveryEnabled: true, + eligible: false, + bypassReason: "non_streaming", + events: [], + }; + const html = renderWithIntl( + + ); + + expect(html).toContain("Legacy serial fallback"); + expect(html).toContain("Non-streaming request"); + expect(html).toContain("legacy-provider"); + expect(html).not.toContain("Discovery rounds"); + }); + + test("shows late terminal failure and Sticky binding result after a first-byte winner", () => { + const failedTrace: RoutingTraceV1 = { + ...discoveryTrace, + events: [ + ...discoveryTrace.events, + { + type: "request_finished", + at: 5_000, + elapsedMs: 4_000, + outcome: "failed", + statusCode: 502, + }, + { + type: "binding_finalized", + at: 5_100, + elapsedMs: 4_100, + provider: { id: 12, name: "candidate-b" }, + bindingAction: "renew", + outcome: "skipped", + reason: "generation_conflict", + }, + ], + summary: { ...discoveryTrace.summary!, outcome: "failed", statusCode: 502 }, + }; + const html = renderWithIntl( + + ); + const document = parseHtml(html); + const terminal = document.querySelector("[data-testid='discovery-terminal-status']"); + + expect(terminal?.textContent).toContain("Request result"); + expect(terminal?.textContent).toContain("Failed"); + expect(terminal?.textContent).toContain("HTTP 502"); + expect(terminal?.textContent).toContain("Sticky binding"); + expect(terminal?.textContent).toContain("Renew"); + expect(terminal?.textContent).toContain("generation_conflict"); + expect(document.querySelector("[data-testid='discovery-round-1']")?.textContent).toContain( + "Failed" + ); + }); + + test("derives live attempt concurrency while the terminal summary is not available", () => { + const { summary: _summary, ...liveTrace } = discoveryTrace; + const html = renderWithIntl( + + ); + const document = parseHtml(html); + const discoveryView = document.querySelector("[data-testid='discovery-trace-view']"); + + expect(discoveryView?.textContent).toContain("1 rounds · 3 attempts · max 2 active"); + expect(discoveryView?.textContent).not.toContain("0 attempts · max 0 active"); + }); + + test("falls back to the old chain for an unsupported trace version", () => { + const html = renderWithIntl( + + ); + + expect(html).toContain("old-provider"); + expect(html).not.toContain("Bounded Discovery"); + }); +}); + describe("error-details-dialog origin decision chain", () => { test("shows origin chain trigger for session reuse flow with sessionId", () => { const html = renderWithIntl( 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 new file mode 100644 index 000000000..0905540b1 --- /dev/null +++ b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/DiscoveryTraceView.tsx @@ -0,0 +1,877 @@ +"use client"; + +import { + CheckCircle, + ChevronDown, + ChevronRight, + CircleDot, + Clock3, + GitBranch, + Link2, + Server, + ShieldCheck, + XCircle, + Zap, +} from "lucide-react"; +import { useTranslations } from "next-intl"; +import { useState } from "react"; +import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/utils"; +import { redactJsonString } from "@/lib/utils/message-redaction"; +import { sanitizeErrorTextForDetail } from "@/lib/utils/upstream-error-detection"; +import type { ProviderChainItem } from "@/types/message"; +import type { RoutingTraceV1 } from "@/types/routing-trace"; + +const KNOWN_BYPASS_REASONS = new Set([ + "disabled", + "non_streaming", + "retry_not_allowed", + "provider_switch_not_allowed", + "raw_passthrough", + "unsupported_protocol", + "websocket", + "streaming_hedge_disabled", + "raw_cross_provider_fallback", + "missing_session", + "missing_key", + "rollout_ineligible", + "redis_capability_unavailable", + "binding_conflict", + "lease_conflict", + "lease_unavailable", +]); + +const KNOWN_CANCELLATION_KINDS = new Set([ + "discovery_loser", + "discovery_sla_timeout", + "round_timeout", + "sticky_timeout", + "request_deadline", + "client_abort", + "winner_committed", +]); + +type TraceRecord = Record; + +type AttemptView = { + id: string; + providerId: number | null; + providerName: string | null; + sequence: number | null; + round: number; + role: "sticky" | "normal" | "fallback"; + promotedFrom: "sticky" | "normal" | null; + priority: number | null; + startedAt: number | null; + elapsedMs: number | null; + outcome: + | "pending" + | "ready" + | "held" + | "winner" + | "failed" + | "cancelled" + | "timeout" + | "client_abort" + | "deadline"; + statusCode: number | null; + cancellationKind: string | null; + reason: string | null; + fallbackPromoted: boolean; + winnerCommitted: boolean; + chainItem: ProviderChainItem | null; + history: Array<{ + type: string; + elapsedMs: number | null; + outcome: AttemptView["outcome"] | null; + cancellationKind: string | null; + reason: string | null; + }>; +}; + +function asRecord(value: unknown): TraceRecord { + return value && typeof value === "object" && !Array.isArray(value) ? (value as TraceRecord) : {}; +} + +function asString(value: unknown): string | null { + return typeof value === "string" && value.length > 0 ? value : null; +} + +function asNumber(value: unknown): number | null { + return typeof value === "number" && Number.isFinite(value) ? value : null; +} + +function parseAttemptSequence(attemptId: string): number | null { + const match = /:(\d+)$/.exec(attemptId); + return match ? Number(match[1]) : null; +} + +function truncateForDisplay(value: string, maxLength = 8_192): string { + return value.length > maxLength ? `${value.slice(0, maxLength)}\n...` : value; +} + +function sanitizeEndpoint(value: string | undefined): string | null { + if (!value) return null; + try { + const url = new URL(value); + url.username = ""; + url.password = ""; + url.search = ""; + url.hash = ""; + return truncateForDisplay(url.toString(), 2_048); + } catch { + return truncateForDisplay(value.split(/[?#]/, 1)[0], 2_048); + } +} + +function getChainErrorMessage(item: ProviderChainItem | null): string | null { + if (!item) return null; + const sanitize = (value: string) => + truncateForDisplay(sanitizeErrorTextForDetail(redactJsonString(value))); + if (item.errorMessage) return sanitize(item.errorMessage); + if (item.errorDetails?.provider?.upstreamBody) { + return sanitize(item.errorDetails.provider.upstreamBody); + } + if (item.errorDetails?.system?.errorMessage) { + return sanitize(item.errorDetails.system.errorMessage); + } + if (item.errorDetails?.clientError) return sanitize(item.errorDetails.clientError); + return null; +} + +function buildProviderChainLookup(providerChain: ProviderChainItem[]): { + byAttempt: Map; + uniqueByProvider: Map; +} { + const byAttempt = new Map(); + const byProvider = new Map(); + for (const item of providerChain) { + const items = byProvider.get(item.id) ?? []; + items.push(item); + byProvider.set(item.id, items); + if (Number.isFinite(item.attemptNumber)) { + byAttempt.set(`${item.id}:${item.attemptNumber}`, item); + } + } + + const uniqueByProvider = new Map(); + for (const [providerId, items] of byProvider) { + if (items.length === 1) uniqueByProvider.set(providerId, items[0]); + } + return { byAttempt, uniqueByProvider }; +} + +function findChainItem( + attemptId: string, + providerId: number | null, + lookup: ReturnType +): ProviderChainItem | null { + if (providerId == null) return null; + const sequence = parseAttemptSequence(attemptId); + if (sequence != null) { + const exact = lookup.byAttempt.get(`${providerId}:${sequence}`); + if (exact) return exact; + } + return lookup.uniqueByProvider.get(providerId) ?? null; +} + +function eventType(event: TraceRecord): string { + return asString(event.type) ?? asString(event.event) ?? "unknown"; +} + +function eventProvider(event: TraceRecord): { + id: number | null; + name: string | null; + priority: number | null; +} { + const provider = asRecord(event.provider); + return { + id: asNumber(event.providerId) ?? asNumber(provider.id), + name: asString(event.providerName) ?? asString(provider.name), + priority: asNumber(event.priority) ?? asNumber(provider.priority), + }; +} + +function normalizeRole(value: unknown, round: number): AttemptView["role"] { + if (value === "fallback") return "fallback"; + if (value === "sticky" || round === 0) return "sticky"; + return "normal"; +} + +function normalizeOutcome(value: unknown): AttemptView["outcome"] | null { + switch (value) { + case "pending": + case "ready": + case "held": + case "winner": + case "failed": + case "cancelled": + case "timeout": + case "client_abort": + case "deadline": + return value; + case "sla_timeout": + return "timeout"; + default: + return null; + } +} + +function normalizeTerminalOutcome( + value: unknown +): "success" | "failed" | "client_abort" | "deadline" | "pending" { + switch (value) { + case "success": + case "failed": + case "client_abort": + case "deadline": + return value; + default: + return "pending"; + } +} + +function applyEventOutcome( + attempt: AttemptView, + type: string, + event: TraceRecord, + roleBeforeEvent: AttemptView["role"] | null +): void { + const explicit = normalizeOutcome(event.outcome); + if (explicit) attempt.outcome = explicit; + + switch (type) { + case "attempt_ready": + attempt.outcome = "ready"; + break; + case "attempt_held": + attempt.outcome = "held"; + break; + case "attempt_failed": + attempt.outcome = "failed"; + break; + case "attempt_cancelled": + attempt.outcome = "cancelled"; + break; + case "fallback_promoted": + if (roleBeforeEvent && roleBeforeEvent !== "fallback") { + attempt.promotedFrom = roleBeforeEvent; + } + attempt.role = "fallback"; + attempt.fallbackPromoted = true; + break; + case "winner_committed": + attempt.outcome = "winner"; + attempt.winnerCommitted = true; + break; + } + + const cancellationKind = asString(event.cancellationKind); + if (cancellationKind === "client_abort") attempt.outcome = "client_abort"; + if (cancellationKind === "request_deadline") attempt.outcome = "deadline"; + if ( + cancellationKind === "discovery_sla_timeout" || + cancellationKind === "round_timeout" || + cancellationKind === "sticky_timeout" + ) { + attempt.outcome = "timeout"; + } +} + +function buildAttempts(trace: RoutingTraceV1, providerChain: ProviderChainItem[]): AttemptView[] { + const attempts = new Map(); + const lookup = buildProviderChainLookup(providerChain); + + for (const rawEvent of trace.events) { + const event = asRecord(rawEvent); + const type = eventType(event); + const provider = eventProvider(event); + const attemptId = + asString(event.attemptId) ?? + (provider.id != null && type.startsWith("attempt_") + ? `${provider.id}:${asNumber(event.sequence) ?? attempts.size + 1}` + : null); + if (!attemptId) continue; + + const round = Math.max(0, asNumber(event.round) ?? 0); + const existing = attempts.get(attemptId); + const initialRole = normalizeRole(event.attemptKind ?? event.role ?? event.kind, round); + const attempt: AttemptView = existing ?? { + id: attemptId, + providerId: provider.id, + providerName: provider.name, + sequence: parseAttemptSequence(attemptId), + round, + role: initialRole, + promotedFrom: null, + priority: provider.priority, + startedAt: null, + elapsedMs: null, + outcome: "pending", + statusCode: null, + cancellationKind: null, + reason: null, + fallbackPromoted: false, + winnerCommitted: false, + chainItem: findChainItem(attemptId, provider.id, lookup), + history: [], + }; + + attempt.providerId ??= provider.id; + attempt.providerName ??= provider.name; + attempt.sequence ??= parseAttemptSequence(attempt.id); + attempt.chainItem ??= findChainItem(attempt.id, attempt.providerId, lookup); + attempt.round = Math.max(attempt.round, round); + attempt.priority ??= provider.priority; + attempt.statusCode ??= asNumber(event.statusCode); + attempt.cancellationKind ??= asString(event.cancellationKind); + attempt.reason ??= asString(event.reason); + const elapsedMs = asNumber(event.elapsedMs); + if (type === "attempt_started") attempt.startedAt = elapsedMs; + if (elapsedMs != null) attempt.elapsedMs = elapsedMs; + const roleBeforeEvent = existing ? attempt.role : null; + attempt.role = + attempt.role === "fallback" + ? "fallback" + : normalizeRole( + event.attemptKind ?? event.role ?? event.kind ?? attempt.role, + attempt.round + ); + applyEventOutcome(attempt, type, event, roleBeforeEvent); + if ( + type === "attempt_started" || + type === "attempt_ready" || + type === "attempt_held" || + type === "attempt_finished" || + type === "fallback_promoted" || + type === "winner_committed" + ) { + attempt.history.push({ + type, + elapsedMs, + outcome: normalizeOutcome(event.outcome), + cancellationKind: asString(event.cancellationKind), + reason: asString(event.reason), + }); + } + attempts.set(attemptId, attempt); + } + + const terminalEvent = trace.events.findLast((event) => event.type === "request_finished"); + const terminalOutcome = normalizeTerminalOutcome(terminalEvent?.outcome); + if (terminalEvent && terminalOutcome !== "success") { + for (const attempt of attempts.values()) { + if (!attempt.winnerCommitted) continue; + attempt.outcome = + terminalOutcome === "client_abort" || terminalOutcome === "deadline" + ? terminalOutcome + : "failed"; + attempt.statusCode = terminalEvent?.statusCode ?? attempt.statusCode; + } + } + + return [...attempts.values()].sort((a, b) => { + if (a.round !== b.round) return a.round - b.round; + return (a.startedAt ?? Number.MAX_SAFE_INTEGER) - (b.startedAt ?? Number.MAX_SAFE_INTEGER); + }); +} + +function numberFrom(record: TraceRecord, ...keys: string[]): number | null { + for (const key of keys) { + const value = asNumber(record[key]); + if (value != null) return value; + } + return null; +} + +function deriveRuntimeStats(trace: RoutingTraceV1): { + rounds: number; + attemptCount: number; + maxActive: number; +} { + const startedAttempts = new Set(); + const activeAttempts = new Set(); + let anonymousAttempts = 0; + let maxActive = 0; + let maxRound = 0; + + for (const event of trace.events) { + if (typeof event.round === "number" && Number.isFinite(event.round)) { + maxRound = Math.max(maxRound, event.round); + } + + if (event.type === "attempt_started") { + if (!event.attemptId) { + anonymousAttempts += 1; + continue; + } + if (!startedAttempts.has(event.attemptId)) { + startedAttempts.add(event.attemptId); + activeAttempts.add(event.attemptId); + maxActive = Math.max(maxActive, activeAttempts.size); + } + continue; + } + + if (event.type === "attempt_finished" && event.attemptId) { + activeAttempts.delete(event.attemptId); + } + } + + return { + rounds: maxRound, + attemptCount: startedAttempts.size + anonymousAttempts, + maxActive, + }; +} + +function outcomeStyle(outcome: AttemptView["outcome"]): { + icon: typeof Server; + className: string; +} { + switch (outcome) { + case "winner": + return { + icon: CheckCircle, + className: + "border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-800 dark:bg-emerald-950/20 dark:text-emerald-300", + }; + case "failed": + return { + icon: XCircle, + className: + "border-rose-200 bg-rose-50 text-rose-700 dark:border-rose-800 dark:bg-rose-950/20 dark:text-rose-300", + }; + case "cancelled": + case "client_abort": + case "deadline": + return { + icon: XCircle, + className: + "border-slate-200 bg-slate-50 text-slate-600 dark:border-slate-700 dark:bg-slate-900/40 dark:text-slate-300", + }; + case "held": + case "timeout": + return { + icon: Clock3, + className: + "border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-800 dark:bg-amber-950/20 dark:text-amber-300", + }; + case "ready": + return { + icon: ShieldCheck, + className: + "border-blue-200 bg-blue-50 text-blue-700 dark:border-blue-800 dark:bg-blue-950/20 dark:text-blue-300", + }; + default: + return { + icon: Server, + className: "border-border bg-muted/30 text-muted-foreground dark:bg-slate-900/30", + }; + } +} + +export function RoutingModeBanner({ trace }: { trace: RoutingTraceV1 }) { + const t = useTranslations("dashboard.logs.details.routingTrace"); + const isLeaseConflict = + trace.mode === "single_upstream" && trace.bypassReason === "lease_conflict"; + const Icon = + trace.mode === "discovery" + ? Zap + : isLeaseConflict + ? ShieldCheck + : trace.mode === "single_upstream" + ? Server + : GitBranch; + const rawReason = trace.bypassReason; + const reason = + rawReason && KNOWN_BYPASS_REASONS.has(rawReason) ? rawReason : rawReason ? "unknown" : null; + + return ( +
+
+ + {t("title")} + + {isLeaseConflict ? t("modes.lease_conflict") : t(`modes.${trace.mode}`)} + +
+ {reason && ( +

+ {t("bypassed", { reason: t(`bypassReasons.${reason}`) })} +

+ )} +
+ ); +} + +export function DiscoveryTraceView({ + trace, + providerChain = [], +}: { + trace: RoutingTraceV1; + providerChain?: ProviderChainItem[]; +}) { + const t = useTranslations("dashboard.logs.details.routingTrace"); + const attempts = buildAttempts(trace, providerChain); + const grouped = new Map(); + for (const attempt of attempts) { + const group = grouped.get(attempt.round) ?? []; + group.push(attempt); + grouped.set(attempt.round, group); + } + + const summary = asRecord(trace.summary); + const config = asRecord(trace.config); + const runtimeStats = deriveRuntimeStats(trace); + const rounds = + numberFrom(summary, "rounds", "roundsVisited") ?? + Math.max(runtimeStats.rounds, ...attempts.map((attempt) => attempt.round)); + const attemptCount = + numberFrom(summary, "attemptsPerRequest", "attempts", "attemptsStarted") ?? + Math.max(runtimeStats.attemptCount, attempts.length); + const maxActive = numberFrom(summary, "maxActive", "maxActiveAttempts") ?? runtimeStats.maxActive; + 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); + const bindingOutcome = + bindingEvent?.outcome === "updated" || + bindingEvent?.outcome === "cleared" || + bindingEvent?.outcome === "skipped" || + bindingEvent?.outcome === "failed" + ? bindingEvent.outcome + : "unknown"; + const isFallbackWinnerBinding = + bindingEvent?.bindingAction === "none" && + bindingOutcome === "skipped" && + bindingEvent.reason === "fallback_winner" && + asString(summary.winnerOrigin) === "fallback"; + const bindingSummary = isFallbackWinnerBinding + ? t("bindingFallbackWinner") + : bindingEvent + ? `${t(`bindingActions.${bindingEvent.bindingAction ?? "none"}`)} · ${t(`bindingOutcomes.${bindingOutcome}`)}` + : null; + + return ( +
+
+
+

+ + {t("discoveryTitle")} +

+

+ {t("summary", { rounds, attempts: attemptCount, maxActive })} +

+
+ {trace.truncated && ( + + {t("traceTruncated")} + + )} +
+ + {(terminalEvent || bindingEvent) && ( +
+ {terminalEvent && ( +
+ {t("terminalOutcome")} +
+ {t(`outcomes.${terminalOutcome}`)} + {terminalEvent.statusCode != null && ( + HTTP {terminalEvent.statusCode} + )} +
+
+ )} + {bindingEvent && ( +
+ {t("bindingResult")} +
+
+ {bindingSummary} +
+ {bindingEvent.reason && !isFallbackWinnerBinding && ( +
+ {bindingEvent.reason} +
+ )} +
+
+ )} +
+ )} + + {Object.keys(config).length > 0 && ( +
+
{t("config")}
+
+ {numberFrom(config, "discoveryConcurrency", "concurrency") != null && ( + + )} + {numberFrom(config, "maxDiscoveryRounds", "maxRounds") != null && ( + + )} + {numberFrom(config, "discoverySlaMs") != null && ( + + )} + {numberFrom(config, "stickySlaMs") != null && ( + + )} + {numberFrom(config, "racingTotalTimeoutMs", "totalTimeoutMs") != null && ( + + )} + {numberFrom(config, "stickyTimeoutCooldownMs") != null && ( + + )} + {numberFrom(config, "sessionTtlSeconds") != null && ( + + )} +
+
+ )} + + {grouped.size === 0 ? ( +
+ + {t("noAttempts")} +
+ ) : ( +
+ {[...grouped.entries()].map(([round, roundAttempts]) => ( +
+
+
+ {round === 0 ? ( + + ) : ( + + )} +
+ {round === 0 ? t("stickyPhase") : t("round", { round })} +
+
+ + {t("attempts", { count: roundAttempts.length })} + +
+
+ {roundAttempts.map((attempt) => ( + + ))} +
+
+ ))} +
+ )} +
+ ); +} + +function TraceValue({ label, value }: { label: string; value: string | number | null }) { + return ( +
+
{label}
+
{value}
+
+ ); +} + +function AttemptCard({ attempt }: { attempt: AttemptView }) { + const t = useTranslations("dashboard.logs.details.routingTrace"); + const [expanded, setExpanded] = useState(false); + const style = outcomeStyle(attempt.outcome); + const Icon = style.icon; + const providerName = + attempt.providerName ?? t("providerFallback", { id: attempt.providerId ?? "-" }); + const chainItem = attempt.chainItem; + const statusCode = chainItem?.statusCode ?? attempt.statusCode; + const endpoint = sanitizeEndpoint(chainItem?.endpointUrl); + const errorMessage = getChainErrorMessage(chainItem); + const roleLabel = attempt.promotedFrom + ? t("roleTransition", { + from: t(`roles.${attempt.promotedFrom}`), + to: t(`roles.${attempt.role}`), + }) + : t(`roles.${attempt.role}`); + const cancellationLabel = attempt.cancellationKind + ? KNOWN_CANCELLATION_KINDS.has(attempt.cancellationKind) + ? t(`cancellationKinds.${attempt.cancellationKind}`) + : attempt.cancellationKind + : null; + + return ( +
+ + {expanded && ( +
+
+ {attempt.providerId != null && ( + + )} + {attempt.sequence != null && ( + + )} + {statusCode != null && ( + + )} +
+ {endpoint && ( +
+
{t("attemptDetails.endpoint")}
+ {endpoint} +
+ )} + {errorMessage && ( +
+
+ {t("attemptDetails.error")} +
+
+                {errorMessage}
+              
+
+ )} + {cancellationLabel && ( +
+ {t("attemptDetails.cancellation")}:{" "} + {cancellationLabel} +
+ )} + {attempt.history.length > 0 && ( +
+
{t("attemptDetails.timeline")}
+ {attempt.history.map((event, index) => ( +
+ + + {formatAttemptEvent(t, event)} + + {event.elapsedMs != null && ( + + {t("elapsed", { elapsed: Math.max(0, Math.round(event.elapsedMs)) })} + + )} +
+ ))} +
+ )} +
+ )} +
+ ); +} + +function formatAttemptEvent( + t: ReturnType>, + event: AttemptView["history"][number] +): string { + switch (event.type) { + case "attempt_started": + return t("events.started"); + case "attempt_ready": + return t("events.ready"); + case "attempt_held": + return t("events.held"); + case "fallback_promoted": + return t("events.fallbackPromoted"); + case "winner_committed": + return t("events.winnerCommitted"); + case "attempt_finished": + return t("events.finished", { + outcome: t(`outcomes.${event.outcome ?? "pending"}`), + }); + default: + return event.type; + } +} 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 90c1ece95..22bc1263e 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 @@ -15,6 +15,7 @@ import { Link2, RefreshCw, Server, + ShieldCheck, XCircle, Zap, } from "lucide-react"; @@ -28,7 +29,9 @@ import { formatCurrency } from "@/lib/utils/currency"; import { findHedgeLoserCost, summarizeHedgeBilling } from "@/lib/utils/hedge-billing"; import { formatProbability, formatProviderTimeline } from "@/lib/utils/provider-chain-formatter"; import type { ProviderChainItem } from "@/types/message"; +import { normalizeRoutingTrace } from "@/types/routing-trace"; import { type LogicTraceTabProps, parseBlockedReason } from "../types"; +import { DiscoveryTraceView, RoutingModeBanner } from "./DiscoveryTraceView"; import { StepCard, type StepStatus } from "./StepCard"; function getRequestStatus(item: ProviderChainItem): StepStatus { @@ -63,6 +66,7 @@ function getRequestStatus(item: ProviderChainItem): StepStatus { export function LogicTraceTab({ statusCode: _statusCode, providerChain, + routingTrace, sessionId, blockedBy, blockedReason, @@ -184,10 +188,23 @@ export function LogicTraceTab({ // Count providers at each stage const totalProviders = decisionContext?.totalProviders || 0; const afterHealthCheck = decisionContext?.afterHealthCheck || 0; + const normalizedRoutingTrace = normalizeRoutingTrace(routingTrace); + const isLeaseConflictProtection = + normalizedRoutingTrace?.mode === "single_upstream" && + normalizedRoutingTrace.bypassReason === "lease_conflict"; // Calculate step offset for session reuse flow const sessionReuseStepOffset = isSessionReuseFlow ? 1 : 0; + if (normalizedRoutingTrace?.mode === "discovery") { + return ( +
+ + +
+ ); + } + return (
{/* Warmup Skip Info */} @@ -243,17 +260,23 @@ export function LogicTraceTab({
)} + {normalizedRoutingTrace && } + {/* Decision Chain Header */} {providerChain && providerChain.length > 0 && (

- {isSessionReuseFlow ? ( + {isLeaseConflictProtection ? ( + + ) : isSessionReuseFlow ? ( ) : ( )} - {t("logicTrace.title")} + {isLeaseConflictProtection + ? t("logicTrace.singleRouteSelectionTitle") + : t("logicTrace.title")}

{isSessionReuseFlow ? ( diff --git a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/index.ts b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/index.ts index 641795a9a..67223ae82 100644 --- a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/index.ts +++ b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/index.ts @@ -1,3 +1,4 @@ +export { DiscoveryTraceView, RoutingModeBanner } from "./DiscoveryTraceView"; export { LatencyBreakdownBar } from "./LatencyBreakdownBar"; export { LogicTraceTab } from "./LogicTraceTab"; export { PerformanceTab } from "./PerformanceTab"; diff --git a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/index.tsx b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/index.tsx index 74a66d790..901dd1a75 100644 --- a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/index.tsx +++ b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/index.tsx @@ -11,6 +11,7 @@ import { hasSessionMessages } from "@/lib/api-client/v1/actions/active-sessions" import { cn } from "@/lib/utils"; import type { HedgeLoserBilling, StoredCostBreakdown } from "@/types/cost-breakdown"; import type { ProviderChainItem } from "@/types/message"; +import type { RoutingTraceV1 } from "@/types/routing-trace"; import type { SpecialSetting } from "@/types/special-settings"; import type { BillingModelSource } from "@/types/system-config"; import { LogicTraceTab, PerformanceTab, SummaryTab } from "./components"; @@ -19,6 +20,7 @@ interface ErrorDetailsDialogProps { statusCode: number | null; errorMessage: string | null; providerChain: ProviderChainItem[] | null; + routingTrace?: RoutingTraceV1 | null; sessionId: string | null; requestSequence?: number | null; blockedBy?: string | null; @@ -63,6 +65,7 @@ export function ErrorDetailsDialog({ statusCode, errorMessage, providerChain, + routingTrace, sessionId, requestSequence, blockedBy, @@ -212,6 +215,7 @@ export function ErrorDetailsDialog({ statusCode, errorMessage, providerChain, + routingTrace, sessionId, requestSequence, blockedBy, diff --git a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/types.ts b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/types.ts index ad65c070a..dc1ea1247 100644 --- a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/types.ts +++ b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/types.ts @@ -1,5 +1,6 @@ import type { HedgeLoserBilling, StoredCostBreakdown } from "@/types/cost-breakdown"; import type { ProviderChainItem } from "@/types/message"; +import type { RoutingTraceV1 } from "@/types/routing-trace"; import type { SpecialSetting } from "@/types/special-settings"; import type { BillingModelSource } from "@/types/system-config"; @@ -13,6 +14,8 @@ export interface TabSharedProps { errorMessage: string | null; /** Provider decision chain */ providerChain: ProviderChainItem[] | null; + /** Versioned routing trace for Discovery and legacy routing decisions */ + routingTrace?: RoutingTraceV1 | null; /** Session ID */ sessionId: string | null; /** Request sequence number within session */ diff --git a/src/app/[locale]/dashboard/logs/_components/provider-chain-popover.test.tsx b/src/app/[locale]/dashboard/logs/_components/provider-chain-popover.test.tsx index 4d65e49e2..8644b2ef9 100644 --- a/src/app/[locale]/dashboard/logs/_components/provider-chain-popover.test.tsx +++ b/src/app/[locale]/dashboard/logs/_components/provider-chain-popover.test.tsx @@ -3,7 +3,9 @@ import { renderToStaticMarkup } from "react-dom/server"; import { NextIntlClientProvider } from "next-intl"; import { Window } from "happy-dom"; import { describe, expect, test, vi } from "vitest"; +import dashboardMessages from "../../../../../../messages/en/dashboard.json"; import providerChainMessages from "../../../../../../messages/en/provider-chain.json"; +import type { RoutingTraceV1 } from "@/types/routing-trace"; vi.mock("@/lib/utils/provider-chain-formatter", async (importOriginal) => { const actual = await importOriginal(); @@ -66,8 +68,13 @@ vi.mock("@/components/ui/button", () => ({ })); vi.mock("@/components/ui/badge", () => ({ - Badge: ({ children, className }: React.ComponentProps<"span"> & { variant?: string }) => ( - + Badge: ({ + children, + className, + variant: _variant, + ...props + }: React.ComponentProps<"span"> & { variant?: string }) => ( + {children} ), @@ -85,6 +92,7 @@ const messages = { decisionChain: "Decision chain", }, details: { + routingTrace: dashboardMessages.logs.details.routingTrace, clickStatusCode: "Click status code", fake200ForwardedNotice: "Note: payload may have been forwarded", fake200DetectedReason: "Detected reason: {reason}", @@ -131,6 +139,10 @@ function parseHtml(html: string) { return window.document; } +function getCompactDiscoveryRouteBadge(html: string) { + return parseHtml(html).querySelector("[data-testid='discovery-route-badge']"); +} + describe("provider-chain-popover probability formatting", () => { test("renders probability 0.5 as 50% in tooltip", () => { const html = renderWithIntl( @@ -480,3 +492,679 @@ describe("provider-chain-popover hedge/abort reason handling", () => { expect(html).toContain("p1"); }); }); + +describe("provider-chain-popover Discovery summary", () => { + test("shows cold start, rounds, attempts and fallback winner source", () => { + const routingTrace: RoutingTraceV1 = { + version: 1, + mode: "discovery", + startedAt: 1_000, + updatedAt: 3_000, + discoveryEnabled: true, + eligible: true, + events: [], + summary: { + outcome: "success", + statusCode: 200, + durationMs: 2_000, + ttfbMs: 1_500, + attemptsPerRequest: 4, + maxActiveAttempts: 2, + rounds: 2, + providerMs: 3_400, + fallbackPromotions: 1, + cancelFailures: 0, + winnerOrigin: "fallback", + winnerProviderId: 2, + winnerRound: 1, + }, + }; + + const html = renderWithIntl( + undefined} + /> + ); + + expect(html).toContain("Cold start"); + expect(html).toContain("2R · 4 tries"); + expect(html).toContain("Final provider"); + expect(html).toContain("winner"); + expect(html).toContain("Route mode"); + expect(html).toContain("Winner origin"); + expect(html).toContain("Fallback takeover"); + expect(html).toContain("View Discovery details"); + expect(html).not.toContain("1 times"); + + const document = parseHtml(html); + const coldStartBadges = Array.from(document.querySelectorAll("[data-slot='badge']")).filter( + (node) => node.textContent === "Cold start" + ); + expect(coldStartBadges.some((node) => node.classList.contains("max-w-[45%]"))).toBe(true); + expect(coldStartBadges.some((node) => node.classList.contains("whitespace-normal"))).toBe(true); + const fallbackBadge = Array.from(document.querySelectorAll("[data-slot='badge']")).find( + (node) => node.textContent === "Fallback takeover" + ); + expect(fallbackBadge?.classList.contains("whitespace-normal")).toBe(true); + const compactRouteBadge = getCompactDiscoveryRouteBadge(html); + expect(compactRouteBadge?.classList.contains("bg-amber-50")).toBe(true); + expect(compactRouteBadge?.classList.contains("text-amber-800")).toBe(true); + expect(compactRouteBadge?.getAttribute("data-route-mode")).toBe("cold_start"); + expect(compactRouteBadge?.getAttribute("data-winner-origin")).toBe("fallback"); + expect(compactRouteBadge?.getAttribute("title")).toBe("Cold start · Fallback takeover"); + expect(document.querySelector("button")?.getAttribute("aria-label")).toContain( + "Cold start · Fallback takeover" + ); + }); + + test("labels a healthy Sticky request and shows the full winner name from the trace", () => { + const routingTrace: RoutingTraceV1 = { + version: 1, + mode: "discovery", + startedAt: 1_000, + updatedAt: 3_000, + discoveryEnabled: true, + eligible: true, + events: [ + { + type: "sticky_probe_started", + at: 1_000, + elapsedMs: 0, + round: 0, + attemptKind: "sticky", + provider: { id: 9, name: "full-sticky-provider-name" }, + }, + { + type: "winner_committed", + at: 2_000, + elapsedMs: 1_000, + round: 0, + attemptId: "sticky:1", + attemptKind: "sticky", + provider: { id: 9, name: "full-sticky-provider-name" }, + statusCode: 200, + }, + ], + summary: { + outcome: "success", + statusCode: 200, + durationMs: 2_000, + ttfbMs: 1_000, + attemptsPerRequest: 1, + maxActiveAttempts: 1, + rounds: 0, + providerMs: 1_000, + fallbackPromotions: 0, + cancelFailures: 0, + winnerOrigin: "sticky", + winnerProviderId: 9, + winnerRound: 0, + }, + }; + + const html = renderWithIntl( + + ); + + expect(html).toContain("full-sticky-provider-name"); + expect(html).toContain("Route mode"); + expect(html).toContain("Sticky"); + expect(html).toContain("0R · 1 tries"); + const compactRouteBadge = getCompactDiscoveryRouteBadge(html); + expect(compactRouteBadge?.classList.contains("bg-violet-50")).toBe(true); + expect(compactRouteBadge?.classList.contains("text-violet-700")).toBe(true); + expect(compactRouteBadge?.getAttribute("data-winner-origin")).toBe("sticky"); + }); + + test("labels Discovery after a Sticky timeout as rediscovery", () => { + const routingTrace: RoutingTraceV1 = { + version: 1, + mode: "discovery", + startedAt: 1_000, + updatedAt: 4_000, + discoveryEnabled: true, + eligible: true, + events: [ + { + type: "sticky_probe_started", + at: 1_000, + elapsedMs: 0, + round: 0, + attemptKind: "sticky", + provider: { id: 1, name: "old-sticky" }, + }, + { + type: "sticky_timeout", + at: 2_000, + elapsedMs: 1_000, + round: 0, + attemptKind: "sticky", + provider: { id: 1, name: "old-sticky" }, + outcome: "timeout", + }, + { type: "round_started", at: 2_001, elapsedMs: 1_001, round: 1 }, + { + type: "winner_committed", + at: 3_000, + elapsedMs: 2_000, + round: 1, + attemptId: "fallback:2", + attemptKind: "fallback", + provider: { id: 2, name: "new-winner" }, + statusCode: 200, + }, + ], + summary: { + outcome: "success", + statusCode: 200, + durationMs: 3_000, + ttfbMs: 2_000, + attemptsPerRequest: 2, + maxActiveAttempts: 2, + rounds: 1, + providerMs: 3_000, + fallbackPromotions: 1, + cancelFailures: 0, + winnerOrigin: "fallback", + winnerProviderId: 2, + winnerRound: 1, + }, + }; + + const html = renderWithIntl( + + ); + + expect(html).toContain("Rediscovery"); + expect(html).toContain("Fallback takeover"); + expect(html).not.toContain("Cold start"); + const compactRouteBadge = getCompactDiscoveryRouteBadge(html); + expect(compactRouteBadge?.classList.contains("bg-slate-100")).toBe(true); + expect(compactRouteBadge?.classList.contains("text-slate-700")).toBe(true); + expect(compactRouteBadge?.getAttribute("data-route-mode")).toBe("rediscovery"); + expect(compactRouteBadge?.getAttribute("data-winner-origin")).toBe("fallback"); + }); + + test("labels a Sticky attempt that fails before discovery as Sticky", () => { + const routingTrace: RoutingTraceV1 = { + version: 1, + mode: "discovery", + startedAt: 1_000, + updatedAt: 3_000, + discoveryEnabled: true, + eligible: true, + events: [ + { + type: "sticky_probe_started", + at: 1_000, + elapsedMs: 0, + round: 0, + attemptKind: "sticky", + provider: { id: 1, name: "sticky-provider" }, + }, + { + type: "attempt_finished", + at: 2_000, + elapsedMs: 1_000, + round: 0, + attemptKind: "sticky", + outcome: "failed", + provider: { id: 1, name: "sticky-provider" }, + }, + { + type: "request_finished", + at: 3_000, + elapsedMs: 2_000, + outcome: "failed", + statusCode: 503, + }, + ], + summary: { + outcome: "failed", + statusCode: 503, + durationMs: 2_000, + ttfbMs: null, + attemptsPerRequest: 1, + maxActiveAttempts: 1, + rounds: 0, + providerMs: 1_000, + fallbackPromotions: 0, + cancelFailures: 0, + winnerOrigin: "none", + winnerProviderId: null, + winnerRound: 0, + }, + }; + + const html = renderWithIntl( + + ); + + expect(html).toContain("Sticky"); + expect(html).not.toContain("Rediscovery"); + const compactRouteBadge = getCompactDiscoveryRouteBadge(html); + expect(compactRouteBadge?.getAttribute("data-winner-origin")).toBe("none"); + expect(compactRouteBadge?.classList.contains("bg-violet-50")).toBe(false); + expect(compactRouteBadge?.classList.contains("bg-blue-50")).toBe(false); + expect(compactRouteBadge?.classList.contains("bg-amber-50")).toBe(false); + expect(compactRouteBadge?.classList.contains("bg-teal-50")).toBe(false); + expect(compactRouteBadge?.classList.contains("bg-slate-100")).toBe(false); + }); + + test("does not infer Rediscovery from a stale session_reuse chain", () => { + const routingTrace: RoutingTraceV1 = { + version: 1, + mode: "discovery", + startedAt: 1_000, + updatedAt: 3_000, + discoveryEnabled: true, + eligible: true, + events: [ + { type: "round_started", at: 1_000, elapsedMs: 0, round: 1 }, + { + type: "attempt_started", + at: 1_010, + elapsedMs: 10, + round: 1, + attemptKind: "normal", + provider: { id: 2, name: "cold-start-provider" }, + }, + ], + }; + + const html = renderWithIntl( + + ); + + expect(html).toContain("Cold start"); + expect(html).not.toContain("Rediscovery"); + }); + + test("uses session reuse only as a fallback for a truncated trace", () => { + const routingTrace: RoutingTraceV1 = { + version: 1, + mode: "discovery", + startedAt: 1_000, + updatedAt: 3_000, + discoveryEnabled: true, + eligible: true, + truncated: true, + events: [{ type: "round_started", at: 2_000, elapsedMs: 1_000, round: 1 }], + summary: { + outcome: "success", + statusCode: 200, + durationMs: 2_000, + ttfbMs: 1_000, + attemptsPerRequest: 2, + maxActiveAttempts: 2, + rounds: 1, + providerMs: 2_000, + fallbackPromotions: 0, + cancelFailures: 0, + winnerOrigin: "normal", + winnerProviderId: 2, + winnerRound: 1, + }, + }; + + const html = renderWithIntl( + + ); + + expect(html).toContain("Rediscovery"); + }); + + test("recognizes Rediscovery from a retained non-Sticky winner event", () => { + const routingTrace: RoutingTraceV1 = { + version: 1, + mode: "discovery", + startedAt: 1_000, + updatedAt: 3_000, + discoveryEnabled: true, + eligible: true, + truncated: true, + events: [ + { + type: "sticky_probe_started", + at: 1_000, + elapsedMs: 0, + round: 0, + attemptKind: "sticky", + provider: { id: 1, name: "old-sticky" }, + }, + { + type: "winner_committed", + at: 3_000, + elapsedMs: 2_000, + round: 1, + attemptKind: "normal", + provider: { id: 2, name: "new-winner" }, + statusCode: 200, + }, + ], + }; + + const html = renderWithIntl( + + ); + + expect(html).toContain("Rediscovery"); + expect(html).toContain("Normal candidate"); + const compactRouteBadge = getCompactDiscoveryRouteBadge(html); + expect(compactRouteBadge?.classList.contains("bg-teal-50")).toBe(true); + expect(compactRouteBadge?.classList.contains("text-teal-700")).toBe(true); + expect(compactRouteBadge?.getAttribute("data-winner-origin")).toBe("normal"); + }); + + test("uses live winner events when the final summary has not been persisted yet", () => { + const routingTrace: RoutingTraceV1 = { + version: 1, + mode: "discovery", + startedAt: 1_000, + updatedAt: 2_000, + discoveryEnabled: true, + eligible: true, + events: [ + { + type: "winner_committed", + at: 1_500, + elapsedMs: 500, + round: 1, + attemptId: "normal:1", + attemptKind: "normal", + provider: { id: 7, name: "live-winner-name" }, + statusCode: 200, + }, + ], + }; + + const html = renderWithIntl( + + ); + + expect(html).toContain("live-winner-name"); + expect(html).toContain("Normal candidate"); + expect(html).toContain("Cold start"); + const compactRouteBadge = getCompactDiscoveryRouteBadge(html); + expect(compactRouteBadge?.classList.contains("bg-blue-50")).toBe(true); + expect(compactRouteBadge?.classList.contains("text-blue-700")).toBe(true); + expect(compactRouteBadge?.getAttribute("data-winner-origin")).toBe("normal"); + }); + + test("derives live rounds and attempt count before a terminal summary exists", () => { + const routingTrace: RoutingTraceV1 = { + version: 1, + mode: "discovery", + startedAt: 1_000, + updatedAt: 3_000, + discoveryEnabled: true, + eligible: true, + events: [ + { type: "round_started", at: 1_000, elapsedMs: 0, round: 1 }, + { + type: "attempt_started", + at: 1_010, + elapsedMs: 10, + round: 1, + attemptId: "normal:1", + attemptKind: "normal", + provider: { id: 1, name: "candidate-a" }, + }, + { + type: "attempt_started", + at: 1_020, + elapsedMs: 20, + round: 1, + attemptId: "normal:2", + attemptKind: "normal", + provider: { id: 2, name: "candidate-b" }, + }, + { + type: "attempt_finished", + at: 2_000, + elapsedMs: 1_000, + round: 1, + attemptId: "normal:1", + attemptKind: "normal", + outcome: "cancelled", + }, + { type: "round_started", at: 2_010, elapsedMs: 1_010, round: 2 }, + { + type: "attempt_started", + at: 2_020, + elapsedMs: 1_020, + round: 2, + attemptId: "normal:3", + attemptKind: "normal", + provider: { id: 3, name: "candidate-c" }, + }, + ], + }; + + const html = renderWithIntl( + + ); + + expect(html).toContain("2R · 3 tries"); + expect(html).toContain("Cold start"); + expect(html).toContain("Request result"); + expect(html).toContain("Pending"); + expect(html).not.toContain("0R · 0 tries"); + }); + + test("uses the terminal failure over a committed winner and preserves fake-200 warnings", () => { + const routingTrace: RoutingTraceV1 = { + version: 1, + mode: "discovery", + startedAt: 1_000, + updatedAt: 5_000, + discoveryEnabled: true, + eligible: true, + events: [ + { + type: "winner_committed", + at: 2_000, + elapsedMs: 1_000, + round: 1, + attemptId: "normal:1", + attemptKind: "normal", + provider: { id: 1, name: "candidate-a" }, + statusCode: 200, + }, + { + type: "request_finished", + at: 5_000, + elapsedMs: 4_000, + outcome: "failed", + statusCode: 502, + reason: "FAKE_200_EMPTY_BODY", + }, + ], + summary: { + outcome: "success", + statusCode: 200, + durationMs: 1_000, + ttfbMs: 1_000, + attemptsPerRequest: 1, + maxActiveAttempts: 1, + rounds: 1, + providerMs: 1_000, + fallbackPromotions: 0, + cancelFailures: 0, + winnerOrigin: "normal", + winnerProviderId: 1, + winnerRound: 1, + }, + }; + + const html = renderWithIntl( + + ); + const document = parseHtml(html); + const terminal = document.querySelector("[data-testid='discovery-compact-terminal']"); + + expect(terminal?.textContent).toContain("Failed"); + expect(terminal?.textContent).toContain("HTTP 502"); + expect(terminal?.innerHTML).toContain("text-rose-600"); + expect(terminal?.innerHTML).not.toContain("text-emerald-600"); + expect(html).toContain("Detected reason: Empty response body"); + expect(html).toContain("Note: payload may have been forwarded"); + expect(html).toContain("Why CCH cannot retry this response on the server"); + }); + + test("marks lease-conflict single-upstream routing without hiding selector priority", () => { + const routingTrace: RoutingTraceV1 = { + version: 1, + mode: "single_upstream", + startedAt: 1_000, + updatedAt: 2_000, + discoveryEnabled: true, + eligible: false, + bypassReason: "lease_conflict", + events: [], + }; + const html = renderWithIntl( + + ); + + expect(html).toContain("Single-route protection"); + expect(html).toContain("Another request owns the Discovery lease"); + expect(html).toContain("P1"); + expect(html).toContain("lucide-shield-check"); + const document = parseHtml(html); + const protectionBadge = Array.from(document.querySelectorAll("[data-slot='badge']")).find( + (node) => node.textContent === "Single-route protection" + ); + expect(protectionBadge?.classList.contains("bg-amber-50")).toBe(true); + expect(protectionBadge?.classList.contains("text-amber-700")).toBe(true); + }); + + test("keeps lease-conflict protection visible after serial provider fallback", () => { + const routingTrace: RoutingTraceV1 = { + version: 1, + mode: "single_upstream", + startedAt: 1_000, + updatedAt: 3_000, + discoveryEnabled: true, + eligible: false, + bypassReason: "lease_conflict", + events: [], + }; + const html = renderWithIntl( + + ); + + expect(html).toContain("Single-route protection"); + expect(html).toContain("lucide-shield-check"); + expect(html).toContain("primary"); + expect(html).toContain("backup"); + }); + + test("does not label a binding conflict as single-route protection", () => { + const routingTrace: RoutingTraceV1 = { + version: 1, + mode: "single_upstream", + startedAt: 1_000, + updatedAt: 2_000, + discoveryEnabled: true, + eligible: false, + bypassReason: "binding_conflict", + events: [], + }; + const html = renderWithIntl( + + ); + + expect(html).not.toContain("Single-route protection"); + expect(html).not.toContain("lucide-shield-check"); + }); +}); 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 958257769..3b5e5b6f2 100644 --- a/src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx +++ b/src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx @@ -4,11 +4,14 @@ import { AlertTriangle, CheckCircle, ChevronRight, + Clock3, GitBranch, InfoIcon, Link2, MinusCircle, RefreshCw, + Server, + ShieldCheck, XCircle, Zap, } from "lucide-react"; @@ -26,11 +29,17 @@ import { } from "@/lib/utils/provider-chain-formatter"; import { parseProviderGroups } from "@/lib/utils/provider-group"; import type { ProviderChainItem } from "@/types/message"; +import { + normalizeRoutingTrace, + type RoutingTraceV1, + type RoutingTraceWinnerOrigin, +} from "@/types/routing-trace"; import { getFake200ReasonKey } from "./fake200-reason"; import { Fake200RetryTooltip } from "./fake200-retry-tooltip"; interface ProviderChainPopoverProps { chain: ProviderChainItem[]; + routingTrace?: RoutingTraceV1 | null; finalProvider: string; /** Whether a cost badge is displayed, affects name max width */ hasCostBadge?: boolean; @@ -42,6 +51,166 @@ function parseGroupTags(groupTag?: string | null): string[] { return Array.from(new Set(parseProviderGroups(groupTag))); } +function deriveDiscoveryStats(trace: RoutingTraceV1): { + rounds: number; + attempts: number; +} { + const startedAttempts = new Set(); + let anonymousAttempts = 0; + let maxRound = 0; + + for (const event of trace.events) { + if (typeof event.round === "number" && Number.isFinite(event.round)) { + maxRound = Math.max(maxRound, event.round); + } + if (event.type !== "attempt_started") continue; + if (event.attemptId) startedAttempts.add(event.attemptId); + else anonymousAttempts += 1; + } + + return { + rounds: trace.summary?.rounds ?? maxRound, + attempts: trace.summary?.attemptsPerRequest ?? startedAttempts.size + anonymousAttempts, + }; +} + +type DiscoveryTerminalOutcome = "success" | "failed" | "client_abort" | "deadline" | "pending"; + +function getDiscoveryTerminal(trace: RoutingTraceV1): { + outcome: DiscoveryTerminalOutcome; + statusCode: number | null; +} { + const terminalEvent = trace.events.findLast((event) => event.type === "request_finished"); + const rawOutcome = terminalEvent?.outcome ?? trace.summary?.outcome; + const outcome: DiscoveryTerminalOutcome = + rawOutcome === "success" || + rawOutcome === "failed" || + rawOutcome === "client_abort" || + rawOutcome === "deadline" + ? rawOutcome + : "pending"; + + return { + outcome, + statusCode: terminalEvent?.statusCode ?? trace.summary?.statusCode ?? null, + }; +} + +type DiscoveryRouteMode = "sticky" | "cold_start" | "rediscovery"; + +function getStickyEvidenceIndex(trace: RoutingTraceV1): number { + return trace.events.findIndex( + (event) => + event.type === "sticky_probe_started" || + event.type === "sticky_timeout" || + event.attemptKind === "sticky" + ); +} + +function deriveDiscoveryRouteMode( + trace: RoutingTraceV1, + chain: ProviderChainItem[] +): DiscoveryRouteMode { + const stickyEvidenceIndex = getStickyEvidenceIndex(trace); + if (stickyEvidenceIndex < 0) { + if (trace.summary?.winnerOrigin === "sticky") return "sticky"; + + const truncatedStickyFallback = + trace.truncated === true && + chain.some( + (item) => item.reason === "session_reuse" || item.selectionMethod === "session_reuse" + ) && + ((trace.summary?.rounds ?? 0) >= 1 || (trace.summary?.winnerRound ?? 0) >= 1); + return truncatedStickyFallback ? "rediscovery" : "cold_start"; + } + + const transitionEvents = trace.events.slice(stickyEvidenceIndex); + const enteredRediscovery = + transitionEvents.some( + (event) => + event.type === "sticky_timeout" || + (typeof event.round === "number" && + event.round >= 1 && + (event.type === "round_started" || + (event.attemptKind !== undefined && event.attemptKind !== "sticky"))) + ) || + (trace.summary?.rounds ?? 0) >= 1 || + (trace.summary?.winnerRound ?? 0) >= 1; + return enteredRediscovery ? "rediscovery" : "sticky"; +} + +function getDiscoveryWinnerOrigin(trace: RoutingTraceV1): RoutingTraceWinnerOrigin { + const summaryOrigin = trace.summary?.winnerOrigin; + if (summaryOrigin === "sticky" || summaryOrigin === "normal" || summaryOrigin === "fallback") { + return summaryOrigin; + } + + const winnerKind = trace.events.findLast( + (event) => event.type === "winner_committed" + )?.attemptKind; + return winnerKind === "sticky" || winnerKind === "normal" || winnerKind === "fallback" + ? winnerKind + : "none"; +} + +function getCompactDiscoveryRouteBadgeClass( + routeMode: DiscoveryRouteMode, + winnerOrigin: RoutingTraceWinnerOrigin +): string { + if (winnerOrigin === "none") { + return ""; + } + + if (routeMode === "sticky" && winnerOrigin === "sticky") { + return "border-violet-200 bg-violet-50 text-violet-700 dark:border-violet-800 dark:bg-violet-950/20 dark:text-violet-300"; + } + + if (routeMode === "cold_start" && winnerOrigin === "normal") { + return "border-blue-200 bg-blue-50 text-blue-700 dark:border-blue-800 dark:bg-blue-950/20 dark:text-blue-300"; + } + + if (routeMode === "cold_start" && winnerOrigin === "fallback") { + return "border-amber-300 bg-amber-50 text-amber-800 dark:border-amber-800 dark:bg-amber-950/20 dark:text-amber-300"; + } + + if (routeMode === "rediscovery" && winnerOrigin === "normal") { + return "border-teal-200 bg-teal-50 text-teal-700 dark:border-teal-800 dark:bg-teal-950/20 dark:text-teal-300"; + } + + if (routeMode === "rediscovery" && winnerOrigin === "fallback") { + return "border-slate-300 bg-slate-100 text-slate-700 dark:border-slate-600 dark:bg-slate-800/50 dark:text-slate-200"; + } + + return ""; +} + +function getDiscoveryFinalProviderName( + trace: RoutingTraceV1, + chain: ProviderChainItem[], + fallbackName: string, + winnerOrigin: RoutingTraceWinnerOrigin +): string | null { + const winnerProviderId = trace.summary?.winnerProviderId; + const winnerEvent = [...trace.events] + .reverse() + .find( + (event) => + (event.type === "winner_committed" || + (event.type === "attempt_finished" && event.outcome === "winner")) && + (winnerProviderId == null || event.provider?.id === winnerProviderId) && + event.provider?.name + ); + if (winnerEvent?.provider?.name) return winnerEvent.provider.name; + const hasSuccessfulChainItem = chain.some( + (item) => + (item.reason === "request_success" || + item.reason === "retry_success" || + item.reason === "hedge_winner") && + item.statusCode != null + ); + return winnerOrigin !== "none" || hasSuccessfulChainItem ? fallbackName || null : null; +} + /** * Get status icon and color for a provider chain item */ @@ -126,12 +295,15 @@ function getItemStatus(item: ProviderChainItem): { export function ProviderChainPopover({ chain, + routingTrace, finalProvider, hasCostBadge = false, onChainItemClick, }: ProviderChainPopoverProps) { const t = useTranslations("dashboard"); const tChain = useTranslations("provider-chain"); + const tRouting = useTranslations("dashboard.logs.details.routingTrace"); + const normalizedRoutingTrace = normalizeRoutingTrace(routingTrace); // “假 200”识别发生在 SSE 流式结束后:此时响应内容可能已透传给客户端,但内部会按失败统计/熔断。 const hasFake200PostStreamFailure = chain.some( @@ -151,8 +323,190 @@ export function ProviderChainPopover({ // Fallback for empty string const displayName = finalProvider || "-"; + if (normalizedRoutingTrace?.mode === "discovery") { + const { rounds, attempts } = deriveDiscoveryStats(normalizedRoutingTrace); + const routeMode = deriveDiscoveryRouteMode(normalizedRoutingTrace, chain); + const winnerOrigin = getDiscoveryWinnerOrigin(normalizedRoutingTrace); + const discoveryFinalProvider = getDiscoveryFinalProviderName( + normalizedRoutingTrace, + chain, + displayName, + winnerOrigin + ); + const triggerProviderName = discoveryFinalProvider ?? displayName; + const terminal = getDiscoveryTerminal(normalizedRoutingTrace); + const terminalPresentation = + terminal.outcome === "success" + ? { icon: CheckCircle, className: "text-emerald-600" } + : terminal.outcome === "failed" + ? { icon: XCircle, className: "text-rose-600" } + : terminal.outcome === "deadline" + ? { icon: Clock3, className: "text-amber-600" } + : terminal.outcome === "client_abort" + ? { icon: MinusCircle, className: "text-amber-600" } + : { icon: RefreshCw, className: "text-muted-foreground" }; + const TerminalIcon = terminalPresentation.icon; + const routeLabel = tRouting(`routeModes.${routeMode}`); + const winnerSourceLabel = + winnerOrigin === "none" ? null : tRouting(`winnerSources.${winnerOrigin}`); + const compactRouteDescription = + winnerSourceLabel && winnerSourceLabel !== routeLabel + ? `${routeLabel} · ${winnerSourceLabel}` + : routeLabel; + return ( + + + + + +
+

+ + {tRouting("discoveryTitle")} +

+ + {tRouting("modes.discovery")} + +
+
+
+ + {tRouting("finalProvider")}: + + {discoveryFinalProvider ?? "-"} + +
+
+ +
+ {tRouting("routeMode")}: + + {routeLabel} + +
+
+
+
+
{tRouting("roundsLabel")}
+
{rounds}
+
+
+
{tRouting("attemptsLabel")}
+
{attempts}
+
+
+
+ +
+ {tRouting("terminalOutcome")}: + + {tRouting(`outcomes.${terminal.outcome}`)} + + {terminal.statusCode != null && ( + HTTP {terminal.statusCode} + )} +
+
+ {winnerOrigin !== "none" && ( +
+ +
+ {tRouting("winnerOrigin")}: + + {winnerSourceLabel} + +
+
+ )} +
+ {(hasFake200PostStreamFailure || onChainItemClick) && ( +
+ {hasFake200PostStreamFailure && ( +
+
+ )} + {onChainItemClick && ( + + )} +
+ )} +
+
+ ); + } + // Determine max width based on whether cost badge is present const maxWidthClass = hasCostBadge ? "max-w-[140px]" : "max-w-[180px]"; + const isLeaseConflictProtection = + normalizedRoutingTrace?.mode === "single_upstream" && + normalizedRoutingTrace.bypassReason === "lease_conflict"; // Check if this is a session reuse const isSessionReuse = @@ -176,22 +530,41 @@ export function ProviderChainPopover({ - + + {isLeaseConflictProtection && ( +
{/* Provider name */}
{displayName}
+ {isLeaseConflictProtection && ( +
+
+ )} {singleRequestItem?.statusCode && (
{/* Request count badge */} - {isHedge ? ( + {isLeaseConflictProtection ? ( +