mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-09 02:47:45 +08:00
972 lines
36 KiB
Rust
972 lines
36 KiB
Rust
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<Body> {
|
|
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<Response<Body>> {
|
|
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<Response<Body>> {
|
|
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::<Vec<_>>();
|
|
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::<Vec<_>>();
|
|
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::<usize>().ok())
|
|
.unwrap_or(0);
|
|
let limit = query_param_value(request_context.request_query_string.as_deref(), "limit")
|
|
.and_then(|value| value.parse::<usize>().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::<Vec<_>>();
|
|
|
|
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::<u64>().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::<usize>().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::<u64>().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::<usize>().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::<usize>().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::<u64>().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::<usize>().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::<usize>().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::<Vec<_>>(),
|
|
}))
|
|
.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::<Vec<_>>();
|
|
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::<Vec<_>>();
|
|
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::<Vec<_>>();
|
|
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::<Vec<_>>(),
|
|
}))
|
|
.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
|
|
}
|