use super::{ build_api_format_health_monitor_payload, build_model_health_monitor_payload, build_public_auth_modules_status_payload, build_public_catalog_models_payload, build_public_catalog_search_models_payload, build_public_providers_payload, build_related_health_monitor_payload, capability_detail_by_name, ldap_module_config_is_valid, sanitize_public_model_capabilities, sanitize_public_model_config_for_user, sanitize_public_tiered_pricing, serialize_public_capability, supported_capability_names, ApiFormatHealthMonitorOptions, HealthMonitorRelationDimension, ModelHealthMonitorOptions, PUBLIC_CAPABILITY_DEFINITIONS, }; use crate::control::GatewayPublicRequestContext; use crate::handlers::shared::{ decrypt_catalog_secret_with_fallbacks, decrypt_or_migrate_auth_api_key_secret, decrypt_or_migrate_ldap_bind_password, decrypt_or_migrate_system_config_secret, encrypt_catalog_secret_with_fallbacks, escape_admin_email_template_html, module_available_from_env, query_param_bool, query_param_optional_bool, query_param_value, read_admin_email_template_payload, render_admin_email_template_html, system_config_bool, system_config_string, unix_secs_to_rfc3339, }; use crate::{AppState, GatewayError}; use aether_data_contracts::repository::global_models::PublicGlobalModelQuery; use axum::body::{Body, Bytes}; use axum::http::{self, Response}; use axum::response::IntoResponse; use axum::Json; use serde_json::json; use std::time::{SystemTime, UNIX_EPOCH}; #[path = "support/announcements.rs"] mod support_announcements; #[path = "support/auth.rs"] mod support_auth; #[path = "support/auth_cookie_policy.rs"] mod support_auth_cookie_policy; #[path = "support/billing.rs"] mod support_billing; #[path = "support/ccswitch.rs"] mod support_ccswitch; #[path = "support/dashboard.rs"] mod support_dashboard; #[path = "support/install.rs"] mod support_install; #[path = "support/models.rs"] mod support_models; #[path = "support/monitoring.rs"] mod support_monitoring; #[path = "support/oauth.rs"] mod support_oauth; #[path = "support/payment.rs"] mod support_payment; #[path = "support/test_connection.rs"] mod support_test_connection; #[path = "support/user_me.rs"] mod support_user_me; #[path = "support/user_me_vscodex.rs"] mod support_vscodex; #[path = "support/wallet.rs"] mod support_wallet; pub(crate) use self::support_announcements::maybe_build_local_admin_announcements_response; #[cfg(test)] pub(crate) use self::support_models::filter_eligible_model_rows; pub(crate) use self::support_models::matches_model_mapping_for_models; use self::support_announcements::{ maybe_build_local_announcement_user_response, maybe_build_local_public_announcements_response, }; use self::support_auth::auth_registration::{ auth_password_policy_level, validate_auth_register_password, }; use self::support_auth::auth_session::{ build_auth_wallet_summary_payload, handle_auth_me, resolve_authenticated_local_user, AuthenticatedLocalUserContext, }; use self::support_auth::{auth_email_is_verified, consume_auth_email_registration_proof}; use self::support_auth::{ build_auth_error_response, build_auth_json_response, build_auth_refresh_cookie_clear_header, build_auth_registration_settings_payload, build_auth_settings_payload, extract_client_device_id, mark_sensitive_response_no_store, maybe_build_local_auth_response, }; use self::support_billing::maybe_build_local_billing_response; use self::support_ccswitch::maybe_build_local_ccswitch_response; use self::support_dashboard::maybe_build_local_dashboard_response; pub(crate) use self::support_install::{ base_url_from_request, build_api_key_install_session_response, build_proxy_node_install_session_response, CreateApiKeyInstallSessionRequest, }; use self::support_install::{ handle_users_me_api_key_install_session_create, maybe_build_local_install_response, users_me_api_key_install_sessions_path_matches, }; use self::support_models::{ build_models_auth_error_response, maybe_build_local_models_response, models_api_format, }; use self::support_monitoring::maybe_build_local_user_monitoring_response; use self::support_oauth::maybe_build_local_oauth_response; use self::support_payment::maybe_build_local_payment_callback_response; use self::support_test_connection::maybe_build_local_test_connection_response; use self::support_user_me::maybe_build_local_users_me_response; pub(crate) use self::support_vscodex::vscodex_ws_proxy; use self::support_vscodex::{handle_users_me_vscodex_request, maybe_build_local_vscodex_response}; use self::support_wallet::{ build_wallet_balance_payload_for_auth_scope, build_wallet_balance_payload_for_user, build_wallet_live_today_usage_payload_for_api_key, build_wallet_live_today_usage_payload_for_user, direct_gateway_channels, maybe_build_local_wallet_response, prepare_billing_gateway_response_for_storage, prepare_wallet_gateway_response_for_storage, resolve_direct_gateway_channel, sanitize_wallet_gateway_response, wallet_normalize_optional_string_field, wallet_payment_instructions_from_checkout, wallet_payment_instructions_from_stored, }; pub(crate) fn build_unhandled_public_support_response( request_context: &GatewayPublicRequestContext, ) -> Response { let decision = request_context .control_decision .as_ref() .expect("public support response requires control decision"); ( http::StatusCode::NOT_IMPLEMENTED, Json(json!({ "detail": "public support route not implemented in rust frontdoor", "route_family": decision.route_family, "route_kind": decision.route_kind, "request_path": request_context.request_path, })), ) .into_response() } pub(crate) async fn maybe_build_local_public_support_response( state: &AppState, request_context: &GatewayPublicRequestContext, headers: &http::HeaderMap, remote_addr: &std::net::SocketAddr, client_ip: std::net::IpAddr, request_body: Option<&Bytes>, ) -> Option> { let response = build_local_public_support_response( state, request_context, headers, remote_addr, client_ip, request_body, ) .await?; Some(support_auth_cookie_policy::finalize_refresh_cookie( response, headers, request_context.host_header.as_deref(), remote_addr, )) } async fn build_local_public_support_response( state: &AppState, request_context: &GatewayPublicRequestContext, headers: &http::HeaderMap, remote_addr: &std::net::SocketAddr, client_ip: std::net::IpAddr, request_body: Option<&Bytes>, ) -> Option> { let decision = request_context.control_decision.as_ref()?; if decision.route_class.as_deref() != Some("public_support") { return None; } if decision.route_family.as_deref() == Some("auth") { return maybe_build_local_auth_response( state, request_context, headers, client_ip, request_body, ) .await; } if decision.route_family.as_deref() == Some("oauth") { return maybe_build_local_oauth_response( state, request_context, headers, client_ip, request_body, ) .await; } if decision.route_family.as_deref() == Some("dashboard") { return Some(maybe_build_local_dashboard_response(state, request_context, headers).await); } if decision.route_family.as_deref() == Some("monitoring_user") { return maybe_build_local_user_monitoring_response(state, request_context, headers).await; } if decision.route_family.as_deref() == Some("announcement_user") { return maybe_build_local_announcement_user_response( state, request_context, headers, request_body, ) .await; } if decision.route_family.as_deref() == Some("wallet") { if let Some(response) = maybe_build_local_wallet_response( state, request_context, headers, client_ip, request_body, ) .await { return Some(response); } return Some(build_unhandled_public_support_response(request_context)); } if decision.route_family.as_deref() == Some("billing") { if let Some(response) = maybe_build_local_billing_response( state, request_context, headers, client_ip, request_body, ) .await { return Some(response); } return Some(build_unhandled_public_support_response(request_context)); } if decision.route_family.as_deref() == Some("ccswitch") { if let Some(response) = maybe_build_local_ccswitch_response(state, request_context).await { return Some(response); } return Some(build_unhandled_public_support_response(request_context)); } if decision.route_family.as_deref() == Some("users_me") { return maybe_build_local_users_me_response( state, request_context, headers, remote_addr, request_body, ) .await; } if decision.route_family.as_deref() == Some("vscodex") { return maybe_build_local_vscodex_response(state, request_context, client_ip, request_body) .await; } if decision.route_family.as_deref() == Some("install") { return maybe_build_local_install_response(state, request_context).await; } if decision.route_family.as_deref() == Some("payment_callback") { return maybe_build_local_payment_callback_response( state, request_context, headers, request_body, ) .await; } if decision.route_family.as_deref() == Some("models") { if decision.auth_context.is_none() { return Some(build_models_auth_error_response( models_api_format(request_context).unwrap_or("openai:chat"), )); } if let Some(response) = maybe_build_local_models_response(state, request_context).await { return Some(response); } } if decision.route_family.as_deref() == Some("auth_public") { if decision.route_kind.as_deref() == Some("registration_settings") && request_context.request_path == "/api/auth/registration-settings" { let payload = build_auth_registration_settings_payload(state).await.ok()?; return Some(Json(payload).into_response()); } if decision.route_kind.as_deref() == Some("settings") && request_context.request_path == "/api/auth/settings" { let payload = build_auth_settings_payload(state).await.ok()?; return Some(Json(payload).into_response()); } } if decision.route_family.as_deref() == Some("announcements") { return maybe_build_local_public_announcements_response(state, request_context, headers) .await; } if decision.route_family.as_deref() == Some("public_catalog") { if decision.route_kind.as_deref() == Some("site_info") && request_context.request_path == "/api/public/site-info" { let site_name = state .read_system_config_json_value("site_name") .await .ok() .flatten() .and_then(|value| value.as_str().map(ToOwned::to_owned)) .unwrap_or_else(|| "Aether".to_string()); let site_subtitle = state .read_system_config_json_value("site_subtitle") .await .ok() .flatten() .and_then(|value| value.as_str().map(ToOwned::to_owned)) .unwrap_or_else(|| "AI Gateway".to_string()); return Some( Json(json!({ "site_name": site_name, "site_subtitle": site_subtitle, })) .into_response(), ); } if decision.route_kind.as_deref() == Some("providers") && request_context.request_path == "/api/public/providers" { let payload = build_public_providers_payload( state, request_context.request_query_string.as_deref(), ) .await?; return Some(Json(payload).into_response()); } if decision.route_kind.as_deref() == Some("models") && request_context.request_path == "/api/public/models" { let payload = build_public_catalog_models_payload( state, request_context.request_query_string.as_deref(), ) .await?; return Some(Json(payload).into_response()); } if decision.route_kind.as_deref() == Some("search_models") && request_context.request_path == "/api/public/search/models" { let payload = build_public_catalog_search_models_payload( state, request_context.request_query_string.as_deref(), ) .await?; return Some(Json(payload).into_response()); } if decision.route_kind.as_deref() == Some("stats") && request_context.request_path == "/api/public/stats" { let providers = state .list_provider_catalog_providers(true) .await .ok() .unwrap_or_default(); let active_providers = providers.len(); let provider_ids = providers .iter() .map(|provider| provider.id.clone()) .collect::>(); let endpoints = if provider_ids.is_empty() { Vec::new() } else { state .list_provider_catalog_endpoints_by_provider_ids(&provider_ids) .await .ok() .unwrap_or_default() }; let mut supported_formats = endpoints .iter() .filter(|endpoint| endpoint.is_active) .map(|endpoint| endpoint.api_format.clone()) .collect::>(); supported_formats.sort(); supported_formats.dedup(); let mut active_model_ids = std::collections::BTreeSet::new(); if state.has_minimal_candidate_selection_reader() { for api_format in &supported_formats { let rows = state .list_minimal_candidate_selection_rows_for_api_format(api_format) .await .ok() .unwrap_or_default(); for row in rows { active_model_ids.insert(row.model_id); } } } let active_models = active_model_ids.len(); return Some( Json(json!({ "total_providers": active_providers, "active_providers": active_providers, "total_models": active_models, "active_models": active_models, "supported_formats": supported_formats, })) .into_response(), ); } if decision.route_kind.as_deref() == Some("global_models") && request_context.request_path == "/api/public/global-models" { if !state.has_global_model_data_reader() { return None; } let skip = query_param_value(request_context.request_query_string.as_deref(), "skip") .and_then(|value| value.parse::().ok()) .unwrap_or(0); let limit = query_param_value(request_context.request_query_string.as_deref(), "limit") .and_then(|value| value.parse::().ok()) .filter(|value| *value > 0 && *value <= 1000) .unwrap_or(100); let search = query_param_value(request_context.request_query_string.as_deref(), "search"); let page = state .list_public_global_models(&PublicGlobalModelQuery { offset: skip, limit, is_active: Some(true), search, }) .await .ok()?; let models = page .items .into_iter() .map(|model| { json!({ "id": model.id, "name": model.name, "display_name": model.display_name, "is_active": model.is_active, "default_price_per_request": model.default_price_per_request, "default_tiered_pricing": sanitize_public_tiered_pricing(model.default_tiered_pricing), "supported_capabilities": sanitize_public_model_capabilities(model.supported_capabilities), "config": sanitize_public_model_config_for_user(model.config), "usage_count": model.usage_count, }) }) .collect::>(); return Some( Json(json!({ "models": models, "total": page.total, })) .into_response(), ); } if decision.route_kind.as_deref() == Some("health_api_formats") && request_context.request_path == "/api/public/health/api-formats" { let lookback_hours = query_param_value( request_context.request_query_string.as_deref(), "lookback_hours", ) .and_then(|value| value.parse::().ok()) .filter(|value| (1..=168).contains(value)) .unwrap_or(6); let per_format_limit = query_param_value( request_context.request_query_string.as_deref(), "per_format_limit", ) .and_then(|value| value.parse::().ok()) .filter(|value| (10..=500).contains(value)) .unwrap_or(100); let payload = build_api_format_health_monitor_payload( state, lookback_hours, per_format_limit, ApiFormatHealthMonitorOptions { include_api_path: true, include_provider_count: false, include_key_count: false, }, ) .await?; return Some(Json(payload).into_response()); } if decision.route_kind.as_deref() == Some("health_models") && request_context.request_path == "/api/public/health/models" { let lookback_hours = query_param_value( request_context.request_query_string.as_deref(), "lookback_hours", ) .and_then(|value| value.parse::().ok()) .filter(|value| (1..=168).contains(value)) .unwrap_or(6); let model_limit = query_param_value( request_context.request_query_string.as_deref(), "model_limit", ) .and_then(|value| value.parse::().ok()) .filter(|value| (1..=50).contains(value)) .unwrap_or(12); let per_model_limit = query_param_value( request_context.request_query_string.as_deref(), "per_model_limit", ) .and_then(|value| value.parse::().ok()) .filter(|value| (10..=500).contains(value)) .unwrap_or(100); let payload = build_model_health_monitor_payload( state, lookback_hours, model_limit, per_model_limit, ModelHealthMonitorOptions { include_provider_count: false, }, ) .await?; return Some(Json(payload).into_response()); } if decision.route_kind.as_deref() == Some("health_related") && request_context.request_path == "/api/public/health/related" { let Some(dimension) = query_param_value(request_context.request_query_string.as_deref(), "dimension") .and_then(|value| HealthMonitorRelationDimension::parse(&value)) else { return Some( ( http::StatusCode::BAD_REQUEST, Json(json!({ "detail": "dimension 必须是 endpoint 或 model" })), ) .into_response(), ); }; if matches!(dimension, HealthMonitorRelationDimension::Provider) { return Some( ( http::StatusCode::BAD_REQUEST, Json(json!({ "detail": "公开健康监控不支持 provider 维度" })), ) .into_response(), ); } let Some(value) = query_param_value(request_context.request_query_string.as_deref(), "value") .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()) else { return Some( ( http::StatusCode::BAD_REQUEST, Json(json!({ "detail": "value 不能为空" })), ) .into_response(), ); }; let lookback_hours = query_param_value( request_context.request_query_string.as_deref(), "lookback_hours", ) .and_then(|value| value.parse::().ok()) .filter(|value| (1..=168).contains(value)) .unwrap_or(6); let related_limit = query_param_value( request_context.request_query_string.as_deref(), "related_limit", ) .and_then(|value| value.parse::().ok()) .filter(|value| (1..=50).contains(value)) .unwrap_or(8); let per_item_limit = query_param_value( request_context.request_query_string.as_deref(), "per_item_limit", ) .and_then(|value| value.parse::().ok()) .filter(|value| (10..=500).contains(value)) .unwrap_or(100); let payload = build_related_health_monitor_payload( state, lookback_hours, dimension, &value, related_limit, per_item_limit, false, ) .await?; return Some(Json(payload).into_response()); } } if decision.route_family.as_deref() == Some("capabilities") { if decision.route_kind.as_deref() == Some("list") && request_context.request_path == "/api/capabilities" { return Some( Json(json!({ "capabilities": PUBLIC_CAPABILITY_DEFINITIONS .iter() .copied() .map(serialize_public_capability) .collect::>(), })) .into_response(), ); } if decision.route_kind.as_deref() == Some("user_configurable") && request_context.request_path == "/api/capabilities/user-configurable" { let capabilities = PUBLIC_CAPABILITY_DEFINITIONS .iter() .copied() .filter(|capability| capability.config_mode == "user_configurable") .map(serialize_public_capability) .collect::>(); return Some(Json(json!({ "capabilities": capabilities })).into_response()); } if decision.route_kind.as_deref() == Some("model") && request_context .request_path .starts_with("/api/capabilities/model/") { let model_name = request_context .request_path .trim_start_matches("/api/capabilities/model/") .trim(); if model_name.is_empty() { return Some( Json(json!({ "model": "", "supported_capabilities": [], "capability_details": [], "error": "模型不存在", })) .into_response(), ); } let model = state .get_public_global_model_by_name(model_name) .await .ok() .flatten(); let Some(model) = model else { return Some( Json(json!({ "model": model_name, "supported_capabilities": [], "capability_details": [], "error": "模型不存在", })) .into_response(), ); }; let supported_capabilities = supported_capability_names(model.supported_capabilities.as_ref()); let capability_details = supported_capabilities .iter() .filter_map(|capability| capability_detail_by_name(capability)) .collect::>(); return Some( Json(json!({ "model": model_name, "global_model_id": model.id, "global_model_name": model.name, "supported_capabilities": supported_capabilities, "capability_details": capability_details, })) .into_response(), ); } } if decision.route_family.as_deref() == Some("modules") { if decision.route_kind.as_deref() == Some("auth_status") && request_context.request_path == "/api/modules/auth-status" { let payload = build_public_auth_modules_status_payload(state).await.ok()?; return Some(Json(payload).into_response()); } } if decision.route_family.as_deref() == Some("system_catalog") { let timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) .map(|duration| duration.as_secs().to_string()) .unwrap_or_else(|_| "0".to_string()); if decision.route_kind.as_deref() == Some("health") && request_context.request_path == "/health" { return Some( Json(json!({ "status": "healthy", "timestamp": timestamp, "database_pool": { "checked_out": 0, "pool_size": 0, "overflow": 0, "max_capacity": 0, "usage_rate": "0.0%", "source": "rust_frontdoor", }, })) .into_response(), ); } if decision.route_kind.as_deref() == Some("health") && request_context.request_path == "/v1/health" { let active_providers = state .list_provider_catalog_providers(true) .await .map(|providers| providers.len()) .unwrap_or(0); return Some( Json(json!({ "status": "ok", "timestamp": timestamp, "stats": { "active_providers": active_providers, "active_models": 0, }, "dependencies": { "database": { "status": if state.has_data_backends() { "ok" } else { "degraded" }, }, "redis": { "status": if state.has_redis_data_backend() { "ok" } else { "degraded" }, }, }, })) .into_response(), ); } if decision.route_kind.as_deref() == Some("root") && request_context.request_path == "/" { let providers = state .list_provider_catalog_providers(true) .await .ok() .unwrap_or_default(); return Some( Json(json!({ "message": "AI Proxy with Modular Architecture v4.0.0", "status": "running", "current_provider": serde_json::Value::Null, "available_providers": providers.len(), "config": {}, "endpoints": { "messages": "/v1/messages", "count_tokens": "/v1/messages/count_tokens", "health": "/v1/health", "providers": "/v1/providers", "test_connection": "/v1/test-connection", }, })) .into_response(), ); } if decision.route_kind.as_deref() == Some("providers") && request_context.request_path == "/v1/providers" { if !state.has_provider_catalog_data_reader() { return None; } let include_models = query_param_bool( request_context.request_query_string.as_deref(), "include_models", false, ); let include_endpoints = query_param_bool( request_context.request_query_string.as_deref(), "include_endpoints", false, ); if include_models { return None; } let providers = state .list_provider_catalog_providers(true) .await .ok() .unwrap_or_default(); let provider_ids = providers .iter() .map(|provider| provider.id.clone()) .collect::>(); let endpoints = if include_endpoints { state .list_provider_catalog_endpoints_by_provider_ids(&provider_ids) .await .ok() .unwrap_or_default() } else { Vec::new() }; return Some( Json(json!({ "providers": providers .into_iter() .map(|provider| { let provider_id = provider.id.clone(); let mut payload = json!({ "id": provider_id.clone(), "is_active": provider.is_active, "provider_priority": provider.provider_priority, }); if include_endpoints { payload["endpoints"] = serde_json::Value::Array( endpoints .iter() .filter(|endpoint| { endpoint.provider_id == provider_id && endpoint.is_active }) .map(|endpoint| json!({ "id": endpoint.id, "api_format": endpoint.api_format, "is_active": endpoint.is_active, })) .collect(), ); } payload }) .collect::>(), })) .into_response(), ); } if decision.route_kind.as_deref() == Some("provider_detail") && request_context.request_path.starts_with("/v1/providers/") { let include_models = query_param_bool( request_context.request_query_string.as_deref(), "include_models", false, ); let include_endpoints = query_param_bool( request_context.request_query_string.as_deref(), "include_endpoints", false, ); if include_models { return None; } let provider_identifier = request_context .request_path .trim_start_matches("/v1/providers/") .trim(); if provider_identifier.is_empty() { return None; } let provider = if let Ok(providers) = state .read_provider_catalog_providers_by_ids(&[provider_identifier.to_string()]) .await { providers.into_iter().next() } else { None }; let provider = match provider { Some(provider) if provider.is_active => provider, None => { return Some( ( http::StatusCode::NOT_FOUND, Json(json!({ "detail": "Provider not found" })), ) .into_response(), ); } Some(_) => { return Some( ( http::StatusCode::NOT_FOUND, Json(json!({ "detail": "Provider not found" })), ) .into_response(), ); } }; let provider_id = provider.id.clone(); let mut payload = json!({ "id": provider_id.clone(), "is_active": provider.is_active, "provider_priority": provider.provider_priority, }); if include_endpoints { let endpoints = state .list_provider_catalog_endpoints_by_provider_ids(std::slice::from_ref( &provider_id, )) .await .ok() .unwrap_or_default(); payload["endpoints"] = serde_json::Value::Array( endpoints .into_iter() .filter(|endpoint| endpoint.is_active) .map(|endpoint| { json!({ "id": endpoint.id, "api_format": endpoint.api_format, "is_active": endpoint.is_active, }) }) .collect(), ); } return Some(Json(payload).into_response()); } if decision.route_kind.as_deref() == Some("test_connection") && request_context.request_path == "/v1/test-connection" { return maybe_build_local_test_connection_response(state, request_context, headers) .await; } if decision.route_kind.as_deref() == Some("test_connection") && request_context.request_path == "/test-connection" { return Some( ( http::StatusCode::GONE, Json(json!({ "detail": "Deprecated endpoint. Please use /v1/test-connection.", })), ) .into_response(), ); } } None }