From b5d8cc6484209c6b6774df24a0ac7e3b80aafcab Mon Sep 17 00:00:00 2001 From: ding113 Date: Thu, 23 Jul 2026 12:56:34 -0700 Subject: [PATCH 01/12] feat(proxy): add runtime toggles for replay and cache effectiveness Add replay_enabled and cache_effectiveness_enabled columns to system_settings so operators can flip these features without redeploying. Database values override the ENABLE_REQUEST_REPLAY and ENABLE_CACHE_EFFECTIVENESS environment variables; null defers to the environment default. Replay identity derivation, spool creation, and the cache effectiveness scheduler all consume the runtime toggle. The settings form exposes both switches with descriptive labels across all supported locales, and the degradation ladder covers the new columns for databases that have not yet migrated. --- drizzle/0113_reflective_centennial.sql | 2 + drizzle/meta/0113_snapshot.json | 5163 +++++++++++++++++ drizzle/meta/_journal.json | 7 + messages/en/settings/config.json | 6 +- messages/ja/settings/config.json | 6 +- messages/ru/settings/config.json | 6 +- messages/zh-CN/settings/config.json | 4 + messages/zh-TW/settings/config.json | 6 +- src/actions/system-config.ts | 4 + .../_components/system-settings-form.tsx | 57 + src/app/[locale]/settings/config/page.tsx | 2 + .../v1/_lib/proxy/replay/replay-identity.ts | 13 +- src/app/v1/_lib/proxy/replay/replay-spool.ts | 4 +- src/drizzle/schema.ts | 6 + src/instrumentation.ts | 7 +- src/lib/api-client/v1/openapi-types.gen.ts | 12 + src/lib/api/v1/schemas/system-config.ts | 12 + src/lib/config/system-settings-cache.ts | 2 + src/lib/system-settings/proxy-runtime.ts | 37 +- src/lib/validation/schemas.ts | 4 + src/repository/_shared/transformers.ts | 2 + src/repository/system-config.ts | 25 + src/types/system-config.ts | 14 + .../system-config-degradation-ladder.test.ts | 28 +- ...stem-config-update-missing-columns.test.ts | 8 +- 25 files changed, 5409 insertions(+), 28 deletions(-) create mode 100644 drizzle/0113_reflective_centennial.sql create mode 100644 drizzle/meta/0113_snapshot.json diff --git a/drizzle/0113_reflective_centennial.sql b/drizzle/0113_reflective_centennial.sql new file mode 100644 index 000000000..37b058143 --- /dev/null +++ b/drizzle/0113_reflective_centennial.sql @@ -0,0 +1,2 @@ +ALTER TABLE "system_settings" ADD COLUMN IF NOT EXISTS "replay_enabled" boolean;--> statement-breakpoint +ALTER TABLE "system_settings" ADD COLUMN IF NOT EXISTS "cache_effectiveness_enabled" boolean; \ No newline at end of file diff --git a/drizzle/meta/0113_snapshot.json b/drizzle/meta/0113_snapshot.json new file mode 100644 index 000000000..3d384a7ce --- /dev/null +++ b/drizzle/meta/0113_snapshot.json @@ -0,0 +1,5163 @@ +{ + "id": "87e53bfd-94b5-42d6-b589-0ec54a331157", + "prevId": "5fd1703b-6bd3-4594-9c92-9f9c889dcd24", + "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 + }, + "cache_compatibility_key": { + "name": "cache_compatibility_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "cache_score_eligible": { + "name": "cache_score_eligible", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cache_score_excluded_reason": { + "name": "cache_score_excluded_reason", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "theoretical_cache_tokens": { + "name": "theoretical_cache_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_ttl_bucket": { + "name": "cache_ttl_bucket", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_message_request_user_date_cost": { + "name": "idx_message_request_user_date_cost", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_user_created_at_cost_stats": { + "name": "idx_message_request_user_created_at_cost_stats", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_user_query": { + "name": "idx_message_request_user_query", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_provider_created_at_active": { + "name": "idx_message_request_provider_created_at_active", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_provider_created_at_finalized_active": { + "name": "idx_message_request_provider_created_at_finalized_active", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"status_code\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_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_batch_apply_operations": { + "name": "provider_batch_apply_operations", + "schema": "", + "columns": { + "claim_key": { + "name": "claim_key", + "type": "varchar(256)", + "primaryKey": true, + "notNull": true + }, + "preview_token": { + "name": "preview_token", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "payload_fingerprint": { + "name": "payload_fingerprint", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "operation_id": { + "name": "operation_id", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "undo_token": { + "name": "undo_token", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "undo_expires_at": { + "name": "undo_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "undo_consumed_at": { + "name": "undo_consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uniq_provider_batch_apply_operations_preview_token": { + "name": "uniq_provider_batch_apply_operations_preview_token", + "columns": [ + { + "expression": "preview_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "uniq_provider_batch_apply_operations_operation_id": { + "name": "uniq_provider_batch_apply_operations_operation_id", + "columns": [ + { + "expression": "operation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "uniq_provider_batch_apply_operations_undo_token": { + "name": "uniq_provider_batch_apply_operations_undo_token", + "columns": [ + { + "expression": "undo_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_batch_apply_operations_expires_at": { + "name": "idx_provider_batch_apply_operations_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_cache_effectiveness": { + "name": "provider_cache_effectiveness", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "cache_ttl_bucket": { + "name": "cache_ttl_bucket", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true + }, + "window_start": { + "name": "window_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "window_end": { + "name": "window_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "sample_count": { + "name": "sample_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "eligible_count": { + "name": "eligible_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "theoretical_cache_tokens": { + "name": "theoretical_cache_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "observed_cache_read_tokens": { + "name": "observed_cache_read_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "raw_effectiveness_bp": { + "name": "raw_effectiveness_bp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "confidence_bp": { + "name": "confidence_bp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "effectiveness_bp": { + "name": "effectiveness_bp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_provider_cache_effectiveness_window": { + "name": "idx_provider_cache_effectiveness_window", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_start", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_endpoint_probe_logs": { + "name": "provider_endpoint_probe_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "endpoint_id": { + "name": "endpoint_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'scheduled'" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error_type": { + "name": "error_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_provider_endpoint_probe_logs_endpoint_created_at": { + "name": "idx_provider_endpoint_probe_logs_endpoint_created_at", + "columns": [ + { + "expression": "endpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoint_probe_logs_created_at": { + "name": "idx_provider_endpoint_probe_logs_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "provider_endpoint_probe_logs_endpoint_id_provider_endpoints_id_fk": { + "name": "provider_endpoint_probe_logs_endpoint_id_provider_endpoints_id_fk", + "tableFrom": "provider_endpoint_probe_logs", + "tableTo": "provider_endpoints", + "columnsFrom": [ + "endpoint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_endpoints": { + "name": "provider_endpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provider_type": { + "name": "provider_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'claude'" + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_probed_at": { + "name": "last_probed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_probe_ok": { + "name": "last_probe_ok", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "last_probe_status_code": { + "name": "last_probe_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_probe_latency_ms": { + "name": "last_probe_latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_probe_error_type": { + "name": "last_probe_error_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "last_probe_error_message": { + "name": "last_probe_error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "uniq_provider_endpoints_vendor_type_url": { + "name": "uniq_provider_endpoints_vendor_type_url", + "columns": [ + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"provider_endpoints\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_vendor_type": { + "name": "idx_provider_endpoints_vendor_type", + "columns": [ + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"provider_endpoints\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_enabled": { + "name": "idx_provider_endpoints_enabled", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"provider_endpoints\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_pick_enabled": { + "name": "idx_provider_endpoints_pick_enabled", + "columns": [ + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"provider_endpoints\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_created_at": { + "name": "idx_provider_endpoints_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_deleted_at": { + "name": "idx_provider_endpoints_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "provider_endpoints_vendor_id_provider_vendors_id_fk": { + "name": "provider_endpoints_vendor_id_provider_vendors_id_fk", + "tableFrom": "provider_endpoints", + "tableTo": "provider_vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_groups": { + "name": "provider_groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "cost_multiplier": { + "name": "cost_multiplier", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": true, + "default": "'1.0'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "provider_groups_name_unique": { + "name": "provider_groups_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_vendors": { + "name": "provider_vendors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "website_domain": { + "name": "website_domain", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "website_url": { + "name": "website_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "favicon_url": { + "name": "favicon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "uniq_provider_vendors_website_domain": { + "name": "uniq_provider_vendors_website_domain", + "columns": [ + { + "expression": "website_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_vendors_created_at": { + "name": "idx_provider_vendors_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.providers": { + "name": "providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "provider_vendor_id": { + "name": "provider_vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "weight": { + "name": "weight", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "group_priorities": { + "name": "group_priorities", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'null'::jsonb" + }, + "cost_multiplier": { + "name": "cost_multiplier", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": false, + "default": "'1.0'" + }, + "group_tag": { + "name": "group_tag", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "provider_type": { + "name": "provider_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'claude'" + }, + "preserve_client_ip": { + "name": "preserve_client_ip", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "disable_session_reuse": { + "name": "disable_session_reuse", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "model_redirects": { + "name": "model_redirects", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "allowed_models": { + "name": "allowed_models", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'null'::jsonb" + }, + "allowed_clients": { + "name": "allowed_clients", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "blocked_clients": { + "name": "blocked_clients", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "active_time_start": { + "name": "active_time_start", + "type": "varchar(5)", + "primaryKey": false, + "notNull": false + }, + "active_time_end": { + "name": "active_time_end", + "type": "varchar(5)", + "primaryKey": false, + "notNull": false + }, + "codex_instructions_strategy": { + "name": "codex_instructions_strategy", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "default": "'auto'" + }, + "mcp_passthrough_type": { + "name": "mcp_passthrough_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "mcp_passthrough_url": { + "name": "mcp_passthrough_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "limit_5h_usd": { + "name": "limit_5h_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_5h_reset_mode": { + "name": "limit_5h_reset_mode", + "type": "daily_reset_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'rolling'" + }, + "limit_daily_usd": { + "name": "limit_daily_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "daily_reset_mode": { + "name": "daily_reset_mode", + "type": "daily_reset_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'fixed'" + }, + "daily_reset_time": { + "name": "daily_reset_time", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'00:00'" + }, + "limit_weekly_usd": { + "name": "limit_weekly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_monthly_usd": { + "name": "limit_monthly_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_total_usd": { + "name": "limit_total_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "total_cost_reset_at": { + "name": "total_cost_reset_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "limit_concurrent_sessions": { + "name": "limit_concurrent_sessions", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "max_retry_attempts": { + "name": "max_retry_attempts", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "circuit_breaker_failure_threshold": { + "name": "circuit_breaker_failure_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5 + }, + "circuit_breaker_open_duration": { + "name": "circuit_breaker_open_duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1800000 + }, + "circuit_breaker_half_open_success_threshold": { + "name": "circuit_breaker_half_open_success_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 2 + }, + "proxy_url": { + "name": "proxy_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "proxy_fallback_to_direct": { + "name": "proxy_fallback_to_direct", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "custom_headers": { + "name": "custom_headers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "first_byte_timeout_streaming_ms": { + "name": "first_byte_timeout_streaming_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "streaming_idle_timeout_ms": { + "name": "streaming_idle_timeout_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "request_timeout_non_streaming_ms": { + "name": "request_timeout_non_streaming_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "website_url": { + "name": "website_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "favicon_url": { + "name": "favicon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cache_ttl_preference": { + "name": "cache_ttl_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "swap_cache_ttl_billing": { + "name": "swap_cache_ttl_billing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "context_1m_preference": { + "name": "context_1m_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "codex_reasoning_effort_preference": { + "name": "codex_reasoning_effort_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "codex_reasoning_summary_preference": { + "name": "codex_reasoning_summary_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "codex_text_verbosity_preference": { + "name": "codex_text_verbosity_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "codex_parallel_tool_calls_preference": { + "name": "codex_parallel_tool_calls_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "codex_image_generation_preference": { + "name": "codex_image_generation_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "codex_service_tier_preference": { + "name": "codex_service_tier_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "anthropic_max_tokens_preference": { + "name": "anthropic_max_tokens_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "anthropic_thinking_budget_preference": { + "name": "anthropic_thinking_budget_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "anthropic_adaptive_thinking": { + "name": "anthropic_adaptive_thinking", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'null'::jsonb" + }, + "gemini_google_search_preference": { + "name": "gemini_google_search_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "tpm": { + "name": "tpm", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "rpm": { + "name": "rpm", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "rpd": { + "name": "rpd", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "cc": { + "name": "cc", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_providers_enabled_priority": { + "name": "idx_providers_enabled_priority", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "weight", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"providers\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_group": { + "name": "idx_providers_group", + "columns": [ + { + "expression": "group_tag", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"providers\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_vendor_type_url_active": { + "name": "idx_providers_vendor_type_url_active", + "columns": [ + { + "expression": "provider_vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"providers\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_created_at": { + "name": "idx_providers_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_deleted_at": { + "name": "idx_providers_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_vendor_type": { + "name": "idx_providers_vendor_type", + "columns": [ + { + "expression": "provider_vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"providers\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_enabled_vendor_type": { + "name": "idx_providers_enabled_vendor_type", + "columns": [ + { + "expression": "provider_vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"providers\".\"deleted_at\" IS NULL AND \"providers\".\"is_enabled\" = true AND \"providers\".\"provider_vendor_id\" IS NOT NULL AND \"providers\".\"provider_vendor_id\" > 0", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "providers_provider_vendor_id_provider_vendors_id_fk": { + "name": "providers_provider_vendor_id_provider_vendors_id_fk", + "tableFrom": "providers", + "tableTo": "provider_vendors", + "columnsFrom": [ + "provider_vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.replay_payloads": { + "name": "replay_payloads", + "schema": "", + "columns": { + "replay_id": { + "name": "replay_id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "verifier": { + "name": "verifier", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "scope_tag": { + "name": "scope_tag", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "format": { + "name": "format", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "headers_json": { + "name": "headers_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "source_message_request_id": { + "name": "source_message_request_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_replay_payloads_key_id": { + "name": "idx_replay_payloads_key_id", + "columns": [ + { + "expression": "key_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_replay_payloads_expires_at": { + "name": "idx_replay_payloads_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.request_filters": { + "name": "request_filters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true + }, + "match_type": { + "name": "match_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "target": { + "name": "target", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "replacement": { + "name": "replacement", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "binding_type": { + "name": "binding_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'global'" + }, + "provider_ids": { + "name": "provider_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "group_tags": { + "name": "group_tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rule_mode": { + "name": "rule_mode", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'simple'" + }, + "execution_phase": { + "name": "execution_phase", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'guard'" + }, + "operations": { + "name": "operations", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_request_filters_enabled": { + "name": "idx_request_filters_enabled", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_request_filters_scope": { + "name": "idx_request_filters_scope", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_request_filters_action": { + "name": "idx_request_filters_action", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_request_filters_binding": { + "name": "idx_request_filters_binding", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "binding_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_request_filters_phase": { + "name": "idx_request_filters_phase", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_phase", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sensitive_words": { + "name": "sensitive_words", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "word": { + "name": "word", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "match_type": { + "name": "match_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'contains'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_sensitive_words_enabled": { + "name": "idx_sensitive_words_enabled", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "match_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sensitive_words_created_at": { + "name": "idx_sensitive_words_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.system_settings": { + "name": "system_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "site_title": { + "name": "site_title", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "default": "'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 + }, + "stream_gate_mode": { + "name": "stream_gate_mode", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true, + "default": "'enforce'" + }, + "affinity_ignore_client_session_id": { + "name": "affinity_ignore_client_session_id", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "replay_enabled": { + "name": "replay_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cache_effectiveness_enabled": { + "name": "cache_effectiveness_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_ledger": { + "name": "usage_ledger", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "final_provider_id": { + "name": "final_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "original_model": { + "name": "original_model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "actual_response_model": { + "name": "actual_response_model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "endpoint": { + "name": "endpoint", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "api_type": { + "name": "api_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "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 91847a65c..d5f102ac7 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -792,6 +792,13 @@ "when": 1784800591867, "tag": "0112_complex_sabra", "breakpoints": true + }, + { + "idx": 113, + "version": "7", + "when": 1784833275913, + "tag": "0113_reflective_centennial", + "breakpoints": true } ] } \ No newline at end of file diff --git a/messages/en/settings/config.json b/messages/en/settings/config.json index 9c50f60bc..77fa27be1 100644 --- a/messages/en/settings/config.json +++ b/messages/en/settings/config.json @@ -169,7 +169,11 @@ "off": "Off", "shadow": "Shadow mode", "enforce": "Enabled" - } + }, + "replayEnabled": "Request Replay", + "replayEnabledDesc": "Caches upstream responses and reuses upstream connections: identical concurrent or reconnecting requests attach to the in-flight stream instead of re-hitting the provider. Follows the ENABLE_REQUEST_REPLAY environment variable until saved here. Default off.", + "cacheEffectivenessEnabled": "Prefix Cache Simulation", + "cacheEffectivenessEnabledDesc": "Simulates longest-prefix cache hit rates (theoretical vs actual) for observability only; never affects routing. Follows the ENABLE_CACHE_EFFECTIVENESS environment variable until saved here. Default on." }, "ipLogging": { "title": "IP logging & extraction", diff --git a/messages/ja/settings/config.json b/messages/ja/settings/config.json index 7cdea6512..ddb081fb3 100644 --- a/messages/ja/settings/config.json +++ b/messages/ja/settings/config.json @@ -169,7 +169,11 @@ "off": "オフ", "shadow": "シャドウモード", "enforce": "有効" - } + }, + "replayEnabled": "リクエスト Replay", + "replayEnabledDesc": "上流レスポンスをキャッシュし上流接続を再利用します。同一リクエストの並行実行や再接続は進行中のストリームに追随し、プロバイダーへ再送しません。保存するまでは環境変数 ENABLE_REQUEST_REPLAY に従います。デフォルトはオフ。", + "cacheEffectivenessEnabled": "プレフィックスキャッシュシミュレーション", + "cacheEffectivenessEnabledDesc": "最長プレフィックス一致のキャッシュヒット率(理論値 vs 実測値)を観測目的でシミュレートします。ルーティングには影響しません。保存するまでは環境変数 ENABLE_CACHE_EFFECTIVENESS に従います。デフォルトはオン。" }, "ipLogging": { "title": "IP ログと抽出", diff --git a/messages/ru/settings/config.json b/messages/ru/settings/config.json index 9fd5abca0..5781aab60 100644 --- a/messages/ru/settings/config.json +++ b/messages/ru/settings/config.json @@ -169,7 +169,11 @@ "off": "Выключено", "shadow": "Теневой режим", "enforce": "Включено" - } + }, + "replayEnabled": "Replay запросов", + "replayEnabledDesc": "Кэширует ответы провайдера и переиспользует соединения: одинаковые параллельные или переподключающиеся запросы присоединяются к текущему потоку вместо повторного обращения к провайдеру. До сохранения следует переменной окружения ENABLE_REQUEST_REPLAY. По умолчанию выключено.", + "cacheEffectivenessEnabled": "Симуляция префиксного кэша", + "cacheEffectivenessEnabledDesc": "Симулирует хит-рейт кэша по наибольшему префиксу (теория и факт) только для наблюдаемости; не влияет на маршрутизацию. До сохранения следует переменной окружения ENABLE_CACHE_EFFECTIVENESS. По умолчанию включено." }, "ipLogging": { "title": "Журналирование и извлечение IP", diff --git a/messages/zh-CN/settings/config.json b/messages/zh-CN/settings/config.json index f3497f20c..ae3d6ff7c 100644 --- a/messages/zh-CN/settings/config.json +++ b/messages/zh-CN/settings/config.json @@ -94,6 +94,10 @@ "shadow": "影子模式", "enforce": "启用" }, + "replayEnabled": "请求 Replay", + "replayEnabledDesc": "缓存上游响应并复用上游连接:并发或断线重连的相同请求直接跟尾在途流,不再重复请求供应商。保存前跟随环境变量 ENABLE_REQUEST_REPLAY,默认关闭。", + "cacheEffectivenessEnabled": "前缀缓存模拟", + "cacheEffectivenessEnabledDesc": "模拟最长前缀匹配的缓存命中率(理论 vs 实际),仅用于观测,不影响路由。保存前跟随环境变量 ENABLE_CACHE_EFFECTIVENESS,默认开启。", "affinityIgnoreClientSessionId": "忽略客户端 Session ID", "affinityIgnoreClientSessionIdDesc": "开启后,可指纹化的请求强制使用最长前缀亲和做供应商粘性(跳过客户端 Session ID 绑定);不可指纹化的请求仍走会话复用。默认开启。", "fakeStreaming": { diff --git a/messages/zh-TW/settings/config.json b/messages/zh-TW/settings/config.json index 3b6411888..991b84948 100644 --- a/messages/zh-TW/settings/config.json +++ b/messages/zh-TW/settings/config.json @@ -169,7 +169,11 @@ "off": "關閉", "shadow": "影子模式", "enforce": "啟用" - } + }, + "replayEnabled": "請求 Replay", + "replayEnabledDesc": "快取上游回應並重用上游連線:並發或斷線重連的相同請求直接跟尾在途串流,不再重複請求供應商。儲存前跟隨環境變數 ENABLE_REQUEST_REPLAY,預設關閉。", + "cacheEffectivenessEnabled": "前綴快取模擬", + "cacheEffectivenessEnabledDesc": "模擬最長前綴匹配的快取命中率(理論 vs 實際),僅用於觀測,不影響路由。儲存前跟隨環境變數 ENABLE_CACHE_EFFECTIVENESS,預設開啟。" }, "ipLogging": { "title": "IP 記錄與提取", diff --git a/src/actions/system-config.ts b/src/actions/system-config.ts index 3dac0a71d..639f6dbe0 100644 --- a/src/actions/system-config.ts +++ b/src/actions/system-config.ts @@ -105,6 +105,8 @@ export async function saveSystemSettings(formData: { fakeStreamingWhitelist?: FakeStreamingWhitelistEntry[]; streamGateMode?: StreamGateSettingMode; affinityIgnoreClientSessionId?: boolean; + replayEnabled?: boolean | null; + cacheEffectivenessEnabled?: boolean | null; enableCodexSessionIdCompletion?: boolean; enableClaudeMetadataUserIdInjection?: boolean; enableResponseFixer?: boolean; @@ -193,6 +195,8 @@ export async function saveSystemSettings(formData: { fakeStreamingWhitelist: validated.fakeStreamingWhitelist, streamGateMode: validated.streamGateMode, affinityIgnoreClientSessionId: validated.affinityIgnoreClientSessionId, + replayEnabled: validated.replayEnabled, + cacheEffectivenessEnabled: validated.cacheEffectivenessEnabled, enableCodexSessionIdCompletion: validated.enableCodexSessionIdCompletion, enableClaudeMetadataUserIdInjection: validated.enableClaudeMetadataUserIdInjection, enableResponseFixer: validated.enableResponseFixer, diff --git a/src/app/[locale]/settings/config/_components/system-settings-form.tsx b/src/app/[locale]/settings/config/_components/system-settings-form.tsx index 56d47ae15..0564c0079 100644 --- a/src/app/[locale]/settings/config/_components/system-settings-form.tsx +++ b/src/app/[locale]/settings/config/_components/system-settings-form.tsx @@ -9,10 +9,12 @@ import { Eye, FileCode, Filter, + Gauge, Globe, MapPin, Network, Pencil, + Repeat, Route, Terminal, Thermometer, @@ -95,6 +97,8 @@ interface SystemSettingsFormProps { | "fakeStreamingWhitelist" | "streamGateMode" | "affinityIgnoreClientSessionId" + | "replayEnabled" + | "cacheEffectivenessEnabled" | "enableCodexSessionIdCompletion" | "enableClaudeMetadataUserIdInjection" | "enableResponseFixer" @@ -210,6 +214,11 @@ export function SystemSettingsForm({ const [affinityIgnoreClientSessionId, setAffinityIgnoreClientSessionId] = useState( initialSettings.affinityIgnoreClientSessionId ); + // null = 尚未覆写(跟随环境变量默认:Replay 关 / 缓存模拟开);保存后写显式值 + const [replayEnabled, setReplayEnabled] = useState(initialSettings.replayEnabled ?? false); + const [cacheEffectivenessEnabled, setCacheEffectivenessEnabled] = useState( + initialSettings.cacheEffectivenessEnabled ?? true + ); const [enableThinkingBudgetRectifier, setEnableThinkingBudgetRectifier] = useState( initialSettings.enableThinkingBudgetRectifier ); @@ -395,6 +404,8 @@ export function SystemSettingsForm({ fakeStreamingWhitelist: sanitizedFakeStreamingWhitelist, streamGateMode, affinityIgnoreClientSessionId, + replayEnabled, + cacheEffectivenessEnabled, enableThinkingBudgetRectifier, enableThinkingEffortConflictRectifier, enableGeminiFunctionIdRectifier, @@ -459,6 +470,8 @@ export function SystemSettingsForm({ ); setStreamGateMode(result.data.streamGateMode); setAffinityIgnoreClientSessionId(result.data.affinityIgnoreClientSessionId); + setReplayEnabled(result.data.replayEnabled ?? false); + setCacheEffectivenessEnabled(result.data.cacheEffectivenessEnabled ?? true); setEnableThinkingBudgetRectifier(result.data.enableThinkingBudgetRectifier); setEnableThinkingEffortConflictRectifier(result.data.enableThinkingEffortConflictRectifier); setEnableGeminiFunctionIdRectifier(result.data.enableGeminiFunctionIdRectifier); @@ -1159,6 +1172,50 @@ export function SystemSettingsForm({ /> + {/* F2 Request Replay */} +
+
+
+ +
+
+

{t("replayEnabled")}

+

{t("replayEnabledDesc")}

+
+
+ setReplayEnabled(checked)} + disabled={isPending} + /> +
+ + {/* F3b Cache Effectiveness Simulation */} +
+
+
+ +
+
+

+ {t("cacheEffectivenessEnabled")} +

+

+ {t("cacheEffectivenessEnabledDesc")} +

+
+
+ setCacheEffectivenessEnabled(checked)} + disabled={isPending} + /> +
+ {/* Enable Codex Session ID Completion */}
diff --git a/src/app/[locale]/settings/config/page.tsx b/src/app/[locale]/settings/config/page.tsx index 192369a1e..7da0a1264 100644 --- a/src/app/[locale]/settings/config/page.tsx +++ b/src/app/[locale]/settings/config/page.tsx @@ -80,6 +80,8 @@ async function SettingsConfigContent({ locale }: { locale: string }) { fakeStreamingWhitelist: settings.fakeStreamingWhitelist, streamGateMode: settings.streamGateMode, affinityIgnoreClientSessionId: settings.affinityIgnoreClientSessionId, + replayEnabled: settings.replayEnabled, + cacheEffectivenessEnabled: settings.cacheEffectivenessEnabled, enableCodexSessionIdCompletion: settings.enableCodexSessionIdCompletion, enableClaudeMetadataUserIdInjection: settings.enableClaudeMetadataUserIdInjection, enableResponseFixer: settings.enableResponseFixer, diff --git a/src/app/v1/_lib/proxy/replay/replay-identity.ts b/src/app/v1/_lib/proxy/replay/replay-identity.ts index e07a7cb67..652eef17e 100644 --- a/src/app/v1/_lib/proxy/replay/replay-identity.ts +++ b/src/app/v1/_lib/proxy/replay/replay-identity.ts @@ -1,5 +1,6 @@ import { getEnvConfig } from "@/lib/config/env.schema"; import { buildScopeTag, sha256Hex, stableStringify } from "@/lib/request-identity"; +import { getCachedProxyRuntimeSettings } from "@/lib/system-settings/proxy-runtime"; import type { ClientFormat } from "../format-mapper"; import type { ProxySession } from "../session"; @@ -35,10 +36,18 @@ export interface ReplayIdentity { export const REPLAY_BYPASS_HEADER = "x-cch-no-replay"; +/** F2 有效开关:系统设置覆写优先(同步快照),null/无快照时跟随 env。 */ +export function isReplayEnabled(): boolean { + try { + return getCachedProxyRuntimeSettings()?.replayEnabled ?? getEnvConfig().ENABLE_REQUEST_REPLAY; + } catch { + return false; + } +} + export function deriveReplayIdentity(session: ProxySession): ReplayIdentity | null { try { - const env = getEnvConfig(); - if (!env.ENABLE_REQUEST_REPLAY) return null; + if (!isReplayEnabled()) return null; if (session.getEndpointPolicy().kind !== "default") return null; if (session.method !== "POST") return null; diff --git a/src/app/v1/_lib/proxy/replay/replay-spool.ts b/src/app/v1/_lib/proxy/replay/replay-spool.ts index baa20cbe9..ab7f1b9f4 100644 --- a/src/app/v1/_lib/proxy/replay/replay-spool.ts +++ b/src/app/v1/_lib/proxy/replay/replay-spool.ts @@ -1,7 +1,7 @@ import { getEnvConfig } from "@/lib/config/env.schema"; import { logger } from "@/lib/logger"; import type { ProxySession } from "../session"; -import type { ReplayIdentity } from "./replay-identity"; +import { isReplayEnabled, type ReplayIdentity } from "./replay-identity"; import { getReplayStore, type ReplayMeta } from "./replay-store"; /** @@ -344,7 +344,7 @@ export function createReplaySpoolIfOwner( }; try { const env = getEnvConfig(); - if (!env.ENABLE_REQUEST_REPLAY) return declineOwnership(); + if (!isReplayEnabled()) return declineOwnership(); if (activeSpoolCount >= env.REPLAY_MAX_CONCURRENT_SPOOLS) { logger.debug("[ReplaySpool] concurrent spool cap reached, skipping replay", { active: activeSpoolCount, diff --git a/src/drizzle/schema.ts b/src/drizzle/schema.ts index 60312e8e3..a4c0b6463 100644 --- a/src/drizzle/schema.ts +++ b/src/drizzle/schema.ts @@ -1020,6 +1020,12 @@ export const systemSettings = pgTable('system_settings', { .notNull() .default(true), + // F2 Replay 开关覆写(null = 跟随环境变量 ENABLE_REQUEST_REPLAY) + replayEnabled: boolean('replay_enabled'), + + // F3b 最长前缀匹配缓存模拟开关覆写(null = 跟随环境变量 ENABLE_CACHE_EFFECTIVENESS) + cacheEffectivenessEnabled: boolean('cache_effectiveness_enabled'), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(), }); diff --git a/src/instrumentation.ts b/src/instrumentation.ts index 2ba076002..118e796f6 100644 --- a/src/instrumentation.ts +++ b/src/instrumentation.ts @@ -262,14 +262,13 @@ async function startCacheEffectivenessScheduler(): Promise { } try { - const { getEnvConfig } = await import("@/lib/config/env.schema"); - if (!getEnvConfig().ENABLE_CACHE_EFFECTIVENESS) { - return; - } + // 开关支持系统设置运行时覆写:调度器常驻,每 tick 检查有效开关 + const { isCacheEffectivenessEnabled } = await import("@/lib/system-settings/proxy-runtime"); const { aggregateCacheEffectiveness } = await import("@/lib/cache-effectiveness/service"); const intervalMs = 5 * 60 * 1000; instrumentationState.__CCH_CACHE_EFFECTIVENESS_INTERVAL_ID__ = setInterval(() => { + if (!isCacheEffectivenessEnabled()) return; void aggregateCacheEffectiveness().catch((error) => { logger.warn("[Instrumentation] Cache effectiveness aggregation tick failed", { error: error instanceof Error ? error.message : String(error), diff --git a/src/lib/api-client/v1/openapi-types.gen.ts b/src/lib/api-client/v1/openapi-types.gen.ts index c8bc16148..0e14d5feb 100644 --- a/src/lib/api-client/v1/openapi-types.gen.ts +++ b/src/lib/api-client/v1/openapi-types.gen.ts @@ -12240,6 +12240,10 @@ export interface operations { streamGateMode: "off" | "shadow" | "enforce"; /** @description Whether fingerprintable requests force longest-prefix affinity for provider stickiness, skipping client session id binding. */ affinityIgnoreClientSessionId: boolean; + /** @description Request replay (response caching and upstream connection reuse) override. Null follows the ENABLE_REQUEST_REPLAY environment variable. */ + replayEnabled: boolean | null; + /** @description Longest-prefix cache-effectiveness simulation override (observability only). Null follows the ENABLE_CACHE_EFFECTIVENESS environment variable. */ + cacheEffectivenessEnabled: boolean | null; /** * Format: date-time * @description Creation time. @@ -12522,6 +12526,10 @@ export interface operations { streamGateMode?: "off" | "shadow" | "enforce"; /** @description Whether fingerprintable requests force longest-prefix affinity for provider stickiness, skipping client session id binding. */ affinityIgnoreClientSessionId?: boolean; + /** @description Request replay (response caching and upstream connection reuse) override. Null follows the ENABLE_REQUEST_REPLAY environment variable. */ + replayEnabled?: boolean | null; + /** @description Longest-prefix cache-effectiveness simulation override (observability only). Null follows the ENABLE_CACHE_EFFECTIVENESS environment variable. */ + cacheEffectivenessEnabled?: boolean | null; }; }; }; @@ -12679,6 +12687,10 @@ export interface operations { streamGateMode: "off" | "shadow" | "enforce"; /** @description Whether fingerprintable requests force longest-prefix affinity for provider stickiness, skipping client session id binding. */ affinityIgnoreClientSessionId: boolean; + /** @description Request replay (response caching and upstream connection reuse) override. Null follows the ENABLE_REQUEST_REPLAY environment variable. */ + replayEnabled: boolean | null; + /** @description Longest-prefix cache-effectiveness simulation override (observability only). Null follows the ENABLE_CACHE_EFFECTIVENESS environment variable. */ + cacheEffectivenessEnabled: boolean | null; /** * Format: date-time * @description Creation time. diff --git a/src/lib/api/v1/schemas/system-config.ts b/src/lib/api/v1/schemas/system-config.ts index bdb409111..fd0887a41 100644 --- a/src/lib/api/v1/schemas/system-config.ts +++ b/src/lib/api/v1/schemas/system-config.ts @@ -222,6 +222,18 @@ export const SystemSettingsSchema = z .describe( "Whether fingerprintable requests force longest-prefix affinity for provider stickiness, skipping client session id binding." ), + replayEnabled: z + .boolean() + .nullable() + .describe( + "Request replay (response caching and upstream connection reuse) override. Null follows the ENABLE_REQUEST_REPLAY environment variable." + ), + cacheEffectivenessEnabled: z + .boolean() + .nullable() + .describe( + "Longest-prefix cache-effectiveness simulation override (observability only). Null follows the ENABLE_CACHE_EFFECTIVENESS environment variable." + ), createdAt: IsoDateTimeStringSchema.describe("Creation time."), updatedAt: IsoDateTimeStringSchema.describe("Last update time."), }) diff --git a/src/lib/config/system-settings-cache.ts b/src/lib/config/system-settings-cache.ts index b090337c4..cc26ffb48 100644 --- a/src/lib/config/system-settings-cache.ts +++ b/src/lib/config/system-settings-cache.ts @@ -215,6 +215,8 @@ export async function getCachedSystemSettings(): Promise { DEFAULT_SETTINGS.publicStatusAggregationIntervalMinutes, streamGateMode: DEFAULT_SETTINGS.streamGateMode, affinityIgnoreClientSessionId: DEFAULT_SETTINGS.affinityIgnoreClientSessionId, + replayEnabled: null, + cacheEffectivenessEnabled: null, discoveryEnabled: DEFAULT_SETTINGS.discoveryEnabled, discoveryConcurrency: DEFAULT_SETTINGS.discoveryConcurrency, maxDiscoveryRounds: DEFAULT_SETTINGS.maxDiscoveryRounds, diff --git a/src/lib/system-settings/proxy-runtime.ts b/src/lib/system-settings/proxy-runtime.ts index cf9e3879f..94d1a5604 100644 --- a/src/lib/system-settings/proxy-runtime.ts +++ b/src/lib/system-settings/proxy-runtime.ts @@ -10,6 +10,8 @@ import { getCachedSystemSettings } from "@/lib/config/system-settings-cache"; * - affinityIgnoreClientSessionId:F3a「忽略客户端 Session ID」开关(默认开)—— * 可指纹化的请求强制使用最长前缀亲和做供应商粘性,跳过 session-ID 绑定读取; * 不可指纹化的请求仍走既有 session 复用。 + * - replayEnabled:F2 Replay 有效开关(系统设置覆写优先,null 时跟随 env) + * - cacheEffectivenessEnabled:F3b 缓存模拟有效开关(同上) * * 读取约定:热路径用 getCachedProxyRuntimeSettings()(同步、最近快照), * 异步场景用 getProxyRuntimeSettings()(带 TTL 缓存)。 @@ -17,20 +19,45 @@ import { getCachedSystemSettings } from "@/lib/config/system-settings-cache"; export interface ProxyRuntimeSettings { streamGateMode: "off" | "shadow" | "enforce"; affinityIgnoreClientSessionId: boolean; + replayEnabled: boolean; + cacheEffectivenessEnabled: boolean; } // 最近一次成功读取的快照;同步热路径消费,异步读取与开机预热负责保鲜。 let lastKnown: ProxyRuntimeSettings | null = null; +function envReplayDefault(): boolean { + try { + return getEnvConfig().ENABLE_REQUEST_REPLAY; + } catch { + return false; + } +} + +function envCacheEffectivenessDefault(): boolean { + try { + return getEnvConfig().ENABLE_CACHE_EFFECTIVENESS; + } catch { + return true; + } +} + function envFallback(): ProxyRuntimeSettings { try { const env = getEnvConfig(); return { streamGateMode: env.STREAM_GATE_MODE, affinityIgnoreClientSessionId: true, + replayEnabled: env.ENABLE_REQUEST_REPLAY, + cacheEffectivenessEnabled: env.ENABLE_CACHE_EFFECTIVENESS, }; } catch { - return { streamGateMode: "off", affinityIgnoreClientSessionId: true }; + return { + streamGateMode: "off", + affinityIgnoreClientSessionId: true, + replayEnabled: false, + cacheEffectivenessEnabled: true, + }; } } @@ -40,6 +67,9 @@ export async function getProxyRuntimeSettings(): Promise { lastKnown = { streamGateMode: settings.streamGateMode, affinityIgnoreClientSessionId: settings.affinityIgnoreClientSessionId, + replayEnabled: settings.replayEnabled ?? envReplayDefault(), + cacheEffectivenessEnabled: + settings.cacheEffectivenessEnabled ?? envCacheEffectivenessDefault(), }; return lastKnown; } catch { @@ -54,3 +84,8 @@ export async function getProxyRuntimeSettings(): Promise { export function getCachedProxyRuntimeSettings(): ProxyRuntimeSettings | null { return lastKnown; } + +/** F3b 有效开关(同步):系统设置覆写优先,无快照时跟随 env。 */ +export function isCacheEffectivenessEnabled(): boolean { + return lastKnown?.cacheEffectivenessEnabled ?? envCacheEffectivenessDefault(); +} diff --git a/src/lib/validation/schemas.ts b/src/lib/validation/schemas.ts index 6617cb1f0..c5bba2c71 100644 --- a/src/lib/validation/schemas.ts +++ b/src/lib/validation/schemas.ts @@ -1118,6 +1118,10 @@ export const UpdateSystemSettingsSchema = z .optional(), // 忽略客户端 Session ID(可选) affinityIgnoreClientSessionId: z.boolean().optional(), + // F2 Replay 响应缓存与复用(可选;null = 跟随环境变量) + replayEnabled: z.boolean().nullable().optional(), + // F3b 最长前缀匹配缓存模拟(可选;null = 跟随环境变量) + cacheEffectivenessEnabled: z.boolean().nullable().optional(), // Codex Session ID 补全(可选) enableCodexSessionIdCompletion: z.boolean().optional(), // Claude metadata.user_id 注入(可选) diff --git a/src/repository/_shared/transformers.ts b/src/repository/_shared/transformers.ts index 5dfbf830b..e57c69a43 100644 --- a/src/repository/_shared/transformers.ts +++ b/src/repository/_shared/transformers.ts @@ -316,6 +316,8 @@ export function toSystemSettings(dbSettings: any): SystemSettings { ? dbSettings.streamGateMode : "enforce", affinityIgnoreClientSessionId: dbSettings?.affinityIgnoreClientSessionId ?? true, + replayEnabled: dbSettings?.replayEnabled ?? null, + cacheEffectivenessEnabled: dbSettings?.cacheEffectivenessEnabled ?? null, createdAt: dbSettings?.createdAt ? new Date(dbSettings.createdAt) : new Date(), updatedAt: dbSettings?.updatedAt ? new Date(dbSettings.updatedAt) : new Date(), }; diff --git a/src/repository/system-config.ts b/src/repository/system-config.ts index a672d1a9d..ae2a0eb1d 100644 --- a/src/repository/system-config.ts +++ b/src/repository/system-config.ts @@ -202,6 +202,8 @@ function createFallbackSettings(): SystemSettings { ipGeoLookupEnabled: true, streamGateMode: "enforce", affinityIgnoreClientSessionId: true, + replayEnabled: null, + cacheEffectivenessEnabled: null, createdAt: now, updatedAt: now, }; @@ -280,6 +282,19 @@ const RECENT_COLUMN_LADDER: ReadonlyArray<{ // 本层更新失败(仍有列缺失)时记录的告警 updateWarn: string; }> = [ + { + key: "cacheEffectivenessEnabled", + column: systemSettings.cacheEffectivenessEnabled, + selectWarn: + "system_settings 表除 cacheEffectivenessEnabled 外仍有列缺失,继续回退到上一代字段集。", + updateWarn: "system_settings 表除 cacheEffectivenessEnabled 外仍有列缺失,继续降级更新。", + }, + { + key: "replayEnabled", + column: systemSettings.replayEnabled, + selectWarn: "system_settings 表除 replayEnabled 外仍有列缺失,继续回退到上一代字段集。", + updateWarn: "system_settings 表除 replayEnabled 外仍有列缺失,继续降级更新。", + }, { key: "affinityIgnoreClientSessionId", column: systemSettings.affinityIgnoreClientSessionId, @@ -876,6 +891,16 @@ export async function updateSystemSettings( updates.affinityIgnoreClientSessionId = payload.affinityIgnoreClientSessionId; } + // F2 Replay 开关覆写(如果提供;null = 清除覆写跟随环境变量) + if (payload.replayEnabled !== undefined) { + updates.replayEnabled = payload.replayEnabled; + } + + // F3b 缓存模拟开关覆写(如果提供;null = 清除覆写跟随环境变量) + if (payload.cacheEffectivenessEnabled !== undefined) { + updates.cacheEffectivenessEnabled = payload.cacheEffectivenessEnabled; + } + let updated; try { [updated] = await executor diff --git a/src/types/system-config.ts b/src/types/system-config.ts index 79441b62b..f3bfe0e42 100644 --- a/src/types/system-config.ts +++ b/src/types/system-config.ts @@ -158,6 +158,14 @@ export interface SystemSettings { // 不可指纹化的请求仍走会话复用 affinityIgnoreClientSessionId: boolean; + // F2 Replay(响应缓存与上游连接复用)开关覆写 + // null = 跟随环境变量 ENABLE_REQUEST_REPLAY(默认 false) + replayEnabled: boolean | null; + + // F3b 最长前缀匹配缓存模拟(理论 vs 实际缓存命中率,仅观测不影响路由)开关覆写 + // null = 跟随环境变量 ENABLE_CACHE_EFFECTIVENESS(默认 true) + cacheEffectivenessEnabled: boolean | null; + /** Bounded streaming Discovery settings. */ discoveryEnabled: boolean; discoveryConcurrency: number; @@ -284,4 +292,10 @@ export interface UpdateSystemSettingsInput { // 忽略客户端 Session ID(可选) affinityIgnoreClientSessionId?: boolean; + + // F2 Replay 开关(可选;null = 清除覆写跟随环境变量) + replayEnabled?: boolean | null; + + // F3b 缓存模拟开关(可选;null = 清除覆写跟随环境变量) + cacheEffectivenessEnabled?: boolean | null; } diff --git a/tests/unit/repository/system-config-degradation-ladder.test.ts b/tests/unit/repository/system-config-degradation-ladder.test.ts index ea75e70d7..9afac3765 100644 --- a/tests/unit/repository/system-config-degradation-ladder.test.ts +++ b/tests/unit/repository/system-config-degradation-ladder.test.ts @@ -7,6 +7,8 @@ import type { UpdateSystemSettingsInput } from "@/types/system-config"; // 近代新增列(最新在前),降级链按引入顺序逐层累计剥离。 const RECENT_COLUMNS = [ + "cacheEffectivenessEnabled", + "replayEnabled", "affinityIgnoreClientSessionId", "streamGateMode", "stickyTimeoutCooldownMs", @@ -27,6 +29,8 @@ const RECENT_COLUMNS = [ // 全量字段集(46 列)。 const FULL_COLUMNS = [ + "cacheEffectivenessEnabled", + "replayEnabled", "affinityIgnoreClientSessionId", "streamGateMode", "discoveryEnabled", @@ -142,7 +146,7 @@ function createResolvingSelectQuery(rows: unknown[]) { } describe("SystemSettings:列降级阶梯的尝试序列锁定", () => { - test("getSystemSettings 全部列缺失时按既定顺序尝试 14 套字段集", async () => { + test("getSystemSettings 全部列缺失时按既定顺序尝试全部字段集", async () => { vi.resetModules(); const selections: string[][] = []; @@ -185,7 +189,7 @@ describe("SystemSettings:列降级阶梯的尝试序列锁定", () => { const selectMock = vi.fn((selection: Record) => { selections.push(sortedKeys(selection)); callIndex += 1; - if (callIndex < 18) { + if (callIndex < 20) { return createRejectingSelectQuery({ code: "42703" }); } return createResolvingSelectQuery([ @@ -218,14 +222,14 @@ describe("SystemSettings:列降级阶梯的尝试序列锁定", () => { const result = await getSystemSettings(); - expect(selectMock).toHaveBeenCalledTimes(18); - // 第 17 次(近代链末层)不含这些新列;第 18 次(passThrough 世代)重新包含旧列。 - expect(selections[16]).not.toContain("enableThinkingEffortConflictRectifier"); - expect(selections[16]).not.toContain("allowNonConversationEndpointProviderFallback"); - expect(selections[16]).toContain("passThroughUpstreamErrorMessage"); - expect(selections[17]).toContain("enableThinkingEffortConflictRectifier"); - expect(selections[17]).toContain("allowNonConversationEndpointProviderFallback"); - expect(selections[17]).not.toContain("passThroughUpstreamErrorMessage"); + expect(selectMock).toHaveBeenCalledTimes(20); + // 第 19 次(近代链末层)不含这些新列;第 20 次(passThrough 世代)重新包含旧列。 + expect(selections[18]).not.toContain("enableThinkingEffortConflictRectifier"); + expect(selections[18]).not.toContain("allowNonConversationEndpointProviderFallback"); + expect(selections[18]).toContain("passThroughUpstreamErrorMessage"); + expect(selections[19]).toContain("enableThinkingEffortConflictRectifier"); + expect(selections[19]).toContain("allowNonConversationEndpointProviderFallback"); + expect(selections[19]).not.toContain("passThroughUpstreamErrorMessage"); // 世代字段集选出的真实值要透传,缺失列由 transformer 落默认值。 expect(result.siteTitle).toBe("Era Row"); @@ -236,7 +240,7 @@ describe("SystemSettings:列降级阶梯的尝试序列锁定", () => { expect(result.passThroughUpstreamErrorMessage).toBe(true); }); - test("updateSystemSettings 全部列缺失时按既定顺序尝试 13 套 set/returning 组合", async () => { + test("updateSystemSettings 全部列缺失时按既定顺序尝试全部 set/returning 组合", async () => { vi.resetModules(); const now = new Date("2026-01-04T00:00:00.000Z"); @@ -305,7 +309,7 @@ describe("SystemSettings:列降级阶梯的尝试序列锁定", () => { "system_settings 表列缺失,请执行数据库迁移以升级数据库结构。" ); - expect(updateMock).toHaveBeenCalledTimes(20); + expect(updateMock).toHaveBeenCalledTimes(22); const expectedReturningSequence = [ [...FULL_COLUMNS], diff --git a/tests/unit/repository/system-config-update-missing-columns.test.ts b/tests/unit/repository/system-config-update-missing-columns.test.ts index b602dc559..b03d055dc 100644 --- a/tests/unit/repository/system-config-update-missing-columns.test.ts +++ b/tests/unit/repository/system-config-update-missing-columns.test.ts @@ -291,7 +291,7 @@ describe("SystemSettings:数据库缺列时的保存兜底", () => { vi.useRealTimers(); }); - test("getSystemSettings 在仅缺 affinity_ignore_client_session_id 新列时应降级读取并默认开启", async () => { + test("getSystemSettings 在仅缺 cache_effectiveness_enabled 新列时应降级读取", async () => { vi.resetModules(); const now = new Date("2026-01-04T00:00:00.000Z"); @@ -299,7 +299,7 @@ describe("SystemSettings:数据库缺列时的保存兜底", () => { vi.setSystemTime(now); // 第一次 select(fullSelection) 因新列缺失而抛 42703; - // 第二次 select(selectionWithoutAffinityIgnore) 命中——验证新列已加入降级链最外层。 + // 第二次 select(去掉 cacheEffectivenessEnabled)命中——验证新列已加入降级链最外层。 const selectMock = vi .fn() .mockReturnValueOnce(createRejectedThenableQuery({ code: "42703" })) @@ -344,7 +344,9 @@ describe("SystemSettings:数据库缺列时的保存兜底", () => { // 关键回归保护:第二次 select 必须恰好剥离了最新列(最外层降级), // 而非旧行为先剥离更早引入的列。若新列未加入降级链最外层,下面断言会失败。 const secondSelection = selectMock.mock.calls[1]?.[0] as Record; - expect(secondSelection).not.toHaveProperty("affinityIgnoreClientSessionId"); + expect(secondSelection).not.toHaveProperty("cacheEffectivenessEnabled"); + expect(secondSelection).toHaveProperty("replayEnabled"); + expect(secondSelection).toHaveProperty("affinityIgnoreClientSessionId"); expect(secondSelection).toHaveProperty("streamGateMode"); expect(secondSelection).toHaveProperty("stickyTimeoutCooldownMs"); expect(secondSelection).toHaveProperty("racingTotalTimeoutMs"); From 1542c4dbdb08272c2f50a2554b8ab08a2023cc60 Mon Sep 17 00:00:00 2001 From: ding113 Date: Thu, 23 Jul 2026 12:56:34 -0700 Subject: [PATCH 02/12] feat(proxy): add idle timeout and echo exclusion to stream gate Add per-read idle timeout to the stream content gate so silent upstream connections fail fast with a 524 streaming_idle_timeout error, matching post-commit behavior. Exclude openai-responses lifecycle echo frames (response.created, response.in_progress, response.queued) from the prebuffer byte cap so large request bodies reflected in the first SSE event do not trigger false overflow. Capture commit markers (frame/chunk index, event name, buffered bytes, gate wait) for observability when not in high-concurrency mode. Invoke an onFirstByte callback on the first non-empty upstream chunk to preserve first-byte timeout semantics. Raise the default byte cap from 256 KB to 10 MB and exempt SSE event views from line and character limits in CodeDisplay so large echo frames render correctly. --- src/app/v1/_lib/proxy/forwarder.ts | 77 +++++++++++- src/app/v1/_lib/proxy/response-handler.ts | 4 +- src/app/v1/_lib/proxy/session.ts | 5 +- src/app/v1/_lib/proxy/stream-finalization.ts | 3 + .../proxy/stream-gate/frame-classifier.ts | 27 +++++ .../proxy/stream-gate/stream-content-gate.ts | 111 ++++++++++++++++-- .../ui/__tests__/code-display.test.tsx | 27 +++++ src/components/ui/code-display.tsx | 22 +++- src/lib/config/env.schema.ts | 5 +- src/types/message.ts | 17 +++ .../proxy/stream-gate-content-gate.test.ts | 103 ++++++++++++++++ .../stream-gate-forwarder-integration.test.ts | 13 +- .../stream-gate-frame-classifier.test.ts | 31 +++++ 13 files changed, 419 insertions(+), 26 deletions(-) diff --git a/src/app/v1/_lib/proxy/forwarder.ts b/src/app/v1/_lib/proxy/forwarder.ts index b7b40fd3a..d7f21cde8 100644 --- a/src/app/v1/_lib/proxy/forwarder.ts +++ b/src/app/v1/_lib/proxy/forwarder.ts @@ -119,6 +119,7 @@ import { resolveStreamGateCaps, resolveStreamGateMode, runStreamContentGate, + StreamPrecommitError, } from "./stream-gate/stream-content-gate"; import { detectThinkingBudgetRectifierTrigger, @@ -330,6 +331,8 @@ type StreamingHedgeAttempt = { * usage lives in the first chunk). */ firstChunk: Uint8Array | null; + /** F1 门控提交标记(该 attempt 门控提交时记录,随 hedge_winner 链条目落库)。 */ + gateAudit?: ProviderChainItem["streamGate"]; /** * Billing context snapshot for the INITIAL provider's losing attempt, captured BEFORE * commitWinner overwrites the shared session's model/context with the winner's. Null for @@ -1655,6 +1658,7 @@ export class ProxyForwarder { // - 首字节计时器(doForward 设置,response-handler 读到首字节才清除) // 在门控期间继续生效,天然升级为「首个有效内容超时」。 let streamingResponse = response; + let gateChainAudit: ProviderChainItem["streamGate"]; const gateMode = resolveStreamGateMode(); if ( gateMode === "enforce" && @@ -1663,20 +1667,27 @@ export class ProxyForwarder { ) { const gateFamily = mapProviderTypeToFamily(currentProvider.providerType); if (gateFamily) { + const runtime = session as ProxySession & { + responseController?: AbortController; + clearResponseTimeout?: () => void; + releaseAgent?: () => void; + }; const gateReader = response.body.getReader(); + const gateStartedAt = Date.now(); const gate = await runStreamContentGate(gateReader, { family: gateFamily, providerId: currentProvider.id, providerName: currentProvider.name, ...resolveStreamGateCaps(), + // 首字节到达即清除首字节计时器,保持「首字节超时」的原始语义—— + // 思考型模型可在首个内容帧前长时间输出中性帧,不应触发该计时器 + onFirstByte: () => runtime.clearResponseTimeout?.(), + // 门控等待期沿用供应商静默超时(与提交后 response-handler 的行为对齐) + idleTimeoutMs: currentProvider.streamingIdleTimeoutMs, + captureCommitMarker: !session.isHighConcurrencyModeEnabled(), }); if (!gate.committed) { - const runtime = session as ProxySession & { - responseController?: AbortController; - clearResponseTimeout?: () => void; - releaseAgent?: () => void; - }; // 先于清理读取超时来源:区分首字节/首内容超时与客户端断开 const timedOutBeforeContent = runtime.responseController?.signal.aborted === true && @@ -1687,6 +1698,36 @@ export class ProxyForwarder { runtime.clearResponseTimeout?.(); runtime.releaseAgent?.(); + if ( + gate.error instanceof StreamPrecommitError && + gate.error.gateReason === "idle_timeout" + ) { + // 与提交后的静默超时同构(524 + streaming_idle_timeout): + // 错误规则/熔断/切换逻辑无需区分静默发生在门控前后 + throw new ProxyError( + `供应商流式响应静默超时: ${currentProvider.streamingIdleTimeoutMs}ms 内未收到新数据`, + 524, + { + body: JSON.stringify({ + error: { + type: "streaming_idle_timeout", + message: `Provider stopped sending data for ${currentProvider.streamingIdleTimeoutMs}ms`, + timeout_ms: currentProvider.streamingIdleTimeoutMs, + }, + }), + parsed: { + error: { + type: "streaming_idle_timeout", + message: `Provider stopped sending data for ${currentProvider.streamingIdleTimeoutMs}ms`, + timeout_ms: currentProvider.streamingIdleTimeoutMs, + }, + }, + providerId: currentProvider.id, + providerName: currentProvider.name, + } + ); + } + if (timedOutBeforeContent) { throw new ProxyError( `供应商首个有效内容超时: 门控在收到有效内容帧前被首字节计时器中止`, @@ -1707,12 +1748,26 @@ export class ProxyForwarder { throw gate.error; } + if (gate.commitMarker) { + gateChainAudit = { + ...gate.commitMarker, + gateWaitMs: Date.now() - gateStartedAt, + }; + } + logger.info("ProxyForwarder: Stream content gate committed", { providerId: currentProvider.id, providerName: currentProvider.name, framesSeen: gate.framesSeen, prefixChunks: gate.prefixChunks.length, readerDone: gate.readerDone, + ...(gateChainAudit + ? { + commitEventName: gateChainAudit.eventName, + gateWaitMs: gateChainAudit.gateWaitMs, + echoExcludedBytes: gateChainAudit.echoExcludedBytes, + } + : {}), }); streamingResponse = new Response( @@ -1727,6 +1782,7 @@ export class ProxyForwarder { } setDeferredStreamingFinalization(session, { + ...(gateChainAudit ? { streamGate: gateChainAudit } : {}), providerId: currentProvider.id, providerName: currentProvider.name, providerPriority: currentProvider.priority || 0, @@ -4655,15 +4711,25 @@ export class ProxyForwarder { : null; if (hedgeGateFamily) { + const gateStartedAt = Date.now(); const gate = await runStreamContentGate(attempt.reader, { family: hedgeGateFamily, providerId: attempt.provider.id, providerName: attempt.provider.name, ...resolveStreamGateCaps(), + // 竞速路径首字节计时器已在响应头到达时清除;门控等待期沿用供应商静默超时 + idleTimeoutMs: attempt.provider.streamingIdleTimeoutMs, + captureCommitMarker: !session.isHighConcurrencyModeEnabled(), }); if (!gate.committed) { throw gate.error; } + if (gate.commitMarker) { + attempt.gateAudit = { + ...gate.commitMarker, + gateWaitMs: Date.now() - gateStartedAt, + }; + } // 保留完整门控前缀:若本 attempt 落败且需要计费,drain 时补回前缀里的 usage。 attempt.firstChunk = concatChunks(gate.prefixChunks); await commitWinner(attempt, gate.prefixChunks); @@ -5006,6 +5072,7 @@ export class ProxyForwarder { attemptNumber: attempt.sequence, statusCode: attempt.response.status, modelRedirect: getAttemptModelRedirect(attempt), + streamGate: attempt.gateAudit, }); abortAllAttempts(attempt, "hedge_loser"); diff --git a/src/app/v1/_lib/proxy/response-handler.ts b/src/app/v1/_lib/proxy/response-handler.ts index f628f390c..5825ef903 100644 --- a/src/app/v1/_lib/proxy/response-handler.ts +++ b/src/app/v1/_lib/proxy/response-handler.ts @@ -18,6 +18,7 @@ import type { SessionBindingSnapshot } from "@/lib/redis/session-binding"; import { SessionManager } from "@/lib/session-manager"; import { SessionTracker } from "@/lib/session-tracker"; import { CODEX_1M_CONTEXT_TOKEN_THRESHOLD } from "@/lib/special-attributes"; +import { isCacheEffectivenessEnabled } from "@/lib/system-settings/proxy-runtime"; import type { CostBreakdown, RequestCostCalculationOptions, @@ -2132,6 +2133,7 @@ function finalizeDeferredStreamingFinalizationIfNeeded( reason: meta.isFirstAttempt ? "request_success" : "retry_success", attemptNumber: meta.attemptNumber, statusCode: meta.upstreamStatusCode, + streamGate: meta.streamGate, }); } @@ -4456,7 +4458,7 @@ export class ProxyResponseHandler { latestStreamCommitSideEffects = postTerminalSideEffects; // F3b 缓存模拟列:仅开关开启时派生(关闭时保持 undefined,不落值) - const cacheScoreFields = getEnvConfig().ENABLE_CACHE_EFFECTIVENESS + const cacheScoreFields = isCacheEffectivenessEnabled() ? computeCacheScoreFields({ affinity: session.affinity, succeeded: effectiveStatusCode >= 200 && effectiveStatusCode < 300, diff --git a/src/app/v1/_lib/proxy/session.ts b/src/app/v1/_lib/proxy/session.ts index b14042f5c..0c864af8e 100644 --- a/src/app/v1/_lib/proxy/session.ts +++ b/src/app/v1/_lib/proxy/session.ts @@ -63,7 +63,6 @@ export interface SessionAffinityState { nominatedProviderId: number | null; /** 查找命中的边界指纹(未命中为 null) */ matchedFp: string | null; - matchedTier: "conversation" | "system" | null; } /** @@ -731,6 +730,8 @@ export class ProxySession { endpointFilterStats?: ProviderChainItem["endpointFilterStats"]; // endpoint filter statistics modelRedirect?: ProviderChainItem["modelRedirect"]; rawCrossProviderFallbackEnabled?: boolean; + streamGate?: ProviderChainItem["streamGate"]; // F1 门控提交标记 + affinity?: ProviderChainItem["affinity"]; // F3a 亲和命中详情 } ): void { const item: ProviderChainItem = { @@ -762,6 +763,8 @@ export class ProxySession { endpointFilterStats: metadata?.endpointFilterStats, modelRedirect: metadata?.modelRedirect ?? this.getCurrentModelRedirect(provider.id), rawCrossProviderFallbackEnabled: metadata?.rawCrossProviderFallbackEnabled, + streamGate: metadata?.streamGate, + affinity: metadata?.affinity, }; // 避免重复添加同一个供应商 diff --git a/src/app/v1/_lib/proxy/stream-finalization.ts b/src/app/v1/_lib/proxy/stream-finalization.ts index 383266c0d..6f34eb6db 100644 --- a/src/app/v1/_lib/proxy/stream-finalization.ts +++ b/src/app/v1/_lib/proxy/stream-finalization.ts @@ -1,4 +1,5 @@ import type { SessionBindingSnapshot } from "@/lib/redis/session-binding"; +import type { ProviderChainItem } from "@/types/message"; import type { ProxySession } from "./session"; export type DeferredStreamingDiscoveryLease = { @@ -70,6 +71,8 @@ export type DeferredStreamingFinalization = { hedgeBindingAuthorityPromise?: Promise; /** ResponseHandler-owned runtime lifecycle; attached when streaming starts. */ hedgeBindingHeartbeat?: DeferredStreamingBindingHeartbeat; + /** F1 门控提交标记:随成功链条目落库(高并发模式下为空)。 */ + streamGate?: ProviderChainItem["streamGate"]; }; const deferredMeta = new WeakMap(); diff --git a/src/app/v1/_lib/proxy/stream-gate/frame-classifier.ts b/src/app/v1/_lib/proxy/stream-gate/frame-classifier.ts index 7913f7c00..2bbfb65b6 100644 --- a/src/app/v1/_lib/proxy/stream-gate/frame-classifier.ts +++ b/src/app/v1/_lib/proxy/stream-gate/frame-classifier.ts @@ -254,6 +254,33 @@ export function mapProviderTypeToFamily( } } +/** + * 请求回显帧:openai-responses 家族的生命周期首帧(response.created / + * response.in_progress / response.queued)会在 data.response 里回显完整请求体 + * (instructions + input),大上下文请求单帧即可达数百 KB。 + * 门控 prebuffer 的字节计数应排除这类帧,避免把「请求大」误判成「流异常」。 + */ +const REQUEST_ECHO_EVENTS: Partial>> = { + "openai-responses": new Set(["response.created", "response.in_progress", "response.queued"]), +}; + +export function isRequestEchoFrame( + family: ProtocolFamily, + eventName: string | null, + data: string +): boolean { + const events = REQUEST_ECHO_EVENTS[family]; + if (!events) return false; + const effective = (eventName ?? "").trim(); + if (effective !== "") return events.has(effective); + // 无 event 行时嗅探 data 头部的 type 字段(上游实践中 type 总在最前) + const head = data.slice(0, 64); + for (const event of events) { + if (head.includes(`"type":"${event}"`)) return true; + } + return false; +} + /** * 对单个完整 SSE 帧分类。 * diff --git a/src/app/v1/_lib/proxy/stream-gate/stream-content-gate.ts b/src/app/v1/_lib/proxy/stream-gate/stream-content-gate.ts index 9f3db0e17..7bf4c8f85 100644 --- a/src/app/v1/_lib/proxy/stream-gate/stream-content-gate.ts +++ b/src/app/v1/_lib/proxy/stream-gate/stream-content-gate.ts @@ -2,7 +2,12 @@ import { getEnvConfig } from "@/lib/config/env.schema"; import { logger } from "@/lib/logger"; import { getCachedProxyRuntimeSettings } from "@/lib/system-settings/proxy-runtime"; import { ProxyError } from "../errors"; -import { classifyFrame, type FrameVerdict, type ProtocolFamily } from "./frame-classifier"; +import { + classifyFrame, + type FrameVerdict, + isRequestEchoFrame, + type ProtocolFamily, +} from "./frame-classifier"; import { SseFrameParser } from "./sse-frames"; /** @@ -12,6 +17,8 @@ import { SseFrameParser } from "./sse-frames"; * - error / malformed 帧 -> precommit 失败:调用方抛错走现有供应商切换循环 * - terminal 先于 content / 流提前结束 -> 空流失败 * - neutral 帧入缓冲;超过 event/byte 上限 -> prebuffer_overflow 失败 + * (请求回显帧不计入字节上限,见 isRequestEchoFrame) + * - 读间隔超过 idleTimeoutMs -> idle_timeout 失败(调用方按静默超时归类) * - read 拒绝(首字节超时 abort / 客户端断开)-> 原样返回错误,由调用方按来源归类 * * 客户端在提交前收到的字节数恒为 0:失败时整段前缀被丢弃。 @@ -21,7 +28,8 @@ export type StreamGateFailureReason = | "gate_error" | "decode_error" | "empty_stream" - | "prebuffer_overflow"; + | "prebuffer_overflow" + | "idle_timeout"; /** * 门控 precommit 错误。继承 ProxyError(statusCode 502)—— @@ -41,6 +49,7 @@ export class StreamPrecommitError extends ProxyError { frameData?: string; framesSeen?: number; bufferedBytes?: number; + echoExcludedBytes?: number; } ) { const message = `Stream content gate rejected upstream before first valid content (${reason})`; @@ -61,6 +70,7 @@ function buildGateErrorBody( frameData?: string; framesSeen?: number; bufferedBytes?: number; + echoExcludedBytes?: number; } ): string { if (reason === "gate_error" && detail.frameData) { @@ -74,6 +84,7 @@ function buildGateErrorBody( family: detail.family, frames_seen: detail.framesSeen, buffered_bytes: detail.bufferedBytes, + ...(detail.echoExcludedBytes ? { echo_excluded_bytes: detail.echoExcludedBytes } : {}), ...(detail.frameData ? { frame_preview: detail.frameData.slice(0, 500) } : {}), }, }); @@ -106,7 +117,7 @@ export function resolveStreamGateCaps(): StreamGateCaps { prebufferByteCap: env.STREAM_GATE_PREBUFFER_BYTE_CAP, }; } catch { - return { prebufferEventCap: 64, prebufferByteCap: 256 * 1024 }; + return { prebufferEventCap: 64, prebufferByteCap: 10 * 1024 * 1024 }; } } @@ -114,10 +125,36 @@ export interface StreamGateOptions extends StreamGateCaps { family: ProtocolFamily; providerId: number; providerName: string; + /** 首个非空上游 chunk 到达时回调一次(调用方用于清除首字节计时器,恢复其原始语义) */ + onFirstByte?: () => void; + /** 门控等待期的读间隔静默上限(毫秒;<=0 或未设不启用),对齐提交后 response-handler 的静默超时 */ + idleTimeoutMs?: number; + /** 记录触发提交的帧信息(高并发模式下关闭以省开销) */ + captureCommitMarker?: boolean; +} + +/** 触发门控提交的帧/chunk 标记(用于 Message 详情可观测性)。 */ +export interface StreamGateCommitMarker { + /** 触发提交的帧序号(1-based,含中性前缀帧) */ + frameIndex: number; + /** 触发提交的帧所在网络 chunk 序号(1-based) */ + chunkIndex: number; + /** 触发提交的 SSE event 名(无事件行时为 null) */ + eventName: string | null; + /** 提交时已缓冲的前缀字节数 */ + bufferedBytes: number; + /** 被排除出字节计数的请求回显帧字节数 */ + echoExcludedBytes: number; } export type StreamGateResult = - | { committed: true; prefixChunks: Uint8Array[]; framesSeen: number; readerDone: boolean } + | { + committed: true; + prefixChunks: Uint8Array[]; + framesSeen: number; + readerDone: boolean; + commitMarker: StreamGateCommitMarker | null; + } | { committed: false; error: Error }; /** @@ -134,7 +171,10 @@ export async function runStreamContentGate( const parser = new SseFrameParser(); const buffered: Uint8Array[] = []; let bufferedBytes = 0; + let echoExcludedBytes = 0; let framesSeen = 0; + let chunkIndex = 0; + let firstByteSeen = false; const failure = (reason: StreamGateFailureReason, frameData?: string): StreamGateResult => ({ committed: false, @@ -145,13 +185,28 @@ export async function runStreamContentGate( frameData, framesSeen, bufferedBytes, + echoExcludedBytes, }), }); + const commit = (eventName: string | null, readerDone: boolean): StreamGateResult => ({ + committed: true, + prefixChunks: buffered, + framesSeen, + readerDone, + commitMarker: options.captureCommitMarker + ? { frameIndex: framesSeen, chunkIndex, eventName, bufferedBytes, echoExcludedBytes } + : null, + }); + while (true) { let readResult: ReadableStreamReadResult; try { - readResult = await reader.read(); + const raced = await readWithIdleTimeout(reader, options.idleTimeoutMs); + if (raced === IDLE_TIMEOUT) { + return failure("idle_timeout"); + } + readResult = raced; } catch (readError) { // 首字节超时 abort / 客户端断开 / 传输错误:原样上抛,调用方按来源归类 return { @@ -166,7 +221,7 @@ export async function runStreamContentGate( framesSeen++; const verdict = classifyFrame(options.family, frame.eventName, frame.data); if (verdict === "content") { - return { committed: true, prefixChunks: buffered, framesSeen, readerDone: true }; + return commit(frame.eventName, true); } if (verdict === "error") return failure("gate_error", frame.data); if (verdict === "malformed") return failure("decode_error", frame.data); @@ -178,6 +233,12 @@ export async function runStreamContentGate( if (!chunk || chunk.byteLength === 0) { continue; } + if (!firstByteSeen) { + firstByteSeen = true; + // 上游已开始响应:调用方在此清除首字节计时器(保持「首字节」而非「首内容」语义) + options.onFirstByte?.(); + } + chunkIndex++; buffered.push(chunk); bufferedBytes += chunk.byteLength; @@ -185,7 +246,7 @@ export async function runStreamContentGate( framesSeen++; const verdict: FrameVerdict = classifyFrame(options.family, frame.eventName, frame.data); if (verdict === "content") { - return { committed: true, prefixChunks: buffered, framesSeen, readerDone: false }; + return commit(frame.eventName, false); } if (verdict === "error") { return failure("gate_error", frame.data); @@ -197,18 +258,50 @@ export async function runStreamContentGate( // 干净终止先于任何内容 = 空流 return failure("empty_stream", frame.data); } - // neutral: 继续缓冲;event 上限为逐帧硬上限(单 chunk 大量小帧也会触发) + // neutral: 继续缓冲;请求回显帧的载荷不计入字节上限(内存仍占用,由回显体积自然有界) + if (isRequestEchoFrame(options.family, frame.eventName, frame.data)) { + echoExcludedBytes += Buffer.byteLength(frame.data, "utf8"); + } + // event 上限为逐帧硬上限(单 chunk 大量小帧也会触发) if (framesSeen > options.prebufferEventCap) { return failure("prebuffer_overflow"); } } - if (bufferedBytes > options.prebufferByteCap) { + if (bufferedBytes - echoExcludedBytes > options.prebufferByteCap) { return failure("prebuffer_overflow"); } } } +const IDLE_TIMEOUT = Symbol("stream_gate_idle_timeout"); + +/** + * 单次 read 与静默计时器竞速。计时器胜出时挂起的 read 由调用方随后的 + * reader.cancel 收尾;对其附加空 catch 防止孤儿 rejection。 + */ +async function readWithIdleTimeout( + reader: ReadableStreamDefaultReader, + idleTimeoutMs: number | undefined +): Promise | typeof IDLE_TIMEOUT> { + if (!idleTimeoutMs || idleTimeoutMs <= 0) { + return reader.read(); + } + const readPromise = reader.read(); + let timer: NodeJS.Timeout | undefined; + try { + return await Promise.race([ + readPromise, + new Promise((resolve) => { + timer = setTimeout(() => resolve(IDLE_TIMEOUT), idleTimeoutMs); + }), + ]); + } finally { + clearTimeout(timer); + readPromise.catch(() => undefined); + } +} + /** 拼接门控前缀字节(供竞速败者计费 drain 恢复 usage 时复用现有单块逻辑)。 */ export function concatChunks(chunks: Uint8Array[]): Uint8Array | null { if (chunks.length === 0) return null; diff --git a/src/components/ui/__tests__/code-display.test.tsx b/src/components/ui/__tests__/code-display.test.tsx index 836bf1793..d3397ce5e 100644 --- a/src/components/ui/__tests__/code-display.test.tsx +++ b/src/components/ui/__tests__/code-display.test.tsx @@ -329,4 +329,31 @@ describe("CodeDisplay", () => { expect(container.textContent).toContain(dashboardMessages.sessions.codeDisplay.hardLimit.title); unmount(); }); + + test("sse keeps pretty event view for content over the pretty-mode char cap", () => { + // 请求回显帧场景:单个 SSE 事件即可超过 100K 字符,事件视图不得降级为 raw + const hugeEcho = `event: response.created\ndata: {"payload":"${"x".repeat(150_000)}"}\n\n`; + const sse = `${hugeEcho}event: response.output_text.delta\ndata: {"delta":"hi"}\n\n`; + const { container, unmount } = renderWithIntl( + + ); + + expect(container.querySelectorAll("[data-testid='code-display-sse-row']").length).toBe(2); + unmount(); + }); + + test("sse ignores the line cap and renders events beyond the max-lines limit", () => { + // 多 data 行事件轻松超过文本行数上限;SSE 视图不得因行数上限整体拒绝渲染 + const dataLines = Array.from({ length: 300 }, (_, i) => `data: line-${i}`).join("\n"); + const sse = `event: big\n${dataLines}\n\n`; + const { container, unmount } = renderWithIntl( + + ); + + expect(container.textContent).not.toContain( + dashboardMessages.sessions.codeDisplay.hardLimit.title + ); + expect(container.querySelectorAll("[data-testid='code-display-sse-row']").length).toBe(1); + unmount(); + }); }); diff --git a/src/components/ui/code-display.tsx b/src/components/ui/code-display.tsx index c08002d20..4face0d32 100644 --- a/src/components/ui/code-display.tsx +++ b/src/components/ui/code-display.tsx @@ -26,6 +26,10 @@ export type CodeDisplayLanguage = "json" | "sse" | "text"; const DEFAULT_MAX_CONTENT_BYTES = 1_000_000; // 1MB const DEFAULT_MAX_LINES = 10_000; const PRETTY_MODE_DEFAULT_MAX_CHARS = 100_000; +// SSE 事件视图为虚拟滚动 + 折叠预览,渲染成本与内容体积解耦: +// 不受 PRETTY_MODE_DEFAULT_MAX_CHARS 强制降级与行数上限约束, +// 字节上限对齐流式统计缓冲(response-handler STREAM_STATS_MAX_BUFFER_BYTES) +const SSE_MAX_CONTENT_BYTES = 10_000_000; export interface CodeDisplayProps { content: string; @@ -89,14 +93,20 @@ export function CodeDisplay({ }: CodeDisplayProps) { const t = useTranslations("dashboard.sessions"); const tActions = useTranslations("dashboard.actions"); - const resolvedMaxContentBytes = maxContentBytes ?? DEFAULT_MAX_CONTENT_BYTES; + const isSse = language === "sse"; + const resolvedMaxContentBytes = + maxContentBytes ?? (isSse ? SSE_MAX_CONTENT_BYTES : DEFAULT_MAX_CONTENT_BYTES); const resolvedMaxLines = maxLines ?? DEFAULT_MAX_LINES; const contentBytes = useMemo(() => new Blob([content]).size, [content]); const isOverMaxBytes = contentBytes > resolvedMaxContentBytes; const [mode, setMode] = useState<"raw" | "pretty">(() => { const defaultMode = getDefaultMode(language); - if (defaultMode === "pretty" && content.length > PRETTY_MODE_DEFAULT_MAX_CHARS) { + if ( + defaultMode === "pretty" && + language !== "sse" && + content.length > PRETTY_MODE_DEFAULT_MAX_CHARS + ) { return "raw"; } return defaultMode; @@ -124,18 +134,18 @@ export function CodeDisplay({ useEffect(() => { if (mode !== "pretty") return; - if (language === "text") return; + if (language === "text" || language === "sse") return; if (content.length <= PRETTY_MODE_DEFAULT_MAX_CHARS) return; setMode("raw"); }, [content, language, mode]); const lineCount = useMemo(() => { - if (isOverMaxBytes) return 0; + if (isOverMaxBytes || isSse) return 0; return countLinesUpTo(content, resolvedMaxLines + 1); - }, [content, isOverMaxBytes, resolvedMaxLines]); + }, [content, isOverMaxBytes, isSse, resolvedMaxLines]); const isLargeContent = content.length > 4000 || lineCount > 200; const isExpanded = expanded || !isLargeContent; - const isHardLimited = isOverMaxBytes || lineCount > resolvedMaxLines; + const isHardLimited = isOverMaxBytes || (!isSse && lineCount > resolvedMaxLines); const formattedJson = useMemo(() => { if (language !== "json") return content; diff --git a/src/lib/config/env.schema.ts b/src/lib/config/env.schema.ts index a7088249c..c974a0a51 100644 --- a/src/lib/config/env.schema.ts +++ b/src/lib/config/env.schema.ts @@ -187,13 +187,14 @@ export const EnvSchema = z.object({ // 流式内容门控:off=关闭;shadow=旁路分类只记录分歧;enforce=首个有效内容帧前缓冲+failover STREAM_GATE_MODE: z.enum(["off", "shadow", "enforce"]).default("off"), // 门控 precommit 缓冲上限:超限即视为该供应商流异常,failover 释放内存 + // (字节计数排除请求回显帧,见 stream-gate/frame-classifier.ts isRequestEchoFrame) STREAM_GATE_PREBUFFER_EVENT_CAP: z.coerce.number().int().min(1).max(4096).default(64), STREAM_GATE_PREBUFFER_BYTE_CAP: z.coerce .number() .int() .min(1024) - .max(16 * 1024 * 1024) - .default(256 * 1024), + .max(64 * 1024 * 1024) + .default(10 * 1024 * 1024), // 请求分离 + Replay:客户端断开后上游继续引流缓存,相同请求体重发续传 ENABLE_REQUEST_REPLAY: z.string().default("false").transform(booleanTransform), // owner 客户端仍在线时的并发相同请求去重(attached-live);关闭后仅 detached/completed 可命中 diff --git a/src/types/message.ts b/src/types/message.ts index 64d19730c..64e5ce130 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -248,6 +248,23 @@ export interface ProviderChainItem { | "ws_not_yet_implemented" | "ws_error_pre_first_event"; }; + + // === F1 流式内容门控提交标记(enforce 提交成功时记录;高并发模式下省略) === + streamGate?: { + frameIndex: number; // 触发提交的帧序号(1-based,含中性前缀帧) + chunkIndex: number; // 触发提交的帧所在网络 chunk 序号(1-based) + eventName: string | null; // 触发提交的 SSE event 名 + bufferedBytes: number; // 提交时已缓冲的前缀字节数 + echoExcludedBytes: number; // 被排除出字节计数的请求回显帧字节数 + gateWaitMs: number; // 门控等待时长(首字节到提交) + }; + + // === F3a 亲和命中详情(reason === "affinity_hit" 时记录) === + affinity?: { + matchedDepth: number | null; // 命中边界的消息深度(null = 无法定位) + matchedPrefixBytes: number | null; // 命中边界的规范化前缀字节数 + matchedFp: string; // 命中的链式指纹(截断哈希) + }; } /** diff --git a/tests/unit/proxy/stream-gate-content-gate.test.ts b/tests/unit/proxy/stream-gate-content-gate.test.ts index ff1c5a09d..11c89070e 100644 --- a/tests/unit/proxy/stream-gate-content-gate.test.ts +++ b/tests/unit/proxy/stream-gate-content-gate.test.ts @@ -248,3 +248,106 @@ describe("StreamPrecommitError classification", () => { expect(error).not.toBeInstanceOf(EmptyResponseError); }); }); + +describe("request echo frame byte-cap exclusion", () => { + const RESPONSES_OPTIONS = { + ...GATE_OPTIONS, + family: "openai-responses" as const, + prebufferByteCap: 1024, + }; + const bigPayload = "x".repeat(4096); + const ECHO_FRAME = `event: response.created\ndata: {"type":"response.created","response":{"instructions":"${bigPayload}"}}\n\n`; + const RESPONSES_DELTA = + 'event: response.output_text.delta\ndata: {"type":"response.output_text.delta","delta":"hi"}\n\n'; + + it("does not count request echo frames against the byte cap", async () => { + const reader = readerFromChunks([ECHO_FRAME, RESPONSES_DELTA]); + const result = await runStreamContentGate(reader, RESPONSES_OPTIONS); + expect(result.committed).toBe(true); + if (!result.committed) return; + expect(await drainPrefix(result.prefixChunks)).toContain("response.created"); + }); + + it("still overflows on oversized non-echo neutral frames", async () => { + const bigNeutral = `event: response.output_item.added\ndata: {"type":"response.output_item.added","item":"${bigPayload}"}\n\n`; + const reader = readerFromChunks([bigNeutral, RESPONSES_DELTA]); + const result = await runStreamContentGate(reader, RESPONSES_OPTIONS); + expect(result.committed).toBe(false); + if (result.committed) return; + expect(result.error).toBeInstanceOf(StreamPrecommitError); + expect((result.error as StreamPrecommitError).gateReason).toBe("prebuffer_overflow"); + }); + + it("reports echo-excluded bytes in the overflow error body", async () => { + const bigNonEcho = `event: response.in_progress\ndata: {"type":"response.in_progress","response":{"instructions":"${bigPayload}"}}\n\n`; + const oversizedTail = `event: response.output_item.added\ndata: {"item":"${"y".repeat(4096)}"}\n\n`; + const reader = readerFromChunks([bigNonEcho, oversizedTail, RESPONSES_DELTA]); + const result = await runStreamContentGate(reader, RESPONSES_OPTIONS); + expect(result.committed).toBe(false); + if (result.committed) return; + const body = JSON.parse((result.error as StreamPrecommitError).upstreamError?.body ?? "{}"); + expect(body.error.echo_excluded_bytes).toBeGreaterThan(4000); + }); +}); + +describe("gate idle timeout", () => { + it("fails with idle_timeout when no chunk arrives within idleTimeoutMs", async () => { + const neverEnding = new ReadableStream({ pull: () => new Promise(() => {}) }); + const result = await runStreamContentGate(neverEnding.getReader(), { + ...GATE_OPTIONS, + idleTimeoutMs: 20, + }); + expect(result.committed).toBe(false); + if (result.committed) return; + expect((result.error as StreamPrecommitError).gateReason).toBe("idle_timeout"); + }); + + it("does not time out while chunks keep arriving", async () => { + const reader = readerFromChunks([PING, MESSAGE_START, TEXT_DELTA]); + const result = await runStreamContentGate(reader, { ...GATE_OPTIONS, idleTimeoutMs: 5000 }); + expect(result.committed).toBe(true); + }); +}); + +describe("commit marker and first-byte callback", () => { + it("captures the committing frame/chunk marker when enabled", async () => { + const reader = readerFromChunks([PING, MESSAGE_START, TEXT_DELTA]); + const result = await runStreamContentGate(reader, { + ...GATE_OPTIONS, + captureCommitMarker: true, + }); + expect(result.committed).toBe(true); + if (!result.committed) return; + expect(result.commitMarker).toMatchObject({ + frameIndex: 3, + chunkIndex: 3, + eventName: "content_block_delta", + echoExcludedBytes: 0, + }); + expect(result.commitMarker?.bufferedBytes).toBeGreaterThan(0); + }); + + it("omits the marker when capture is disabled (high-concurrency mode)", async () => { + const reader = readerFromChunks([MESSAGE_START, TEXT_DELTA]); + const result = await runStreamContentGate(reader, { + ...GATE_OPTIONS, + captureCommitMarker: false, + }); + expect(result.committed).toBe(true); + if (!result.committed) return; + expect(result.commitMarker).toBeNull(); + }); + + it("invokes onFirstByte exactly once on the first non-empty chunk", async () => { + let calls = 0; + const reader = readerFromChunks([PING, MESSAGE_START, TEXT_DELTA]); + const result = await runStreamContentGate(reader, { + ...GATE_OPTIONS, + onFirstByte: () => { + calls += 1; + }, + }); + expect(result.committed).toBe(true); + expect(calls).toBe(1); + }); +}); diff --git a/tests/unit/proxy/stream-gate-forwarder-integration.test.ts b/tests/unit/proxy/stream-gate-forwarder-integration.test.ts index ea4c9d510..61adc58f0 100644 --- a/tests/unit/proxy/stream-gate-forwarder-integration.test.ts +++ b/tests/unit/proxy/stream-gate-forwarder-integration.test.ts @@ -369,7 +369,15 @@ describe("F1 stream content gate x ProxyForwarder sequential path", () => { }); return createSseResponse([PING_FRAME, ERROR_FRAME]); }); - doForward.mockImplementationOnce(async () => createSseResponse(WINNER_FRAMES)); + const clearResponseTimeout2 = vi.fn(); + const releaseAgent2 = vi.fn(); + doForward.mockImplementationOnce(async (attemptSession) => { + attachAttemptRuntime(attemptSession, { + clearResponseTimeout: clearResponseTimeout2, + releaseAgent: releaseAgent2, + }); + return createSseResponse(WINNER_FRAMES); + }); const response = await ProxyForwarder.send(session); const text = await response.text(); @@ -383,8 +391,9 @@ describe("F1 stream content gate x ProxyForwarder sequential path", () => { expect(text).not.toContain("overloaded_error"); // precommit 失败按 PROVIDER_ERROR 结算:计入熔断器并清理计时器 / agent 引用 + // 首字节到达即清一次(onFirstByte 保持首字节超时语义),失败清理再兜底一次 expect(mocks.recordFailure).toHaveBeenCalledWith(provider1.id, expect.any(Error)); - expect(clearResponseTimeout1).toHaveBeenCalledTimes(1); + expect(clearResponseTimeout1).toHaveBeenCalledTimes(2); expect(releaseAgent1).toHaveBeenCalledTimes(1); expect(session.provider?.id).toBe(provider2.id); diff --git a/tests/unit/proxy/stream-gate-frame-classifier.test.ts b/tests/unit/proxy/stream-gate-frame-classifier.test.ts index 6fcd89b9c..ecc3f4d7e 100644 --- a/tests/unit/proxy/stream-gate-frame-classifier.test.ts +++ b/tests/unit/proxy/stream-gate-frame-classifier.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { classifyFrame, + isRequestEchoFrame, mapProviderTypeToFamily, } from "@/app/v1/_lib/proxy/stream-gate/frame-classifier"; @@ -400,3 +401,33 @@ describe("classifyFrame: shared edge cases", () => { expect(classifyFrame("openai-chat", null, "[]")).toBe("neutral"); }); }); + +describe("isRequestEchoFrame", () => { + it("recognizes openai-responses lifecycle echo frames by event name", () => { + expect(isRequestEchoFrame("openai-responses", "response.created", "{}")).toBe(true); + expect(isRequestEchoFrame("openai-responses", "response.in_progress", "{}")).toBe(true); + expect(isRequestEchoFrame("openai-responses", "response.queued", "{}")).toBe(true); + expect(isRequestEchoFrame("openai-responses", "response.output_text.delta", "{}")).toBe(false); + }); + + it("sniffs the data head when the event line is absent", () => { + expect( + isRequestEchoFrame("openai-responses", null, '{"type":"response.created","response":{}}') + ).toBe(true); + expect(isRequestEchoFrame("openai-responses", null, '{"type":"other"}')).toBe(false); + // type 不在头部 64 字节内则不嗅探(上游实践中 type 总在最前) + expect( + isRequestEchoFrame( + "openai-responses", + null, + `{"pad":"${"z".repeat(80)}","type":"response.created"}` + ) + ).toBe(false); + }); + + it("never matches for families without echo frames", () => { + expect(isRequestEchoFrame("anthropic", "response.created", "{}")).toBe(false); + expect(isRequestEchoFrame("openai-chat", null, '{"type":"response.created"}')).toBe(false); + expect(isRequestEchoFrame("gemini", null, '{"type":"response.created"}')).toBe(false); + }); +}); From 914c1a96808772091d8cbfd31d76c158e237e17f Mon Sep 17 00:00:00 2001 From: ding113 Date: Thu, 23 Jul 2026 12:56:34 -0700 Subject: [PATCH 03/12] fix(proxy): exclude system-only prefixes from affinity matching Remove the F_sys fingerprint from the longest-prefix lookup sequence and winner write-back. System-prompt-only matches caused over-broad cross-conversation stickiness, binding unrelated dialogs to the same upstream. Only conversation-message boundaries participate in affinity matching now. Drop the now-unused tier field from AffinityHint. Record matched boundary details (depth, prefix bytes, fingerprint) on the provider chain item for observability. Suppress duplicate initial_selection entries when the provider was already nominated by session reuse or affinity hit. --- .../_lib/proxy/affinity/affinity-recorder.ts | 6 +- .../v1/_lib/proxy/affinity/affinity-store.ts | 14 ++--- src/app/v1/_lib/proxy/affinity/fingerprint.ts | 8 ++- src/app/v1/_lib/proxy/provider-selector.ts | 32 +++++++--- tests/unit/proxy/affinity-fingerprint.test.ts | 8 ++- tests/unit/proxy/affinity-recorder.test.ts | 11 ++-- tests/unit/proxy/affinity-store.test.ts | 62 ++++++------------- ...r-selector-affinity-ignore-session.test.ts | 2 + ...rovider-selector-affinity-priority.test.ts | 13 +++- ...r-selector-select-provider-by-type.test.ts | 2 + 10 files changed, 84 insertions(+), 74 deletions(-) diff --git a/src/app/v1/_lib/proxy/affinity/affinity-recorder.ts b/src/app/v1/_lib/proxy/affinity/affinity-recorder.ts index 1f212e40b..8eca681c8 100644 --- a/src/app/v1/_lib/proxy/affinity/affinity-recorder.ts +++ b/src/app/v1/_lib/proxy/affinity/affinity-recorder.ts @@ -11,7 +11,8 @@ import { fingerprintTip } from "./fingerprint"; */ /** - * 成功终态写回:tip + sys 两键绑定到胜出供应商,滑动 TTL。 + * 成功终态写回:tip 单键绑定到胜出供应商,滑动 TTL。 + * 不写 F_sys 键:仅系统提示词相同不构成前缀亲和(防跨对话过宽匹配)。 * 调用点:流式 commitSideEffects(计费持久化成功后)与非流式成功分支。 * replay serve / 竞速败者 / 失败重试不得调用。 */ @@ -24,10 +25,11 @@ export async function recordAffinityWinner( try { if (!(await isAffinityRoutingEnabled())) return; const tip = fingerprintTip(affinity.chain); + // tip 落在系统段(无会话消息)时不写绑定:与查找侧的 sys 排除保持一致 + if (tip.depth === 0) return; await getAffinityStore().put( affinity.scopeTag, tip.fp, - affinity.chain.sys.fp, providerId, getEnvConfig().PREFIX_AFFINITY_TTL_SECONDS ); diff --git a/src/app/v1/_lib/proxy/affinity/affinity-store.ts b/src/app/v1/_lib/proxy/affinity/affinity-store.ts index e4f6c4b6b..08306fc8f 100644 --- a/src/app/v1/_lib/proxy/affinity/affinity-store.ts +++ b/src/app/v1/_lib/proxy/affinity/affinity-store.ts @@ -40,8 +40,6 @@ const TOMBSTONE_TTL_SECONDS = 60; export interface AffinityHint { providerId: number; - /** 命中的边界在传入序列中的位置换算出的深度语义 */ - tier: "conversation" | "system"; matchedFp: string; /** 0-based:0 = 最深(tip),越大越浅;仅用于观测 */ matchedIndex: number; @@ -76,7 +74,8 @@ export class AffinityStore { } /** - * 最长前缀查找。fpsDeepestFirst 为最深->最浅指纹序列(最后一个是 F_sys)。 + * 最长前缀查找。fpsDeepestFirst 为最深->最浅的会话消息边界指纹序列 + * (不含 F_sys:仅系统提示词相同不构成前缀命中)。 * 命中活跃绑定即返回并滑动续期;墓碑被 Lua 跳过继续向浅。 */ async lookup( @@ -111,8 +110,6 @@ export class AffinityStore { providerId, matchedIndex, matchedFp: fpsDeepestFirst[matchedIndex] ?? "", - // 最后一个键是 F_sys:仅系统提示词命中 - tier: matchedIndex >= fpsDeepestFirst.length - 1 ? "system" : "conversation", }; } catch (error) { logger.warn("[AffinityStore] lookup failed, falling back to no-affinity", { @@ -124,13 +121,13 @@ export class AffinityStore { } /** - * 成功终态写回:只写 tip + sys 两键(对话推进天然累积链条,无需写全窗口)。 + * 成功终态写回:只写 tip 一键(对话推进天然累积链条,无需写全窗口)。 + * 不写 F_sys 键:仅系统提示词相同的跨对话请求不应互相粘连。 * 仅 owner 成功请求调用;replay serve / 竞速败者 / 失败重试不写。 */ async put( scopeTag: string, tipFp: string, - sysFp: string, providerId: number, ttlSeconds: number ): Promise { @@ -141,9 +138,6 @@ export class AffinityStore { const value = `1|${providerId}`; try { await redis.set(this.buildKey(scopeTag, tipFp), value, "EX", ttlSeconds); - if (sysFp && sysFp !== tipFp) { - await redis.set(this.buildKey(scopeTag, sysFp), value, "EX", ttlSeconds); - } } catch (error) { logger.warn("[AffinityStore] put failed", { error: error instanceof Error ? error.message : String(error), diff --git a/src/app/v1/_lib/proxy/affinity/fingerprint.ts b/src/app/v1/_lib/proxy/affinity/fingerprint.ts index cd6be60ad..82041026b 100644 --- a/src/app/v1/_lib/proxy/affinity/fingerprint.ts +++ b/src/app/v1/_lib/proxy/affinity/fingerprint.ts @@ -48,13 +48,17 @@ export function fingerprintTip(chain: FingerprintChain): FingerprintBoundary { return chain.tail.length > 0 ? chain.tail[chain.tail.length - 1] : chain.sys; } -/** 供查找使用的最深 -> 最浅指纹序列(最后一个永远是 Sys)。 */ +/** + * 供查找使用的最深 -> 最浅指纹序列。 + * + * 只包含会话消息边界(tail),不含 F_sys:仅系统提示词 + 工具相同的 + * 跨对话请求不应命中亲和(过宽匹配会把无关对话粘到同一供应商)。 + */ export function fingerprintsDeepestFirst(chain: FingerprintChain): string[] { const out: string[] = []; for (let i = chain.tail.length - 1; i >= 0; i--) { out.push(chain.tail[i].fp); } - out.push(chain.sys.fp); return out; } diff --git a/src/app/v1/_lib/proxy/provider-selector.ts b/src/app/v1/_lib/proxy/provider-selector.ts index 2df37b345..464b5205e 100644 --- a/src/app/v1/_lib/proxy/provider-selector.ts +++ b/src/app/v1/_lib/proxy/provider-selector.ts @@ -6,7 +6,10 @@ import { logger } from "@/lib/logger"; import { RateLimitService } from "@/lib/rate-limit"; import { buildScopeTag } from "@/lib/request-identity"; import { SessionManager } from "@/lib/session-manager"; -import { getProxyRuntimeSettings } from "@/lib/system-settings/proxy-runtime"; +import { + getProxyRuntimeSettings, + isCacheEffectivenessEnabled, +} from "@/lib/system-settings/proxy-runtime"; import { parseProviderGroups, resolveBillingProviderGroups, @@ -367,8 +370,14 @@ export class ProxyProviderResolver { attempt: attemptCount, }); - // 只在首次选择时记录到决策链(重试时的记录由 forwarder.ts 在请求完成后统一记录) - if (attemptCount === 1) { + // 只在首次选择时记录到决策链(重试时的记录由 forwarder.ts 在请求完成后统一记录)。 + // 供应商由会话复用/亲和提名预先确定时,对应链条目已写入且携带真实 selectionMethod, + // 不得再补一条 initial_selection(否则决策链看起来全部是加权随机初选)。 + const lastChainItem = session.getProviderChain().at(-1); + const stickySelectionRecorded = + lastChainItem?.id === session.provider.id && + (lastChainItem.reason === "session_reuse" || lastChainItem.reason === "affinity_hit"); + if (attemptCount === 1 && !stickySelectionRecorded) { const successContext = session.getLastSelectionContext(); session.addProviderToChain(session.provider, { reason: "initial_selection", @@ -537,7 +546,7 @@ export class ProxyProviderResolver { if (!keyId) return false; const env = getEnvConfig(); - if (!affinityRoutingEnabled && !env.ENABLE_CACHE_EFFECTIVENESS) return false; + if (!affinityRoutingEnabled && !isCacheEffectivenessEnabled()) return false; const chain = computeFingerprintChain( session.request.message, @@ -551,7 +560,6 @@ export class ProxyProviderResolver { chain, nominatedProviderId: null, matchedFp: null, - matchedTier: null, }; return true; } @@ -577,7 +585,6 @@ export class ProxyProviderResolver { if (!hint) return; affinity.matchedFp = hint.matchedFp; - affinity.matchedTier = hint.tier; const provider = await ProxyProviderResolver.validateAffinityCandidate( session, @@ -587,17 +594,26 @@ export class ProxyProviderResolver { // 候选不过硬校验:软回落,不写墓碑(可能只是临时熔断/调度窗口外) logger.debug("ProviderSelector: Affinity candidate rejected by hard validation", { providerId: hint.providerId, - tier: hint.tier, + matchedIndex: hint.matchedIndex, }); return; } + // 命中边界详情:随决策链落库,供请求详情展示「具体匹配到哪个前缀」 + const matchedBoundary = + affinity.chain.tail.find((boundary) => boundary.fp === hint.matchedFp) ?? null; + affinity.nominatedProviderId = provider.id; session.setProvider(provider); session.addProviderToChain(provider, { reason: "affinity_hit", selectionMethod: "prefix_affinity", circuitState: getCircuitState(provider.id), + affinity: { + matchedDepth: matchedBoundary?.depth ?? null, + matchedPrefixBytes: matchedBoundary?.prefixBytes ?? null, + matchedFp: hint.matchedFp, + }, decisionContext: { totalProviders: 0, enabledProviders: 0, @@ -624,8 +640,8 @@ export class ProxyProviderResolver { logger.info("ProviderSelector: Prefix affinity nomination accepted", { providerId: provider.id, providerName: provider.name, - tier: hint.tier, matchedIndex: hint.matchedIndex, + matchedDepth: matchedBoundary?.depth ?? null, }); } catch (error) { // 亲和路径任何异常都不影响主选路 diff --git a/tests/unit/proxy/affinity-fingerprint.test.ts b/tests/unit/proxy/affinity-fingerprint.test.ts index 377ebc144..95f6c566b 100644 --- a/tests/unit/proxy/affinity-fingerprint.test.ts +++ b/tests/unit/proxy/affinity-fingerprint.test.ts @@ -314,7 +314,8 @@ describe("computeFingerprintChain - edge cases", () => { expect(chain.tail).toHaveLength(0); expect(chain.sys.fp).toMatch(HEX32); expect(fingerprintTip(chain)).toBe(chain.sys); - expect(fingerprintsDeepestFirst(chain)).toEqual([chain.sys.fp]); + // 仅系统提示词不构成前缀亲和:查找序列不含 F_sys + expect(fingerprintsDeepestFirst(chain)).toEqual([]); }); it("missing system and tools still produce a valid chain", () => { @@ -471,10 +472,11 @@ describe("computeFingerprintChain - gemini formats", () => { }); describe("fingerprintsDeepestFirst", () => { - it("orders tail deepest-first with sys always last", () => { + it("orders tail deepest-first and excludes sys (system-only prefixes never match)", () => { const chain = mustChain(claudeBody([U1, A1, U2])); const fps = fingerprintsDeepestFirst(chain); - expect(fps).toEqual([chain.tail[2].fp, chain.tail[1].fp, chain.tail[0].fp, chain.sys.fp]); + expect(fps).toEqual([chain.tail[2].fp, chain.tail[1].fp, chain.tail[0].fp]); + expect(fps).not.toContain(chain.sys.fp); }); }); diff --git a/tests/unit/proxy/affinity-recorder.test.ts b/tests/unit/proxy/affinity-recorder.test.ts index 5605f4208..03915f606 100644 --- a/tests/unit/proxy/affinity-recorder.test.ts +++ b/tests/unit/proxy/affinity-recorder.test.ts @@ -59,7 +59,6 @@ function makeAffinity(overrides: Partial = {}): SessionAff chain: makeChain(), nominatedProviderId: null, matchedFp: null, - matchedTier: null, ...overrides, }; } @@ -75,15 +74,15 @@ beforeEach(() => { }); describe("recordAffinityWinner", () => { - it("writes tip + sys bindings for the winning provider with the configured TTL", async () => { + it("writes a single tip binding for the winning provider with the configured TTL", async () => { await recordAffinityWinner(makeSession(makeAffinity()), 42); expect(storeMocks.put).toHaveBeenCalledTimes(1); - expect(storeMocks.put).toHaveBeenCalledWith("scope123", "fp2", "sysfp", 42, 3600); + expect(storeMocks.put).toHaveBeenCalledWith("scope123", "fp2", 42, 3600); }); - it("uses sys as tip when the chain has no conversation boundaries", async () => { + it("skips writing when the chain has no conversation boundaries (system-only tip)", async () => { await recordAffinityWinner(makeSession(makeAffinity({ chain: makeChain(0) })), 7); - expect(storeMocks.put).toHaveBeenCalledWith("scope123", "sysfp", "sysfp", 7, 3600); + expect(storeMocks.put).not.toHaveBeenCalled(); }); it("is a no-op when both the env flag and the ignore-session setting are off", async () => { @@ -97,7 +96,7 @@ describe("recordAffinityWinner", () => { envControl.enabled = false; settingsControl.ignoreClientSessionId = true; await recordAffinityWinner(makeSession(makeAffinity()), 42); - expect(storeMocks.put).toHaveBeenCalledWith("scope123", "fp2", "sysfp", 42, 3600); + expect(storeMocks.put).toHaveBeenCalledWith("scope123", "fp2", 42, 3600); }); it("is a no-op without affinity state or with a non-positive provider id", async () => { diff --git a/tests/unit/proxy/affinity-store.test.ts b/tests/unit/proxy/affinity-store.test.ts index 2a9254ebd..a9689b085 100644 --- a/tests/unit/proxy/affinity-store.test.ts +++ b/tests/unit/proxy/affinity-store.test.ts @@ -52,7 +52,6 @@ describe("AffinityStore.lookup", () => { providerId: 42, matchedIndex: 0, matchedFp: "deep", - tier: "conversation", }); }); @@ -79,14 +78,13 @@ describe("AffinityStore.lookup", () => { providerId: 7, matchedIndex: 1, matchedFp: "mid", - tier: "conversation", }); }); - it("maps a match on the last (sys) fingerprint to the system tier", async () => { - const { client } = createLuaFakeRedis({ [key("s1", "sysf")]: "1|9" }); - const hint = await makeStore(client).lookup("s1", ["deep", "mid", "sysf"], 600); - expect(hint?.tier).toBe("system"); + it("matches the shallowest boundary when only it is active", async () => { + const { client } = createLuaFakeRedis({ [key("s1", "shallow")]: "1|9" }); + const hint = await makeStore(client).lookup("s1", ["deep", "mid", "shallow"], 600); + expect(hint?.matchedFp).toBe("shallow"); expect(hint?.matchedIndex).toBe(2); }); @@ -133,31 +131,20 @@ describe("AffinityStore.lookup", () => { }); describe("AffinityStore.put", () => { - it("writes only tip + sys boundaries with the active encoding and TTL", async () => { + it("writes only the tip boundary with the active encoding and TTL", async () => { const { client } = createLuaFakeRedis(); - await makeStore(client).put("s1", "tipfp", "sysfp", 42, 900); - expect(client.set).toHaveBeenCalledTimes(2); - expect(client.set).toHaveBeenNthCalledWith(1, key("s1", "tipfp"), "1|42", "EX", 900); - expect(client.set).toHaveBeenNthCalledWith(2, key("s1", "sysfp"), "1|42", "EX", 900); - }); - - it("writes a single key when tip and sys collide or sys is empty", async () => { - const { client } = createLuaFakeRedis(); - const store = makeStore(client); - await store.put("s1", "same", "same", 42, 900); - await store.put("s1", "solo", "", 42, 900); - expect(client.set).toHaveBeenCalledTimes(2); - expect(client.set).toHaveBeenNthCalledWith(1, key("s1", "same"), "1|42", "EX", 900); - expect(client.set).toHaveBeenNthCalledWith(2, key("s1", "solo"), "1|42", "EX", 900); + await makeStore(client).put("s1", "tipfp", 42, 900); + expect(client.set).toHaveBeenCalledTimes(1); + expect(client.set).toHaveBeenCalledWith(key("s1", "tipfp"), "1|42", "EX", 900); }); it("ignores invalid arguments", async () => { const { client } = createLuaFakeRedis(); const store = makeStore(client); - await store.put("", "tip", "sys", 42, 900); - await store.put("s1", "", "sys", 42, 900); - await store.put("s1", "tip", "sys", 0, 900); - await store.put("s1", "tip", "sys", 42, 0); + await store.put("", "tip", 42, 900); + await store.put("s1", "", 42, 900); + await store.put("s1", "tip", 0, 900); + await store.put("s1", "tip", 42, 0); expect(client.set).not.toHaveBeenCalled(); }); }); @@ -188,28 +175,19 @@ describe("AffinityStore.tombstone", () => { }); describe("AffinityStore round-trip through the fake Lua", () => { - it("put -> lookup hits, tombstone on tip falls back to sys, tombstone on sys misses", async () => { + it("put -> lookup hits the tip; tombstone on tip misses (no sys fallback)", async () => { const { client } = createLuaFakeRedis(); const store = makeStore(client); - await store.put("s1", "tip", "sysf", 42, 600); - expect(await store.lookup("s1", ["tip", "sysf"], 600)).toEqual({ + await store.put("s1", "tip", 42, 600); + expect(await store.lookup("s1", ["tip"], 600)).toEqual({ providerId: 42, matchedIndex: 0, matchedFp: "tip", - tier: "conversation", }); await store.tombstone("s1", "tip", "failover"); - expect(await store.lookup("s1", ["tip", "sysf"], 600)).toEqual({ - providerId: 42, - matchedIndex: 1, - matchedFp: "sysf", - tier: "system", - }); - - await store.tombstone("s1", "sysf", "failover"); - expect(await store.lookup("s1", ["tip", "sysf"], 600)).toBeNull(); + expect(await store.lookup("s1", ["tip"], 600)).toBeNull(); }); }); @@ -217,14 +195,14 @@ describe("AffinityStore fail-open behavior", () => { it("fails open when redis is unavailable or not ready", async () => { const nullStore = makeStore(null); expect(await nullStore.lookup("s1", ["fp"], 600)).toBeNull(); - await expect(nullStore.put("s1", "tip", "sys", 42, 600)).resolves.toBeUndefined(); + await expect(nullStore.put("s1", "tip", 42, 600)).resolves.toBeUndefined(); await expect(nullStore.tombstone("s1", "fp", "r")).resolves.toBeUndefined(); const { client } = createLuaFakeRedis(); client.status = "connecting"; const store = makeStore(client); expect(await store.lookup("s1", ["fp"], 600)).toBeNull(); - await store.put("s1", "tip", "sys", 42, 600); + await store.put("s1", "tip", 42, 600); await store.tombstone("s1", "fp", "r"); expect(client.eval).not.toHaveBeenCalled(); expect(client.set).not.toHaveBeenCalled(); @@ -236,7 +214,7 @@ describe("AffinityStore fail-open behavior", () => { client.set.mockRejectedValue(new Error("boom")); const store = makeStore(client); expect(await store.lookup("s1", ["fp"], 600)).toBeNull(); - await expect(store.put("s1", "tip", "sys", 42, 600)).resolves.toBeUndefined(); + await expect(store.put("s1", "tip", 42, 600)).resolves.toBeUndefined(); await expect(store.tombstone("s1", "fp", "r")).resolves.toBeUndefined(); }); @@ -246,7 +224,7 @@ describe("AffinityStore fail-open behavior", () => { client.set.mockRejectedValue("string failure"); const store = makeStore(client); expect(await store.lookup("s1", ["fp"], 600)).toBeNull(); - await expect(store.put("s1", "tip", "sys", 42, 600)).resolves.toBeUndefined(); + await expect(store.put("s1", "tip", 42, 600)).resolves.toBeUndefined(); await expect(store.tombstone("s1", "fp", "r")).resolves.toBeUndefined(); }); }); diff --git a/tests/unit/proxy/provider-selector-affinity-ignore-session.test.ts b/tests/unit/proxy/provider-selector-affinity-ignore-session.test.ts index dc11811e1..a5ba6a7bd 100644 --- a/tests/unit/proxy/provider-selector-affinity-ignore-session.test.ts +++ b/tests/unit/proxy/provider-selector-affinity-ignore-session.test.ts @@ -91,6 +91,7 @@ vi.mock("@/lib/system-settings/proxy-runtime", () => ({ streamGateMode: "off" as const, affinityIgnoreClientSessionId: settingsControl.ignoreClientSessionId, })), + isCacheEffectivenessEnabled: () => envControl.cacheEffectiveness, })); vi.mock("@/lib/config/env.schema", async (importOriginal) => { const actual = await importOriginal(); @@ -163,6 +164,7 @@ function makeSession(overrides: Record = {}): any { session.provider = p; }, addProviderToChain: vi.fn(), + getProviderChain: vi.fn(() => []), setLastSelectionContext: vi.fn((ctx: unknown) => { session._ctx = ctx; }), diff --git a/tests/unit/proxy/provider-selector-affinity-priority.test.ts b/tests/unit/proxy/provider-selector-affinity-priority.test.ts index b1d81963d..5199567bd 100644 --- a/tests/unit/proxy/provider-selector-affinity-priority.test.ts +++ b/tests/unit/proxy/provider-selector-affinity-priority.test.ts @@ -86,6 +86,8 @@ vi.mock("@/lib/system-settings/proxy-runtime", () => ({ streamGateMode: "off" as const, affinityIgnoreClientSessionId: settingsControl.ignoreClientSessionId, })), + + isCacheEffectivenessEnabled: () => false, })); vi.mock("@/lib/config/env.schema", async (importOriginal) => { const actual = await importOriginal(); @@ -143,6 +145,8 @@ const claudeMessage = { // Minimal ProxySession stub; loose typing matches sibling selector tests. function makeSession(overrides: Record = {}): any { + // 链条目联动:addProviderToChain 推入、getProviderChain 读出(覆盖粘性选择去重逻辑) + const chainItems: Array> = []; const session: any = { sessionId: null, provider: null, @@ -158,7 +162,10 @@ function makeSession(overrides: Record = {}): any { setProvider(p: Provider) { session.provider = p; }, - addProviderToChain: vi.fn(), + addProviderToChain: vi.fn((provider: Provider, metadata: Record = {}) => { + chainItems.push({ id: provider.id, ...metadata }); + }), + getProviderChain: vi.fn(() => chainItems), setLastSelectionContext: vi.fn((ctx: unknown) => { session._ctx = ctx; }), @@ -232,6 +239,10 @@ describe("ensure() nomination priority", () => { expect.objectContaining({ id: 42 }), expect.objectContaining({ reason: "affinity_hit", selectionMethod: "prefix_affinity" }) ); + // 亲和提名已写入链:ensure 不得再补 initial_selection(否则决策链显示为加权随机初选) + expect(session.getProviderChain().map((item: { reason?: string }) => item.reason)).toEqual([ + "affinity_hit", + ]); const [, luaKeysCount] = storeMocks.lookup.mock.calls[0] as unknown as [string, string[]]; expect(Array.isArray(luaKeysCount)).toBe(true); diff --git a/tests/unit/proxy/provider-selector-select-provider-by-type.test.ts b/tests/unit/proxy/provider-selector-select-provider-by-type.test.ts index 284bae735..b98a2d509 100644 --- a/tests/unit/proxy/provider-selector-select-provider-by-type.test.ts +++ b/tests/unit/proxy/provider-selector-select-provider-by-type.test.ts @@ -159,6 +159,7 @@ describe("ProxyProviderResolver.ensure - 分组倍率", () => { getLastSelectionContext: vi.fn(() => null), setGroupCostMultiplier, addProviderToChain: vi.fn(), + getProviderChain: vi.fn(() => []), getOriginalModel: vi.fn(() => "gpt-5.5"), } as unknown as Parameters[0]; @@ -243,6 +244,7 @@ describe("ProxyProviderResolver.ensure - 分组倍率", () => { getLastSelectionContext: vi.fn(() => context), setGroupCostMultiplier, addProviderToChain: vi.fn(), + getProviderChain: vi.fn(() => []), getOriginalModel: vi.fn(() => "gpt-5.5"), recordProviderSessionRef: vi.fn(), } as unknown as Parameters[0]; From bc848dfa4d9b6ef98e0488c1b60d176d858d3a32 Mon Sep 17 00:00:00 2001 From: ding113 Date: Thu, 23 Jul 2026 12:56:34 -0700 Subject: [PATCH 04/12] feat(ui): unify thinking effort display across Codex and Anthropic Replace the Codex-only reasoning effort component with a unified ThinkingEffortDisplay that extracts effort from both Codex reasoning.effort and Anthropic output_config.effort audit entries. Codex takes priority when both are present. Make the reasoning effort column toggleable in the logs table instead of always visible. Column visibility state persists per user and table. --- .../column-visibility-dropdown.tsx | 1 + .../components/SummaryTab.tsx | 32 ++- .../thinking-effort-display.test.tsx | 182 ++++++++++++++++++ .../_components/thinking-effort-display.tsx | 66 +++++++ .../_components/usage-logs-table.test.tsx | 79 +++++++- .../logs/_components/usage-logs-table.tsx | 26 ++- .../virtualized-logs-table.test.tsx | 50 ++++- .../_components/virtualized-logs-table.tsx | 27 +-- src/lib/column-visibility.test.ts | 22 ++- src/lib/column-visibility.ts | 4 +- src/lib/utils/thinking-effort.ts | 50 +++++ 11 files changed, 495 insertions(+), 44 deletions(-) create mode 100644 src/app/[locale]/dashboard/logs/_components/thinking-effort-display.test.tsx create mode 100644 src/app/[locale]/dashboard/logs/_components/thinking-effort-display.tsx create mode 100644 src/lib/utils/thinking-effort.ts diff --git a/src/app/[locale]/dashboard/logs/_components/column-visibility-dropdown.tsx b/src/app/[locale]/dashboard/logs/_components/column-visibility-dropdown.tsx index 651fdb298..9d6e59b64 100644 --- a/src/app/[locale]/dashboard/logs/_components/column-visibility-dropdown.tsx +++ b/src/app/[locale]/dashboard/logs/_components/column-visibility-dropdown.tsx @@ -33,6 +33,7 @@ const COLUMN_LABEL_KEYS: Record = { sessionId: "logs.columns.sessionId", ip: "logs.columns.ip", provider: "logs.columns.provider", + reasoningEffort: "logs.columns.reasoningEffort", tokens: "logs.columns.tokens", cost: "logs.columns.cost", cache: "logs.columns.cache", diff --git a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx index 9fce9036a..7e70d4679 100644 --- a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx +++ b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx @@ -26,8 +26,6 @@ import { Button } from "@/components/ui/button"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { Link } from "@/i18n/routing"; import { cn, formatTokenAmount } from "@/lib/utils"; -import { extractAnthropicEffortInfo } from "@/lib/utils/anthropic-effort"; -import { extractCodexReasoningEffortInfo } from "@/lib/utils/codex-reasoning-effort"; import { formatCurrency } from "@/lib/utils/currency"; import { buildHedgeBillingTable } from "@/lib/utils/hedge-billing"; import { resolveModelAuditDisplay } from "@/lib/utils/model-audit-display"; @@ -36,6 +34,7 @@ import { getThinkingSignatureModelDetectionSpecialSetting, hasPriorityServiceTierSpecialSetting, } from "@/lib/utils/special-settings"; +import { extractThinkingEffortInfo } from "@/lib/utils/thinking-effort"; import { getFake200ReasonKey } from "../../fake200-reason"; import { Fake200RetryTooltip } from "../../fake200-retry-tooltip"; import { @@ -109,27 +108,18 @@ export function SummaryTab({ getThinkingSignatureModelDetectionSpecialSetting(specialSettings); const showNoSignatureBadge = thinkingSignatureDetection?.source === "fallback_no_signature_with_thinking"; - const anthropicEffortInfo = extractAnthropicEffortInfo(specialSettings); - const codexReasoningEffortInfo = extractCodexReasoningEffortInfo(specialSettings); - const effortDisplay = codexReasoningEffortInfo + const thinkingEffortInfo = extractThinkingEffortInfo(specialSettings); + const effortMessageKey = thinkingEffortInfo?.source === "codex" ? "reasoningEffort" : "effort"; + const effortDisplay = thinkingEffortInfo ? { - requestedEffort: codexReasoningEffortInfo.requestedEffort, - effectiveEffort: codexReasoningEffortInfo.effectiveEffort, - isOverridden: codexReasoningEffortInfo.isOverridden, - label: t("reasoningEffort.label"), - tooltip: t("reasoningEffort.tooltip"), - overridden: t("reasoningEffort.overridden"), + requestedEffort: thinkingEffortInfo.requestedEffort, + effectiveEffort: thinkingEffortInfo.effectiveEffort, + isOverridden: thinkingEffortInfo.isOverridden, + label: t(`${effortMessageKey}.label`), + tooltip: t(`${effortMessageKey}.tooltip`), + overridden: t(`${effortMessageKey}.overridden`), } - : anthropicEffortInfo - ? { - requestedEffort: anthropicEffortInfo.originalEffort, - effectiveEffort: anthropicEffortInfo.overriddenEffort, - isOverridden: anthropicEffortInfo.isOverridden, - label: t("effort.label"), - tooltip: t("effort.tooltip"), - overridden: t("effort.overridden"), - } - : null; + : null; const isFake200PostStreamFailure = typeof errorMessage === "string" && errorMessage.startsWith("FAKE_200_"); const fake200Code = diff --git a/src/app/[locale]/dashboard/logs/_components/thinking-effort-display.test.tsx b/src/app/[locale]/dashboard/logs/_components/thinking-effort-display.test.tsx new file mode 100644 index 000000000..d9f75b192 --- /dev/null +++ b/src/app/[locale]/dashboard/logs/_components/thinking-effort-display.test.tsx @@ -0,0 +1,182 @@ +import type { ReactNode } from "react"; +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, test, vi } from "vitest"; +import { ThinkingEffortDisplay } from "./thinking-effort-display"; + +vi.mock("next-intl", () => ({ + useTranslations: () => (key: string) => key, +})); + +vi.mock("@/components/ui/tooltip", () => ({ + TooltipProvider: ({ children }: { children?: ReactNode }) =>
{children}
, + Tooltip: ({ children }: { children?: ReactNode }) =>
{children}
, + TooltipTrigger: ({ children }: { children?: ReactNode }) =>
{children}
, + TooltipContent: ({ children }: { children?: ReactNode }) =>
{children}
, +})); + +describe("ThinkingEffortDisplay", () => { + test("未记录思考强度时显示占位符", () => { + const html = renderToStaticMarkup(); + + expect(html).toContain(">-"); + expect(html).not.toContain('data-slot="thinking-effort"'); + }); + + test("显示 Codex 请求中的思考强度", () => { + const html = renderToStaticMarkup( + + ); + + expect(html).toContain('data-slot="thinking-effort"'); + expect(html).toContain("high"); + expect(html).toContain("reasoningEffort.tooltip"); + expect(html).not.toContain("overridden"); + }); + + test("供应商覆写 Codex 强度时显示请求值和实际值", () => { + const html = renderToStaticMarkup( + + ); + + expect(html).toContain("low"); + expect(html).toContain("max"); + expect(html).toContain("reasoningEffort.overridden"); + expect(html).toContain("lucide-arrow-right"); + }); + + test("显示 Anthropic 请求中的思考强度", () => { + const html = renderToStaticMarkup( + + ); + + expect(html).toContain('data-slot="thinking-effort"'); + expect(html).toContain("medium"); + expect(html).toContain("effort.tooltip"); + expect(html).not.toContain("overridden"); + }); + + test("供应商覆写 Anthropic 强度时显示请求值和实际值", () => { + const html = renderToStaticMarkup( + + ); + + expect(html).toContain("medium"); + expect(html).toContain("high"); + expect(html).toContain("effort.overridden"); + expect(html).toContain("lucide-arrow-right"); + }); + + test("供应商剥离 Anthropic 强度时仅显示请求值与覆写说明", () => { + const html = renderToStaticMarkup( + + ); + + expect(html).toContain("medium"); + expect(html).toContain("effort.overridden"); + expect(html).not.toContain("lucide-arrow-right"); + }); + + test("同时存在两种审计时优先展示 Codex 强度", () => { + const html = renderToStaticMarkup( + + ); + + expect(html).toContain("xhigh"); + expect(html).toContain("reasoningEffort.tooltip"); + expect(html).not.toContain(">medium<"); + }); +}); diff --git a/src/app/[locale]/dashboard/logs/_components/thinking-effort-display.tsx b/src/app/[locale]/dashboard/logs/_components/thinking-effort-display.tsx new file mode 100644 index 000000000..48ed68ba3 --- /dev/null +++ b/src/app/[locale]/dashboard/logs/_components/thinking-effort-display.tsx @@ -0,0 +1,66 @@ +"use client"; + +import { ArrowRight } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { ThinkingEffortBadge } from "@/components/customs/thinking-effort-badge"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { extractThinkingEffortInfo } from "@/lib/utils/thinking-effort"; +import type { SpecialSetting } from "@/types/special-settings"; + +/** 思考强度展示属性。 */ +interface ThinkingEffortDisplayProps { + /** 使用记录中的请求参数与供应商覆写审计。 */ + specialSettings: SpecialSetting[] | null | undefined; +} + +/** + * 在使用记录中展示任意模型的思考强度(Codex reasoning.effort 或 Anthropic effort)。 + * + * 供应商改变强度时同时展示请求值和实际转发值,避免只看到客户端参数而误判上游行为。 + */ +export function ThinkingEffortDisplay({ specialSettings }: ThinkingEffortDisplayProps) { + const t = useTranslations("dashboard.logs.details"); + const effortInfo = extractThinkingEffortInfo(specialSettings); + + if (!effortInfo) { + return -; + } + + const messageNamespace = effortInfo.source === "codex" ? "reasoningEffort" : "effort"; + const showEffectiveBadge = effortInfo.isOverridden && effortInfo.effectiveEffort != null; + + return ( + + + + + {effortInfo.requestedEffort && ( + + )} + {showEffectiveBadge && effortInfo.requestedEffort && ( + + + +

{t(`${messageNamespace}.tooltip`)}

+ {effortInfo.isOverridden && ( +

{t(`${messageNamespace}.overridden`)}

+ )} +
+
+
+ ); +} diff --git a/src/app/[locale]/dashboard/logs/_components/usage-logs-table.test.tsx b/src/app/[locale]/dashboard/logs/_components/usage-logs-table.test.tsx index d57182e3d..7b9dd0f0a 100644 --- a/src/app/[locale]/dashboard/logs/_components/usage-logs-table.test.tsx +++ b/src/app/[locale]/dashboard/logs/_components/usage-logs-table.test.tsx @@ -104,7 +104,7 @@ const discoveryTrace: RoutingTraceV1 = { events: [], }; -describe("usage-logs-table Codex reasoning effort", () => { +describe("usage-logs-table thinking effort", () => { test("在计费模型右侧显示思考强度列", () => { const html = renderToStaticMarkup( { expect(cells[7]?.textContent).toContain("max"); expect(cells[7]?.className).toContain("overflow-hidden"); }); + + test("显示 Anthropic 请求的思考强度", () => { + const html = renderToStaticMarkup( + {}} + isPending={false} + /> + ); + const container = document.createElement("div"); + container.innerHTML = html; + + const cells = [...container.querySelectorAll("tbody tr:first-child td")]; + expect(cells[7]?.textContent).toContain("medium"); + }); + + test("hiddenColumns 含 reasoningEffort 时隐藏思考强度列", () => { + const html = renderToStaticMarkup( + {}} + isPending={false} + hiddenColumns={["reasoningEffort"]} + /> + ); + const container = document.createElement("div"); + container.innerHTML = html; + + const headers = [...container.querySelectorAll("thead th")].map((node) => node.textContent); + expect(headers).not.toContain("logs.columns.reasoningEffort"); + expect(headers).toHaveLength(12); + expect(container.querySelectorAll("tbody tr:first-child td")).toHaveLength(12); + expect(html).not.toContain('data-slot="thinking-effort"'); + }); + + test("隐藏思考强度列后空态占满全部可见列", () => { + const html = renderToStaticMarkup( + {}} + isPending={false} + hiddenColumns={["reasoningEffort"]} + /> + ); + const container = document.createElement("div"); + container.innerHTML = html; + + const emptyCell = container.querySelector("tbody td"); + expect(emptyCell?.getAttribute("colspan")).toBe("12"); + }); }); describe("usage-logs-table multiplier badge", () => { diff --git a/src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx b/src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx index a60d16849..880eae98a 100644 --- a/src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx +++ b/src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx @@ -17,6 +17,7 @@ import { TableRow, } from "@/components/ui/table"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import type { LogsTableColumn } from "@/lib/column-visibility"; import { cn, formatTokenAmount } from "@/lib/utils"; import { copyTextToClipboard } from "@/lib/utils/clipboard"; import type { CurrencyCode } from "@/lib/utils/currency"; @@ -36,10 +37,10 @@ import { } from "@/lib/utils/special-settings"; import type { UsageLogRow } from "@/repository/usage-logs"; import type { BillingModelSource } from "@/types/system-config"; -import { CodexReasoningEffortDisplay } from "./codex-reasoning-effort-display"; import { ErrorDetailsDialog } from "./error-details-dialog"; import { ModelDisplayWithRedirect } from "./model-display-with-redirect"; import { ProviderChainPopover } from "./provider-chain-popover"; +import { ThinkingEffortDisplay } from "./thinking-effort-display"; interface UsageLogsTableProps { logs: UsageLogRow[]; @@ -51,6 +52,7 @@ interface UsageLogsTableProps { newLogIds?: Set; // 新增记录 ID 集合(用于动画高亮) currencyCode?: CurrencyCode; billingModelSource?: BillingModelSource; + hiddenColumns?: LogsTableColumn[]; } export function UsageLogsTable({ @@ -63,10 +65,13 @@ export function UsageLogsTable({ newLogIds, currencyCode = "USD", billingModelSource = "original", + hiddenColumns, }: UsageLogsTableProps) { const t = useTranslations("dashboard"); const tChain = useTranslations("provider-chain"); const totalPages = Math.ceil(total / pageSize); + const hideReasoningEffortColumn = hiddenColumns?.includes("reasoningEffort") ?? false; + const visibleColumnCount = 13 - (hideReasoningEffortColumn ? 1 : 0); const getPricingSourceLabel = (source: string) => t(`logs.billingDetails.pricingSource.${source}`); @@ -106,7 +111,11 @@ export function UsageLogsTable({ {t("logs.columns.ip")} {t("logs.columns.provider")} {t("logs.columns.model")} - {t("logs.columns.reasoningEffort")} + {hideReasoningEffortColumn ? null : ( + + {t("logs.columns.reasoningEffort")} + + )} {t("logs.columns.tokens")} {t("logs.columns.cache")} {t("logs.columns.cost")} @@ -117,7 +126,10 @@ export function UsageLogsTable({ {logs.length === 0 ? ( - + {t("logs.table.noData")} @@ -304,9 +316,11 @@ export function UsageLogsTable({ - - - + {hideReasoningEffortColumn ? null : ( + + + + )} diff --git a/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.test.tsx b/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.test.tsx index 27136c7a1..a16e6c5e4 100644 --- a/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.test.tsx +++ b/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.test.tsx @@ -195,7 +195,7 @@ function renderCostTooltipWithLog(overrides: Partial) { return tooltip; } -describe("virtualized-logs-table Codex reasoning effort", () => { +describe("virtualized-logs-table thinking effort", () => { test("在计费模型右侧显示思考强度列", () => { const html = renderTableWithLog({ model: "gpt-5.4", @@ -229,11 +229,57 @@ describe("virtualized-logs-table Codex reasoning effort", () => { expect(effortIndex).toBeGreaterThan(modelIndex); expect(tokensIndex).toBeGreaterThan(effortIndex); - const effortDisplay = container.querySelector('[data-slot="codex-reasoning-effort"]'); + const effortDisplay = container.querySelector('[data-slot="thinking-effort"]'); expect(effortDisplay?.textContent).toContain("low"); expect(effortDisplay?.textContent).toContain("max"); expect(effortDisplay?.closest(".overflow-hidden")).not.toBeNull(); }); + + test("显示 Anthropic 请求的思考强度", () => { + const html = renderTableWithLog({ + model: "claude-opus-4-5", + specialSettings: [ + { + type: "anthropic_effort", + scope: "request", + hit: true, + effort: "medium", + }, + ], + }); + const container = document.createElement("div"); + container.innerHTML = html; + + const effortDisplay = container.querySelector('[data-slot="thinking-effort"]'); + expect(effortDisplay?.textContent).toContain("medium"); + }); + + test("hides reasoning effort column when hiddenColumns includes reasoningEffort", () => { + mockIsLoading = false; + mockIsError = false; + mockError = null; + mockHasNextPage = false; + mockIsFetchingNextPage = false; + + mockLogs = [ + makeLog({ + id: 1, + specialSettings: [ + { type: "codex_reasoning_effort", scope: "request", hit: true, effort: "high" }, + ], + }), + ]; + + const htmlHidden = renderToStaticMarkup( + + ); + expect(htmlHidden).not.toContain("logs.columns.reasoningEffort"); + expect(htmlHidden).not.toContain('data-slot="thinking-effort"'); + }); }); describe("virtualized-logs-table multiplier badge", () => { diff --git a/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx b/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx index 38f497eb9..a8235282f 100644 --- a/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx +++ b/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx @@ -42,10 +42,10 @@ import { isProviderFinalized } from "@/lib/utils/provider-display"; import { hasPriorityServiceTierSpecialSetting } from "@/lib/utils/special-settings"; import type { UsageLogRow, UsageLogsBatchResult } from "@/repository/usage-logs"; import type { BillingModelSource } from "@/types/system-config"; -import { CodexReasoningEffortDisplay } from "./codex-reasoning-effort-display"; import { ErrorDetailsDialog } from "./error-details-dialog"; import { ModelDisplayWithRedirect } from "./model-display-with-redirect"; import { ProviderChainPopover } from "./provider-chain-popover"; +import { ThinkingEffortDisplay } from "./thinking-effort-display"; const BATCH_SIZE = 50; const ROW_HEIGHT = 52; // Estimated row height in pixels @@ -139,6 +139,7 @@ export function VirtualizedLogsTable({ const shouldPoll = autoRefreshEnabled && !isHistoryBrowsing; const hideProviderColumn = hiddenColumns?.includes("provider") ?? false; + const hideReasoningEffortColumn = hiddenColumns?.includes("reasoningEffort") ?? false; const hideUserColumn = hiddenColumns?.includes("user") ?? false; const hideKeyColumn = hiddenColumns?.includes("key") ?? false; const hideSessionIdColumn = hiddenColumns?.includes("sessionId") ?? false; @@ -693,12 +694,14 @@ export function VirtualizedLogsTable({ > {t("logs.columns.model")}
-
- {t("logs.columns.reasoningEffort")} -
+ {hideReasoningEffortColumn ? null : ( +
+ {t("logs.columns.reasoningEffort")} +
+ )} {hideTokensColumn ? null : (
- {/* Codex Reasoning Effort */} -
- -
+ {/* Thinking Effort */} + {hideReasoningEffortColumn ? null : ( +
+ +
+ )} {/* Tokens */} {hideTokensColumn ? null : ( diff --git a/src/lib/column-visibility.test.ts b/src/lib/column-visibility.test.ts index 59df779bb..0c0036538 100644 --- a/src/lib/column-visibility.test.ts +++ b/src/lib/column-visibility.test.ts @@ -52,8 +52,14 @@ describe("column-visibility", () => { vi.restoreAllMocks(); }); - test("keeps reasoning effort visible beside the billing model", () => { - expect(ALWAYS_VISIBLE_COLUMNS).toEqual(["time", "model", "reasoningEffort", "status"]); + test("keeps only structural columns always visible", () => { + expect(ALWAYS_VISIBLE_COLUMNS).toEqual(["time", "model", "status"]); + }); + + test("makes reasoning effort toggleable and visible by default", () => { + expect(DEFAULT_VISIBLE_COLUMNS).toContain("reasoningEffort"); + expect(DEFAULT_HIDDEN_COLUMNS).not.toContain("reasoningEffort"); + expect(getVisibleColumns(userId, tableId)).toContain("reasoningEffort"); }); describe("getHiddenColumns", () => { @@ -199,6 +205,17 @@ describe("column-visibility", () => { expect(visibleAfterToggleBack).not.toContain("cost"); expect(getVisibleColumns(userId, tableId)).toContain("cost"); }); + + test("toggles reasoning effort column visibility and persists it", () => { + const hiddenAfterToggle = toggleColumn(userId, tableId, "reasoningEffort"); + expect(hiddenAfterToggle).toContain("reasoningEffort"); + expect(getVisibleColumns(userId, tableId)).not.toContain("reasoningEffort"); + expect(mockStorage[storageKey]).toContain("reasoningEffort"); + + const visibleAfterToggleBack = toggleColumn(userId, tableId, "reasoningEffort"); + expect(visibleAfterToggleBack).not.toContain("reasoningEffort"); + expect(getVisibleColumns(userId, tableId)).toContain("reasoningEffort"); + }); }); describe("resetColumns", () => { @@ -228,6 +245,7 @@ describe("column-visibility", () => { expect(DEFAULT_VISIBLE_COLUMNS).toContain("key"); expect(DEFAULT_VISIBLE_COLUMNS).toContain("sessionId"); expect(DEFAULT_VISIBLE_COLUMNS).toContain("provider"); + expect(DEFAULT_VISIBLE_COLUMNS).toContain("reasoningEffort"); expect(DEFAULT_VISIBLE_COLUMNS).toContain("tokens"); expect(DEFAULT_VISIBLE_COLUMNS).toContain("cost"); expect(DEFAULT_VISIBLE_COLUMNS).toContain("cache"); diff --git a/src/lib/column-visibility.ts b/src/lib/column-visibility.ts index db580587e..dd28e7011 100644 --- a/src/lib/column-visibility.ts +++ b/src/lib/column-visibility.ts @@ -16,6 +16,7 @@ export type LogsTableColumn = | "sessionId" | "ip" | "provider" + | "reasoningEffort" | "tokens" | "cache" | "performance" @@ -30,6 +31,7 @@ export const DEFAULT_VISIBLE_COLUMNS: LogsTableColumn[] = [ "sessionId", "ip", "provider", + "reasoningEffort", "tokens", "cache", "performance", @@ -44,7 +46,7 @@ export const DEFAULT_HIDDEN_COLUMNS: LogsTableColumn[] = ["ip"]; /** * Columns that cannot be hidden (always visible) */ -export const ALWAYS_VISIBLE_COLUMNS = ["time", "model", "reasoningEffort", "status"] as const; +export const ALWAYS_VISIBLE_COLUMNS = ["time", "model", "status"] as const; /** * Get the storage key for a specific user and table diff --git a/src/lib/utils/thinking-effort.ts b/src/lib/utils/thinking-effort.ts new file mode 100644 index 000000000..aad4d3279 --- /dev/null +++ b/src/lib/utils/thinking-effort.ts @@ -0,0 +1,50 @@ +import { extractAnthropicEffortInfo } from "@/lib/utils/anthropic-effort"; +import { extractCodexReasoningEffortInfo } from "@/lib/utils/codex-reasoning-effort"; +import type { SpecialSetting } from "@/types/special-settings"; + +/** 思考强度审计来源:Codex 的 reasoning.effort 或 Anthropic 的 output_config.effort。 */ +export type ThinkingEffortSource = "codex" | "anthropic"; + +/** 任意模型统一后的思考强度展示信息,供列表列与请求详情共用。 */ +export interface ThinkingEffortInfo { + source: ThinkingEffortSource; + /** 客户端请求声明的思考强度;历史记录可能缺失。 */ + requestedEffort: string | null; + /** 实际转发给上游的思考强度;供应商覆写移除该参数时为 null。 */ + effectiveEffort: string | null; + isOverridden: boolean; +} + +/** + * 从 specialSettings 中提取任意模型的思考强度。 + * + * 复用 Codex 与 Anthropic 两个提取器并统一返回结构:Codex 审计优先, + * 其次回退到 Anthropic effort,两者都无则返回 null。 + */ +export function extractThinkingEffortInfo( + specialSettings: SpecialSetting[] | null | undefined +): ThinkingEffortInfo | null { + const codexInfo = extractCodexReasoningEffortInfo(specialSettings); + if (codexInfo) { + return { + source: "codex", + requestedEffort: codexInfo.requestedEffort, + effectiveEffort: codexInfo.effectiveEffort, + isOverridden: codexInfo.isOverridden, + }; + } + + const anthropicInfo = extractAnthropicEffortInfo(specialSettings); + if (anthropicInfo) { + return { + source: "anthropic", + requestedEffort: anthropicInfo.originalEffort, + effectiveEffort: anthropicInfo.isOverridden + ? anthropicInfo.overriddenEffort + : anthropicInfo.originalEffort, + isOverridden: anthropicInfo.isOverridden, + }; + } + + return null; +} From 2ad82e1787f3569fea9ec88832b59b95eee8db7c Mon Sep 17 00:00:00 2001 From: ding113 Date: Thu, 23 Jul 2026 12:56:34 -0700 Subject: [PATCH 05/12] feat(ui): add affinity, stream gate, and replay panels to log details Visualize prefix affinity hits in the logic trace timeline and provider chain popover with matched depth, prefix bytes, and fingerprint. Display stream gate commit markers (trigger frame, buffered bytes, gate wait) on committed chain entries. Add a replay serve info panel showing when a request was served from cache without an upstream call. Add i18n strings for all new UI elements across en, ja, ru, zh-CN, and zh-TW locales. --- messages/en/dashboard.json | 8 +- messages/en/provider-chain.json | 24 ++- messages/ja/dashboard.json | 6 + messages/ja/provider-chain.json | 24 ++- messages/ru/dashboard.json | 6 + messages/ru/provider-chain.json | 24 ++- messages/zh-CN/dashboard.json | 6 + messages/zh-CN/provider-chain.json | 24 ++- messages/zh-TW/dashboard.json | 6 + messages/zh-TW/provider-chain.json | 24 ++- .../components/LogicTraceTab.tsx | 203 +++++++++++++++--- .../_components/error-details-dialog/types.ts | 11 +- .../_components/provider-chain-popover.tsx | 53 +++++ 13 files changed, 382 insertions(+), 37 deletions(-) diff --git a/messages/en/dashboard.json b/messages/en/dashboard.json index 6256fb9cf..96ff6a8c6 100644 --- a/messages/en/dashboard.json +++ b/messages/en/dashboard.json @@ -132,6 +132,7 @@ "provider": "Provider", "model": "Billing Model", "reasoningEffort": "Reasoning Effort", + "reasoningEffortTooltip": "Thinking effort of the model request", "endpoint": "Endpoint", "inputTokens": "Input", "outputTokens": "Output", @@ -258,7 +259,7 @@ "responseModelLabel": "Actual Response Model", "mismatchTooltip": "The upstream provider returned a different model than the one requested. Billing is still based on the requested model.", "secondaryLineAriaLabel": "Actual response model: {model}", - "arrowPrefix": "\u21b3", + "arrowPrefix": "↳", "noSignatureBadge": "No thinking signature", "noSignatureTooltip": "Thinking was enabled but no thinking signature was returned in the stream; the actual response model falls back to the plain model field from message_start." }, @@ -549,6 +550,11 @@ "originDecisionLoading": "Loading original decision...", "originDecisionUnavailable": "Original decision record unavailable", "originDecisionExpand": "View original selection" + }, + "replayServe": { + "title": "Served from Replay Cache", + "desc": "This request was served from the replay cache (identical request already in flight or completed). No upstream provider call was made and no cost was incurred.", + "replayId": "Replay ID" } }, "providerChain": { diff --git a/messages/en/provider-chain.json b/messages/en/provider-chain.json index 3b596c093..90b57631f 100644 --- a/messages/en/provider-chain.json +++ b/messages/en/provider-chain.json @@ -60,6 +60,7 @@ "concurrent_limit_failed": "Concurrent Limit", "http2_fallback": "HTTP/2 Fallback", "session_reuse": "Session Reuse", + "affinity_hit": "Cache Reuse (Prefix Affinity)", "initial_selection": "Initial Selection", "endpoint_pool_exhausted": "Endpoint Pool Exhausted", "vendor_type_all_timeout": "Vendor-Type All Endpoints Timeout", @@ -249,6 +250,27 @@ "session_reuse": "Session Reuse", "weighted_random": "Weighted Random", "group_filtered": "Group Filtered", - "fail_open_fallback": "Fail-Open Fallback" + "fail_open_fallback": "Fail-Open Fallback", + "prefix_affinity": "Longest-Prefix Affinity" + }, + "affinity": { + "matchedDepth": "Matched depth", + "matchedPrefixBytes": "Matched prefix bytes", + "matchedFp": "Matched fingerprint" + }, + "streamGate": { + "title": "Stream Gate Commit", + "frameIndex": "Trigger frame", + "chunkIndex": "Trigger chunk", + "eventName": "Trigger event", + "bufferedBytes": "Buffered bytes", + "echoExcludedBytes": "Echo-excluded bytes", + "gateWaitMs": "Gate wait" + }, + "replayServe": { + "sources": { + "completed": "Completed replay", + "attached_live": "Attached to live stream" + } } } diff --git a/messages/ja/dashboard.json b/messages/ja/dashboard.json index 01121a9fc..617593896 100644 --- a/messages/ja/dashboard.json +++ b/messages/ja/dashboard.json @@ -132,6 +132,7 @@ "provider": "プロバイダー", "model": "課金モデル", "reasoningEffort": "推論強度", + "reasoningEffortTooltip": "このモデルリクエストの推論強度", "endpoint": "エンドポイント", "inputTokens": "入力", "outputTokens": "出力", @@ -549,6 +550,11 @@ "originDecisionLoading": "元の決定を読み込み中...", "originDecisionUnavailable": "元の決定記録は利用できません", "originDecisionExpand": "元の選択を表示" + }, + "replayServe": { + "title": "Replay キャッシュから応答", + "desc": "このリクエストは Replay キャッシュから直接応答されました(同一リクエストが進行中または完了済み)。上流プロバイダーへの呼び出しは行われず、費用は発生しません。", + "replayId": "Replay ID" } }, "providerChain": { diff --git a/messages/ja/provider-chain.json b/messages/ja/provider-chain.json index 58dcb8e4d..3106f1a13 100644 --- a/messages/ja/provider-chain.json +++ b/messages/ja/provider-chain.json @@ -60,6 +60,7 @@ "concurrent_limit_failed": "同時実行制限", "http2_fallback": "HTTP/2 フォールバック", "session_reuse": "セッション再利用", + "affinity_hit": "キャッシュ再利用(プレフィックス親和)", "initial_selection": "初期選択", "endpoint_pool_exhausted": "エンドポイントプール枯渇", "vendor_type_all_timeout": "ベンダータイプ全エンドポイントタイムアウト", @@ -249,6 +250,27 @@ "session_reuse": "セッション再利用", "weighted_random": "重み付きランダム", "group_filtered": "グループフィルタ", - "fail_open_fallback": "フェイルオープンフォールバック" + "fail_open_fallback": "フェイルオープンフォールバック", + "prefix_affinity": "最長プレフィックス親和" + }, + "affinity": { + "matchedDepth": "一致深度", + "matchedPrefixBytes": "一致プレフィックスバイト数", + "matchedFp": "一致フィンガープリント" + }, + "streamGate": { + "title": "ストリームゲートコミット", + "frameIndex": "トリガーフレーム番号", + "chunkIndex": "トリガーチャンク番号", + "eventName": "トリガーイベント", + "bufferedBytes": "バッファ済みバイト数", + "echoExcludedBytes": "エコー除外バイト数", + "gateWaitMs": "ゲート待機" + }, + "replayServe": { + "sources": { + "completed": "完了済みリプレイ", + "attached_live": "ライブストリームに追随" + } } } diff --git a/messages/ru/dashboard.json b/messages/ru/dashboard.json index 9010fe27d..1a5bf7db0 100644 --- a/messages/ru/dashboard.json +++ b/messages/ru/dashboard.json @@ -132,6 +132,7 @@ "provider": "Поставщик", "model": "Модель тарификации", "reasoningEffort": "Интенсивность рассуждений", + "reasoningEffortTooltip": "Интенсивность рассуждений для запроса этой модели", "endpoint": "Эндпоинт", "inputTokens": "Вход", "outputTokens": "Выход", @@ -549,6 +550,11 @@ "originDecisionLoading": "Загрузка исходного решения...", "originDecisionUnavailable": "Запись исходного решения недоступна", "originDecisionExpand": "Просмотр исходного выбора" + }, + "replayServe": { + "title": "Обслужено из Replay-кэша", + "desc": "Запрос обслужен из Replay-кэша (идентичный запрос уже выполняется или завершён). Обращение к провайдеру не выполнялось, затрат нет.", + "replayId": "Replay ID" } }, "providerChain": { diff --git a/messages/ru/provider-chain.json b/messages/ru/provider-chain.json index bbc34ad9d..e6aef87af 100644 --- a/messages/ru/provider-chain.json +++ b/messages/ru/provider-chain.json @@ -60,6 +60,7 @@ "concurrent_limit_failed": "Лимит параллельных запросов", "http2_fallback": "Откат HTTP/2", "session_reuse": "Повторное использование сессии", + "affinity_hit": "Переиспользование кэша (префиксная аффинность)", "initial_selection": "Первоначальный выбор", "endpoint_pool_exhausted": "Пул конечных точек исчерпан", "vendor_type_all_timeout": "Тайм-аут всех конечных точек типа поставщика", @@ -249,6 +250,27 @@ "session_reuse": "Повторное использование сессии", "weighted_random": "Взвешенный случайный", "group_filtered": "Фильтрация по группе", - "fail_open_fallback": "Резервный вариант при сбое" + "fail_open_fallback": "Резервный вариант при сбое", + "prefix_affinity": "Аффинность по наибольшему префиксу" + }, + "affinity": { + "matchedDepth": "Глубина совпадения", + "matchedPrefixBytes": "Байты совпавшего префикса", + "matchedFp": "Совпавший отпечаток" + }, + "streamGate": { + "title": "Коммит стрим-гейта", + "frameIndex": "Триггерный кадр", + "chunkIndex": "Триггерный чанк", + "eventName": "Триггерное событие", + "bufferedBytes": "Буферизовано байт", + "echoExcludedBytes": "Исключено байт эха", + "gateWaitMs": "Ожидание гейта" + }, + "replayServe": { + "sources": { + "completed": "Завершённый повтор", + "attached_live": "Присоединение к живому потоку" + } } } diff --git a/messages/zh-CN/dashboard.json b/messages/zh-CN/dashboard.json index ad2ce3cfe..dd06bf527 100644 --- a/messages/zh-CN/dashboard.json +++ b/messages/zh-CN/dashboard.json @@ -132,6 +132,7 @@ "provider": "供应商", "model": "计费模型", "reasoningEffort": "思考强度", + "reasoningEffortTooltip": "该模型请求的思考强度", "endpoint": "端点", "inputTokens": "输入", "outputTokens": "输出", @@ -549,6 +550,11 @@ "originDecisionLoading": "正在加载原始决策...", "originDecisionUnavailable": "原始决策记录不可用", "originDecisionExpand": "查看原始选择" + }, + "replayServe": { + "title": "由 Replay 缓存服务", + "desc": "该请求由 Replay 缓存直接服务(相同请求正在进行或已完成),未发起上游供应商调用,不产生费用。", + "replayId": "Replay ID" } }, "providerChain": { diff --git a/messages/zh-CN/provider-chain.json b/messages/zh-CN/provider-chain.json index d136eed35..45c8b6934 100644 --- a/messages/zh-CN/provider-chain.json +++ b/messages/zh-CN/provider-chain.json @@ -60,6 +60,7 @@ "concurrent_limit_failed": "并发限制", "http2_fallback": "HTTP/2 回退", "session_reuse": "会话复用", + "affinity_hit": "缓存复用(前缀亲和)", "initial_selection": "首次选择", "endpoint_pool_exhausted": "端点池耗尽", "vendor_type_all_timeout": "供应商类型全端点超时", @@ -249,6 +250,27 @@ "session_reuse": "会话复用", "weighted_random": "加权随机", "group_filtered": "分组过滤", - "fail_open_fallback": "故障开放回退" + "fail_open_fallback": "故障开放回退", + "prefix_affinity": "最长前缀亲和" + }, + "affinity": { + "matchedDepth": "命中深度", + "matchedPrefixBytes": "命中前缀字节", + "matchedFp": "命中指纹" + }, + "streamGate": { + "title": "流式门控提交", + "frameIndex": "触发帧序号", + "chunkIndex": "触发 Chunk 序号", + "eventName": "触发事件", + "bufferedBytes": "已缓冲字节", + "echoExcludedBytes": "回显排除字节", + "gateWaitMs": "门控等待" + }, + "replayServe": { + "sources": { + "completed": "已完成重放", + "attached_live": "跟尾在途流" + } } } diff --git a/messages/zh-TW/dashboard.json b/messages/zh-TW/dashboard.json index bef309e78..3d262da4e 100644 --- a/messages/zh-TW/dashboard.json +++ b/messages/zh-TW/dashboard.json @@ -132,6 +132,7 @@ "provider": "供應商", "model": "計費模型", "reasoningEffort": "思考強度", + "reasoningEffortTooltip": "該模型請求的思考強度", "endpoint": "端點", "inputTokens": "輸入", "outputTokens": "輸出", @@ -549,6 +550,11 @@ "originDecisionLoading": "正在載入原始決策...", "originDecisionUnavailable": "原始決策記錄不可用", "originDecisionExpand": "查看原始選擇" + }, + "replayServe": { + "title": "由 Replay 快取服務", + "desc": "該請求由 Replay 快取直接服務(相同請求正在進行或已完成),未發起上游供應商呼叫,不產生費用。", + "replayId": "Replay ID" } }, "providerChain": { diff --git a/messages/zh-TW/provider-chain.json b/messages/zh-TW/provider-chain.json index de200a133..b51c8ce31 100644 --- a/messages/zh-TW/provider-chain.json +++ b/messages/zh-TW/provider-chain.json @@ -60,6 +60,7 @@ "concurrent_limit_failed": "並發限制", "http2_fallback": "HTTP/2 回退", "session_reuse": "會話複用", + "affinity_hit": "快取複用(前綴親和)", "initial_selection": "首次選擇", "endpoint_pool_exhausted": "端點池耗盡", "vendor_type_all_timeout": "供應商類型全端點逾時", @@ -249,6 +250,27 @@ "session_reuse": "會話複用", "weighted_random": "加權隨機", "group_filtered": "分組過濾", - "fail_open_fallback": "故障開放回退" + "fail_open_fallback": "故障開放回退", + "prefix_affinity": "最長前綴親和" + }, + "affinity": { + "matchedDepth": "命中深度", + "matchedPrefixBytes": "命中前綴位元組", + "matchedFp": "命中指紋" + }, + "streamGate": { + "title": "串流門控提交", + "frameIndex": "觸發幀序號", + "chunkIndex": "觸發 Chunk 序號", + "eventName": "觸發事件", + "bufferedBytes": "已緩衝位元組", + "echoExcludedBytes": "回顯排除位元組", + "gateWaitMs": "門控等待" + }, + "replayServe": { + "sources": { + "completed": "已完成重放", + "attached_live": "跟尾在途串流" + } } } 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 c03f50fe5..0a143e9ef 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 @@ -8,6 +8,7 @@ import { Clock, Copy, Database, + DatabaseZap, Filter, GitBranch, Globe, @@ -39,6 +40,10 @@ function getRequestStatus(item: ProviderChainItem): StepStatus { if (item.reason === "session_reuse" || item.selectionMethod === "session_reuse") { return "session_reuse"; } + // Affinity hit is a reuse-style nomination step: same visual family as session reuse + if (item.reason === "affinity_hit" || item.selectionMethod === "prefix_affinity") { + return "session_reuse"; + } if ( item.reason === "request_success" || item.reason === "retry_success" || @@ -174,8 +179,15 @@ export function LogicTraceTab({ const sessionReuseContext = isSessionReuseFlow ? providerChain?.[0]?.decisionContext : undefined; const sessionReuseProvider = isSessionReuseFlow ? providerChain?.[0] : undefined; - // Extract decision context from first chain item (not used for session reuse) - const decisionContext = isSessionReuseFlow ? undefined : providerChain?.[0]?.decisionContext; + // F3a: prefix affinity hit flow (cache-reuse nomination replaces initial selection) + const isAffinityHitFlow = + providerChain?.[0]?.reason === "affinity_hit" || + providerChain?.[0]?.selectionMethod === "prefix_affinity"; + + // Extract decision context from first chain item (not used for reuse-style flows, + // whose first item carries an empty placeholder context) + const decisionContext = + isSessionReuseFlow || isAffinityHitFlow ? undefined : providerChain?.[0]?.decisionContext; // Extract filtered providers from all chain items (not applicable for session reuse) const filteredProviders = isSessionReuseFlow @@ -234,8 +246,36 @@ export function LogicTraceTab({
)} + {/* F2 Replay Serve Info (cache hit served without upstream call) */} + {blockedBy === "replay_serve" && ( +
+
+ + + {t("replayServe.title")} + + {parsedBlockedReason?.source && ( + + {tChain.has(`replayServe.sources.${parsedBlockedReason.source}`) + ? tChain(`replayServe.sources.${parsedBlockedReason.source}`) + : parsedBlockedReason.source} + + )} +
+

{t("replayServe.desc")}

+ {parsedBlockedReason?.replayId && ( +
+ {t("replayServe.replayId")}: + + {parsedBlockedReason.replayId} + +
+ )} +
+ )} + {/* Block Info */} - {isBlocked && blockedBy && ( + {isBlocked && blockedBy && blockedBy !== "replay_serve" && (
@@ -282,6 +322,8 @@ export function LogicTraceTab({ ) : isSessionReuseFlow ? ( + ) : isAffinityHitFlow ? ( + ) : ( )} @@ -297,6 +339,13 @@ export function LogicTraceTab({ > {t("logicTrace.sessionReuse")} + ) : isAffinityHitFlow ? ( + + {tChain("reasons.affinity_hit")} + ) : ( <> @@ -820,6 +869,8 @@ export function LogicTraceTab({ const isRetry = item.attemptNumber && item.attemptNumber > 1; const isSessionReuse = item.reason === "session_reuse" || item.selectionMethod === "session_reuse"; + const isAffinityHit = + item.reason === "affinity_hit" || item.selectionMethod === "prefix_affinity"; // Determine icon based on type const isHedgeTriggered = item.reason === "hedge_triggered"; @@ -835,35 +886,39 @@ export function LogicTraceTab({ : null; const stepIcon = isSessionReuse ? Link2 - : isHedgeTriggered - ? GitBranch - : isHedgeLoser || isHedgeLoserBilled || isClientAbort - ? XCircle - : isRetry - ? RefreshCw - : status === "success" - ? CheckCircle - : status === "failure" - ? XCircle - : Server; + : isAffinityHit + ? DatabaseZap + : isHedgeTriggered + ? GitBranch + : isHedgeLoser || isHedgeLoserBilled || isClientAbort + ? XCircle + : isRetry + ? RefreshCw + : status === "success" + ? CheckCircle + : status === "failure" + ? XCircle + : Server; // Determine title based on type // For session reuse flow, show simplified "Execute Request" title for the first item const stepTitle = isSessionReuse ? t("logicTrace.executeRequest") - : isHedgeTriggered - ? tChain("timeline.hedgeTriggered") - : isHedgeLoser - ? tChain("timeline.hedgeLoserCancelled") - : isHedgeLoserBilled - ? tChain("timeline.hedgeLoserBilled") - : isClientAbort - ? tChain("timeline.clientAbort") - : isRetry - ? t("logicTrace.retryAttempt", { number: item.attemptNumber ?? 1 }) - : item.reason === "hedge_winner" - ? tChain("timeline.hedgeWinner") - : t("logicTrace.attemptProvider", { provider: item.name }); + : isAffinityHit + ? tChain("reasons.affinity_hit") + : isHedgeTriggered + ? tChain("timeline.hedgeTriggered") + : isHedgeLoser + ? tChain("timeline.hedgeLoserCancelled") + : isHedgeLoserBilled + ? tChain("timeline.hedgeLoserBilled") + : isClientAbort + ? tChain("timeline.clientAbort") + : isRetry + ? t("logicTrace.retryAttempt", { number: item.attemptNumber ?? 1 }) + : item.reason === "hedge_winner" + ? tChain("timeline.hedgeWinner") + : t("logicTrace.attemptProvider", { provider: item.name }); return ( )} + {/* F3a Affinity Hit Info */} + {isAffinityHit && ( +
+
+ + {tChain("reasons.affinity_hit")} +
+
+ {item.affinity?.matchedDepth != null && ( +
+ + {tChain("affinity.matchedDepth")}: + {" "} + {item.affinity.matchedDepth} +
+ )} + {item.affinity?.matchedPrefixBytes != null && ( +
+ + {tChain("affinity.matchedPrefixBytes")}: + {" "} + {item.affinity.matchedPrefixBytes} +
+ )} + {item.affinity?.matchedFp && ( +
+ + {tChain("affinity.matchedFp")}: + {" "} + + {item.affinity.matchedFp} + +
+ )} +
+
+ )} + + {/* F1 Stream Gate Commit Marker */} + {item.streamGate && ( +
+
+ + {tChain("streamGate.title")} +
+
+
+ + {tChain("streamGate.frameIndex")}: + {" "} + #{item.streamGate.frameIndex} +
+
+ + {tChain("streamGate.chunkIndex")}: + {" "} + #{item.streamGate.chunkIndex} +
+ {item.streamGate.eventName && ( +
+ + {tChain("streamGate.eventName")}: + {" "} + + {item.streamGate.eventName} + +
+ )} +
+ + {tChain("streamGate.bufferedBytes")}: + {" "} + {item.streamGate.bufferedBytes} +
+ {item.streamGate.echoExcludedBytes > 0 && ( +
+ + {tChain("streamGate.echoExcludedBytes")}: + {" "} + {item.streamGate.echoExcludedBytes} +
+ )} +
+ + {tChain("streamGate.gateWaitMs")}: + {" "} + {item.streamGate.gateWaitMs}ms +
+
+
+ )} + {/* Basic Info */}
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 dc1ea1247..d68695b61 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 @@ -114,9 +114,14 @@ export interface MetadataTabProps extends TabSharedProps { /** * Parse blocked reason JSON string */ -export function parseBlockedReason( - blockedReason: string | null | undefined -): { word?: string; matchType?: string; matchedText?: string } | null { +export function parseBlockedReason(blockedReason: string | null | undefined): { + word?: string; + matchType?: string; + matchedText?: string; + // F2 replay_serve audit payload + source?: string; + replayId?: string; +} | null { if (!blockedReason) return null; try { return JSON.parse(blockedReason); 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 3b5e5b6f2..67dc0c10b 100644 --- a/src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx +++ b/src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx @@ -5,6 +5,7 @@ import { CheckCircle, ChevronRight, Clock3, + DatabaseZap, GitBranch, InfoIcon, Link2, @@ -512,6 +513,12 @@ export function ProviderChainPopover({ const isSessionReuse = chain[0]?.reason === "session_reuse" || chain[0]?.selectionMethod === "session_reuse"; + // F3a: prefix affinity hit (cache reuse nomination) + const affinityHitItem = chain.find( + (item) => item.reason === "affinity_hit" || item.selectionMethod === "prefix_affinity" + ); + const isAffinityHit = Boolean(affinityHitItem); + // Get initial selection context for tooltip const initialSelection = chain.find((item) => item.reason === "initial_selection"); const selectionContext = initialSelection?.decisionContext; @@ -538,6 +545,10 @@ export function ProviderChainPopover({ {isSessionReuse && !isLeaseConflictProtection && ( )} + {/* Affinity hit (cache reuse) indicator */} + {isAffinityHit && !isSessionReuse && !isLeaseConflictProtection && ( + + )} {/* Initial selection: show compact priority badge before name */} {!isSessionReuse && selectionContext && ( @@ -668,6 +679,48 @@ export function ProviderChainPopover({
)} + {/* Affinity hit (cache reuse) detailed info */} + {isAffinityHit && !isSessionReuse && ( +
+
+ + {tChain("reasons.affinity_hit")} +
+
+ {affinityHitItem?.affinity?.matchedDepth != null && ( +
+ + {tChain("affinity.matchedDepth")}: + {" "} + + {affinityHitItem.affinity.matchedDepth} + +
+ )} + {affinityHitItem?.affinity?.matchedPrefixBytes != null && ( +
+ + {tChain("affinity.matchedPrefixBytes")}: + {" "} + + {affinityHitItem.affinity.matchedPrefixBytes} + +
+ )} + {affinityHitItem?.affinity?.matchedFp && ( +
+ + {tChain("affinity.matchedFp")}: + {" "} + + {affinityHitItem.affinity.matchedFp.slice(0, 16)} + +
+ )} +
+
+ )} + {/* Initial selection detailed info */} {!isSessionReuse && selectionContext && (
From a5962297f6e6b9c996fb368503a75bb35a0237b2 Mon Sep 17 00:00:00 2001 From: ding113 Date: Thu, 23 Jul 2026 12:57:21 -0700 Subject: [PATCH 06/12] refactor(ui): remove superseded Codex reasoning effort display Delete the Codex-only reasoning effort display component and its test. The component was replaced by the unified ThinkingEffortDisplay that handles both Codex and Anthropic effort audit entries. --- .../codex-reasoning-effort-display.test.tsx | 74 ------------------- .../codex-reasoning-effort-display.tsx | 63 ---------------- 2 files changed, 137 deletions(-) delete mode 100644 src/app/[locale]/dashboard/logs/_components/codex-reasoning-effort-display.test.tsx delete mode 100644 src/app/[locale]/dashboard/logs/_components/codex-reasoning-effort-display.tsx diff --git a/src/app/[locale]/dashboard/logs/_components/codex-reasoning-effort-display.test.tsx b/src/app/[locale]/dashboard/logs/_components/codex-reasoning-effort-display.test.tsx deleted file mode 100644 index 9fc307ca5..000000000 --- a/src/app/[locale]/dashboard/logs/_components/codex-reasoning-effort-display.test.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import type { ReactNode } from "react"; -import { renderToStaticMarkup } from "react-dom/server"; -import { describe, expect, test, vi } from "vitest"; -import { CodexReasoningEffortDisplay } from "./codex-reasoning-effort-display"; - -vi.mock("next-intl", () => ({ - useTranslations: () => (key: string) => key, -})); - -vi.mock("@/components/ui/tooltip", () => ({ - TooltipProvider: ({ children }: { children?: ReactNode }) =>
{children}
, - Tooltip: ({ children }: { children?: ReactNode }) =>
{children}
, - TooltipTrigger: ({ children }: { children?: ReactNode }) =>
{children}
, - TooltipContent: ({ children }: { children?: ReactNode }) =>
{children}
, -})); - -describe("CodexReasoningEffortDisplay", () => { - test("未记录 Codex 思考强度时显示占位符", () => { - const html = renderToStaticMarkup(); - - expect(html).toContain(">-"); - expect(html).not.toContain('data-slot="codex-reasoning-effort"'); - }); - - test("显示 Codex 请求中的思考强度", () => { - const html = renderToStaticMarkup( - - ); - - expect(html).toContain('data-slot="codex-reasoning-effort"'); - expect(html).toContain("high"); - expect(html).toContain("tooltip"); - expect(html).not.toContain("overridden"); - }); - - test("供应商覆写时显示请求值和实际值", () => { - const html = renderToStaticMarkup( - - ); - - expect(html).toContain("low"); - expect(html).toContain("max"); - expect(html).toContain("overridden"); - expect(html).toContain("lucide-arrow-right"); - }); -}); diff --git a/src/app/[locale]/dashboard/logs/_components/codex-reasoning-effort-display.tsx b/src/app/[locale]/dashboard/logs/_components/codex-reasoning-effort-display.tsx deleted file mode 100644 index eb7098da2..000000000 --- a/src/app/[locale]/dashboard/logs/_components/codex-reasoning-effort-display.tsx +++ /dev/null @@ -1,63 +0,0 @@ -"use client"; - -import { ArrowRight } from "lucide-react"; -import { useTranslations } from "next-intl"; -import { ThinkingEffortBadge } from "@/components/customs/thinking-effort-badge"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; -import { extractCodexReasoningEffortInfo } from "@/lib/utils/codex-reasoning-effort"; -import type { SpecialSetting } from "@/types/special-settings"; - -/** Codex 思考强度展示属性。 */ -interface CodexReasoningEffortDisplayProps { - /** 使用记录中的请求参数与供应商覆写审计。 */ - specialSettings: SpecialSetting[] | null | undefined; -} - -/** - * 在使用记录中展示 Codex reasoning.effort。 - * - * 供应商改变强度时同时展示请求值和实际转发值,避免只看到客户端参数而误判上游行为。 - */ -export function CodexReasoningEffortDisplay({ specialSettings }: CodexReasoningEffortDisplayProps) { - const t = useTranslations("dashboard.logs.details.reasoningEffort"); - const effortInfo = extractCodexReasoningEffortInfo(specialSettings); - - if (!effortInfo) { - return -; - } - - return ( - - - - - {effortInfo.requestedEffort && ( - - )} - {effortInfo.isOverridden && effortInfo.requestedEffort && ( - - - -

{t("tooltip")}

- {effortInfo.isOverridden && ( -

{t("overridden")}

- )} -
-
-
- ); -} From c8ba6ced67959fbdbe96345721cd6d33a36929ff Mon Sep 17 00:00:00 2001 From: ding113 Date: Thu, 23 Jul 2026 21:07:38 -0700 Subject: [PATCH 07/12] style(i18n): use full-width CJK parentheses in zh locale strings Replace half-width parentheses with full-width CJK equivalents in Simplified and Traditional Chinese dashboard and provider-chain translations for typographic consistency. --- messages/zh-CN/dashboard.json | 2 +- messages/zh-CN/provider-chain.json | 2 +- messages/zh-TW/dashboard.json | 2 +- messages/zh-TW/provider-chain.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/messages/zh-CN/dashboard.json b/messages/zh-CN/dashboard.json index dd06bf527..c3b8dbc16 100644 --- a/messages/zh-CN/dashboard.json +++ b/messages/zh-CN/dashboard.json @@ -553,7 +553,7 @@ }, "replayServe": { "title": "由 Replay 缓存服务", - "desc": "该请求由 Replay 缓存直接服务(相同请求正在进行或已完成),未发起上游供应商调用,不产生费用。", + "desc": "该请求由 Replay 缓存直接服务(相同请求正在进行或已完成),未发起上游供应商调用,不产生费用。", "replayId": "Replay ID" } }, diff --git a/messages/zh-CN/provider-chain.json b/messages/zh-CN/provider-chain.json index 45c8b6934..ca6f974b7 100644 --- a/messages/zh-CN/provider-chain.json +++ b/messages/zh-CN/provider-chain.json @@ -60,7 +60,7 @@ "concurrent_limit_failed": "并发限制", "http2_fallback": "HTTP/2 回退", "session_reuse": "会话复用", - "affinity_hit": "缓存复用(前缀亲和)", + "affinity_hit": "缓存复用(前缀亲和)", "initial_selection": "首次选择", "endpoint_pool_exhausted": "端点池耗尽", "vendor_type_all_timeout": "供应商类型全端点超时", diff --git a/messages/zh-TW/dashboard.json b/messages/zh-TW/dashboard.json index 3d262da4e..f2cbb699f 100644 --- a/messages/zh-TW/dashboard.json +++ b/messages/zh-TW/dashboard.json @@ -553,7 +553,7 @@ }, "replayServe": { "title": "由 Replay 快取服務", - "desc": "該請求由 Replay 快取直接服務(相同請求正在進行或已完成),未發起上游供應商呼叫,不產生費用。", + "desc": "該請求由 Replay 快取直接服務(相同請求正在進行或已完成),未發起上游供應商呼叫,不產生費用。", "replayId": "Replay ID" } }, diff --git a/messages/zh-TW/provider-chain.json b/messages/zh-TW/provider-chain.json index b51c8ce31..1b91777d9 100644 --- a/messages/zh-TW/provider-chain.json +++ b/messages/zh-TW/provider-chain.json @@ -60,7 +60,7 @@ "concurrent_limit_failed": "並發限制", "http2_fallback": "HTTP/2 回退", "session_reuse": "會話複用", - "affinity_hit": "快取複用(前綴親和)", + "affinity_hit": "快取複用(前綴親和)", "initial_selection": "首次選擇", "endpoint_pool_exhausted": "端點池耗盡", "vendor_type_all_timeout": "供應商類型全端點逾時", From d8fe85eb4693346019c573cebe82f57eab09d10a Mon Sep 17 00:00:00 2001 From: ding113 Date: Thu, 23 Jul 2026 21:07:38 -0700 Subject: [PATCH 08/12] fix(proxy): cap echo-frame byte exemption at prebufferByteCap Request echo frames were fully excluded from the prebuffer byte cap, allowing a malicious or oversized echo flood to grow the buffer without bound. The exemption is now capped at prebufferByteCap, so echo bytes beyond that threshold count normally and trigger prebuffer_overflow. This keeps worst-case buffer memory at 2x cap. --- .../proxy/stream-gate/stream-content-gate.ts | 6 ++++-- .../proxy/stream-gate-content-gate.test.ts | 20 ++++++++++++++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/app/v1/_lib/proxy/stream-gate/stream-content-gate.ts b/src/app/v1/_lib/proxy/stream-gate/stream-content-gate.ts index 7bf4c8f85..e286306a4 100644 --- a/src/app/v1/_lib/proxy/stream-gate/stream-content-gate.ts +++ b/src/app/v1/_lib/proxy/stream-gate/stream-content-gate.ts @@ -258,7 +258,7 @@ export async function runStreamContentGate( // 干净终止先于任何内容 = 空流 return failure("empty_stream", frame.data); } - // neutral: 继续缓冲;请求回显帧的载荷不计入字节上限(内存仍占用,由回显体积自然有界) + // neutral: 继续缓冲;请求回显帧的载荷不计入字节上限(豁免额度另有上限,见下方判定) if (isRequestEchoFrame(options.family, frame.eventName, frame.data)) { echoExcludedBytes += Buffer.byteLength(frame.data, "utf8"); } @@ -268,7 +268,9 @@ export async function runStreamContentGate( } } - if (bufferedBytes - echoExcludedBytes > options.prebufferByteCap) { + // 豁免额度以 cap 为自身上限:伪装成回显的中性帧最多把缓冲总量抬到 2×cap,不会无界占用内存 + const cappedEchoExcluded = Math.min(echoExcludedBytes, options.prebufferByteCap); + if (bufferedBytes - cappedEchoExcluded > options.prebufferByteCap) { return failure("prebuffer_overflow"); } } diff --git a/tests/unit/proxy/stream-gate-content-gate.test.ts b/tests/unit/proxy/stream-gate-content-gate.test.ts index 11c89070e..10cb9066d 100644 --- a/tests/unit/proxy/stream-gate-content-gate.test.ts +++ b/tests/unit/proxy/stream-gate-content-gate.test.ts @@ -261,13 +261,27 @@ describe("request echo frame byte-cap exclusion", () => { 'event: response.output_text.delta\ndata: {"type":"response.output_text.delta","delta":"hi"}\n\n'; it("does not count request echo frames against the byte cap", async () => { + // cap 4096 < 帧总字节(约 4186):若不豁免必溢出;回显在豁免额度(=cap)内则放行 const reader = readerFromChunks([ECHO_FRAME, RESPONSES_DELTA]); - const result = await runStreamContentGate(reader, RESPONSES_OPTIONS); + const result = await runStreamContentGate(reader, { + ...RESPONSES_OPTIONS, + prebufferByteCap: 4096, + }); expect(result.committed).toBe(true); if (!result.committed) return; expect(await drainPrefix(result.prefixChunks)).toContain("response.created"); }); + it("caps the echo exemption at prebufferByteCap so echo floods still overflow", async () => { + // 豁免额度上限 = cap(1024):4KB 回显超出上限部分照常计入,缓冲总量被压在 2×cap 内 + const reader = readerFromChunks([ECHO_FRAME, RESPONSES_DELTA]); + const result = await runStreamContentGate(reader, RESPONSES_OPTIONS); + expect(result.committed).toBe(false); + if (result.committed) return; + expect(result.error).toBeInstanceOf(StreamPrecommitError); + expect((result.error as StreamPrecommitError).gateReason).toBe("prebuffer_overflow"); + }); + it("still overflows on oversized non-echo neutral frames", async () => { const bigNeutral = `event: response.output_item.added\ndata: {"type":"response.output_item.added","item":"${bigPayload}"}\n\n`; const reader = readerFromChunks([bigNeutral, RESPONSES_DELTA]); @@ -279,9 +293,9 @@ describe("request echo frame byte-cap exclusion", () => { }); it("reports echo-excluded bytes in the overflow error body", async () => { - const bigNonEcho = `event: response.in_progress\ndata: {"type":"response.in_progress","response":{"instructions":"${bigPayload}"}}\n\n`; + const bigEchoFrame = `event: response.in_progress\ndata: {"type":"response.in_progress","response":{"instructions":"${bigPayload}"}}\n\n`; const oversizedTail = `event: response.output_item.added\ndata: {"item":"${"y".repeat(4096)}"}\n\n`; - const reader = readerFromChunks([bigNonEcho, oversizedTail, RESPONSES_DELTA]); + const reader = readerFromChunks([bigEchoFrame, oversizedTail, RESPONSES_DELTA]); const result = await runStreamContentGate(reader, RESPONSES_OPTIONS); expect(result.committed).toBe(false); if (result.committed) return; From 4f89df89297f84e475002dff4aaca0e7c3bd2795 Mon Sep 17 00:00:00 2001 From: ding113 Date: Thu, 23 Jul 2026 21:07:38 -0700 Subject: [PATCH 09/12] fix(proxy): refresh runtime settings snapshot before replay and scheduler ticks The replay guard read a synchronous cached snapshot that could stay stale on low-traffic instances where no request path refreshed it, delaying admin toggle of replayEnabled. The guard now calls getProxyRuntimeSettings() on each request to pull the latest override. The cache-effectiveness scheduler tick similarly switches from the synchronous snapshot to an async refresh so toggles take effect without relying on request-path cache warming. --- src/app/v1/_lib/proxy/replay/replay-guard.ts | 4 ++++ src/instrumentation.ts | 12 ++++++++---- tests/unit/proxy/replay-guard.test.ts | 11 +++++++++++ 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/app/v1/_lib/proxy/replay/replay-guard.ts b/src/app/v1/_lib/proxy/replay/replay-guard.ts index c91fe78aa..065366192 100644 --- a/src/app/v1/_lib/proxy/replay/replay-guard.ts +++ b/src/app/v1/_lib/proxy/replay/replay-guard.ts @@ -3,6 +3,7 @@ import { db } from "@/drizzle/db"; import { messageRequest } from "@/drizzle/schema"; import { getEnvConfig } from "@/lib/config/env.schema"; import { logger } from "@/lib/logger"; +import { getProxyRuntimeSettings } from "@/lib/system-settings/proxy-runtime"; import type { ProxySession } from "../session"; import { deriveReplayIdentity, REPLAY_BYPASS_HEADER, type ReplayIdentity } from "./replay-identity"; import { getReplayStore, type ReplayMeta, type ReplayStore } from "./replay-store"; @@ -37,6 +38,9 @@ const ATTACH_MAX_WAIT_MS = 10 * 60 * 1000; export class ProxyReplayGuard { static async ensure(session: ProxySession): Promise { try { + // guard 位于 provider 步骤之前:先刷新运行时覆写快照,管理端刚保存的 + // replayEnabled 首个请求即生效(底层系统设置缓存有 TTL,常态为缓存命中) + await getProxyRuntimeSettings(); const identity = deriveReplayIdentity(session); if (!identity) return null; diff --git a/src/instrumentation.ts b/src/instrumentation.ts index 118e796f6..8c9d6c38b 100644 --- a/src/instrumentation.ts +++ b/src/instrumentation.ts @@ -262,14 +262,18 @@ async function startCacheEffectivenessScheduler(): Promise { } try { - // 开关支持系统设置运行时覆写:调度器常驻,每 tick 检查有效开关 - const { isCacheEffectivenessEnabled } = await import("@/lib/system-settings/proxy-runtime"); + // 开关支持系统设置运行时覆写:调度器常驻,每 tick 异步刷新有效开关—— + // 低流量实例没有请求路径保鲜快照,只读同步快照会一直陈旧 + const { getProxyRuntimeSettings } = await import("@/lib/system-settings/proxy-runtime"); const { aggregateCacheEffectiveness } = await import("@/lib/cache-effectiveness/service"); const intervalMs = 5 * 60 * 1000; instrumentationState.__CCH_CACHE_EFFECTIVENESS_INTERVAL_ID__ = setInterval(() => { - if (!isCacheEffectivenessEnabled()) return; - void aggregateCacheEffectiveness().catch((error) => { + void (async () => { + const settings = await getProxyRuntimeSettings(); + if (!settings.cacheEffectivenessEnabled) return; + await aggregateCacheEffectiveness(); + })().catch((error) => { logger.warn("[Instrumentation] Cache effectiveness aggregation tick failed", { error: error instanceof Error ? error.message : String(error), }); diff --git a/tests/unit/proxy/replay-guard.test.ts b/tests/unit/proxy/replay-guard.test.ts index 27e024759..699b5c0d7 100644 --- a/tests/unit/proxy/replay-guard.test.ts +++ b/tests/unit/proxy/replay-guard.test.ts @@ -60,6 +60,17 @@ vi.mock("@/lib/config/env.schema", async (importOriginal) => { }; }); +vi.mock("@/lib/system-settings/proxy-runtime", () => ({ + // ensure() 每请求刷新一次快照;单测让同步快照为空,isReplayEnabled 走上方 env mock + getProxyRuntimeSettings: vi.fn(async () => ({ + streamGateMode: "off", + affinityIgnoreClientSessionId: true, + replayEnabled: false, + cacheEffectivenessEnabled: true, + })), + getCachedProxyRuntimeSettings: () => null, +})); + vi.mock("@/app/v1/_lib/proxy/replay/replay-store", () => ({ getReplayStore: () => storeControl, })); From 620e40181eeb1bca3e217307b705025fcc132797 Mon Sep 17 00:00:00 2001 From: ding113 Date: Thu, 23 Jul 2026 21:07:38 -0700 Subject: [PATCH 10/12] fix(settings): preserve null tri-state for replay and cache toggles The form coerced null (follow-env-var) to boolean defaults on init and after fetch, so saving any unrelated field silently wrote an explicit override. The state now retains null until the user toggles a switch, and the checked prop falls back to the env default only for display. --- .../_components/system-settings-form.tsx | 17 +- ...ettings-form-replay-cache-toggles.test.tsx | 193 ++++++++++++++++++ 2 files changed, 202 insertions(+), 8 deletions(-) create mode 100644 tests/unit/settings/system-settings-form-replay-cache-toggles.test.tsx diff --git a/src/app/[locale]/settings/config/_components/system-settings-form.tsx b/src/app/[locale]/settings/config/_components/system-settings-form.tsx index 0564c0079..1969576cf 100644 --- a/src/app/[locale]/settings/config/_components/system-settings-form.tsx +++ b/src/app/[locale]/settings/config/_components/system-settings-form.tsx @@ -214,10 +214,11 @@ export function SystemSettingsForm({ const [affinityIgnoreClientSessionId, setAffinityIgnoreClientSessionId] = useState( initialSettings.affinityIgnoreClientSessionId ); - // null = 尚未覆写(跟随环境变量默认:Replay 关 / 缓存模拟开);保存后写显式值 - const [replayEnabled, setReplayEnabled] = useState(initialSettings.replayEnabled ?? false); - const [cacheEffectivenessEnabled, setCacheEffectivenessEnabled] = useState( - initialSettings.cacheEffectivenessEnabled ?? true + // null = 跟随环境变量(Replay 默认关 / 缓存模拟默认开):未触碰开关时按 null 原样保存, + // 避免无关字段的保存把覆写写死为布尔值;仅用户切换后才落显式值 + const [replayEnabled, setReplayEnabled] = useState(initialSettings.replayEnabled); + const [cacheEffectivenessEnabled, setCacheEffectivenessEnabled] = useState( + initialSettings.cacheEffectivenessEnabled ); const [enableThinkingBudgetRectifier, setEnableThinkingBudgetRectifier] = useState( initialSettings.enableThinkingBudgetRectifier @@ -470,8 +471,8 @@ export function SystemSettingsForm({ ); setStreamGateMode(result.data.streamGateMode); setAffinityIgnoreClientSessionId(result.data.affinityIgnoreClientSessionId); - setReplayEnabled(result.data.replayEnabled ?? false); - setCacheEffectivenessEnabled(result.data.cacheEffectivenessEnabled ?? true); + setReplayEnabled(result.data.replayEnabled ?? null); + setCacheEffectivenessEnabled(result.data.cacheEffectivenessEnabled ?? null); setEnableThinkingBudgetRectifier(result.data.enableThinkingBudgetRectifier); setEnableThinkingEffortConflictRectifier(result.data.enableThinkingEffortConflictRectifier); setEnableGeminiFunctionIdRectifier(result.data.enableGeminiFunctionIdRectifier); @@ -1186,7 +1187,7 @@ export function SystemSettingsForm({ setReplayEnabled(checked)} disabled={isPending} /> @@ -1210,7 +1211,7 @@ export function SystemSettingsForm({ setCacheEffectivenessEnabled(checked)} disabled={isPending} /> diff --git a/tests/unit/settings/system-settings-form-replay-cache-toggles.test.tsx b/tests/unit/settings/system-settings-form-replay-cache-toggles.test.tsx new file mode 100644 index 000000000..e38519796 --- /dev/null +++ b/tests/unit/settings/system-settings-form-replay-cache-toggles.test.tsx @@ -0,0 +1,193 @@ +import fs from "node:fs"; +import path from "node:path"; +import type { ReactNode } from "react"; +import { act } from "react"; +import { createRoot } from "react-dom/client"; +import { NextIntlClientProvider } from "next-intl"; +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { SystemSettingsForm } from "@/app/[locale]/settings/config/_components/system-settings-form"; +import type { SystemSettings } from "@/types/system-config"; + +vi.mock("next/navigation", () => ({ + useRouter: () => ({ refresh: vi.fn() }), +})); + +const systemConfigActionMocks = vi.hoisted(() => ({ + saveSystemSettings: vi.fn(async () => ({ ok: true })), +})); +vi.mock("@/actions/system-config", () => systemConfigActionMocks); + +const requestFiltersActionMocks = vi.hoisted(() => ({ + getDistinctProviderGroupsAction: vi.fn(async () => ({ ok: true, data: [] })), +})); +vi.mock("@/actions/request-filters", () => requestFiltersActionMocks); + +const sonnerMocks = vi.hoisted(() => ({ + toast: { + success: vi.fn(), + error: vi.fn(), + warning: vi.fn(), + }, +})); +vi.mock("sonner", () => sonnerMocks); + +const baseSettings = { + siteTitle: "Claude Code Hub", + allowGlobalUsageView: true, + currencyDisplay: "USD", + billingModelSource: "original", + codexPriorityBillingSource: "requested", + timezone: "UTC", + verboseProviderError: false, + passThroughUpstreamErrorMessage: true, + enableHttp2: true, + enableHighConcurrencyMode: false, + interceptAnthropicWarmupRequests: false, + enableThinkingSignatureRectifier: true, + enableThinkingBudgetRectifier: true, + enableBillingHeaderRectifier: true, + enableResponseInputRectifier: true, + enableCodexSessionIdCompletion: true, + enableClaudeMetadataUserIdInjection: true, + enableResponseFixer: true, + allowNonConversationEndpointProviderFallback: true, + fakeStreamingWhitelist: [], + responseFixerConfig: { + fixEncoding: true, + fixSseFormat: true, + fixTruncatedJson: true, + }, + quotaDbRefreshIntervalSeconds: 10, + quotaLeasePercent5h: 0.05, + quotaLeasePercentDaily: 0.05, + quotaLeasePercentWeekly: 0.05, + quotaLeasePercentMonthly: 0.05, + quotaLeaseCapUsd: null, + ipGeoLookupEnabled: true, + ipExtractionConfig: null, + // null = 跟随环境变量:本组用例的核心前置 + replayEnabled: null, + cacheEffectivenessEnabled: null, +} satisfies Pick< + SystemSettings, + | "siteTitle" + | "allowGlobalUsageView" + | "currencyDisplay" + | "billingModelSource" + | "codexPriorityBillingSource" + | "timezone" + | "verboseProviderError" + | "passThroughUpstreamErrorMessage" + | "enableHttp2" + | "enableHighConcurrencyMode" + | "interceptAnthropicWarmupRequests" + | "enableThinkingSignatureRectifier" + | "enableThinkingBudgetRectifier" + | "enableBillingHeaderRectifier" + | "enableResponseInputRectifier" + | "enableCodexSessionIdCompletion" + | "enableClaudeMetadataUserIdInjection" + | "enableResponseFixer" + | "allowNonConversationEndpointProviderFallback" + | "fakeStreamingWhitelist" + | "responseFixerConfig" + | "quotaDbRefreshIntervalSeconds" + | "quotaLeasePercent5h" + | "quotaLeasePercentDaily" + | "quotaLeasePercentWeekly" + | "quotaLeasePercentMonthly" + | "quotaLeaseCapUsd" + | "ipGeoLookupEnabled" + | "ipExtractionConfig" + | "replayEnabled" + | "cacheEffectivenessEnabled" +>; + +function loadMessages(locale: string) { + const base = path.join(process.cwd(), `messages/${locale}/settings`); + const read = (name: string) => JSON.parse(fs.readFileSync(path.join(base, name), "utf8")); + + return { + settings: { + common: read("common.json"), + config: read("config.json"), + requestFilters: read("requestFilters.json"), + }, + }; +} + +function render(node: ReactNode) { + const container = document.createElement("div"); + document.body.appendChild(container); + const root = createRoot(container); + + act(() => { + root.render( + + {node} + + ); + }); + + return { + unmount: () => { + act(() => root.unmount()); + container.remove(); + }, + }; +} + +async function submitForm() { + const form = document.body.querySelector("form"); + if (!form) throw new Error("未找到系统设置表单"); + + await act(async () => { + form.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true })); + await Promise.resolve(); + await new Promise((resolve) => setTimeout(resolve, 0)); + }); +} + +describe("SystemSettingsForm replay/cache-effectiveness null 三态", () => { + beforeEach(() => { + document.body.innerHTML = ""; + vi.clearAllMocks(); + }); + + test("未触碰开关时保存保持 null(跟随环境变量),不写死布尔覆写", async () => { + const { unmount } = render(); + + await submitForm(); + + expect(systemConfigActionMocks.saveSystemSettings).toHaveBeenCalledWith( + expect.objectContaining({ + replayEnabled: null, + cacheEffectivenessEnabled: null, + }) + ); + + unmount(); + }); + + test("用户切换开关后保存为显式覆写,未动的另一开关仍为 null", async () => { + const { unmount } = render(); + + const switchEl = document.getElementById("replay-enabled"); + if (!switchEl) throw new Error("未找到 replay-enabled 开关"); + await act(async () => { + (switchEl as HTMLElement).click(); + await Promise.resolve(); + }); + + await submitForm(); + + expect(systemConfigActionMocks.saveSystemSettings).toHaveBeenCalledWith( + expect.objectContaining({ + replayEnabled: true, + cacheEffectivenessEnabled: null, + }) + ); + + unmount(); + }); +}); From b99b129ad361ae7e43f042e7e7cd091d2b27bc16 Mon Sep 17 00:00:00 2001 From: ding113 Date: Thu, 23 Jul 2026 21:07:38 -0700 Subject: [PATCH 11/12] fix(proxy): unify hedge path idle timeout to 524 error The hedge (racing) path threw the raw StreamPrecommitError on gate idle timeout instead of building the 524 streaming_idle_timeout ProxyError used by the serial path. Extract buildStreamingIdleTimeoutError and apply it in both paths so circuit-breaker and failover logic see the same error classification regardless of racing mode. --- src/app/v1/_lib/proxy/forwarder.ts | 61 ++++++++++++++++++------------ 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/src/app/v1/_lib/proxy/forwarder.ts b/src/app/v1/_lib/proxy/forwarder.ts index d7f21cde8..f587d8ba9 100644 --- a/src/app/v1/_lib/proxy/forwarder.ts +++ b/src/app/v1/_lib/proxy/forwarder.ts @@ -568,6 +568,35 @@ export function mergeAnthropicCacheTtlBetaFlag(existing: string | null | undefin return Array.from(betaFlags).join(", "); } +/** + * 门控等待期静默超时 -> 524 streaming_idle_timeout。 + * 与提交后的静默超时同构:错误规则/熔断/切换逻辑无需区分静默发生在门控前后; + * 串行与 hedge 竞速路径共用,保证同一故障归类一致。 + */ +function buildStreamingIdleTimeoutError(provider: { + id: number; + name: string; + streamingIdleTimeoutMs: number; +}): ProxyError { + const parsed = { + error: { + type: "streaming_idle_timeout", + message: `Provider stopped sending data for ${provider.streamingIdleTimeoutMs}ms`, + timeout_ms: provider.streamingIdleTimeoutMs, + }, + }; + return new ProxyError( + `供应商流式响应静默超时: ${provider.streamingIdleTimeoutMs}ms 内未收到新数据`, + 524, + { + body: JSON.stringify(parsed), + parsed, + providerId: provider.id, + providerName: provider.name, + } + ); +} + function clampRetryAttempts(value: number): number { const numeric = Number(value); if (!Number.isFinite(numeric)) return RETRY_LIMITS.MIN; @@ -1702,30 +1731,7 @@ export class ProxyForwarder { gate.error instanceof StreamPrecommitError && gate.error.gateReason === "idle_timeout" ) { - // 与提交后的静默超时同构(524 + streaming_idle_timeout): - // 错误规则/熔断/切换逻辑无需区分静默发生在门控前后 - throw new ProxyError( - `供应商流式响应静默超时: ${currentProvider.streamingIdleTimeoutMs}ms 内未收到新数据`, - 524, - { - body: JSON.stringify({ - error: { - type: "streaming_idle_timeout", - message: `Provider stopped sending data for ${currentProvider.streamingIdleTimeoutMs}ms`, - timeout_ms: currentProvider.streamingIdleTimeoutMs, - }, - }), - parsed: { - error: { - type: "streaming_idle_timeout", - message: `Provider stopped sending data for ${currentProvider.streamingIdleTimeoutMs}ms`, - timeout_ms: currentProvider.streamingIdleTimeoutMs, - }, - }, - providerId: currentProvider.id, - providerName: currentProvider.name, - } - ); + throw buildStreamingIdleTimeoutError(currentProvider); } if (timedOutBeforeContent) { @@ -4722,6 +4728,13 @@ export class ProxyForwarder { captureCommitMarker: !session.isHighConcurrencyModeEnabled(), }); if (!gate.committed) { + if ( + gate.error instanceof StreamPrecommitError && + gate.error.gateReason === "idle_timeout" + ) { + // 与串行路径同一 524 归类,熔断/超时判定不因 hedge 模式而漂移 + throw buildStreamingIdleTimeoutError(attempt.provider); + } throw gate.error; } if (gate.commitMarker) { From f442fc8d0bd949cb2e856941f7fc4deb03b7de70 Mon Sep 17 00:00:00 2001 From: ding113 Date: Thu, 23 Jul 2026 21:07:38 -0700 Subject: [PATCH 12/12] fix(ui): scope affinity-hit detection to chain[0] in provider chain popover Affinity-hit detection used chain.find(), which could match a mid-chain retry and mislabel it as cache reuse. Align with session reuse by checking only chain[0]. Add a dedicated teal DatabaseZap status icon for affinity_hit items so they are visually distinct. --- .../logs/_components/provider-chain-popover.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) 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 67dc0c10b..a3d519c75 100644 --- a/src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx +++ b/src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx @@ -287,6 +287,13 @@ function getItemStatus(item: ProviderChainItem): { bgColor: "bg-amber-50 dark:bg-amber-950/30", }; } + if (item.reason === "affinity_hit") { + return { + icon: DatabaseZap, + color: "text-teal-600", + bgColor: "bg-teal-50 dark:bg-teal-950/30", + }; + } return { icon: RefreshCw, color: "text-slate-500", @@ -513,10 +520,12 @@ export function ProviderChainPopover({ const isSessionReuse = chain[0]?.reason === "session_reuse" || chain[0]?.selectionMethod === "session_reuse"; - // F3a: prefix affinity hit (cache reuse nomination) - const affinityHitItem = chain.find( - (item) => item.reason === "affinity_hit" || item.selectionMethod === "prefix_affinity" - ); + // F3a: prefix affinity hit (cache reuse nomination); chain[0]-based like + // session reuse so mid-chain retries are not misread as cache reuse + const affinityHitItem = + chain[0]?.reason === "affinity_hit" || chain[0]?.selectionMethod === "prefix_affinity" + ? chain[0] + : undefined; const isAffinityHit = Boolean(affinityHitItem); // Get initial selection context for tooltip