diff --git a/drizzle/0114_overconfident_ronan.sql b/drizzle/0114_overconfident_ronan.sql new file mode 100644 index 000000000..28d54ab53 --- /dev/null +++ b/drizzle/0114_overconfident_ronan.sql @@ -0,0 +1,150 @@ +ALTER TABLE "message_request" ADD COLUMN IF NOT EXISTS "first_byte_ms" integer;--> statement-breakpoint +ALTER TABLE "usage_ledger" ADD COLUMN IF NOT EXISTS "first_byte_ms" integer;--> statement-breakpoint + +-- 真 TTFB(first_byte_ms)需要随 message_request 一起投影进 usage_ledger: +-- 重建 fn_upsert_usage_ledger 与触发器列清单(其余内容与 0098/0111 一致)。 +-- 历史行保持 first_byte_ms IS NULL,这正是「无真 TTFB,不计 TPS」的判据,故不做回填。 +CREATE OR REPLACE FUNCTION fn_upsert_usage_ledger() +RETURNS TRIGGER AS $$ +DECLARE + v_final_provider_id integer; + v_is_success boolean; + v_success_rate_outcome varchar; +BEGIN + v_success_rate_outcome := fn_compute_message_request_success_rate_outcome( + NEW.blocked_by, + NEW.status_code, + NEW.error_message, + NEW.provider_chain + ); + + IF NEW.blocked_by = 'warmup' THEN + -- If a ledger row already exists (row was originally non-warmup), mark it as warmup + -- and sync the latest actual_response_model so audit stays consistent across tables. + UPDATE usage_ledger + SET blocked_by = 'warmup', + success_rate_outcome = v_success_rate_outcome, + actual_response_model = NEW.actual_response_model + WHERE request_id = NEW.id; + RETURN NEW; + END IF; + + IF LOWER(REGEXP_REPLACE(COALESCE(NEW.endpoint, ''), '/+$', '')) + IN ('/v1/messages/count_tokens', '/v1/responses/compact') THEN + DELETE FROM usage_ledger WHERE request_id = NEW.id; + RETURN NEW; + END IF; + + IF NEW.provider_chain IS NOT NULL + AND jsonb_typeof(NEW.provider_chain) = 'array' + AND jsonb_array_length(NEW.provider_chain) > 0 + AND jsonb_typeof(NEW.provider_chain -> -1) = 'object' + AND (NEW.provider_chain -> -1 ? 'id') + AND (NEW.provider_chain -> -1 ->> 'id') ~ '^[0-9]+$' THEN + v_final_provider_id := (NEW.provider_chain -> -1 ->> 'id')::integer; + ELSE + v_final_provider_id := NEW.provider_id; + END IF; + + v_is_success := (NEW.error_message IS NULL OR NEW.error_message = '') + AND (NEW.status_code IS NULL OR NEW.status_code < 400); + + INSERT INTO usage_ledger ( + request_id, user_id, key, provider_id, final_provider_id, + model, original_model, actual_response_model, endpoint, api_type, session_id, + status_code, is_success, success_rate_outcome, blocked_by, + cost_usd, cost_multiplier, group_cost_multiplier, + input_tokens, output_tokens, + cache_creation_input_tokens, cache_read_input_tokens, + cache_creation_5m_input_tokens, cache_creation_1h_input_tokens, + cache_ttl_applied, context_1m_applied, swap_cache_ttl_applied, + duration_ms, ttfb_ms, first_byte_ms, client_ip, created_at + ) VALUES ( + NEW.id, NEW.user_id, NEW.key, NEW.provider_id, v_final_provider_id, + NEW.model, NEW.original_model, NEW.actual_response_model, NEW.endpoint, NEW.api_type, NEW.session_id, + NEW.status_code, v_is_success, v_success_rate_outcome, NEW.blocked_by, + NEW.cost_usd, NEW.cost_multiplier, NEW.group_cost_multiplier, + NEW.input_tokens, NEW.output_tokens, + NEW.cache_creation_input_tokens, NEW.cache_read_input_tokens, + NEW.cache_creation_5m_input_tokens, NEW.cache_creation_1h_input_tokens, + NEW.cache_ttl_applied, NEW.context_1m_applied, NEW.swap_cache_ttl_applied, + NEW.duration_ms, NEW.ttfb_ms, NEW.first_byte_ms, NEW.client_ip, NEW.created_at + ) + ON CONFLICT (request_id) DO UPDATE SET + user_id = EXCLUDED.user_id, + key = EXCLUDED.key, + provider_id = EXCLUDED.provider_id, + final_provider_id = EXCLUDED.final_provider_id, + model = EXCLUDED.model, + original_model = EXCLUDED.original_model, + actual_response_model = EXCLUDED.actual_response_model, + endpoint = EXCLUDED.endpoint, + api_type = EXCLUDED.api_type, + session_id = EXCLUDED.session_id, + status_code = EXCLUDED.status_code, + is_success = EXCLUDED.is_success, + success_rate_outcome = EXCLUDED.success_rate_outcome, + blocked_by = EXCLUDED.blocked_by, + cost_usd = EXCLUDED.cost_usd, + cost_multiplier = EXCLUDED.cost_multiplier, + group_cost_multiplier = EXCLUDED.group_cost_multiplier, + input_tokens = EXCLUDED.input_tokens, + output_tokens = EXCLUDED.output_tokens, + cache_creation_input_tokens = EXCLUDED.cache_creation_input_tokens, + cache_read_input_tokens = EXCLUDED.cache_read_input_tokens, + cache_creation_5m_input_tokens = EXCLUDED.cache_creation_5m_input_tokens, + cache_creation_1h_input_tokens = EXCLUDED.cache_creation_1h_input_tokens, + cache_ttl_applied = EXCLUDED.cache_ttl_applied, + context_1m_applied = EXCLUDED.context_1m_applied, + swap_cache_ttl_applied = EXCLUDED.swap_cache_ttl_applied, + duration_ms = EXCLUDED.duration_ms, + ttfb_ms = EXCLUDED.ttfb_ms, + first_byte_ms = EXCLUDED.first_byte_ms, + client_ip = EXCLUDED.client_ip; + -- created_at deliberately NOT updated on conflict: it represents the + -- original insert time of the ledger row, which is immutable by design. + + RETURN NEW; +EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'fn_upsert_usage_ledger failed for request_id=%: %', NEW.id, SQLERRM; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS trg_upsert_usage_ledger ON message_request; + +CREATE TRIGGER trg_upsert_usage_ledger +AFTER INSERT OR UPDATE OF + blocked_by, + status_code, + error_message, + provider_chain, + actual_response_model, + endpoint, + provider_id, + user_id, + "key", + model, + original_model, + api_type, + session_id, + cost_usd, + cost_multiplier, + group_cost_multiplier, + input_tokens, + output_tokens, + cache_creation_input_tokens, + cache_read_input_tokens, + cache_creation_5m_input_tokens, + cache_creation_1h_input_tokens, + cache_ttl_applied, + context_1m_applied, + swap_cache_ttl_applied, + duration_ms, + ttfb_ms, + first_byte_ms, + client_ip, + created_at +ON message_request +FOR EACH ROW +EXECUTE FUNCTION fn_upsert_usage_ledger(); diff --git a/drizzle/meta/0114_snapshot.json b/drizzle/meta/0114_snapshot.json new file mode 100644 index 000000000..cc95d2cd2 --- /dev/null +++ b/drizzle/meta/0114_snapshot.json @@ -0,0 +1,5175 @@ +{ + "id": "63b9d353-72a5-432c-88ff-77e871ddfb68", + "prevId": "87e53bfd-94b5-42d6-b589-0ec54a331157", + "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 + }, + "first_byte_ms": { + "name": "first_byte_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_read_input_tokens": { + "name": "cache_read_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_creation_5m_input_tokens": { + "name": "cache_creation_5m_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_creation_1h_input_tokens": { + "name": "cache_creation_1h_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_ttl_applied": { + "name": "cache_ttl_applied", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "context_1m_applied": { + "name": "context_1m_applied", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "swap_cache_ttl_applied": { + "name": "swap_cache_ttl_applied", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "special_settings": { + "name": "special_settings", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "hedge_losers": { + "name": "hedge_losers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_stack": { + "name": "error_stack", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_cause": { + "name": "error_cause", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "blocked_by": { + "name": "blocked_by", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "blocked_reason": { + "name": "blocked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "client_ip": { + "name": "client_ip", + "type": "varchar(45)", + "primaryKey": false, + "notNull": false + }, + "messages_count": { + "name": "messages_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cache_compatibility_key": { + "name": "cache_compatibility_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "cache_score_eligible": { + "name": "cache_score_eligible", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cache_score_excluded_reason": { + "name": "cache_score_excluded_reason", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "theoretical_cache_tokens": { + "name": "theoretical_cache_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_ttl_bucket": { + "name": "cache_ttl_bucket", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_message_request_user_date_cost": { + "name": "idx_message_request_user_date_cost", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_user_created_at_cost_stats": { + "name": "idx_message_request_user_created_at_cost_stats", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_usd", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_user_query": { + "name": "idx_message_request_user_query", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_provider_created_at_active": { + "name": "idx_message_request_provider_created_at_active", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_provider_created_at_finalized_active": { + "name": "idx_message_request_provider_created_at_finalized_active", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"status_code\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_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 + }, + "first_byte_ms": { + "name": "first_byte_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_ip": { + "name": "client_ip", + "type": "varchar(45)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_usage_ledger_request_id": { + "name": "idx_usage_ledger_request_id", + "columns": [ + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_usage_ledger_user_created_at": { + "name": "idx_usage_ledger_user_created_at", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_ledger\".\"blocked_by\" IS NULL", + "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 d5f102ac7..1f2b7527e 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -799,6 +799,13 @@ "when": 1784833275913, "tag": "0113_reflective_centennial", "breakpoints": true + }, + { + "idx": 114, + "version": "7", + "when": 1784952018550, + "tag": "0114_overconfident_ronan", + "breakpoints": true } ] } \ No newline at end of file diff --git a/messages/en/dashboard.json b/messages/en/dashboard.json index 96ff6a8c6..5fbc5d622 100644 --- a/messages/en/dashboard.json +++ b/messages/en/dashboard.json @@ -348,25 +348,25 @@ "performance": { "title": "Performance", "ttfb": "TTFB", + "tfft": "TFFT", "duration": "Total Duration", - "outputRate": "Output Rate" + "outputRate": "Output Rate", + "outputTokens": "Output Tokens" }, "performanceTab": { "noPerformanceData": "No performance data available", - "ttfbGauge": "Time to First Byte", + "tfftGauge": "Time to First Token", "outputRateGauge": "Output Rate", "latencyBreakdown": "Latency Breakdown", "generationTime": "Generation Time", + "segmentTtfb": "TTFB", + "segmentTfft": "Token Wait", + "segmentTotal": "Total", "assessment": { "excellent": "Excellent", "good": "Good", "warning": "Warning", "poor": "Poor" - }, - "thresholds": { - "ttfbGood": "TTFB < 1s", - "ttfbWarning": "TTFB 1-2s", - "ttfbPoor": "TTFB > 3s" } }, "metadata": { @@ -678,7 +678,7 @@ "totalConsumedAmount": "Total Spend", "successRate": "Success Rate", "avgResponseTime": "Avg Response Time", - "avgTtfbMs": "Avg TTFB", + "avgTtfbMs": "Avg TFFT", "avgTokensPerSecond": "Avg tok/s", "avgCostPerRequest": "Avg Cost/Req", "avgCostPerMillionTokens": "Avg Cost/1M Tokens", diff --git a/messages/en/settings/statusPage.json b/messages/en/settings/statusPage.json index 5eae1b710..38d4d2fc8 100644 --- a/messages/en/settings/statusPage.json +++ b/messages/en/settings/statusPage.json @@ -3,7 +3,7 @@ "description": "Configure the public status page's stats window, chart bucket size, and which groups/models should be exposed.", "form": { "windowHours": "Stats Window (hours)", - "windowHoursDesc": "Window length used to compute TTFB and availability, and the total span the chart covers. Default: 24 hours.", + "windowHoursDesc": "Window length used to compute TFFT and availability, and the total span the chart covers. Default: 24 hours.", "aggregationIntervalMinutes": "Chart Bucket (minutes)", "aggregationIntervalMinutesDesc": "Length of each bucket on the chart timeline; controls chart granularity. Choose 5 / 15 / 30 / 60 minutes.", "aggregationIntervalMinutesInvalid": "Public status aggregation interval must be one of 5, 15, 30, or 60 minutes.", @@ -46,7 +46,7 @@ "heroPrimary": "AI SERVICES", "heroSecondary": "SERVICE STATUS DASHBOARD", "generatedAt": "Updated", - "ttfb": "TTFB", + "ttfb": "TFFT", "tps": "TPS", "history": "History", "freshnessWindow": "Snapshot freshness", @@ -76,7 +76,7 @@ }, "tooltip": { "availability": "Availability", - "ttfb": "TTFB", + "ttfb": "TFFT", "tps": "TPS", "samples": "Samples", "inferredFromNeighbors": "No requests in this window — inferred from neighbors", diff --git a/messages/ja/dashboard.json b/messages/ja/dashboard.json index 617593896..2176634b5 100644 --- a/messages/ja/dashboard.json +++ b/messages/ja/dashboard.json @@ -348,25 +348,25 @@ "performance": { "title": "パフォーマンス", "ttfb": "TTFB", + "tfft": "TFFT", "duration": "総所要時間", - "outputRate": "出力速度" + "outputRate": "出力速度", + "outputTokens": "出力トークン" }, "performanceTab": { "noPerformanceData": "パフォーマンスデータがありません", - "ttfbGauge": "初バイト到達時間", + "tfftGauge": "初トークン到達時間", "outputRateGauge": "出力速度", "latencyBreakdown": "レイテンシ内訳", "generationTime": "生成時間", + "segmentTtfb": "TTFB", + "segmentTfft": "トークン待機", + "segmentTotal": "合計", "assessment": { "excellent": "優秀", "good": "良好", "warning": "警告", "poor": "不良" - }, - "thresholds": { - "ttfbGood": "TTFB < 1s", - "ttfbWarning": "TTFB 1-2s", - "ttfbPoor": "TTFB > 3s" } }, "metadata": { @@ -678,7 +678,7 @@ "totalConsumedAmount": "総消費額", "successRate": "成功率(%)", "avgResponseTime": "平均応答時間", - "avgTtfbMs": "平均TTFB", + "avgTtfbMs": "平均TFFT", "avgTokensPerSecond": "平均トークン/秒", "avgCostPerRequest": "平均リクエスト単価", "avgCostPerMillionTokens": "100万トークンあたりコスト", diff --git a/messages/ja/settings/statusPage.json b/messages/ja/settings/statusPage.json index f41ea87b7..e6574d771 100644 --- a/messages/ja/settings/statusPage.json +++ b/messages/ja/settings/statusPage.json @@ -3,7 +3,7 @@ "description": "公開ステータスページの統計ウィンドウ、チャートのバケットサイズ、および公開するグループ/モデルを設定します。", "form": { "windowHours": "統計ウィンドウ(時間)", - "windowHoursDesc": "TTFB と可用率の算出に使う統計期間であり、チャートがカバーする総期間でもあります。既定値は 24 時間です。", + "windowHoursDesc": "TFFT と可用率の算出に使う統計期間であり、チャートがカバーする総期間でもあります。既定値は 24 時間です。", "aggregationIntervalMinutes": "チャートのバケット(分)", "aggregationIntervalMinutesDesc": "チャート時間軸の各バケットの長さで、チャートの粒度を決定します。5 / 15 / 30 / 60 分から選択できます。", "aggregationIntervalMinutesInvalid": "公開ステータスの集計間隔は 5、15、30、60 分のいずれかである必要があります。", @@ -46,7 +46,7 @@ "heroPrimary": "AI SERVICES", "heroSecondary": "SERVICE STATUS DASHBOARD", "generatedAt": "更新", - "ttfb": "TTFB", + "ttfb": "TFFT", "tps": "TPS", "history": "履歴", "freshnessWindow": "スナップショット有効期限", @@ -76,7 +76,7 @@ }, "tooltip": { "availability": "可用率", - "ttfb": "TTFB", + "ttfb": "TFFT", "tps": "TPS", "samples": "サンプル数", "inferredFromNeighbors": "この期間はリクエストがないため、隣接データから推定", diff --git a/messages/ru/dashboard.json b/messages/ru/dashboard.json index 1a5bf7db0..454fe0803 100644 --- a/messages/ru/dashboard.json +++ b/messages/ru/dashboard.json @@ -348,25 +348,25 @@ "performance": { "title": "Производительность", "ttfb": "TTFB", + "tfft": "TFFT", "duration": "Общее время", - "outputRate": "Скорость вывода" + "outputRate": "Скорость вывода", + "outputTokens": "Токены вывода" }, "performanceTab": { "noPerformanceData": "Нет данных о производительности", - "ttfbGauge": "Время до первого байта", + "tfftGauge": "Время до первого токена", "outputRateGauge": "Скорость вывода", "latencyBreakdown": "Разбивка задержки", "generationTime": "Время генерации", + "segmentTtfb": "TTFB", + "segmentTfft": "Ожидание токена", + "segmentTotal": "Всего", "assessment": { "excellent": "Отлично", "good": "Хорошо", "warning": "Предупреждение", "poor": "Плохо" - }, - "thresholds": { - "ttfbGood": "TTFB < 1с", - "ttfbWarning": "TTFB 1-2с", - "ttfbPoor": "TTFB > 3с" } }, "metadata": { @@ -678,7 +678,7 @@ "totalConsumedAmount": "Общие расходы", "successRate": "Процент успеха", "avgResponseTime": "Среднее время ответа", - "avgTtfbMs": "Средний TTFB", + "avgTtfbMs": "Средний TFFT", "avgTokensPerSecond": "Средн. ток/с", "avgCostPerRequest": "Ср. стоимость/запрос", "avgCostPerMillionTokens": "Ср. стоимость/1М токенов", diff --git a/messages/ru/settings/statusPage.json b/messages/ru/settings/statusPage.json index d7bb45386..640c0984d 100644 --- a/messages/ru/settings/statusPage.json +++ b/messages/ru/settings/statusPage.json @@ -3,7 +3,7 @@ "description": "Настройте окно статистики, размер бакета графика и группы/модели, которые будут показаны публично.", "form": { "windowHours": "Окно статистики (часы)", - "windowHoursDesc": "Длина окна для расчёта TTFB и доступности, а также общий период, покрываемый графиком. По умолчанию: 24 часа.", + "windowHoursDesc": "Длина окна для расчёта TFFT и доступности, а также общий период, покрываемый графиком. По умолчанию: 24 часа.", "aggregationIntervalMinutes": "Бакет графика (минуты)", "aggregationIntervalMinutesDesc": "Длина каждого бакета на оси времени графика; определяет детализацию графика. Допустимые значения: 5 / 15 / 30 / 60 минут.", "aggregationIntervalMinutesInvalid": "Интервал агрегации публичного статуса должен быть одним из 5, 15, 30 или 60 минут.", @@ -46,7 +46,7 @@ "heroPrimary": "AI SERVICES", "heroSecondary": "SERVICE STATUS DASHBOARD", "generatedAt": "Обновлено", - "ttfb": "TTFB", + "ttfb": "TFFT", "tps": "TPS", "history": "История", "freshnessWindow": "Свежесть снимка", @@ -76,7 +76,7 @@ }, "tooltip": { "availability": "Доступность", - "ttfb": "TTFB", + "ttfb": "TFFT", "tps": "TPS", "samples": "Выборки", "inferredFromNeighbors": "Запросов нет — состояние выведено из соседних интервалов", diff --git a/messages/zh-CN/dashboard.json b/messages/zh-CN/dashboard.json index c3b8dbc16..f314c58ec 100644 --- a/messages/zh-CN/dashboard.json +++ b/messages/zh-CN/dashboard.json @@ -348,25 +348,25 @@ "performance": { "title": "性能数据", "ttfb": "首字节时间(TTFB)", + "tfft": "首 Token 时间(TFFT)", "duration": "总耗时", - "outputRate": "输出速率" + "outputRate": "输出速率", + "outputTokens": "输出 Tokens" }, "performanceTab": { "noPerformanceData": "暂无性能数据", - "ttfbGauge": "首字节时间", + "tfftGauge": "首 Token 时间", "outputRateGauge": "输出速率", "latencyBreakdown": "延迟分解", "generationTime": "生成时间", + "segmentTtfb": "TTFB", + "segmentTfft": "等待首 Token", + "segmentTotal": "总计", "assessment": { "excellent": "优秀", "good": "良好", "warning": "警告", "poor": "较差" - }, - "thresholds": { - "ttfbGood": "TTFB < 1s", - "ttfbWarning": "TTFB 1-2s", - "ttfbPoor": "TTFB > 3s" } }, "metadata": { @@ -678,7 +678,7 @@ "totalConsumedAmount": "总消耗金额", "successRate": "成功率", "avgResponseTime": "平均响应时间", - "avgTtfbMs": "平均 TTFB", + "avgTtfbMs": "平均 TFFT", "avgTokensPerSecond": "平均输出速率", "avgCostPerRequest": "平均单次请求成本", "avgCostPerMillionTokens": "平均百万 Token 成本", diff --git a/messages/zh-CN/settings/statusPage.json b/messages/zh-CN/settings/statusPage.json index 8019df3b0..992c4f387 100644 --- a/messages/zh-CN/settings/statusPage.json +++ b/messages/zh-CN/settings/statusPage.json @@ -3,7 +3,7 @@ "description": "配置公开状态页面的统计窗口、图表分桶,以及需要对外展示的分组和模型。", "form": { "windowHours": "统计窗口(小时)", - "windowHoursDesc": "用于计算 TTFB 与在线率的统计窗口长度,也是图表覆盖的总时间跨度。默认 24 小时。", + "windowHoursDesc": "用于计算 TFFT 与在线率的统计窗口长度,也是图表覆盖的总时间跨度。默认 24 小时。", "aggregationIntervalMinutes": "图表分桶(分钟)", "aggregationIntervalMinutesDesc": "图表时间线每个分桶的时长,决定图表粒度。可选 5 / 15 / 30 / 60 分钟。", "aggregationIntervalMinutesInvalid": "公开状态聚合间隔只能是 5、15、30、60 分钟之一。", @@ -46,7 +46,7 @@ "heroPrimary": "AI 服务", "heroSecondary": "服务状态面板", "generatedAt": "更新于", - "ttfb": "TTFB", + "ttfb": "TFFT", "tps": "TPS", "history": "历史", "freshnessWindow": "快照新鲜期剩余", @@ -76,7 +76,7 @@ }, "tooltip": { "availability": "可用率", - "ttfb": "TTFB", + "ttfb": "TFFT", "tps": "TPS", "samples": "样本数", "inferredFromNeighbors": "该时段无请求,根据相邻时段状态推断", diff --git a/messages/zh-TW/dashboard.json b/messages/zh-TW/dashboard.json index f2cbb699f..e8e5fee6f 100644 --- a/messages/zh-TW/dashboard.json +++ b/messages/zh-TW/dashboard.json @@ -348,25 +348,25 @@ "performance": { "title": "效能資料", "ttfb": "首字節時間(TTFB)", + "tfft": "首 Token 時間(TFFT)", "duration": "總耗時", - "outputRate": "輸出速率" + "outputRate": "輸出速率", + "outputTokens": "輸出 Tokens" }, "performanceTab": { "noPerformanceData": "暫無效能資料", - "ttfbGauge": "首字節時間", + "tfftGauge": "首 Token 時間", "outputRateGauge": "輸出速率", "latencyBreakdown": "延遲分解", "generationTime": "生成時間", + "segmentTtfb": "TTFB", + "segmentTfft": "等待首 Token", + "segmentTotal": "總計", "assessment": { "excellent": "優秀", "good": "良好", "warning": "警告", "poor": "較差" - }, - "thresholds": { - "ttfbGood": "TTFB < 1s", - "ttfbWarning": "TTFB 1-2s", - "ttfbPoor": "TTFB > 3s" } }, "metadata": { @@ -678,7 +678,7 @@ "totalConsumedAmount": "總消耗金額", "successRate": "成功率(%)", "avgResponseTime": "平均回覆時間", - "avgTtfbMs": "平均 TTFB(ms)", + "avgTtfbMs": "平均 TFFT(ms)", "avgTokensPerSecond": "平均輸出速率", "avgCostPerRequest": "平均每次請求成本", "avgCostPerMillionTokens": "平均每百萬 Token 成本", diff --git a/messages/zh-TW/settings/statusPage.json b/messages/zh-TW/settings/statusPage.json index 2c8ef1ec3..c6d879e49 100644 --- a/messages/zh-TW/settings/statusPage.json +++ b/messages/zh-TW/settings/statusPage.json @@ -3,7 +3,7 @@ "description": "設定公開狀態頁面的統計視窗、圖表分桶,以及需要對外展示的分組和模型。", "form": { "windowHours": "統計視窗(小時)", - "windowHoursDesc": "用於計算 TTFB 與在線率的統計視窗長度,也是圖表覆蓋的總時間跨度。預設 24 小時。", + "windowHoursDesc": "用於計算 TFFT 與在線率的統計視窗長度,也是圖表覆蓋的總時間跨度。預設 24 小時。", "aggregationIntervalMinutes": "圖表分桶(分鐘)", "aggregationIntervalMinutesDesc": "圖表時間軸每個分桶的時長,決定圖表粒度。可選 5 / 15 / 30 / 60 分鐘。", "aggregationIntervalMinutesInvalid": "公開狀態聚合間隔只能是 5、15、30、60 分鐘之一。", @@ -46,7 +46,7 @@ "heroPrimary": "AI 服務", "heroSecondary": "服務狀態面板", "generatedAt": "更新於", - "ttfb": "TTFB", + "ttfb": "TFFT", "tps": "TPS", "history": "歷史", "freshnessWindow": "快照新鮮期剩餘", @@ -76,7 +76,7 @@ }, "tooltip": { "availability": "可用率", - "ttfb": "TTFB", + "ttfb": "TFFT", "tps": "TPS", "samples": "樣本數", "inferredFromNeighbors": "該時段無請求,依相鄰時段狀態推斷", diff --git a/src/app/[locale]/dashboard/logs/_components/error-details-dialog.test.tsx b/src/app/[locale]/dashboard/logs/_components/error-details-dialog.test.tsx index 64b0bc03b..66f1e9ff6 100644 --- a/src/app/[locale]/dashboard/logs/_components/error-details-dialog.test.tsx +++ b/src/app/[locale]/dashboard/logs/_components/error-details-dialog.test.tsx @@ -255,26 +255,26 @@ const messages = { performance: { title: "Performance", ttfb: "TTFB", + tfft: "TFFT", duration: "Duration", outputRate: "Output rate", + outputTokens: "Output Tokens", }, performanceTab: { noPerformanceData: "No performance data", - ttfbGauge: "Time to First Byte", + tfftGauge: "Time to First Token", outputRateGauge: "Output Rate", latencyBreakdown: "Latency Breakdown", generationTime: "Generation Time", + segmentTtfb: "TTFB", + segmentTfft: "Token Wait", + segmentTotal: "Total", assessment: { excellent: "Excellent", good: "Good", warning: "Warning", poor: "Poor", }, - thresholds: { - ttfbGood: "TTFB < 300ms", - ttfbWarning: "TTFB 300-600ms", - ttfbPoor: "TTFB > 1000ms", - }, }, metadata: { noMetadata: "No metadata", @@ -526,7 +526,8 @@ describe("error-details-dialog layout", () => { inputTokens={100} outputTokens={80} durationMs={900} - ttfbMs={100} + tfftMs={100} + firstByteMs={100} /> ); @@ -546,7 +547,8 @@ describe("error-details-dialog layout", () => { inputTokens={100} outputTokens={0} durationMs={null} - ttfbMs={null} + tfftMs={null} + firstByteMs={null} /> ); @@ -566,7 +568,8 @@ describe("error-details-dialog layout", () => { inputTokens={null} outputTokens={80} durationMs={900} - ttfbMs={100} + tfftMs={100} + firstByteMs={100} /> ); @@ -576,7 +579,7 @@ describe("error-details-dialog layout", () => { test("hides tok/s when TTFB is close to duration and rate is abnormally high", () => { // Rule: generationTimeMs / durationMs < 0.1 && outputRate > 5000 => hide tok/s - // durationMs=1000, ttfbMs=950 => generationTimeMs=50, ratio=0.05 < 0.1 + // durationMs=1000, firstByteMs=950 => generationTimeMs=50, ratio=0.05 < 0.1 // outputTokens=300 => rate = 300 / 0.05 = 6000 > 5000 => should hide const html = renderWithIntl( { inputTokens={null} outputTokens={300} durationMs={1000} - ttfbMs={950} + tfftMs={950} + firstByteMs={950} /> ); @@ -601,7 +605,7 @@ describe("error-details-dialog layout", () => { }); test("shows tok/s in dialog when conditions are normal", () => { - // durationMs=1000, ttfbMs=500 => generationTimeMs=500, ratio=0.5 >= 0.1 + // durationMs=1000, firstByteMs=500 => generationTimeMs=500, ratio=0.5 >= 0.1 // outputTokens=50 => rate = 50 / 0.5 = 100 <= 5000 => should show const html = renderWithIntl( { inputTokens={null} outputTokens={50} durationMs={1000} - ttfbMs={500} + tfftMs={500} + firstByteMs={500} /> ); @@ -1194,12 +1199,13 @@ describe("error-details-dialog tabs", () => { providerChain={null} sessionId={null} durationMs={1000} - ttfbMs={200} + tfftMs={200} + firstByteMs={200} outputTokens={500} /> ); - expect(html).toContain("Time to First Byte"); + expect(html).toContain("Time to First Token"); expect(html).toContain("Output Rate"); expect(html).toContain("Latency Breakdown"); }); diff --git a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/LatencyBreakdownBar.tsx b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/LatencyBreakdownBar.tsx index ce89e537a..48e56939a 100644 --- a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/LatencyBreakdownBar.tsx +++ b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/LatencyBreakdownBar.tsx @@ -4,8 +4,10 @@ import { useTranslations } from "next-intl"; import { cn } from "@/lib/utils"; interface LatencyBreakdownBarProps { - /** Time to first byte in milliseconds */ - ttfbMs: number | null; + /** Time to first byte in milliseconds (null on rows persisted before it was recorded) */ + firstByteMs: number | null; + /** Time to first token in milliseconds */ + tfftMs: number | null; /** Total duration in milliseconds */ durationMs: number | null; /** Optional className */ @@ -22,7 +24,8 @@ function formatMs(ms: number): string { } export function LatencyBreakdownBar({ - ttfbMs, + firstByteMs, + tfftMs, durationMs, className, showLabels = true, @@ -31,70 +34,89 @@ export function LatencyBreakdownBar({ // Handle null/invalid values if ( - ttfbMs === null || + tfftMs === null || durationMs === null || - ttfbMs < 0 || + tfftMs < 0 || durationMs <= 0 || - ttfbMs > durationMs + tfftMs > durationMs ) { return null; } - const generationMs = durationMs - ttfbMs; - const ttfbPercent = (ttfbMs / durationMs) * 100; - const generationPercent = 100 - ttfbPercent; + // 历史行没有真 TTFB:首段退化为整个 TFFT,中间段消失 + const ttfbMs = + firstByteMs !== null && firstByteMs >= 0 && firstByteMs <= tfftMs ? firstByteMs : tfftMs; + const tokenWaitMs = tfftMs - ttfbMs; + const generationMs = durationMs - tfftMs; + const percent = (ms: number) => (ms / durationMs) * 100; // Minimum width for visibility (3%) const minWidth = 3; - const adjustedTtfbPercent = Math.max(ttfbPercent, ttfbMs > 0 ? minWidth : 0); - const adjustedGenerationPercent = Math.max(generationPercent, generationMs > 0 ? minWidth : 0); + const width = (ms: number) => Math.max(percent(ms), ms > 0 ? minWidth : 0); + + const segments = [ + { + key: "ttfb", + ms: ttfbMs, + label: t("segmentTtfb"), + barClass: "bg-blue-500", + dotClass: "bg-blue-500", + }, + { + key: "tokenWait", + ms: tokenWaitMs, + label: t("segmentTfft"), + barClass: "bg-violet-500", + dotClass: "bg-violet-500", + }, + { + key: "generation", + ms: generationMs, + label: t("generationTime"), + barClass: "bg-emerald-500", + dotClass: "bg-emerald-500", + }, + ]; return (
{/* Bar container */}
- {/* TTFB segment */} - {ttfbMs > 0 && ( -
- {ttfbPercent >= 15 && TTFB} -
- )} - - {/* Generation segment */} - {generationMs > 0 && ( -
- {generationPercent >= 15 && Generation} -
+ {segments.map((segment) => + segment.ms > 0 ? ( +
+ {percent(segment.ms) >= 15 && {segment.label}} +
+ ) : null )}
{/* Labels */} {showLabels && ( -
-
-
- TTFB: - {formatMs(ttfbMs)} -
-
-
- {t("generationTime")}: - {formatMs(generationMs)} -
+
+ {segments.map((segment) => + segment.ms > 0 ? ( +
+
+ {segment.label}: + {formatMs(segment.ms)} +
+ ) : null + )}
)} {/* Total */}
- Total: {formatMs(durationMs)} + {t("segmentTotal")}: {formatMs(durationMs)}
); diff --git a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/PerformanceTab.tsx b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/PerformanceTab.tsx index a83522a44..b7d73813f 100644 --- a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/PerformanceTab.tsx +++ b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/PerformanceTab.tsx @@ -5,35 +5,36 @@ import { useTranslations } from "next-intl"; import { Badge } from "@/components/ui/badge"; import { CircularProgress } from "@/components/ui/circular-progress"; import { cn, formatTokenAmount } from "@/lib/utils"; -import { calculateOutputRate, type PerformanceTabProps, shouldHideOutputRate } from "../types"; +import { calculateOutputRate, shouldHideOutputRate } from "@/lib/utils/performance-formatter"; +import type { PerformanceTabProps } from "../types"; import { LatencyBreakdownBar } from "./LatencyBreakdownBar"; /** - * Get TTFB performance assessment + * Get TFFT performance assessment * Thresholds: <1s excellent, <2s good, <3s warning, >=3s poor */ -function getTtfbAssessment(ttfbMs: number | null): { +function getTfftAssessment(tfftMs: number | null): { label: string; color: string; bgColor: string; } | null { - if (ttfbMs === null) return null; + if (tfftMs === null) return null; - if (ttfbMs < 1000) { + if (tfftMs < 1000) { return { label: "excellent", color: "text-emerald-600", bgColor: "bg-emerald-50 dark:bg-emerald-950/20", }; } - if (ttfbMs < 2000) { + if (tfftMs < 2000) { return { label: "good", color: "text-blue-600", bgColor: "bg-blue-50 dark:bg-blue-950/20", }; } - if (ttfbMs < 3000) { + if (tfftMs < 3000) { return { label: "warning", color: "text-amber-600", @@ -85,31 +86,38 @@ function getOutputRateAssessment(rate: number | null): { }; } -export function PerformanceTab({ durationMs, ttfbMs, outputTokens }: PerformanceTabProps) { +export function PerformanceTab({ + durationMs, + tfftMs, + firstByteMs, + outputTokens, +}: PerformanceTabProps) { const t = useTranslations("dashboard.logs.details"); // Normalize undefined to null for consistent handling const normalizedDurationMs = durationMs ?? null; - const normalizedTtfbMs = ttfbMs ?? null; + const normalizedTfftMs = tfftMs ?? null; + const normalizedFirstByteMs = firstByteMs ?? null; const normalizedOutputTokens = outputTokens ?? null; const outputRate = calculateOutputRate( normalizedOutputTokens, normalizedDurationMs, - normalizedTtfbMs + normalizedFirstByteMs ); - const hideRate = shouldHideOutputRate(outputRate, normalizedDurationMs, normalizedTtfbMs); + const hideRate = shouldHideOutputRate(outputRate, normalizedDurationMs, normalizedFirstByteMs); const generationMs = - normalizedDurationMs !== null && normalizedTtfbMs !== null - ? normalizedDurationMs - normalizedTtfbMs + normalizedDurationMs !== null && normalizedTfftMs !== null + ? normalizedDurationMs - normalizedTfftMs : null; - const ttfbAssessment = getTtfbAssessment(normalizedTtfbMs); + const tfftAssessment = getTfftAssessment(normalizedTfftMs); const rateAssessment = getOutputRateAssessment(outputRate); const hasData = normalizedDurationMs !== null || - normalizedTtfbMs !== null || + normalizedTfftMs !== null || + normalizedFirstByteMs !== null || (outputRate !== null && !hideRate) || normalizedOutputTokens !== null; @@ -126,17 +134,17 @@ export function PerformanceTab({ durationMs, ttfbMs, outputTokens }: Performance
{/* Gauges Row */}
- {/* TTFB Gauge */} - {normalizedTtfbMs !== null && ( + {/* TFFT Gauge */} + {normalizedTfftMs !== null && (
-

{t("performanceTab.ttfbGauge")}

+

{t("performanceTab.tfftGauge")}

- {normalizedTtfbMs >= 1000 - ? `${(normalizedTtfbMs / 1000).toFixed(2)}s` - : `${Math.round(normalizedTtfbMs)}ms`} + {normalizedTfftMs >= 1000 + ? `${(normalizedTfftMs / 1000).toFixed(2)}s` + : `${Math.round(normalizedTfftMs)}ms`}

- {ttfbAssessment && ( - - {t(`performanceTab.assessment.${ttfbAssessment.label}`)} + {tfftAssessment && ( + + {t(`performanceTab.assessment.${tfftAssessment.label}`)} )}
@@ -198,14 +206,18 @@ export function PerformanceTab({ durationMs, ttfbMs, outputTokens }: Performance
{/* Latency Breakdown Bar */} - {normalizedTtfbMs !== null && normalizedDurationMs !== null && ( + {normalizedTfftMs !== null && normalizedDurationMs !== null && (

{t("performanceTab.latencyBreakdown")}

- +
)} @@ -214,13 +226,23 @@ export function PerformanceTab({ durationMs, ttfbMs, outputTokens }: Performance

{t("performance.title")}

- {normalizedTtfbMs !== null && ( + {normalizedFirstByteMs !== null && (
{t("performance.ttfb")} - {normalizedTtfbMs >= 1000 - ? `${(normalizedTtfbMs / 1000).toFixed(2)}s` - : `${Math.round(normalizedTtfbMs)}ms`} + {normalizedFirstByteMs >= 1000 + ? `${(normalizedFirstByteMs / 1000).toFixed(2)}s` + : `${Math.round(normalizedFirstByteMs)}ms`} + +
+ )} + {normalizedTfftMs !== null && ( +
+ {t("performance.tfft")} + + {normalizedTfftMs >= 1000 + ? `${(normalizedTfftMs / 1000).toFixed(2)}s` + : `${Math.round(normalizedTfftMs)}ms`}
)} @@ -248,7 +270,7 @@ export function PerformanceTab({ durationMs, ttfbMs, outputTokens }: Performance )} {normalizedOutputTokens !== null && (
- Output Tokens + {t("performance.outputTokens")} {formatTokenAmount(normalizedOutputTokens)} 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 7e70d4679..50f344ace 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 @@ -29,6 +29,7 @@ import { cn, formatTokenAmount } from "@/lib/utils"; import { formatCurrency } from "@/lib/utils/currency"; import { buildHedgeBillingTable } from "@/lib/utils/hedge-billing"; import { resolveModelAuditDisplay } from "@/lib/utils/model-audit-display"; +import { calculateOutputRate, shouldHideOutputRate } from "@/lib/utils/performance-formatter"; import { getPricingResolutionSpecialSetting, getThinkingSignatureModelDetectionSpecialSetting, @@ -37,13 +38,7 @@ import { import { extractThinkingEffortInfo } from "@/lib/utils/thinking-effort"; import { getFake200ReasonKey } from "../../fake200-reason"; import { Fake200RetryTooltip } from "../../fake200-retry-tooltip"; -import { - calculateOutputRate, - isInProgressStatus, - isSuccessStatus, - type SummaryTabProps, - shouldHideOutputRate, -} from "../types"; +import { isInProgressStatus, isSuccessStatus, type SummaryTabProps } from "../types"; export function SummaryTab({ statusCode, @@ -69,7 +64,7 @@ export function SummaryTab({ routingTrace, context1mApplied, durationMs, - ttfbMs, + firstByteMs, sessionId, requestSequence, userAgent, @@ -85,8 +80,12 @@ export function SummaryTab({ const isSuccess = isSuccessStatus(statusCode); const isInProgress = isInProgressStatus(statusCode); - const outputRate = calculateOutputRate(outputTokens, durationMs, ttfbMs); - const hideRate = shouldHideOutputRate(outputRate, durationMs, ttfbMs); + const outputRate = calculateOutputRate( + outputTokens ?? null, + durationMs ?? null, + firstByteMs ?? null + ); + const hideRate = shouldHideOutputRate(outputRate, durationMs ?? null, firstByteMs ?? null); const totalTokens = (inputTokens ?? 0) + (outputTokens ?? 0); const hasRedirect = originalModel && currentModel && originalModel !== currentModel; const modelAudit = resolveModelAuditDisplay({ diff --git a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/index.tsx b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/index.tsx index 901dd1a75..d690debf6 100644 --- a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/index.tsx +++ b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/index.tsx @@ -49,7 +49,8 @@ interface ErrorDetailsDialogProps { hedgeLosers?: HedgeLoserBilling[] | null; context1mApplied?: boolean | null; durationMs?: number | null; - ttfbMs?: number | null; + tfftMs?: number | null; + firstByteMs?: number | null; externalOpen?: boolean; onExternalOpenChange?: (open: boolean) => void; scrollToRedirect?: boolean; @@ -94,7 +95,8 @@ export function ErrorDetailsDialog({ hedgeLosers, context1mApplied, durationMs, - ttfbMs, + tfftMs, + firstByteMs, externalOpen, onExternalOpenChange, scrollToRedirect, @@ -244,7 +246,8 @@ export function ErrorDetailsDialog({ hedgeLosers, context1mApplied, durationMs, - ttfbMs, + tfftMs, + firstByteMs, }; return ( 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 d68695b61..a5b95850e 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 @@ -72,8 +72,10 @@ export interface TabSharedProps { context1mApplied?: boolean | null; /** Total request duration in ms */ durationMs?: number | null; - /** Time to first byte in ms */ - ttfbMs?: number | null; + /** Time to first token in ms */ + tfftMs?: number | null; + /** Time to first byte in ms (null on rows persisted before it was recorded) */ + firstByteMs?: number | null; } /** @@ -130,56 +132,6 @@ export function parseBlockedReason(blockedReason: string | null | undefined): { } } -/** - * Calculate output tokens per second - */ -export function calculateOutputRate( - outputTokens: number | null | undefined, - durationMs: number | null | undefined, - ttfbMs: number | null | undefined -): number | null { - if ( - outputTokens === null || - outputTokens === undefined || - outputTokens <= 0 || - durationMs === null || - durationMs === undefined || - ttfbMs === null || - ttfbMs === undefined || - ttfbMs >= durationMs - ) { - return null; - } - const seconds = (durationMs - ttfbMs) / 1000; - if (seconds <= 0) return null; - return outputTokens / seconds; -} - -/** - * Determine if output rate should be hidden due to blocked streaming request. - * Rule: Hide when generationTimeMs / durationMs < 0.1 AND outputRate > 5000 - * This indicates TTFB is very close to total duration with abnormally high tok/s. - */ -export function shouldHideOutputRate( - outputRate: number | null, - durationMs: number | null | undefined, - ttfbMs: number | null | undefined -): boolean { - if ( - outputRate == null || - !Number.isFinite(outputRate) || - durationMs == null || - durationMs <= 0 || - ttfbMs == null - ) { - return false; - } - const generationTimeMs = durationMs - ttfbMs; - if (generationTimeMs <= 0) return false; - const ratio = generationTimeMs / durationMs; - return ratio < 0.1 && outputRate > 5000; -} - /** * Check if request is successful (2xx status) */ 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 7b9dd0f0a..301ed256a 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 @@ -79,7 +79,8 @@ function makeLog(overrides: Partial): UsageLogRow { costBreakdown: null, hedgeLosers: null, durationMs: 100, - ttfbMs: 50, + tfftMs: 50, + firstByteMs: 50, errorMessage: null, providerChain: null, blockedBy: null, @@ -414,11 +415,13 @@ describe("usage-logs-table multiplier badge", () => { test("hides tok/s when TTFB is close to duration and rate is abnormally high", () => { // Rule: generationTimeMs / durationMs < 0.1 && outputRate > 5000 => hide tok/s - // durationMs=1000, ttfbMs=950 => generationTimeMs=50, ratio=0.05 < 0.1 + // durationMs=1000, firstByteMs=950 => generationTimeMs=50, ratio=0.05 < 0.1 // outputTokens=300 => rate = 300 / 0.05 = 6000 > 5000 => should hide const html = renderToStaticMarkup( { // tok/s should NOT appear expect(html).not.toContain("tok/s"); - // TTFB should still appear - expect(html).toContain("TTFB"); + // TFFT 行仍应出现 + expect(html).toContain("logs.details.performance.tfft"); }); test("shows tok/s when conditions are normal", () => { - // durationMs=1000, ttfbMs=500 => generationTimeMs=500, ratio=0.5 >= 0.1 + // durationMs=1000, firstByteMs=500 => generationTimeMs=500, ratio=0.5 >= 0.1 // outputTokens=50 => rate = 50 / 0.5 = 100 <= 5000 => should show const html = renderToStaticMarkup( { // tok/s should appear expect(html).toContain("tok/s"); - // TTFB should also appear - expect(html).toContain("TTFB"); + // TFFT 行同样应出现 + expect(html).toContain("logs.details.performance.tfft"); }); test("renders swap indicator on cacheTtl badge when swapCacheTtlApplied is true", () => { 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 880eae98a..866e31ab8 100644 --- a/src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx +++ b/src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx @@ -584,13 +584,17 @@ export function UsageLogsTable({ const rate = calculateOutputRate( log.outputTokens, log.durationMs, - log.ttfbMs + log.firstByteMs + ); + const hideRate = shouldHideOutputRate( + rate, + log.durationMs, + log.firstByteMs ); - const hideRate = shouldHideOutputRate(rate, log.durationMs, log.ttfbMs); const secondLine = [ - log.ttfbMs != null && - log.ttfbMs > 0 && - `TTFB ${formatDuration(log.ttfbMs)}`, + log.tfftMs != null && + log.tfftMs > 0 && + `${t("logs.details.performance.tfft")} ${formatDuration(log.tfftMs)}`, rate !== null && !hideRate && `${rate.toFixed(0)} tok/s`, ] .filter(Boolean) @@ -614,10 +618,16 @@ export function UsageLogsTable({ {t("logs.details.performance.duration")}:{" "} {formatDuration(log.durationMs)}
- {log.ttfbMs != null && ( + {log.tfftMs != null && ( +
+ {t("logs.details.performance.tfft")}:{" "} + {formatDuration(log.tfftMs)} +
+ )} + {log.firstByteMs != null && (
{t("logs.details.performance.ttfb")}:{" "} - {formatDuration(log.ttfbMs)} + {formatDuration(log.firstByteMs)}
)} {rate !== null && !hideRate && ( @@ -666,7 +676,8 @@ export function UsageLogsTable({ hedgeLosers={log.hedgeLosers} context1mApplied={log.context1mApplied} durationMs={log.durationMs} - ttfbMs={log.ttfbMs} + tfftMs={log.tfftMs} + firstByteMs={log.firstByteMs} externalOpen={dialogState.logId === log.id ? true : undefined} onExternalOpenChange={(open) => { if (!open) setDialogState({ logId: null, scrollToRedirect: false }); 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 a16e6c5e4..e7f57f5b2 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 @@ -143,7 +143,8 @@ function makeLog(overrides: Partial): UsageLogRow { costBreakdown: null, hedgeLosers: null, durationMs: 100, - ttfbMs: 50, + tfftMs: 50, + firstByteMs: 50, errorMessage: null, providerChain: null, blockedBy: null, @@ -492,17 +493,19 @@ describe("virtualized-logs-table multiplier badge", () => { mockIsFetchingNextPage = false; // Rule: generationTimeMs / durationMs < 0.1 && outputRate > 5000 => hide tok/s - // durationMs=1000, ttfbMs=950 => generationTimeMs=50, ratio=0.05 < 0.1 + // durationMs=1000, firstByteMs=950 => generationTimeMs=50, ratio=0.05 < 0.1 // outputTokens=300 => rate = 300 / 0.05 = 6000 > 5000 => should hide - mockLogs = [makeLog({ id: 1, durationMs: 1000, ttfbMs: 950, outputTokens: 300 })]; + mockLogs = [ + makeLog({ id: 1, durationMs: 1000, tfftMs: 950, firstByteMs: 950, outputTokens: 300 }), + ]; const html = renderToStaticMarkup( ); // tok/s should NOT appear expect(html).not.toContain("tok/s"); - // TTFB should still appear - expect(html).toContain("TTFB"); + // TFFT 行仍应出现 + expect(html).toContain("logs.details.performance.tfft"); }); test("shows tok/s when conditions are normal", () => { @@ -512,17 +515,19 @@ describe("virtualized-logs-table multiplier badge", () => { mockHasNextPage = false; mockIsFetchingNextPage = false; - // durationMs=1000, ttfbMs=500 => generationTimeMs=500, ratio=0.5 >= 0.1 + // durationMs=1000, firstByteMs=500 => generationTimeMs=500, ratio=0.5 >= 0.1 // outputTokens=50 => rate = 50 / 0.5 = 100 <= 5000 => should show - mockLogs = [makeLog({ id: 1, durationMs: 1000, ttfbMs: 500, outputTokens: 50 })]; + mockLogs = [ + makeLog({ id: 1, durationMs: 1000, tfftMs: 500, firstByteMs: 500, outputTokens: 50 }), + ]; const html = renderToStaticMarkup( ); // tok/s should appear expect(html).toContain("tok/s"); - // TTFB should also appear - expect(html).toContain("TTFB"); + // TFFT 行同样应出现 + expect(html).toContain("logs.details.performance.tfft"); }); test("renders swap indicator on cacheTtl badge when swapCacheTtlApplied is true", () => { 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 a8235282f..e0f150534 100644 --- a/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx +++ b/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx @@ -1157,12 +1157,16 @@ export function VirtualizedLogsTable({ const rate = calculateOutputRate( log.outputTokens, log.durationMs, - log.ttfbMs + log.firstByteMs ); - const hideRate = shouldHideOutputRate(rate, log.durationMs, log.ttfbMs); - const ttfbLine = - log.ttfbMs != null && log.ttfbMs > 0 - ? `TTFB ${formatDuration(log.ttfbMs)}` + const hideRate = shouldHideOutputRate( + rate, + log.durationMs, + log.firstByteMs + ); + const tfftLine = + log.tfftMs != null && log.tfftMs > 0 + ? `${t("logs.details.performance.tfft")} ${formatDuration(log.tfftMs)}` : null; const rateLine = rate !== null && !hideRate ? `${rate.toFixed(0)} tok/s` : null; @@ -1173,9 +1177,9 @@ export function VirtualizedLogsTable({
{formatDuration(log.durationMs)} - {ttfbLine && ( + {tfftLine && ( - {ttfbLine} + {tfftLine} )} {rateLine && ( @@ -1190,10 +1194,16 @@ export function VirtualizedLogsTable({ {t("logs.details.performance.duration")}:{" "} {formatDuration(log.durationMs)}
- {log.ttfbMs != null && ( + {log.tfftMs != null && ( +
+ {t("logs.details.performance.tfft")}:{" "} + {formatDuration(log.tfftMs)} +
+ )} + {log.firstByteMs != null && (
{t("logs.details.performance.ttfb")}:{" "} - {formatDuration(log.ttfbMs)} + {formatDuration(log.firstByteMs)}
)} {rate !== null && !hideRate && ( @@ -1248,7 +1258,8 @@ export function VirtualizedLogsTable({ hedgeLosers={log.hedgeLosers} context1mApplied={log.context1mApplied} durationMs={log.durationMs} - ttfbMs={log.ttfbMs} + tfftMs={log.tfftMs} + firstByteMs={log.firstByteMs} externalOpen={dialogState.logId === log.id ? true : undefined} onExternalOpenChange={(open) => { if (!open) setDialogState({ logId: null, scrollToRedirect: false }); diff --git a/src/app/v1/_lib/proxy/forwarder.ts b/src/app/v1/_lib/proxy/forwarder.ts index f587d8ba9..7c4bc351a 100644 --- a/src/app/v1/_lib/proxy/forwarder.ts +++ b/src/app/v1/_lib/proxy/forwarder.ts @@ -333,6 +333,8 @@ type StreamingHedgeAttempt = { firstChunk: Uint8Array | null; /** F1 门控提交标记(该 attempt 门控提交时记录,随 hedge_winner 链条目落库)。 */ gateAudit?: ProviderChainItem["streamGate"]; + /** 该 attempt 首字节到达时刻(epoch ms);只有赢家的值会被记为 session TTFB。 */ + firstByteAt?: number | null; /** * 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 @@ -1703,6 +1705,9 @@ export class ProxyForwarder { }; const gateReader = response.body.getReader(); const gateStartedAt = Date.now(); + // TTFB 只在门控提交后写入 session:提交前失败的尝试不会被服务, + // 记下它的首字节会低估 TTFB 并放大 TPS 的分母。 + let gateFirstByteAt: number | null = null; const gate = await runStreamContentGate(gateReader, { family: gateFamily, providerId: currentProvider.id, @@ -1710,7 +1715,10 @@ export class ProxyForwarder { ...resolveStreamGateCaps(), // 首字节到达即清除首字节计时器,保持「首字节超时」的原始语义—— // 思考型模型可在首个内容帧前长时间输出中性帧,不应触发该计时器 - onFirstByte: () => runtime.clearResponseTimeout?.(), + onFirstByte: () => { + gateFirstByteAt ??= Date.now(); + runtime.clearResponseTimeout?.(); + }, // 门控等待期沿用供应商静默超时(与提交后 response-handler 的行为对齐) idleTimeoutMs: currentProvider.streamingIdleTimeoutMs, captureCommitMarker: !session.isHighConcurrencyModeEnabled(), @@ -1754,6 +1762,11 @@ export class ProxyForwarder { throw gate.error; } + if (gateFirstByteAt !== null) { + session.recordFirstByte(gateFirstByteAt); + } + session.recordTfft(); + if (gate.commitMarker) { gateChainAudit = { ...gate.commitMarker, @@ -4723,6 +4736,10 @@ export class ProxyForwarder { providerId: attempt.provider.id, providerName: attempt.provider.name, ...resolveStreamGateCaps(), + // 首字节时刻先挂在 attempt 上,由 commitWinner 决定是否记为 session TTFB + onFirstByte: () => { + attempt.firstByteAt ??= Date.now(); + }, // 竞速路径首字节计时器已在响应头到达时清除;门控等待期沿用供应商静默超时 idleTimeoutMs: attempt.provider.streamingIdleTimeoutMs, captureCommitMarker: !session.isHighConcurrencyModeEnabled(), @@ -4745,7 +4762,7 @@ export class ProxyForwarder { } // 保留完整门控前缀:若本 attempt 落败且需要计费,drain 时补回前缀里的 usage。 attempt.firstChunk = concatChunks(gate.prefixChunks); - await commitWinner(attempt, gate.prefixChunks); + await commitWinner(attempt, gate.prefixChunks, true); } else { const firstChunk = await ProxyForwarder.readFirstReadableChunk(attempt.reader); if (firstChunk.done) { @@ -4758,7 +4775,7 @@ export class ProxyForwarder { // 保留首块:若本 attempt 落败且需要计费,drain 时需要补回首块的 usage。 attempt.firstChunk = firstChunk.value; - await commitWinner(attempt, [firstChunk.value]); + await commitWinner(attempt, [firstChunk.value], false); } // 本 attempt 读到首块却落败(winner 已先提交,commitWinner 早退): @@ -5028,13 +5045,24 @@ export class ProxyForwarder { await finishIfExhausted(); }; - const commitWinner = async (attempt: StreamingHedgeAttempt, prefixChunks: Uint8Array[]) => { + const commitWinner = async ( + attempt: StreamingHedgeAttempt, + prefixChunks: Uint8Array[], + contentGateCommitted: boolean + ) => { if (settled || winnerCommitted || attempt.settled || !attempt.response || !attempt.reader) return; winnerCommitted = true; winnerAttempt = attempt; + if (attempt.firstByteAt != null) { + session.recordFirstByte(attempt.firstByteAt); + } + if (contentGateCommitted) { + session.recordTfft(); + } + if (attempt.thresholdTimer) { clearTimeout(attempt.thresholdTimer); attempt.thresholdTimer = null; @@ -6061,6 +6089,10 @@ export class ProxyForwarder { }) ); session.setProvider(attempt.provider); + if (attempt.firstByteAt != null) { + session.recordFirstByte(attempt.firstByteAt); + } + session.recordTfft(); if (attempt.session !== session) ProxyForwarder.syncWinningAttemptSession(session, attempt.session); @@ -6508,6 +6540,9 @@ export class ProxyForwarder { throw new EmptyResponseError(provider.id, provider.name, "empty_body"); } if (!item.value || item.value.byteLength === 0) continue; + // 首字节时刻先挂在 attempt 上;DiscoveryValidityParser 的 ready 判定同样基于内容, + // 不在此记录会让 discovery 模式的 TTFB 恒等于 TFFT。 + attempt.firstByteAt ??= Date.now(); attempt.chunks.push(item.value); const validity = attempt.parser.push(item.value); // A single read can contain both deliverable content and the diff --git a/src/app/v1/_lib/proxy/response-handler.ts b/src/app/v1/_lib/proxy/response-handler.ts index 5825ef903..9f0ae0b73 100644 --- a/src/app/v1/_lib/proxy/response-handler.ts +++ b/src/app/v1/_lib/proxy/response-handler.ts @@ -2603,7 +2603,8 @@ export class ProxyResponseHandler { details: { statusCode: finalizedStatusCode, ...errorDetails, - ttfbMs: session.ttfbMs ?? duration, + tfftMs: session.tfftMs ?? duration, + firstByteMs: session.firstByteMs ?? duration, providerChain: session.getProviderChain(), routingTrace: session.finalizeRoutingTrace(finalizedStatusCode), model: session.getCurrentModel() ?? undefined, @@ -2768,7 +2769,8 @@ export class ProxyResponseHandler { const terminalDetails: MessageRequestTerminalDetails = { statusCode: finalizedStatusCode, ...errorDetails, - ttfbMs: session.ttfbMs ?? duration, + tfftMs: session.tfftMs ?? duration, + firstByteMs: session.firstByteMs ?? duration, providerChain: session.getProviderChain(), routingTrace: session.finalizeRoutingTrace(finalizedStatusCode), model: session.getCurrentModel() ?? undefined, // 更新重定向后的模型 @@ -3078,7 +3080,8 @@ export class ProxyResponseHandler { statusCode: statusCode, inputTokens: usageMetrics?.input_tokens, outputTokens: usageMetrics?.output_tokens, - ttfbMs: session.ttfbMs ?? duration, + tfftMs: session.tfftMs ?? duration, + firstByteMs: session.firstByteMs ?? duration, cacheCreationInputTokens: usageMetrics?.cache_creation_input_tokens, cacheReadInputTokens: usageMetrics?.cache_read_input_tokens, cacheCreation5mInputTokens: usageMetrics?.cache_creation_5m_input_tokens, @@ -3524,7 +3527,7 @@ export class ProxyResponseHandler { clearIdleTimer(); if (isFirstChunk) { isFirstChunk = false; - session.recordTtfb(); + session.recordTfft(); clearResponseTimeoutOnce(value.byteLength); } streamTextAccumulator.pushBytes(value); @@ -4477,7 +4480,8 @@ export class ProxyResponseHandler { durationMs: duration, inputTokens: usageForCost?.input_tokens, outputTokens: usageForCost?.output_tokens, - ttfbMs: session.ttfbMs, + tfftMs: session.tfftMs, + firstByteMs: session.firstByteMs, cacheCreationInputTokens: usageForCost?.cache_creation_input_tokens, cacheReadInputTokens: usageForCost?.cache_read_input_tokens, cacheCreation5mInputTokens: usageForCost?.cache_creation_5m_input_tokens, @@ -4566,7 +4570,7 @@ export class ProxyResponseHandler { }); if (isFirstChunk) { - session.recordTtfb(); + session.recordTfft(); isFirstChunk = false; if (clearResponseTimeoutOnce()) { logger.debug("ResponseHandler: First chunk received, response timeout cleared", { @@ -6061,7 +6065,8 @@ export async function finalizeRequestStats( statusCode: statusCode, durationMs: duration, ...(errorMessage ? { errorMessage } : {}), - ttfbMs: session.ttfbMs ?? duration, + tfftMs: session.tfftMs ?? duration, + firstByteMs: session.firstByteMs ?? duration, providerChain: session.getProviderChain(), routingTrace: session.finalizeRoutingTrace(statusCode), model: session.getCurrentModel() ?? undefined, @@ -6174,7 +6179,8 @@ export async function finalizeRequestStats( durationMs: duration, inputTokens: normalizedUsage.input_tokens, outputTokens: normalizedUsage.output_tokens, - ttfbMs: session.ttfbMs ?? duration, + tfftMs: session.tfftMs ?? duration, + firstByteMs: session.firstByteMs ?? duration, cacheCreationInputTokens: normalizedUsage.cache_creation_input_tokens, cacheReadInputTokens: normalizedUsage.cache_read_input_tokens, cacheCreation5mInputTokens: normalizedUsage.cache_creation_5m_input_tokens, @@ -6432,7 +6438,8 @@ async function persistRequestFailure(options: { errorMessage, errorStack, errorCause, - ttfbMs: phase === "non-stream" ? (session.ttfbMs ?? duration) : session.ttfbMs, + tfftMs: phase === "non-stream" ? (session.tfftMs ?? duration) : session.tfftMs, + firstByteMs: phase === "non-stream" ? (session.firstByteMs ?? duration) : session.firstByteMs, providerChain: session.getProviderChain(), routingTrace: session.finalizeRoutingTrace(statusCode), model: session.getCurrentModel() ?? undefined, diff --git a/src/app/v1/_lib/proxy/session.ts b/src/app/v1/_lib/proxy/session.ts index 0c864af8e..46093ca29 100644 --- a/src/app/v1/_lib/proxy/session.ts +++ b/src/app/v1/_lib/proxy/session.ts @@ -142,8 +142,13 @@ export class ProxySession { provider: Provider | null; messageContext: MessageContext | null; - // Time To First Byte (ms). Streaming: first chunk. Non-stream: equals durationMs. - ttfbMs: number | null = null; + // Time To First Token (ms). Streaming: first chunk handed to the response handler, + // which under an enforcing stream gate is the first *content* frame. Non-stream: equals durationMs. + tfftMs: number | null = null; + + // Time To First Byte (ms). First body byte from the upstream, reported by the stream gate. + // Equals tfftMs whenever no gate ran (gate off/shadow, raw passthrough, non-SSE). + firstByteMs: number | null = null; // Timestamp when guard pipeline finished and forwarding started (epoch ms). forwardStartTime: number | null = null; @@ -552,22 +557,45 @@ export class ProxySession { } /** - * Record Time To First Byte (TTFB) for streaming responses. + * Record Time To First Token (TFFT) for streaming responses. + * + * Definition: first body chunk handed to the response handler. With the stream content + * gate enforcing, that chunk is the first content frame, so this is TFFT, not TTFB. + * Non-stream responses should persist TFFT as `durationMs` at finalize time. * - * Definition: first body chunk received. - * Non-stream responses should persist TTFB as `durationMs` at finalize time. + * Doubles as the TTFB fallback: paths where no gate ran never call `recordFirstByte`, + * and there TTFB and TFFT are the same moment. */ - recordTtfb(): number { - if (this.ttfbMs !== null) { - return this.ttfbMs; + recordTfft(): number { + if (this.tfftMs !== null) { + return this.tfftMs; } const value = Math.max(0, Date.now() - this.startTime); - this.ttfbMs = value; + this.tfftMs = value; + if (this.firstByteMs === null) { + this.firstByteMs = value; + } this.persistLiveChain(); return value; } + /** + * Record Time To First Byte (TTFB) from an upstream first-byte timestamp. + * + * Callers must only commit the timestamp of the attempt that actually gets served — + * committing a failed attempt's first byte would understate TTFB and inflate the + * generation window that TPS divides by. + */ + recordFirstByte(atEpochMs: number): void { + if (this.firstByteMs !== null) { + return; + } + + this.firstByteMs = Math.max(0, atEpochMs - this.startTime); + this.persistLiveChain(); + } + /** * Record the timestamp when guard pipeline finished and upstream forwarding begins. * Called once; subsequent calls are no-ops. @@ -952,7 +980,7 @@ export class ProxySession { outcome: resolvedOutcome, statusCode, durationMs: Math.max(0, now - this.routingTrace.startedAt), - ttfbMs: this.ttfbMs, + ttfbMs: this.tfftMs, }; } const terminalEvent = this.routingTrace.events.find( diff --git a/src/app/v1/_lib/proxy/warmup-guard.ts b/src/app/v1/_lib/proxy/warmup-guard.ts index 9dfb82e8f..997313ca7 100644 --- a/src/app/v1/_lib/proxy/warmup-guard.ts +++ b/src/app/v1/_lib/proxy/warmup-guard.ts @@ -80,7 +80,8 @@ export class ProxyWarmupGuard { messagesCount: session.getMessagesLength(), statusCode: 200, durationMs, - ttfbMs: durationMs, + tfftMs: durationMs, + firstByteMs: durationMs, // 不计费:显式写 NULL,避免前端误显示 “$0” costUsd: null, blockedBy: "warmup", diff --git a/src/drizzle/schema.ts b/src/drizzle/schema.ts index a4c0b6463..6ab894638 100644 --- a/src/drizzle/schema.ts +++ b/src/drizzle/schema.ts @@ -579,7 +579,11 @@ export const messageRequest = pgTable('message_request', { // Token 使用信息 inputTokens: bigint('input_tokens', { mode: 'number' }), outputTokens: bigint('output_tokens', { mode: 'number' }), - ttfbMs: integer('ttfb_ms'), + // 首 Token 时间(TFFT)。列名 ttfb_ms 是历史遗留:流式输出门禁上线后, + // 这个时间戳打在首个内容帧上,语义已是 TFFT 而非 TTFB。真 TTFB 见 firstByteMs。 + tfftMs: integer('ttfb_ms'), + // 首字节时间(TTFB):上游响应体第一个字节到达。门禁旁路时等于 tfftMs。 + firstByteMs: integer('first_byte_ms'), cacheCreationInputTokens: bigint('cache_creation_input_tokens', { mode: 'number' }), cacheReadInputTokens: bigint('cache_read_input_tokens', { mode: 'number' }), cacheCreation5mInputTokens: bigint('cache_creation_5m_input_tokens', { mode: 'number' }), @@ -1172,7 +1176,9 @@ export const usageLedger = pgTable('usage_ledger', { context1mApplied: boolean('context_1m_applied').default(false), swapCacheTtlApplied: boolean('swap_cache_ttl_applied').default(false), durationMs: integer('duration_ms'), - ttfbMs: integer('ttfb_ms'), + // 列名 ttfb_ms 存的是 TFFT,见 messageRequest.tfftMs 的说明 + tfftMs: integer('ttfb_ms'), + firstByteMs: integer('first_byte_ms'), // 客户端 IP(从 message_request 拷贝;永久保留,避免被清理任务删除) clientIp: varchar('client_ip', { length: 45 }), createdAt: timestamp('created_at', { withTimezone: true }).notNull(), diff --git a/src/lib/langfuse/emit-proxy-trace.ts b/src/lib/langfuse/emit-proxy-trace.ts index 7a0b0bc22..99d5fb508 100644 --- a/src/lib/langfuse/emit-proxy-trace.ts +++ b/src/lib/langfuse/emit-proxy-trace.ts @@ -77,7 +77,8 @@ function buildLangfuseSessionSnapshot(session: ProxySession): ProxySession { userAgent: session.userAgent, provider: session.provider, messageContext: session.messageContext, - ttfbMs: session.ttfbMs, + tfftMs: session.tfftMs, + firstByteMs: session.firstByteMs, forwardStartTime: session.forwardStartTime, forwardedRequestBody, sessionId: session.sessionId, diff --git a/src/lib/langfuse/trace-proxy-request.ts b/src/lib/langfuse/trace-proxy-request.ts index cf5d3e2d4..c69ab3441 100644 --- a/src/lib/langfuse/trace-proxy-request.ts +++ b/src/lib/langfuse/trace-proxy-request.ts @@ -190,11 +190,11 @@ export async function traceProxyRequest(ctx: TraceContext): Promise { guardPipelineMs, upstreamTotalMs: guardPipelineMs != null ? Math.max(0, durationMs - guardPipelineMs) : durationMs, - ttfbFromForwardMs: - guardPipelineMs != null && session.ttfbMs != null - ? Math.max(0, session.ttfbMs - guardPipelineMs) + tfftFromForwardMs: + guardPipelineMs != null && session.tfftMs != null + ? Math.max(0, session.tfftMs - guardPipelineMs) : null, - tokenGenerationMs: session.ttfbMs != null ? Math.max(0, durationMs - session.ttfbMs) : null, + tokenGenerationMs: session.tfftMs != null ? Math.max(0, durationMs - session.tfftMs) : null, failedAttempts: session.getProviderChain().filter((i) => !isSuccessReason(i.reason)).length, providersAttempted: new Set(session.getProviderChain().map((i) => i.id)).size, }; @@ -278,7 +278,8 @@ export async function traceProxyRequest(ctx: TraceContext): Promise { keyName: messageContext?.key?.name, // Timing durationMs, - ttfbMs: session.ttfbMs, + tfftMs: session.tfftMs, + firstByteMs: session.firstByteMs, timingBreakdown, // Flags isStreaming, @@ -433,9 +434,9 @@ export async function traceProxyRequest(ctx: TraceContext): Promise { ); // Set TTFB as completionStartTime - if (session.ttfbMs != null) { + if (session.tfftMs != null) { generation.update({ - completionStartTime: new Date(session.startTime + session.ttfbMs), + completionStartTime: new Date(session.startTime + session.tfftMs), }); } diff --git a/src/lib/ledger-backfill/service.ts b/src/lib/ledger-backfill/service.ts index c708a499a..e4a720cb1 100644 --- a/src/lib/ledger-backfill/service.ts +++ b/src/lib/ledger-backfill/service.ts @@ -91,6 +91,7 @@ export async function backfillUsageLedger( mr.swap_cache_ttl_applied, mr.duration_ms, mr.ttfb_ms, + mr.first_byte_ms, mr.created_at, ul.request_id AS existing_request_id FROM message_request mr @@ -121,7 +122,7 @@ export async function backfillUsageLedger( cache_creation_input_tokens, cache_read_input_tokens, cache_creation_5m_input_tokens, cache_creation_1h_input_tokens, cache_ttl_applied, context_1m_applied, swap_cache_ttl_applied, - duration_ms, ttfb_ms, created_at + duration_ms, ttfb_ms, first_byte_ms, created_at ) SELECT batch.id, @@ -152,6 +153,7 @@ export async function backfillUsageLedger( batch.swap_cache_ttl_applied, batch.duration_ms, batch.ttfb_ms, + batch.first_byte_ms, batch.created_at FROM batch ON CONFLICT (request_id) DO UPDATE SET diff --git a/src/lib/ledger-backfill/trigger.sql b/src/lib/ledger-backfill/trigger.sql index 06e1e2bef..7d474aaad 100644 --- a/src/lib/ledger-backfill/trigger.sql +++ b/src/lib/ledger-backfill/trigger.sql @@ -202,7 +202,7 @@ BEGIN cache_creation_input_tokens, cache_read_input_tokens, cache_creation_5m_input_tokens, cache_creation_1h_input_tokens, cache_ttl_applied, context_1m_applied, swap_cache_ttl_applied, - duration_ms, ttfb_ms, client_ip, created_at + duration_ms, ttfb_ms, first_byte_ms, client_ip, created_at ) VALUES ( NEW.id, NEW.user_id, NEW.key, NEW.provider_id, v_final_provider_id, NEW.model, NEW.original_model, NEW.actual_response_model, NEW.endpoint, NEW.api_type, NEW.session_id, @@ -212,7 +212,7 @@ BEGIN NEW.cache_creation_input_tokens, NEW.cache_read_input_tokens, NEW.cache_creation_5m_input_tokens, NEW.cache_creation_1h_input_tokens, NEW.cache_ttl_applied, NEW.context_1m_applied, NEW.swap_cache_ttl_applied, - NEW.duration_ms, NEW.ttfb_ms, NEW.client_ip, NEW.created_at + NEW.duration_ms, NEW.ttfb_ms, NEW.first_byte_ms, NEW.client_ip, NEW.created_at ) ON CONFLICT (request_id) DO UPDATE SET user_id = EXCLUDED.user_id, @@ -243,6 +243,7 @@ BEGIN swap_cache_ttl_applied = EXCLUDED.swap_cache_ttl_applied, duration_ms = EXCLUDED.duration_ms, ttfb_ms = EXCLUDED.ttfb_ms, + first_byte_ms = EXCLUDED.first_byte_ms, client_ip = EXCLUDED.client_ip; -- created_at deliberately NOT updated on conflict: it represents the -- original insert time of the ledger row, which is immutable by design. @@ -285,6 +286,7 @@ AFTER INSERT OR UPDATE OF swap_cache_ttl_applied, duration_ms, ttfb_ms, + first_byte_ms, client_ip, created_at ON message_request diff --git a/src/lib/public-status/aggregation-core.ts b/src/lib/public-status/aggregation-core.ts index a3b07d4d9..e16811863 100644 --- a/src/lib/public-status/aggregation-core.ts +++ b/src/lib/public-status/aggregation-core.ts @@ -13,10 +13,16 @@ export interface PublicStatusConfiguredGroup { }>; } +/** + * TPS = 输出 token / 生成窗口,生成窗口以真 TTFB 为起点。 + * + * firstByteMs 缺失即返回 null:流式门禁上线前的历史行只有 TFFT,用它当分母会排除 + * 上游排队/中性帧窗口,系统性高估 TPS。 + */ export function computeTokensPerSecond(input: { outputTokens?: number | null; durationMs?: number | null; - ttfbMs?: number | null; + firstByteMs?: number | null; }): number | null { if (!input.outputTokens || input.outputTokens <= 0) { return null; @@ -26,7 +32,11 @@ export function computeTokensPerSecond(input: { return null; } - const generationMs = input.durationMs - (input.ttfbMs ?? 0); + if (input.firstByteMs == null) { + return null; + } + + const generationMs = input.durationMs - input.firstByteMs; if (generationMs <= 0) { return null; } diff --git a/src/lib/public-status/aggregation.ts b/src/lib/public-status/aggregation.ts index 2ed6fdf22..b403e002d 100644 --- a/src/lib/public-status/aggregation.ts +++ b/src/lib/public-status/aggregation.ts @@ -39,7 +39,8 @@ export interface PublicStatusRequestRow { model?: string | null; originalModel?: string | null; durationMs?: number | null; - ttfbMs?: number | null; + tfftMs?: number | null; + firstByteMs?: number | null; outputTokens?: number | null; providerChain?: PublicStatusRequestChainItem[] | null; } @@ -304,7 +305,7 @@ export function buildPublicStatusPayloadFromRequests(input: { const tps = computeTokensPerSecond({ outputTokens: request.outputTokens, durationMs: request.durationMs, - ttfbMs: request.ttfbMs, + firstByteMs: request.firstByteMs, }); for (const [sourceGroupName, outcome] of groupOutcome.entries()) { @@ -323,8 +324,9 @@ export function buildPublicStatusPayloadFromRequests(input: { bucket.failureCount += 1; } - if (outcome === "success" && typeof request.ttfbMs === "number") { - bucket.ttfbValues.push(request.ttfbMs); + // ttfbValues -> bucket.ttfbMs 是对外 payload 字段,装的是 TFFT + if (outcome === "success" && typeof request.tfftMs === "number") { + bucket.ttfbValues.push(request.tfftMs); } if (outcome === "success" && typeof tps === "number") { bucket.tpsValues.push(tps); @@ -455,7 +457,8 @@ export async function queryPublicStatusRequests(input: { model: messageRequest.model, originalModel: messageRequest.originalModel, durationMs: messageRequest.durationMs, - ttfbMs: messageRequest.ttfbMs, + tfftMs: messageRequest.tfftMs, + firstByteMs: messageRequest.firstByteMs, outputTokens: messageRequest.outputTokens, statusCode: messageRequest.statusCode, errorMessage: messageRequest.errorMessage, @@ -495,7 +498,8 @@ export async function queryPublicStatusRequests(input: { model: row.model, originalModel: row.originalModel, durationMs: row.durationMs, - ttfbMs: row.ttfbMs, + tfftMs: row.tfftMs, + firstByteMs: row.firstByteMs, outputTokens: row.outputTokens, providerChain: existingChain, }, diff --git a/src/lib/public-status/rollup-store.ts b/src/lib/public-status/rollup-store.ts index 275e15edc..3026afc2c 100644 --- a/src/lib/public-status/rollup-store.ts +++ b/src/lib/public-status/rollup-store.ts @@ -39,7 +39,8 @@ export interface PublicStatusRollupEvent { model?: string | null; originalModel?: string | null; durationMs?: number | null; - ttfbMs?: number | null; + tfftMs?: number | null; + firstByteMs?: number | null; outputTokens?: number | null; providerChain?: ProviderChainItem[] | null; } @@ -339,11 +340,12 @@ export function buildPublicStatusRollupIncrements(input: { } } - const ttfbMs = normalizeNumber(input.event.ttfbMs); + // ttfb_sum / ttfb_count 是既有 rollup 键名,存的是 TFFT(改名会作废已积累的桶) + const tfftMs = normalizeNumber(input.event.tfftMs); const tps = computeTokensPerSecond({ outputTokens: input.event.outputTokens, durationMs: input.event.durationMs, - ttfbMs, + firstByteMs: normalizeNumber(input.event.firstByteMs), }); const increments: PublicStatusRollupIncrement[] = []; @@ -367,9 +369,9 @@ export function buildPublicStatusRollupIncrements(input: { metric: outcome === "success" ? "success" : "failure", value: 1, }); - if (outcome === "success" && ttfbMs !== null) { + if (outcome === "success" && tfftMs !== null) { increments.push( - { groupId, modelKey, metric: "ttfb_sum", value: ttfbMs }, + { groupId, modelKey, metric: "ttfb_sum", value: tfftMs }, { groupId, modelKey, metric: "ttfb_count", value: 1 } ); } diff --git a/src/lib/utils/performance-formatter.test.ts b/src/lib/utils/performance-formatter.test.ts new file mode 100644 index 000000000..fb13f6c4e --- /dev/null +++ b/src/lib/utils/performance-formatter.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from "vitest"; +import { calculateOutputRate, shouldHideOutputRate } from "./performance-formatter"; + +describe("calculateOutputRate", () => { + it("以真 TTFB 为生成窗口起点", () => { + // 1000ms 总耗时,TTFB 500ms => 生成窗口 0.5s,50 tokens => 100 tok/s + expect(calculateOutputRate(50, 1000, 500)).toBe(100); + }); + + it("firstByteMs 缺失返回 null,不再回退到总耗时", () => { + // 门禁上线前的历史行只有 TFFT。用总耗时兜底会把上游排队算进生成时间。 + expect(calculateOutputRate(50, 1000, null)).toBeNull(); + }); + + it("TTFB 大于 TFFT 会让 TPS 偏高,TTFB 基准才是准确值", () => { + const basedOnTfft = calculateOutputRate(50, 1000, 900); + const basedOnTtfb = calculateOutputRate(50, 1000, 200); + + expect(basedOnTfft).toBe(500); + expect(basedOnTtfb).toBe(62.5); + expect(basedOnTtfb!).toBeLessThan(basedOnTfft!); + }); + + it("生成窗口非正、无 token、无耗时都返回 null", () => { + expect(calculateOutputRate(50, 1000, 1000)).toBeNull(); + expect(calculateOutputRate(50, 1000, 1200)).toBeNull(); + expect(calculateOutputRate(0, 1000, 100)).toBeNull(); + expect(calculateOutputRate(null, 1000, 100)).toBeNull(); + expect(calculateOutputRate(50, null, 100)).toBeNull(); + expect(calculateOutputRate(50, 0, 100)).toBeNull(); + }); +}); + +describe("shouldHideOutputRate", () => { + it("生成窗口占比 <10% 且速率 >5000 时隐藏", () => { + expect(shouldHideOutputRate(6000, 1000, 950)).toBe(true); + }); + + it("占比或速率任一不满足则不隐藏", () => { + expect(shouldHideOutputRate(100, 1000, 500)).toBe(false); + expect(shouldHideOutputRate(6000, 1000, 500)).toBe(false); + expect(shouldHideOutputRate(100, 1000, 950)).toBe(false); + }); + + it("缺少速率或 firstByteMs 时不隐藏(由 calculateOutputRate 决定是否展示)", () => { + expect(shouldHideOutputRate(null, 1000, 950)).toBe(false); + expect(shouldHideOutputRate(6000, 1000, null)).toBe(false); + expect(shouldHideOutputRate(Number.POSITIVE_INFINITY, 1000, 950)).toBe(false); + }); +}); diff --git a/src/lib/utils/performance-formatter.ts b/src/lib/utils/performance-formatter.ts index c5b89c9c7..28c29a830 100644 --- a/src/lib/utils/performance-formatter.ts +++ b/src/lib/utils/performance-formatter.ts @@ -44,16 +44,20 @@ export function formatDuration(durationMs: number | null): string { /** * 计算输出速率(tokens/second) + * + * 生成窗口以真 TTFB 为起点。firstByteMs 缺失(流式门禁上线前的历史行)返回 null, + * 不再退回总耗时——那会把上游排队和中性帧窗口算进生成时间,高估速率。 */ export function calculateOutputRate( outputTokens: number | null, durationMs: number | null, - ttfbMs: number | null + firstByteMs: number | null ): number | null { if (outputTokens == null || outputTokens <= 0 || durationMs == null || durationMs <= 0) { return null; } - const generationTimeMs = ttfbMs != null ? durationMs - ttfbMs : durationMs; + if (firstByteMs == null) return null; + const generationTimeMs = durationMs - firstByteMs; if (generationTimeMs <= 0) return null; return outputTokens / (generationTimeMs / 1000); } @@ -66,18 +70,18 @@ export function calculateOutputRate( export function shouldHideOutputRate( outputRate: number | null, durationMs: number | null, - ttfbMs: number | null + firstByteMs: number | null ): boolean { if ( outputRate == null || !Number.isFinite(outputRate) || durationMs == null || durationMs <= 0 || - ttfbMs == null + firstByteMs == null ) { return false; } - const generationTimeMs = durationMs - ttfbMs; + const generationTimeMs = durationMs - firstByteMs; if (generationTimeMs <= 0) return false; const ratio = generationTimeMs / durationMs; return ratio < 0.1 && outputRate > 5000; diff --git a/src/repository/leaderboard.ts b/src/repository/leaderboard.ts index 66fa27d18..7f404b38d 100644 --- a/src/repository/leaderboard.ts +++ b/src/repository/leaderboard.ts @@ -663,17 +663,19 @@ async function findProviderLeaderboardWithTimezone( 0::double precision )`; const successRateExpr = LEDGER_SUCCESS_RATE_EXPR; - const avgTtfbMsExpr = sql`COALESCE(avg(${usageLedger.ttfbMs})::double precision, 0::double precision)`; + // 展示用的均值走 ttfb_ms 列,该列存的是 TFFT(见 schema.ts) + const avgTtfbMsExpr = sql`COALESCE(avg(${usageLedger.tfftMs})::double precision, 0::double precision)`; + // TPS 必须以真 TTFB 为基准;first_byte_ms 为 NULL 的历史行由 IS NOT NULL 排除 const avgTokensPerSecondExpr = sql`COALESCE( avg( CASE WHEN ${usageLedger.outputTokens} > 0 AND ${usageLedger.durationMs} IS NOT NULL - AND ${usageLedger.ttfbMs} IS NOT NULL - AND ${usageLedger.ttfbMs} < ${usageLedger.durationMs} - AND (${usageLedger.durationMs} - ${usageLedger.ttfbMs}) >= 100 + AND ${usageLedger.firstByteMs} IS NOT NULL + AND ${usageLedger.firstByteMs} < ${usageLedger.durationMs} + AND (${usageLedger.durationMs} - ${usageLedger.firstByteMs}) >= 100 THEN (${usageLedger.outputTokens}::double precision) - / ((${usageLedger.durationMs} - ${usageLedger.ttfbMs}) / 1000.0) + / ((${usageLedger.durationMs} - ${usageLedger.firstByteMs}) / 1000.0) END )::double precision, 0::double precision diff --git a/src/repository/message-write-buffer.ts b/src/repository/message-write-buffer.ts index e16602b5d..2f517a31b 100644 --- a/src/repository/message-write-buffer.ts +++ b/src/repository/message-write-buffer.ts @@ -17,7 +17,8 @@ export type MessageRequestUpdatePatch = { statusCode?: number; inputTokens?: number; outputTokens?: number; - ttfbMs?: number | null; + tfftMs?: number | null; + firstByteMs?: number | null; cacheCreationInputTokens?: number; cacheReadInputTokens?: number; cacheCreation5mInputTokens?: number; @@ -267,7 +268,9 @@ const COLUMN_MAP: Record = { statusCode: "status_code", inputTokens: "input_tokens", outputTokens: "output_tokens", - ttfbMs: "ttfb_ms", + // ttfb_ms 是 TFFT 的历史列名,见 schema.ts 的说明 + tfftMs: "ttfb_ms", + firstByteMs: "first_byte_ms", cacheCreationInputTokens: "cache_creation_input_tokens", cacheReadInputTokens: "cache_read_input_tokens", cacheCreation5mInputTokens: "cache_creation_5m_input_tokens", diff --git a/src/repository/message.ts b/src/repository/message.ts index 41f0acbd3..fa0627b2a 100644 --- a/src/repository/message.ts +++ b/src/repository/message.ts @@ -44,7 +44,8 @@ type PublicStatusFinalDetails = { durationMs?: number; statusCode?: number; outputTokens?: number; - ttfbMs?: number | null; + tfftMs?: number | null; + firstByteMs?: number | null; providerChain?: CreateMessageRequestData["provider_chain"]; errorMessage?: string; model?: string; @@ -190,7 +191,8 @@ function queuePublicStatusRollupForFinalDetails( model: details.model ?? seed.model, originalModel: seed.originalModel, durationMs: seed.durationMs, - ttfbMs: details.ttfbMs, + tfftMs: details.tfftMs, + firstByteMs: details.firstByteMs, outputTokens: details.outputTokens, providerChain: details.providerChain, }, @@ -493,7 +495,8 @@ export type MessageRequestDetailsUpdate = { statusCode?: number; inputTokens?: number; outputTokens?: number; - ttfbMs?: number | null; + tfftMs?: number | null; + firstByteMs?: number | null; cacheCreationInputTokens?: number; cacheReadInputTokens?: number; cacheCreation5mInputTokens?: number; @@ -553,8 +556,11 @@ export async function updateMessageRequestDetails( if (details.outputTokens !== undefined) { updateData.outputTokens = details.outputTokens; } - if (details.ttfbMs !== undefined) { - updateData.ttfbMs = details.ttfbMs; + if (details.tfftMs !== undefined) { + updateData.tfftMs = details.tfftMs; + } + if (details.firstByteMs !== undefined) { + updateData.firstByteMs = details.firstByteMs; } if (details.cacheCreationInputTokens !== undefined) { updateData.cacheCreationInputTokens = details.cacheCreationInputTokens; @@ -835,7 +841,8 @@ export async function findMessageRequestById(id: number): Promise { providers: Array.from([]), recordFailure: vi.fn(async () => {}), settleLeaseBudgets: vi.fn(async () => {}), + streamGateMode: "off", tasks: Array.from>([]), trackCost: vi.fn(async () => {}), updateMessageRequestCostWithBreakdown: vi.fn(async () => {}), @@ -99,6 +102,18 @@ vi.mock("@/lib/config", async (importOriginal) => { vi.mock("@/lib/config/system-settings-cache", () => ({ getCachedSystemSettings: async () => ({ billNonSuccessfulRequests: false }), })); +vi.mock("@/lib/system-settings/proxy-runtime", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getCachedProxyRuntimeSettings: () => ({ + affinityIgnoreClientSessionId: true, + cacheEffectivenessEnabled: true, + replayEnabled: false, + streamGateMode: state.streamGateMode, + }), + }; +}); vi.mock("@/app/v1/_lib/proxy/provider-selector", () => ({ ProxyProviderResolver: { pickDiscoveryProviders: state.pickDiscovery, @@ -367,6 +382,7 @@ type Upstream = { readonly response: Promise; readonly send: (body: string) => Promise; readonly terminated: Promise; + readonly write: (body: string) => Promise; }; async function startUpstream(): Promise { @@ -416,6 +432,18 @@ async function startUpstream(): Promise { await new Promise((resolve) => response.end(body, resolve)); }, terminated: terminationGate.promise, + write: async (body) => { + const response = await responseGate.promise; + await new Promise((resolve, reject) => { + response.write(body, (error) => { + if (error) { + reject(error); + return; + } + resolve(); + }); + }); + }, }; } @@ -424,13 +452,23 @@ async function createSession( pathname: string = "/v1/messages", signal?: AbortSignal ): Promise { + const isResponsesRequest = pathname === "/v1/responses"; const request = new Request(`https://hub.test${pathname}`, { - body: JSON.stringify({ - max_tokens: 32, - messages: [{ content: "integration", role: "user" }], - model: "claude-test", - stream: true, - }), + body: JSON.stringify( + isResponsesRequest + ? { + input: [{ content: "integration", role: "user" }], + model: "gpt-5.6-sol", + store: false, + stream: true, + } + : { + max_tokens: 32, + messages: [{ content: "integration", role: "user" }], + model: "claude-test", + stream: true, + } + ), headers: { "content-type": "application/json" }, method: "POST", ...(signal ? { signal } : {}), @@ -438,8 +476,8 @@ async function createSession( const session = await ProxySession.fromContext(new Context(request)); session.setAuthState({ apiKey: KEY.key, key: KEY, success: true, user: USER }); session.setMessageContext(MESSAGE); - session.setOriginalFormat("claude"); - session.setOriginalModel("claude-test"); + session.setOriginalFormat(isResponsesRequest ? "response" : "claude"); + session.setOriginalModel(isResponsesRequest ? "gpt-5.6-sol" : "claude-test"); session.setProvider(provider); return session; } @@ -450,6 +488,94 @@ function sse(inputTokens: number, outputTokens: number): string { })}\n\nevent: message_stop\ndata: {"type":"message_stop"}\n\n`; } +function responsesFrame(eventName: string, data: Record): string { + return `event: ${eventName}\ndata: ${JSON.stringify(data)}\n\n`; +} + +function responsesStreamFixture(responseId: string, itemId: string) { + return { + neutralPrefix: [ + responsesFrame("response.created", { + response: { id: responseId, status: "in_progress" }, + sequence_number: 0, + type: "response.created", + }), + responsesFrame("response.in_progress", { + response: { id: responseId, status: "in_progress" }, + sequence_number: 1, + type: "response.in_progress", + }), + responsesFrame("response.output_item.added", { + item: { + content: [], + id: itemId, + role: "assistant", + status: "in_progress", + type: "message", + }, + output_index: 0, + sequence_number: 2, + type: "response.output_item.added", + }), + responsesFrame("response.content_part.added", { + content_index: 0, + item_id: itemId, + output_index: 0, + part: { annotations: [], logprobs: [], text: "", type: "output_text" }, + sequence_number: 3, + type: "response.content_part.added", + }), + ], + firstContent: responsesFrame("response.output_text.delta", { + content_index: 0, + delta: "我", + item_id: itemId, + logprobs: [], + output_index: 0, + sequence_number: 5, + type: "response.output_text.delta", + }), + completed: responsesFrame("response.completed", { + response: { + id: responseId, + status: "completed", + usage: { input_tokens: 1, output_tokens: 1 }, + }, + sequence_number: 6, + type: "response.completed", + }), + } as const; +} + +function watchNeutralResponsesPrefixConsumption() { + const consumed = Promise.withResolvers(); + const decoder = new TextDecoder(); + let observed = ""; + const observe = (chunk: Uint8Array | string) => { + observed += typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true }); + if (observed.includes('"sequence_number":3')) consumed.resolve(); + }; + const originalGatePush = SseFrameParser.prototype.push; + const gateSpy = vi.spyOn(SseFrameParser.prototype, "push").mockImplementation(function (chunk) { + observe(chunk); + return originalGatePush.call(this, chunk); + }); + const originalDiscoveryPush = DiscoveryValidityParser.prototype.push; + const discoverySpy = vi + .spyOn(DiscoveryValidityParser.prototype, "push") + .mockImplementation(function (chunk) { + observe(chunk); + return originalDiscoveryPush.call(this, chunk); + }); + return { + consumed: consumed.promise, + restore: () => { + gateSpy.mockRestore(); + discoverySpy.mockRestore(); + }, + } as const; +} + async function settleTasks(): Promise { while (state.tasks.length > 0) { const settlements = await Promise.allSettled(state.tasks.splice(0, state.tasks.length)); @@ -481,6 +607,7 @@ beforeEach(async () => { state.http2Error = null; state.loserBilled = Promise.withResolvers(); state.providers.length = 0; + state.streamGateMode = "off"; state.tasks.length = 0; state.addLoserCost.mockImplementation(async () => state.loserBilled.resolve()); state.pickAlternative.mockImplementation(async (_session: unknown, excludedIds: number[]) => { @@ -499,6 +626,144 @@ afterEach(async () => { }); describe("proxy hedge transport/lifecycle integration (persistence and control-plane seams mocked)", () => { + it.each([ + { + expectedMode: "legacy_serial", + firstByteTimeoutStreamingMs: 0, + pathName: "sequential", + }, + { + expectedMode: "legacy_hedge", + firstByteTimeoutStreamingMs: 5_000, + pathName: "first-byte hedge", + }, + ])( + "records TFFT at the enforced Responses gate commit before downstream reads ($pathName path)", + async ({ expectedMode, firstByteTimeoutStreamingMs }) => { + const upstream = await startUpstream(); + const client = new AbortController(); + const now = vi.spyOn(Date, "now"); + const neutralPrefixConsumption = watchNeutralResponsesPrefixConsumption(); + try { + // Given: the real fixture's four neutral events precede its first text delta. + now.mockReturnValue(10_000); + state.streamGateMode = "enforce"; + const provider = createProvider(1, upstream.baseUrl, firstByteTimeoutStreamingMs); + provider.providerType = "codex"; + const session = await createSession(provider, "/v1/responses", client.signal); + const agents = watchAgentReleases(1); + const stream = responsesStreamFixture("resp_gate", "msg_gate"); + + const forwarded = ProxyForwarder.send(session); + await upstream.response; + now.mockReturnValue(10_050); + await upstream.write(stream.neutralPrefix.join("")); + await neutralPrefixConsumption.consumed; + expect(session.firstByteMs).toBeNull(); + expect(session.tfftMs).toBeNull(); + + // When: sequence 5 arrives, it is the first user-visible content boundary. + now.mockReturnValue(10_125); + await upstream.write(stream.firstContent); + const forwardedResponse = await forwarded; + + // Then: TTFB and TFFT remain distinct before any downstream read occurs. + expect(session.firstByteMs).toBe(50); + expect(session.tfftMs).toBe(125); + expect(session.getRoutingTrace()?.mode).toBe(expectedMode); + const firstByteMsAtCommit = session.firstByteMs; + + now.mockReturnValue(10_900); + const downstream = await ProxyResponseHandler.dispatch(session, forwardedResponse); + await upstream.send(stream.completed); + await expect(downstream.text()).resolves.toBe( + [...stream.neutralPrefix, stream.firstContent, stream.completed].join("") + ); + await settleTasks(); + await agents.released; + + expect(session.firstByteMs).toBe(firstByteMsAtCommit); + expect(session.tfftMs).toBe(125); + expect(session.getProviderChain()).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + streamGate: expect.objectContaining({ + eventName: "response.output_text.delta", + frameIndex: 5, + }), + }), + ]) + ); + expect(agents.pool.getPoolStats().activeRequests).toBe(0); + } finally { + client.abort(new Error("fixture cleanup")); + neutralPrefixConsumption.restore(); + now.mockRestore(); + await upstream.close(); + } + } + ); + + it("records TFFT when a Discovery Responses winner commits before downstream reads", async () => { + const [loser, winner] = await Promise.all([startUpstream(), startUpstream()]); + const client = new AbortController(); + const now = vi.spyOn(Date, "now"); + const neutralPrefixConsumption = watchNeutralResponsesPrefixConsumption(); + try { + // Given: Discovery has two Codex attempts and only the alternative emits the real fixture. + now.mockReturnValue(10_000); + state.discoveryEnabled = true; + const initialProvider = createProvider(1, loser.baseUrl, 0); + initialProvider.providerType = "codex"; + const winningProvider = createProvider(2, winner.baseUrl, 0); + winningProvider.priority = initialProvider.priority; + winningProvider.providerType = "codex"; + state.providers.push(winningProvider); + const session = await createSession(initialProvider, "/v1/responses", client.signal); + session.sessionId = "integration-discovery-tfft"; + const agents = watchAgentReleases(2); + const stream = responsesStreamFixture("resp_discovery", "msg_discovery"); + + const forwarded = ProxyForwarder.send(session); + await Promise.all([loser.response, winner.response]); + now.mockReturnValue(10_050); + await winner.write(stream.neutralPrefix.join("")); + await neutralPrefixConsumption.consumed; + expect(session.tfftMs).toBeNull(); + + // When: sequence 5 makes the alternative ready and Discovery commits it. + now.mockReturnValue(10_125); + await winner.write(stream.firstContent); + const forwardedResponse = await forwarded; + + // Then: TFFT is fixed at winner commit, before ResponseHandler reads the stream. + expect(session.firstByteMs).toBe(50); + expect(session.tfftMs).toBe(125); + const firstByteMsAtCommit = session.firstByteMs; + + now.mockReturnValue(10_900); + const downstream = await ProxyResponseHandler.dispatch(session, forwardedResponse); + await winner.send(stream.completed); + await expect(downstream.text()).resolves.toBe( + [...stream.neutralPrefix, stream.firstContent, stream.completed].join("") + ); + await settleTasks(); + await loser.terminated; + await agents.released; + + expect(session.firstByteMs).toBe(firstByteMsAtCommit); + expect(session.tfftMs).toBe(125); + expect(loser.abortCount()).toBe(1); + expect(winner.abortCount()).toBe(0); + expect(agents.pool.getPoolStats().activeRequests).toBe(0); + } finally { + client.abort(new Error("fixture cleanup")); + neutralPrefixConsumption.restore(); + now.mockRestore(); + await Promise.all([loser.close(), winner.close()]); + } + }); + it("runs a leased Discovery race over real loopback transports and cancels the loser", async () => { const [loser, winner] = await Promise.all([startUpstream(), startUpstream()]); const client = new AbortController(); diff --git a/tests/unit/dashboard-logs-virtualized-special-settings-ui.test.tsx b/tests/unit/dashboard-logs-virtualized-special-settings-ui.test.tsx index a46b13d8e..c67dbfb3e 100644 --- a/tests/unit/dashboard-logs-virtualized-special-settings-ui.test.tsx +++ b/tests/unit/dashboard-logs-virtualized-special-settings-ui.test.tsx @@ -50,7 +50,7 @@ vi.mock("@/actions/usage-logs", () => ({ costUsd: "0.000001", costMultiplier: null, durationMs: 10, - ttfbMs: 5, + tfftMs: 5, errorMessage: null, providerChain: null, blockedBy: null, diff --git a/tests/unit/dashboard-logs-warmup-ui.test.tsx b/tests/unit/dashboard-logs-warmup-ui.test.tsx index 19b552de0..1f78f16a3 100644 --- a/tests/unit/dashboard-logs-warmup-ui.test.tsx +++ b/tests/unit/dashboard-logs-warmup-ui.test.tsx @@ -75,7 +75,7 @@ describe("UsageLogsTable - warmup 跳过展示", () => { costUsd: null, costMultiplier: null, durationMs: 0, - ttfbMs: 0, + tfftMs: 0, errorMessage: null, providerChain: null, blockedBy: "warmup", @@ -128,7 +128,7 @@ describe("UsageLogsTable - cache badge alignment", () => { costUsd: "0.000001", costMultiplier: null, durationMs: 10, - ttfbMs: 5, + tfftMs: 5, errorMessage: null, providerChain: null, blockedBy: null, diff --git a/tests/unit/error-details-dialog-warmup-ui.test.tsx b/tests/unit/error-details-dialog-warmup-ui.test.tsx index af96b9a8c..eacb43e36 100644 --- a/tests/unit/error-details-dialog-warmup-ui.test.tsx +++ b/tests/unit/error-details-dialog-warmup-ui.test.tsx @@ -164,7 +164,7 @@ describe("ErrorDetailsDialog - warmup skip indicator", () => { costMultiplier={null} context1mApplied={false} durationMs={null} - ttfbMs={null} + tfftMs={null} externalOpen /> ); diff --git a/tests/unit/langfuse/langfuse-trace.test.ts b/tests/unit/langfuse/langfuse-trace.test.ts index 92dcea2f8..0c4bd8262 100644 --- a/tests/unit/langfuse/langfuse-trace.test.ts +++ b/tests/unit/langfuse/langfuse-trace.test.ts @@ -100,7 +100,8 @@ function createMockSession(overrides: Record = {}) { user: { id: 7, name: "testuser" }, key: { name: "default-key" }, }, - ttfbMs: 200, + tfftMs: 200, + firstByteMs: 200, forwardStartTime: startTime + 5, forwardedRequestBody: null, getEndpoint: () => "/v1/messages", @@ -463,12 +464,12 @@ describe("traceProxyRequest", () => { expect(llmCall[1].metadata.originalModel).toBe("claude-sonnet-4-20250514"); }); - test("should set completionStartTime from ttfbMs", async () => { + test("should set completionStartTime from tfftMs", async () => { const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request"); const startTime = Date.now() - 500; await traceProxyRequest({ - session: createMockSession({ startTime, ttfbMs: 200 }), + session: createMockSession({ startTime, tfftMs: 200 }), responseHeaders: new Headers(), durationMs: 500, statusCode: 200, @@ -889,7 +890,8 @@ describe("traceProxyRequest", () => { session: createMockSession({ startTime, forwardStartTime, - ttfbMs: 105, + tfftMs: 105, + firstByteMs: 105, getProviderChain: () => [ { id: 1, name: "p1", reason: "retry_failed", timestamp: startTime + 50 }, { id: 2, name: "p2", reason: "request_success", timestamp: startTime + 100 }, @@ -904,8 +906,8 @@ describe("traceProxyRequest", () => { const expectedTimingBreakdown = { guardPipelineMs: 5, upstreamTotalMs: 495, - ttfbFromForwardMs: 100, // ttfbMs(105) - guardPipelineMs(5) - tokenGenerationMs: 395, // durationMs(500) - ttfbMs(105) + tfftFromForwardMs: 100, // tfftMs(105) - guardPipelineMs(5) + tokenGenerationMs: 395, // durationMs(500) - tfftMs(105) failedAttempts: 1, // only retry_failed is non-success providersAttempted: 2, // 2 unique provider ids }; diff --git a/tests/unit/proxy/response-handler-abort-listener-cleanup.test.ts b/tests/unit/proxy/response-handler-abort-listener-cleanup.test.ts index 02d7446d1..69ecff895 100644 --- a/tests/unit/proxy/response-handler-abort-listener-cleanup.test.ts +++ b/tests/unit/proxy/response-handler-abort-listener-cleanup.test.ts @@ -211,8 +211,9 @@ function makeSession(clientAbortSignal: AbortSignal | null, stream: boolean): Pr shouldPersistSessionDebugArtifacts: () => false, shouldTrackSessionObservability: () => false, getResolvedPricingByBillingSource: async () => null, - recordTtfb: vi.fn(), - ttfbMs: null, + recordTfft: vi.fn(), + tfftMs: null, + firstByteMs: null, addProviderToChain: vi.fn(), clearResponseTimeout: vi.fn(), releaseAgent: vi.fn(), diff --git a/tests/unit/proxy/response-handler-client-abort-drain.test.ts b/tests/unit/proxy/response-handler-client-abort-drain.test.ts index 9d0b093ce..447864632 100644 --- a/tests/unit/proxy/response-handler-client-abort-drain.test.ts +++ b/tests/unit/proxy/response-handler-client-abort-drain.test.ts @@ -296,7 +296,8 @@ function createSession( sessionId: null, specialSettings: [], startTime: Date.now(), - ttfbMs: null, + tfftMs: null, + firstByteMs: null, userAgent: "Go-http-client/1.1", userName: "admin", addProviderToChain(this: ProxySession & { providerChain: unknown[] }, prov: Provider, meta) { @@ -317,7 +318,7 @@ function createSession( getResolvedPricingByBillingSource: async () => null, getSpecialSettings: () => [], isHeaderModified: () => false, - recordTtfb: vi.fn(), + recordTfft: vi.fn(), releaseAgent: vi.fn(), setContext1mApplied: vi.fn(), shouldPersistSessionDebugArtifacts: () => false, @@ -1422,7 +1423,7 @@ describe("ProxyResponseHandler stream client abort finalization", () => { await downstream.text(); await drainAsyncTasks(); - expect(session.recordTtfb).not.toHaveBeenCalled(); + expect(session.recordTfft).not.toHaveBeenCalled(); expect(session.clearResponseTimeout).toHaveBeenCalledTimes(1); }); diff --git a/tests/unit/proxy/response-handler-endpoint-circuit-isolation.test.ts b/tests/unit/proxy/response-handler-endpoint-circuit-isolation.test.ts index e85277206..169ecfdb9 100644 --- a/tests/unit/proxy/response-handler-endpoint-circuit-isolation.test.ts +++ b/tests/unit/proxy/response-handler-endpoint-circuit-isolation.test.ts @@ -232,8 +232,9 @@ function createSession(opts?: { sessionId?: string | null }): ProxySession { getCurrentModel: () => "test-model", getProviderChain: () => session.providerChain, getCachedPriceDataByBillingSource: async () => testPriceData, - recordTtfb: () => 100, - ttfbMs: null, + recordTfft: () => 100, + tfftMs: null, + firstByteMs: null, getRequestSequence: () => 1, addProviderToChain: function ( this: ProxySession & { providerChain: Record[] }, diff --git a/tests/unit/proxy/response-handler-lease-decrement.test.ts b/tests/unit/proxy/response-handler-lease-decrement.test.ts index dd76fdc05..9d542e8c4 100644 --- a/tests/unit/proxy/response-handler-lease-decrement.test.ts +++ b/tests/unit/proxy/response-handler-lease-decrement.test.ts @@ -210,8 +210,9 @@ function createSession(opts: { source: "cloud_exact" as const, priceData: testPriceData, }), - recordTtfb: () => 100, - ttfbMs: null, + recordTfft: () => 100, + tfftMs: null, + firstByteMs: null, getRequestSequence: () => 1, }); diff --git a/tests/unit/proxy/response-handler-non200.test.ts b/tests/unit/proxy/response-handler-non200.test.ts index 6a665d599..d7160ee19 100644 --- a/tests/unit/proxy/response-handler-non200.test.ts +++ b/tests/unit/proxy/response-handler-non200.test.ts @@ -194,8 +194,9 @@ function createSession(opts: { getCurrentModel: () => redirectedModel, getProviderChain: () => session.providerChain, getCachedPriceDataByBillingSource: async () => testPriceData, - recordTtfb: () => 100, - ttfbMs: null, + recordTfft: () => 100, + tfftMs: null, + firstByteMs: null, getRequestSequence: () => 1, addProviderToChain: function ( prov: Provider, diff --git a/tests/unit/proxy/session-ttfb-tfft.test.ts b/tests/unit/proxy/session-ttfb-tfft.test.ts new file mode 100644 index 000000000..798557ec0 --- /dev/null +++ b/tests/unit/proxy/session-ttfb-tfft.test.ts @@ -0,0 +1,92 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("@/repository/model-price", () => ({ + findLatestPriceByModel: vi.fn(), +})); + +vi.mock("@/repository/system-config", () => ({ + getSystemSettings: vi.fn(), +})); + +import { ProxySession } from "@/app/v1/_lib/proxy/session"; + +function createSession(startTime: number): ProxySession { + return new ( + ProxySession as unknown as { + new (init: { + startTime: number; + method: string; + requestUrl: URL; + headers: Headers; + headerLog: string; + request: { message: Record; log: string; model: string | null }; + userAgent: string | null; + context: unknown; + clientAbortSignal: AbortSignal | null; + }): ProxySession; + } + )({ + startTime, + method: "POST", + requestUrl: new URL("http://localhost/v1/messages"), + headers: new Headers(), + headerLog: "", + request: { message: {}, log: "(test)", model: null }, + userAgent: null, + context: {}, + clientAbortSignal: null, + }); +} + +describe("ProxySession TTFB / TFFT", () => { + it("门控旁路时 recordTfft 同时补齐 TTFB(两者同一时刻)", () => { + const session = createSession(Date.now() - 1_200); + + const tfft = session.recordTfft(); + + expect(session.tfftMs).toBe(tfft); + expect(session.firstByteMs).toBe(tfft); + }); + + it("门控提交时先记 TTFB,recordTfft 不覆盖它", () => { + const startTime = Date.now() - 3_000; + const session = createSession(startTime); + + session.recordFirstByte(startTime + 400); + const tfft = session.recordTfft(); + + expect(session.firstByteMs).toBe(400); + expect(session.tfftMs).toBe(tfft); + // TTFB 必须早于 TFFT,否则延迟分解与 TPS 分母都会失真 + expect(session.firstByteMs!).toBeLessThan(session.tfftMs!); + }); + + it("recordFirstByte 首写生效:failover 后不会被后续尝试改写", () => { + const startTime = Date.now() - 5_000; + const session = createSession(startTime); + + session.recordFirstByte(startTime + 900); + session.recordFirstByte(startTime + 2_500); + + expect(session.firstByteMs).toBe(900); + }); + + it("recordFirstByte 对早于 startTime 的时刻钳到 0", () => { + const startTime = Date.now(); + const session = createSession(startTime); + + session.recordFirstByte(startTime - 50); + + expect(session.firstByteMs).toBe(0); + }); + + it("recordTfft 幂等:重复调用不改变已记录的值", () => { + const session = createSession(Date.now() - 800); + + const first = session.recordTfft(); + const second = session.recordTfft(); + + expect(second).toBe(first); + expect(session.firstByteMs).toBe(first); + }); +}); diff --git a/tests/unit/public-status/aggregation-core-tps.test.ts b/tests/unit/public-status/aggregation-core-tps.test.ts new file mode 100644 index 000000000..b2e2f2f23 --- /dev/null +++ b/tests/unit/public-status/aggregation-core-tps.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from "vitest"; +import { computeTokensPerSecond } from "@/lib/public-status/aggregation-core"; + +describe("computeTokensPerSecond", () => { + it("以真 TTFB 为生成窗口起点", () => { + expect(computeTokensPerSecond({ outputTokens: 50, durationMs: 1000, firstByteMs: 500 })).toBe( + 100 + ); + }); + + it("firstByteMs 缺失返回 null(门禁上线前的历史行不参与 TPS)", () => { + expect( + computeTokensPerSecond({ outputTokens: 50, durationMs: 1000, firstByteMs: null }) + ).toBeNull(); + expect(computeTokensPerSecond({ outputTokens: 50, durationMs: 1000 })).toBeNull(); + }); + + it("TTFB 基准得到的 TPS 低于(被门控放大的)TFFT 基准", () => { + const basedOnTfft = computeTokensPerSecond({ + outputTokens: 50, + durationMs: 1000, + firstByteMs: 900, + }); + const basedOnTtfb = computeTokensPerSecond({ + outputTokens: 50, + durationMs: 1000, + firstByteMs: 200, + }); + + expect(basedOnTfft).toBe(500); + expect(basedOnTtfb).toBe(62.5); + }); + + it("生成窗口非正、无 token、无耗时都返回 null", () => { + expect( + computeTokensPerSecond({ outputTokens: 50, durationMs: 1000, firstByteMs: 1000 }) + ).toBeNull(); + expect( + computeTokensPerSecond({ outputTokens: 0, durationMs: 1000, firstByteMs: 100 }) + ).toBeNull(); + expect( + computeTokensPerSecond({ outputTokens: 50, durationMs: null, firstByteMs: 100 }) + ).toBeNull(); + }); +}); diff --git a/tests/unit/public-status/aggregation.test.ts b/tests/unit/public-status/aggregation.test.ts index ff10ffa4e..7d19ed7e4 100644 --- a/tests/unit/public-status/aggregation.test.ts +++ b/tests/unit/public-status/aggregation.test.ts @@ -34,7 +34,8 @@ describe("public-status aggregation", () => { createdAt: "2026-04-21T10:10:00.000Z", originalModel: "gpt-4.1", durationMs: 1000, - ttfbMs: 200, + tfftMs: 200, + firstByteMs: 200, outputTokens: 80, providerChain: [ { @@ -51,7 +52,8 @@ describe("public-status aggregation", () => { createdAt: "2026-04-21T10:40:00.000Z", originalModel: "gpt-4.1", durationMs: 1400, - ttfbMs: 300, + tfftMs: 300, + firstByteMs: 300, outputTokens: 60, providerChain: [ { @@ -101,7 +103,8 @@ describe("public-status aggregation", () => { createdAt: "2026-04-21T10:25:00.000Z", originalModel: "gpt-4.1", durationMs: 1500, - ttfbMs: 500, + tfftMs: 500, + firstByteMs: 500, outputTokens: null, providerChain: [ { @@ -226,7 +229,8 @@ describe("public-status aggregation", () => { createdAt: "2026-04-21T10:10:00.000Z", originalModel: "gpt-4.1", durationMs: 1200, - ttfbMs: 200, + tfftMs: 200, + firstByteMs: 200, outputTokens: 50, providerChain: [ { diff --git a/tests/unit/public-status/rollup-store.test.ts b/tests/unit/public-status/rollup-store.test.ts index 44c9f7a05..466f9207d 100644 --- a/tests/unit/public-status/rollup-store.test.ts +++ b/tests/unit/public-status/rollup-store.test.ts @@ -42,7 +42,8 @@ describe("public-status rollup store", () => { createdAt: "2026-04-21T10:02:00.000Z", originalModel: "gpt-4.1", durationMs: 1200, - ttfbMs: 200, + tfftMs: 200, + firstByteMs: 200, outputTokens: 50, providerChain: [ { @@ -149,7 +150,8 @@ describe("public-status rollup store", () => { createdAt: "2026-04-21T10:02:00.000Z", originalModel: "gpt-4.1", durationMs: 1200, - ttfbMs: 200, + tfftMs: 200, + firstByteMs: 200, outputTokens: 50, providerChain: [ { @@ -225,7 +227,8 @@ describe("public-status rollup store", () => { createdAt: "2026-04-21T10:02:00.000Z", originalModel: "gpt-4.1", durationMs: 1200, - ttfbMs: 200, + tfftMs: 200, + firstByteMs: 200, outputTokens: 50, providerChain: [ { @@ -309,7 +312,8 @@ describe("public-status rollup store", () => { createdAt: "2026-04-21T10:02:00.000Z", originalModel: "gpt-4.1", durationMs: 1200, - ttfbMs: 200, + tfftMs: 200, + firstByteMs: 200, outputTokens: 50, providerChain: [ { @@ -346,7 +350,8 @@ describe("public-status rollup store", () => { createdAt: "2026-04-21T10:02:00.000Z", originalModel: "gpt-4.1", durationMs: 1200, - ttfbMs: 200, + tfftMs: 200, + firstByteMs: 200, outputTokens: 50, providerChain: [ { diff --git a/tests/unit/repository/leaderboard-provider-metrics.test.ts b/tests/unit/repository/leaderboard-provider-metrics.test.ts index 102dbf0f2..ac85f9ae8 100644 --- a/tests/unit/repository/leaderboard-provider-metrics.test.ts +++ b/tests/unit/repository/leaderboard-provider-metrics.test.ts @@ -53,7 +53,8 @@ vi.mock("@/drizzle/schema", () => ({ successRateOutcome: "successRateOutcome", blockedBy: "blockedBy", createdAt: "createdAt", - ttfbMs: "ttfbMs", + tfftMs: "tfftMs", + firstByteMs: "firstByteMs", durationMs: "durationMs", model: "model", originalModel: "originalModel", @@ -70,7 +71,8 @@ vi.mock("@/drizzle/schema", () => ({ errorMessage: "errorMessage", blockedBy: "blockedBy", createdAt: "createdAt", - ttfbMs: "ttfbMs", + tfftMs: "tfftMs", + firstByteMs: "firstByteMs", durationMs: "durationMs", model: "model", originalModel: "originalModel", diff --git a/tests/unit/repository/leaderboard-timezone-parentheses.test.ts b/tests/unit/repository/leaderboard-timezone-parentheses.test.ts index 8eafff952..7cd5195ec 100644 --- a/tests/unit/repository/leaderboard-timezone-parentheses.test.ts +++ b/tests/unit/repository/leaderboard-timezone-parentheses.test.ts @@ -87,7 +87,8 @@ vi.mock("@/drizzle/schema", () => ({ cacheReadInputTokens: "cacheReadInputTokens", blockedBy: "blockedBy", createdAt: "createdAt", - ttfbMs: "ttfbMs", + tfftMs: "tfftMs", + firstByteMs: "firstByteMs", durationMs: "durationMs", statusCode: "statusCode", isSuccess: "isSuccess", @@ -107,7 +108,8 @@ vi.mock("@/drizzle/schema", () => ({ errorMessage: "errorMessage", blockedBy: "blockedBy", createdAt: "createdAt", - ttfbMs: "ttfbMs", + tfftMs: "tfftMs", + firstByteMs: "firstByteMs", durationMs: "durationMs", statusCode: "statusCode", model: "model", diff --git a/tests/unit/repository/leaderboard-tps-basis.test.ts b/tests/unit/repository/leaderboard-tps-basis.test.ts new file mode 100644 index 000000000..9b2f0167a --- /dev/null +++ b/tests/unit/repository/leaderboard-tps-basis.test.ts @@ -0,0 +1,100 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +/** + * 排行榜的两个延迟指标口径不同,必须分开: + * - 展示用的 avgTtfbMs 走 usage_ledger.ttfb_ms(该列存的是 TFFT) + * - avgTokensPerSecond 的分母必须是真 TTFB(first_byte_ms),历史行由 IS NOT NULL 排除 + */ + +const createChainMock = (resolvedData: unknown[]) => ({ + from: vi.fn().mockReturnThis(), + innerJoin: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + groupBy: vi.fn().mockReturnThis(), + orderBy: vi.fn().mockResolvedValue(resolvedData), +}); + +let selectedProjections: unknown[] = []; +const mockSelect = vi.fn((projection: unknown) => { + selectedProjections.push(projection); + return createChainMock([]); +}); + +const mocks = vi.hoisted(() => ({ + resolveSystemTimezone: vi.fn(), + getSystemSettings: vi.fn(), + getProviderCacheCoefficients: vi.fn(), +})); + +vi.mock("@/drizzle/db", () => ({ + db: { + select: (...args: unknown[]) => mockSelect(args[0]), + }, +})); + +vi.mock("@/drizzle/schema", () => ({ + usageLedger: { + providerId: "providerId", + finalProviderId: "finalProviderId", + userId: "userId", + costUsd: "costUsd", + inputTokens: "inputTokens", + outputTokens: "outputTokens", + cacheCreationInputTokens: "cacheCreationInputTokens", + cacheReadInputTokens: "cacheReadInputTokens", + isSuccess: "isSuccess", + successRateOutcome: "successRateOutcome", + blockedBy: "blockedBy", + createdAt: "createdAt", + tfftMs: "tfftMs", + firstByteMs: "firstByteMs", + durationMs: "durationMs", + model: "model", + originalModel: "originalModel", + }, + providers: { id: "id", name: "name" }, + users: { id: "id", name: "name" }, + messageRequest: {}, +})); + +vi.mock("@/lib/utils/timezone", () => ({ + resolveSystemTimezone: mocks.resolveSystemTimezone, +})); + +vi.mock("@/repository/system-config", () => ({ + getSystemSettings: mocks.getSystemSettings, +})); + +vi.mock("@/repository/provider-cache-effectiveness", () => ({ + getProviderCacheCoefficients: mocks.getProviderCacheCoefficients, + resolveLeaderboardWindow: () => ({ start: new Date(0), end: new Date() }), +})); + +beforeEach(() => { + vi.clearAllMocks(); + selectedProjections = []; + mocks.resolveSystemTimezone.mockResolvedValue("UTC"); + mocks.getSystemSettings.mockResolvedValue({ timezone: "UTC" }); + mocks.getProviderCacheCoefficients.mockResolvedValue(new Map()); +}); + +describe("排行榜延迟指标口径", () => { + it("TPS 分母用 first_byte_ms,展示均值仍用 ttfb_ms 列", async () => { + const { findDailyProviderLeaderboard } = await import("@/repository/leaderboard"); + await findDailyProviderLeaderboard(); + + const projection = selectedProjections.find( + (item): item is Record => + typeof item === "object" && item !== null && "avgTokensPerSecond" in item + ); + expect(projection).toBeDefined(); + + const tpsSql = JSON.stringify(projection?.avgTokensPerSecond); + expect(tpsSql).toContain("firstByteMs"); + expect(tpsSql).not.toContain("tfftMs"); + + const avgLatencySql = JSON.stringify(projection?.avgTtfbMs); + expect(avgLatencySql).toContain("tfftMs"); + expect(avgLatencySql).not.toContain("firstByteMs"); + }); +}); diff --git a/tests/unit/repository/leaderboard-user-model-stats.test.ts b/tests/unit/repository/leaderboard-user-model-stats.test.ts index 9049cd32c..6e75b17e1 100644 --- a/tests/unit/repository/leaderboard-user-model-stats.test.ts +++ b/tests/unit/repository/leaderboard-user-model-stats.test.ts @@ -83,7 +83,8 @@ vi.mock("@/drizzle/schema", () => ({ successRateOutcome: "successRateOutcome", blockedBy: "blockedBy", createdAt: "createdAt", - ttfbMs: "ttfbMs", + tfftMs: "tfftMs", + firstByteMs: "firstByteMs", durationMs: "durationMs", model: "model", originalModel: "originalModel", @@ -100,7 +101,8 @@ vi.mock("@/drizzle/schema", () => ({ errorMessage: "errorMessage", blockedBy: "blockedBy", createdAt: "createdAt", - ttfbMs: "ttfbMs", + tfftMs: "tfftMs", + firstByteMs: "firstByteMs", durationMs: "durationMs", model: "model", originalModel: "originalModel", diff --git a/tests/unit/repository/message-public-readback.test.ts b/tests/unit/repository/message-public-readback.test.ts index 8b2e49ea9..47b119bf6 100644 --- a/tests/unit/repository/message-public-readback.test.ts +++ b/tests/unit/repository/message-public-readback.test.ts @@ -61,7 +61,7 @@ const MESSAGE_ROW = { ...LATEST_ROW, model: "gpt-4.1", originalModel: "gpt-4.1-mini", - ttfbMs: 120, + tfftMs: 120, costMultiplier: "1.5", sessionId: "public-session", userAgent: "vitest", @@ -105,7 +105,7 @@ const LEDGER_ROW = { context1mApplied: false, swapCacheTtlApplied: true, durationMs: 1_500, - ttfbMs: 250, + tfftMs: 250, sessionId: "ledger-session", createdAt: CREATED_AT, }; diff --git a/tests/unit/repository/message-public-status-rollup.test.ts b/tests/unit/repository/message-public-status-rollup.test.ts index 864a1af78..70c96b18b 100644 --- a/tests/unit/repository/message-public-status-rollup.test.ts +++ b/tests/unit/repository/message-public-status-rollup.test.ts @@ -440,7 +440,7 @@ describe("repository/message public status rollup hook", () => { const finalDetails = { statusCode: 200, - ttfbMs: 200, + tfftMs: 200, outputTokens: 50, providerChain: [ { @@ -469,7 +469,7 @@ describe("repository/message public status rollup hook", () => { originalModel: "gpt-4.1", model: "gpt-4.1", outputTokens: 50, - ttfbMs: 200, + tfftMs: 200, }), }) ); @@ -489,7 +489,7 @@ describe("repository/message public status rollup hook", () => { await updateMessageRequestDetails(202, { statusCode: 200, - ttfbMs: 250, + tfftMs: 250, outputTokens: 75, providerChain: [ { @@ -513,7 +513,7 @@ describe("repository/message public status rollup hook", () => { originalModel: "gpt-4.1", model: "gpt-4.1", outputTokens: 75, - ttfbMs: 250, + tfftMs: 250, }), }) ); @@ -577,7 +577,7 @@ describe("repository/message public status rollup hook", () => { const finalDetails = { statusCode: 200, - ttfbMs: 300, + tfftMs: 300, outputTokens: 90, providerChain: [ { @@ -604,7 +604,7 @@ describe("repository/message public status rollup hook", () => { createdAt: new Date("2026-04-21T10:04:00.000Z"), durationMs: 1800, outputTokens: 90, - ttfbMs: 300, + tfftMs: 300, }), }) ); @@ -661,7 +661,7 @@ describe("repository/message public status rollup hook", () => { const finalDetails = { statusCode: 200, - ttfbMs: 300, + tfftMs: 300, outputTokens: 90, providerChain: [ { @@ -750,7 +750,7 @@ describe("repository/message public status rollup hook", () => { const finalDetails = { statusCode: 200, - ttfbMs: 320, + tfftMs: 320, outputTokens: 95, providerChain: [ { @@ -776,7 +776,7 @@ describe("repository/message public status rollup hook", () => { createdAt: new Date("2026-04-21T10:06:00.000Z"), durationMs: 1900, outputTokens: 95, - ttfbMs: 320, + tfftMs: 320, }), }) ); diff --git a/tests/unit/repository/message-session-readback.test.ts b/tests/unit/repository/message-session-readback.test.ts index 215915026..895a85363 100644 --- a/tests/unit/repository/message-session-readback.test.ts +++ b/tests/unit/repository/message-session-readback.test.ts @@ -101,7 +101,7 @@ const LEDGER_ROW = { context1mApplied: true, swapCacheTtlApplied: false, durationMs: 1_200, - ttfbMs: 200, + tfftMs: 200, sessionId: "ledger-session-readback", createdAt: CREATED_AT, }; diff --git a/tests/unit/repository/message-terminal-public-status-seam.test.ts b/tests/unit/repository/message-terminal-public-status-seam.test.ts index e684da8ef..33748d342 100644 --- a/tests/unit/repository/message-terminal-public-status-seam.test.ts +++ b/tests/unit/repository/message-terminal-public-status-seam.test.ts @@ -303,7 +303,7 @@ describe("message terminal public-status public seam", () => { statusCode: 502, inputTokens: 31, outputTokens: 3, - ttfbMs: 900, + tfftMs: 900, providerChain: [ { id: 11, @@ -321,7 +321,7 @@ describe("message terminal public-status public seam", () => { durationMs: 1_500, statusCode: 200, outputTokens: 96, - ttfbMs: 300, + tfftMs: 300, providerChain: [ { id: 22, @@ -337,7 +337,7 @@ describe("message terminal public-status public seam", () => { const row: TerminalRow & { inputTokens: number | null; outputTokens: number | null; - ttfbMs: number | null; + tfftMs: number | null; providerChain: unknown; providerId: number | null; } = { @@ -349,7 +349,7 @@ describe("message terminal public-status public seam", () => { statusCode: null, inputTokens: null, outputTokens: null, - ttfbMs: null, + tfftMs: null, providerChain: null, providerId: null, }; @@ -385,7 +385,7 @@ describe("message terminal public-status public seam", () => { row.statusCode = Number(readCaseValue("status_code")); row.inputTokens = Number(readCaseValue("input_tokens")); row.outputTokens = Number(readCaseValue("output_tokens")); - row.ttfbMs = Number(readCaseValue("ttfb_ms")); + row.tfftMs = Number(readCaseValue("ttfb_ms")); row.providerChain = JSON.parse(String(readCaseValue("provider_chain"))); row.providerId = Number(readCaseValue("provider_id")); return [{ id }]; @@ -525,7 +525,7 @@ describe("message terminal public-status public seam", () => { statusCode: oldFailureDetails.statusCode, inputTokens: oldFailureDetails.inputTokens, outputTokens: oldFailureDetails.outputTokens, - ttfbMs: oldFailureDetails.ttfbMs, + tfftMs: oldFailureDetails.tfftMs, providerChain: oldFailureDetails.providerChain, providerId: oldFailureDetails.providerId, }); diff --git a/tests/unit/repository/message-terminal-write-apis.test.ts b/tests/unit/repository/message-terminal-write-apis.test.ts index 970b61226..6dfd4bc27 100644 --- a/tests/unit/repository/message-terminal-write-apis.test.ts +++ b/tests/unit/repository/message-terminal-write-apis.test.ts @@ -245,7 +245,7 @@ describe("message terminal write APIs", () => { const details = { inputTokens: 101, outputTokens: 23, - ttfbMs: null, + tfftMs: null, cacheCreationInputTokens: 7, cacheReadInputTokens: 8, cacheCreation5mInputTokens: 3, diff --git a/tests/unit/repository/message-usage-logs-query.test.ts b/tests/unit/repository/message-usage-logs-query.test.ts index 1cc44667d..fe945e5a4 100644 --- a/tests/unit/repository/message-usage-logs-query.test.ts +++ b/tests/unit/repository/message-usage-logs-query.test.ts @@ -154,7 +154,7 @@ describe("message repository findUsageLogs", () => { context1mApplied: true, swapCacheTtlApplied: false, durationMs: 250, - ttfbMs: 40, + tfftMs: 40, sessionId: "session-ledger", createdAt, }, diff --git a/tests/unit/repository/message-write-buffer.test.ts b/tests/unit/repository/message-write-buffer.test.ts index 3213cfdda..a3c0fefb8 100644 --- a/tests/unit/repository/message-write-buffer.test.ts +++ b/tests/unit/repository/message-write-buffer.test.ts @@ -154,7 +154,7 @@ describe("message_request 异步批量写入", () => { } = await import("@/repository/message-write-buffer"); enqueueMessageRequestUpdate(42, { durationMs: 100 }); - enqueueMessageRequestUpdate(42, { statusCode: 200, ttfbMs: 10 }); + enqueueMessageRequestUpdate(42, { statusCode: 200, tfftMs: 10 }); await flushMessageRequestWriteBuffer(); await stopMessageRequestWriteBuffer(); diff --git a/tests/unit/repository/usage-logs-actual-response-model.test.ts b/tests/unit/repository/usage-logs-actual-response-model.test.ts index 68700b987..b057d226b 100644 --- a/tests/unit/repository/usage-logs-actual-response-model.test.ts +++ b/tests/unit/repository/usage-logs-actual-response-model.test.ts @@ -50,7 +50,7 @@ describe("findUsageLogsBatch: actualResponseModel propagation", () => { groupCostMultiplier: null, costBreakdown: null, durationMs: 500, - ttfbMs: 100, + tfftMs: 100, errorMessage: null, providerChain: null, blockedBy: null, @@ -118,7 +118,7 @@ describe("findUsageLogsBatch: actualResponseModel propagation", () => { costMultiplier: null, groupCostMultiplier: null, durationMs: 400, - ttfbMs: 80, + tfftMs: 80, clientIp: null, context1mApplied: false, swapCacheTtlApplied: false, @@ -178,7 +178,7 @@ describe("findUsageLogsBatch: actualResponseModel propagation", () => { costMultiplier: null, groupCostMultiplier: null, durationMs: 0, - ttfbMs: 0, + tfftMs: 0, clientIp: null, context1mApplied: false, swapCacheTtlApplied: false, diff --git a/tests/unit/repository/usage-logs-sessionid-filter.test.ts b/tests/unit/repository/usage-logs-sessionid-filter.test.ts index ab3ae66e8..3ace29fc2 100644 --- a/tests/unit/repository/usage-logs-sessionid-filter.test.ts +++ b/tests/unit/repository/usage-logs-sessionid-filter.test.ts @@ -137,7 +137,7 @@ describe("Usage logs sessionId filter", () => { costUsd: "0.01", costMultiplier: null, durationMs: 10, - ttfbMs: 5, + tfftMs: 5, errorMessage: null, providerChain: null, blockedBy: null, @@ -171,7 +171,7 @@ describe("Usage logs sessionId filter", () => { costUsd: "0.01", costMultiplier: null, durationMs: 10, - ttfbMs: 5, + tfftMs: 5, errorMessage: null, providerChain: null, blockedBy: null,