diff --git a/config.py b/config.py index 77b5639ba..1c523f2d9 100644 --- a/config.py +++ b/config.py @@ -46,11 +46,20 @@ "PASSWORD": "password", "KEEPALIVE_URL": "keepalive_url", "KEEPALIVE_INTERVAL": "keepalive_interval", + "FF_RETRY_POLICY_V2": "ff_retry_policy_v2", + "FF_HTTP2_POOL_TUNING": "ff_http2_pool_tuning", + "FF_CONVERTER_FAST_PATH": "ff_converter_fast_path", + "FF_PREVIEW_CREDENTIAL_SCHEDULER_V2": "ff_preview_credential_scheduler_v2", + "ROLLOUT_STAGE_PERCENT": "rollout_stage_percent", + "ROLLBACK_TRIGGER_LATENCY_P95_MS": "rollback_trigger_latency_p95_ms", + "ROLLBACK_TRIGGER_THROUGHPUT_DROP_PCT": "rollback_trigger_throughput_drop_pct", + "ROLLBACK_TRIGGER_QUALITY_DROP_PCT": "rollback_trigger_quality_drop_pct", } # ====================== 配置系统 ====================== + async def init_config(): """初始化配置缓存(启动时调用一次)""" global _config_cache, _config_initialized @@ -60,6 +69,7 @@ async def init_config(): try: from src.storage_adapter import get_storage_adapter + storage_adapter = await get_storage_adapter() _config_cache = await storage_adapter.get_all_config() _config_initialized = True @@ -75,10 +85,11 @@ async def reload_config(): try: from src.storage_adapter import get_storage_adapter + storage_adapter = await get_storage_adapter() # 如果后端支持 reload_config_cache,调用它 - if hasattr(storage_adapter._backend, 'reload_config_cache'): + if hasattr(storage_adapter._backend, "reload_config_cache"): await storage_adapter._backend.reload_config_cache() # 重新加载配置缓存 @@ -93,7 +104,9 @@ def _get_cached_config(key: str, default: Any = None) -> Any: return _config_cache.get(key, default) -async def get_config_value(key: str, default: Any = None, env_var: Optional[str] = None) -> Any: +async def get_config_value( + key: str, default: Any = None, env_var: Optional[str] = None +) -> Any: """Get configuration value with priority: ENV > Storage > default.""" # 确保配置已初始化 if not _config_initialized: @@ -178,7 +191,7 @@ async def get_retry_429_interval() -> float: except ValueError: pass - return float(await get_config_value("retry_429_interval", 1)) + return float(await get_config_value("retry_429_interval", 1.0)) async def get_anti_truncation_max_attempts() -> int: @@ -295,7 +308,9 @@ async def get_code_assist_endpoint() -> str: """ return str( await get_config_value( - "code_assist_endpoint", "https://cloudcode-pa.googleapis.com", "CODE_ASSIST_ENDPOINT" + "code_assist_endpoint", + "https://cloudcode-pa.googleapis.com", + "CODE_ASSIST_ENDPOINT", ) ) @@ -354,6 +369,132 @@ async def get_antigravity_stream2nostream() -> bool: return bool(await get_config_value("antigravity_stream2nostream", True)) +async def get_ff_retry_policy_v2() -> bool: + """Get retry policy v2 feature flag.""" + env_value = os.getenv("FF_RETRY_POLICY_V2") + if env_value: + return env_value.lower() in ("true", "1", "yes", "on") + + return bool(await get_config_value("ff_retry_policy_v2", False)) + + +async def get_ff_http2_pool_tuning() -> bool: + """Get http2 pool tuning feature flag.""" + env_value = os.getenv("FF_HTTP2_POOL_TUNING") + if env_value: + return env_value.lower() in ("true", "1", "yes", "on") + + return bool(await get_config_value("ff_http2_pool_tuning", False)) + + +async def get_ff_converter_fast_path() -> bool: + """Get converter fast path feature flag.""" + env_value = os.getenv("FF_CONVERTER_FAST_PATH") + if env_value: + return env_value.lower() in ("true", "1", "yes", "on") + + return bool(await get_config_value("ff_converter_fast_path", False)) + + +async def get_ff_preview_credential_scheduler_v2() -> bool: + """Get preview credential scheduler v2 feature flag.""" + env_value = os.getenv("FF_PREVIEW_CREDENTIAL_SCHEDULER_V2") + if env_value: + return env_value.lower() in ("true", "1", "yes", "on") + + return bool(await get_config_value("ff_preview_credential_scheduler_v2", False)) + + +async def get_rollout_stage_percent() -> int: + """Get rollout stage percent (5/20/50/100).""" + allowed = {5, 20, 50, 100} + + env_value = os.getenv("ROLLOUT_STAGE_PERCENT") + if env_value: + try: + parsed = int(env_value) + if parsed in allowed: + return parsed + except ValueError: + pass + + value = await get_config_value("rollout_stage_percent", 5) + try: + parsed = int(value) + if parsed in allowed: + return parsed + except (TypeError, ValueError): + pass + + return 5 + + +async def get_rollback_trigger_latency_p95_ms() -> int: + """Get rollback trigger latency p95 threshold in ms.""" + env_value = os.getenv("ROLLBACK_TRIGGER_LATENCY_P95_MS") + if env_value: + try: + parsed = int(env_value) + if parsed >= 0: + return parsed + except ValueError: + pass + + value = await get_config_value("rollback_trigger_latency_p95_ms", 2500) + try: + parsed = int(value) + if parsed >= 0: + return parsed + except (TypeError, ValueError): + pass + + return 2500 + + +async def get_rollback_trigger_throughput_drop_pct() -> float: + """Get rollback trigger throughput drop threshold in percent.""" + env_value = os.getenv("ROLLBACK_TRIGGER_THROUGHPUT_DROP_PCT") + if env_value: + try: + parsed = float(env_value) + if parsed >= 0: + return parsed + except ValueError: + pass + + value = await get_config_value("rollback_trigger_throughput_drop_pct", 20.0) + try: + parsed = float(value) + if parsed >= 0: + return parsed + except (TypeError, ValueError): + pass + + return 20.0 + + +async def get_rollback_trigger_quality_drop_pct() -> float: + """Get rollback trigger quality drop threshold in percent.""" + env_value = os.getenv("ROLLBACK_TRIGGER_QUALITY_DROP_PCT") + if env_value: + try: + parsed = float(env_value) + if parsed >= 0: + return parsed + except ValueError: + pass + + value = await get_config_value("rollback_trigger_quality_drop_pct", 10.0) + try: + parsed = float(value) + if parsed >= 0: + return parsed + except (TypeError, ValueError): + pass + + return 10.0 + + async def get_oauth_proxy_url() -> str: """ Get OAuth proxy URL setting. @@ -419,7 +560,9 @@ async def get_service_usage_api_url() -> str: """ return str( await get_config_value( - "service_usage_api_url", "https://serviceusage.googleapis.com", "SERVICE_USAGE_API_URL" + "service_usage_api_url", + "https://serviceusage.googleapis.com", + "SERVICE_USAGE_API_URL", ) ) diff --git a/front/common.js b/front/common.js index 16e5017dc..d9283ea45 100644 --- a/front/common.js +++ b/front/common.js @@ -2668,44 +2668,84 @@ async function loadConfig() { function populateConfigForm() { const c = AppState.currentConfig; - setConfigField('host', c.host || '0.0.0.0'); - setConfigField('port', c.port || 7861); - setConfigField('configApiPassword', c.api_password || ''); - setConfigField('configPanelPassword', c.panel_password || ''); - setConfigField('configPassword', c.password || 'pwd'); - setConfigField('credentialsDir', c.credentials_dir || ''); - setConfigField('proxy', c.proxy || ''); - setConfigField('codeAssistEndpoint', c.code_assist_endpoint || ''); - setConfigField('oauthProxyUrl', c.oauth_proxy_url || ''); - setConfigField('googleapisProxyUrl', c.googleapis_proxy_url || ''); - setConfigField('resourceManagerApiUrl', c.resource_manager_api_url || ''); - setConfigField('serviceUsageApiUrl', c.service_usage_api_url || ''); - setConfigField('antigravityApiUrl', c.antigravity_api_url || ''); - - document.getElementById('autoBanEnabled').checked = Boolean(c.auto_ban_enabled); - setConfigField('autoBanErrorCodes', (c.auto_ban_error_codes || []).join(',')); + setConfigField('host', c.host || '0.0.0.0', 'host'); + setConfigField('port', c.port || 7861, 'port'); + setConfigField('configApiPassword', c.api_password || '', 'api_password'); + setConfigField('configPanelPassword', c.panel_password || '', 'panel_password'); + setConfigField('configPassword', c.password || 'pwd', 'password'); + setConfigField('credentialsDir', c.credentials_dir || '', 'credentials_dir'); + setConfigField('proxy', c.proxy || '', 'proxy'); + setConfigField('codeAssistEndpoint', c.code_assist_endpoint || '', 'code_assist_endpoint'); + setConfigField('oauthProxyUrl', c.oauth_proxy_url || '', 'oauth_proxy_url'); + setConfigField('googleapisProxyUrl', c.googleapis_proxy_url || '', 'googleapis_proxy_url'); + setConfigField('resourceManagerApiUrl', c.resource_manager_api_url || '', 'resource_manager_api_url'); + setConfigField('serviceUsageApiUrl', c.service_usage_api_url || '', 'service_usage_api_url'); + setConfigField('antigravityApiUrl', c.antigravity_api_url || '', 'antigravity_api_url'); + + setConfigCheckbox('autoBanEnabled', Boolean(c.auto_ban_enabled), 'auto_ban_enabled'); + setConfigField('autoBanErrorCodes', (c.auto_ban_error_codes || []).join(','), 'auto_ban_error_codes'); setConfigField('callsPerRotation', c.calls_per_rotation || 10); - document.getElementById('retry429Enabled').checked = Boolean(c.retry_429_enabled); - setConfigField('retry429MaxRetries', c.retry_429_max_retries || 20); - setConfigField('retry429Interval', c.retry_429_interval || 0.1); - - document.getElementById('compatibilityModeEnabled').checked = Boolean(c.compatibility_mode_enabled); - document.getElementById('returnThoughtsToFrontend').checked = Boolean(c.return_thoughts_to_frontend !== false); - document.getElementById('antigravityStream2nostream').checked = Boolean(c.antigravity_stream2nostream !== false); - - setConfigField('antiTruncationMaxAttempts', c.anti_truncation_max_attempts || 3); - - setConfigField('keepaliveUrl', c.keepalive_url || ''); - setConfigField('keepaliveInterval', c.keepalive_interval || 60); + setConfigCheckbox('retry429Enabled', Boolean(c.retry_429_enabled), 'retry_429_enabled'); + setConfigField('retry429MaxRetries', c.retry_429_max_retries || 20, 'retry_429_max_retries'); + setConfigField('retry429Interval', c.retry_429_interval || 0.1, 'retry_429_interval'); + + setConfigCheckbox('ffRetryPolicyV2', Boolean(c.ff_retry_policy_v2), 'ff_retry_policy_v2'); + setConfigCheckbox('ffHttp2PoolTuning', Boolean(c.ff_http2_pool_tuning), 'ff_http2_pool_tuning'); + setConfigCheckbox('ffConverterFastPath', Boolean(c.ff_converter_fast_path), 'ff_converter_fast_path'); + setConfigCheckbox( + 'ffPreviewCredentialSchedulerV2', + Boolean(c.ff_preview_credential_scheduler_v2), + 'ff_preview_credential_scheduler_v2' + ); + setConfigField('rolloutStagePercent', c.rollout_stage_percent ?? 5, 'rollout_stage_percent'); + setConfigField( + 'rollbackTriggerLatencyP95Ms', + c.rollback_trigger_latency_p95_ms ?? 2500, + 'rollback_trigger_latency_p95_ms' + ); + setConfigField( + 'rollbackTriggerThroughputDropPct', + c.rollback_trigger_throughput_drop_pct ?? 20, + 'rollback_trigger_throughput_drop_pct' + ); + setConfigField( + 'rollbackTriggerQualityDropPct', + c.rollback_trigger_quality_drop_pct ?? 10, + 'rollback_trigger_quality_drop_pct' + ); + + setConfigCheckbox('compatibilityModeEnabled', Boolean(c.compatibility_mode_enabled), 'compatibility_mode_enabled'); + setConfigCheckbox('returnThoughtsToFrontend', Boolean(c.return_thoughts_to_frontend !== false), 'return_thoughts_to_frontend'); + setConfigCheckbox('antigravityStream2nostream', Boolean(c.antigravity_stream2nostream !== false), 'antigravity_stream2nostream'); + + setConfigField('antiTruncationMaxAttempts', c.anti_truncation_max_attempts || 3, 'anti_truncation_max_attempts'); + + setConfigField('keepaliveUrl', c.keepalive_url || '', 'keepalive_url'); + setConfigField('keepaliveInterval', c.keepalive_interval || 60, 'keepalive_interval'); +} + +function setConfigField(fieldId, value, configKey = null) { + const field = document.getElementById(fieldId); + if (field) { + field.value = value; + const key = configKey || fieldId.replace(/([A-Z])/g, '_$1').toLowerCase(); + if (AppState.envLockedFields.has(key)) { + field.disabled = true; + field.classList.add('env-locked'); + } else { + field.disabled = false; + field.classList.remove('env-locked'); + } + } } -function setConfigField(fieldId, value) { +function setConfigCheckbox(fieldId, checked, configKey = null) { const field = document.getElementById(fieldId); if (field) { - field.value = value; - const configKey = fieldId.replace(/([A-Z])/g, '_$1').toLowerCase(); - if (AppState.envLockedFields.has(configKey)) { + field.checked = Boolean(checked); + const key = configKey || fieldId.replace(/([A-Z])/g, '_$1').toLowerCase(); + if (AppState.envLockedFields.has(key)) { field.disabled = true; field.classList.add('env-locked'); } else { @@ -2717,10 +2757,24 @@ function setConfigField(fieldId, value) { async function saveConfig() { try { - const getValue = (id, def = '') => document.getElementById(id)?.value.trim() || def; - const getInt = (id, def = 0) => parseInt(document.getElementById(id)?.value) || def; - const getFloat = (id, def = 0.0) => parseFloat(document.getElementById(id)?.value) || def; - const getChecked = (id, def = false) => document.getElementById(id)?.checked || def; + const getValue = (id, def = '') => { + const rawValue = document.getElementById(id)?.value; + if (rawValue === undefined || rawValue === null) return def; + const trimmed = rawValue.trim(); + return trimmed === '' ? def : trimmed; + }; + const getInt = (id, def = 0) => { + const value = parseInt(document.getElementById(id)?.value, 10); + return Number.isNaN(value) ? def : value; + }; + const getFloat = (id, def = 0.0) => { + const value = parseFloat(document.getElementById(id)?.value); + return Number.isNaN(value) ? def : value; + }; + const getChecked = (id, def = false) => { + const field = document.getElementById(id); + return field ? Boolean(field.checked) : def; + }; const config = { host: getValue('host', '0.0.0.0'), @@ -2743,6 +2797,14 @@ async function saveConfig() { retry_429_enabled: getChecked('retry429Enabled'), retry_429_max_retries: getInt('retry429MaxRetries', 20), retry_429_interval: getFloat('retry429Interval', 0.1), + ff_retry_policy_v2: getChecked('ffRetryPolicyV2'), + ff_http2_pool_tuning: getChecked('ffHttp2PoolTuning'), + ff_converter_fast_path: getChecked('ffConverterFastPath'), + ff_preview_credential_scheduler_v2: getChecked('ffPreviewCredentialSchedulerV2'), + rollout_stage_percent: getInt('rolloutStagePercent', 5), + rollback_trigger_latency_p95_ms: getFloat('rollbackTriggerLatencyP95Ms', 2500), + rollback_trigger_throughput_drop_pct: getFloat('rollbackTriggerThroughputDropPct', 20), + rollback_trigger_quality_drop_pct: getFloat('rollbackTriggerQualityDropPct', 10), compatibility_mode_enabled: getChecked('compatibilityModeEnabled'), return_thoughts_to_frontend: getChecked('returnThoughtsToFrontend'), antigravity_stream2nostream: getChecked('antigravityStream2nostream'), diff --git a/front/control_panel.html b/front/control_panel.html index 0ac18ed90..a1c362c1a 100644 --- a/front/control_panel.html +++ b/front/control_panel.html @@ -2058,6 +2058,74 @@