gephyr 1.16.18

Gephyr is a headless local AI relay/proxy API handling OpenAI, Claude, and Gemini-compatible APIs
Documentation
use super::audit;
use super::config_patch;
use crate::models::AppConfig;
use crate::modules::system::config;
use crate::proxy::admin::ErrorResponse;
use crate::proxy::state::AdminState;
use axum::{
    extract::{Json, Path, State},
    http::{HeaderMap, StatusCode},
    response::IntoResponse,
};
use serde::Deserialize;
pub(crate) async fn admin_get_config(
) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
    let cfg = config::load_app_config().map_err(|e| {
        (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(ErrorResponse { error: e }),
        )
    })?;
    Ok(Json(cfg))
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct SaveConfigWrapper {
    config: AppConfig,
}

pub(crate) async fn admin_save_config(
    State(state): State<AdminState>,
    headers: HeaderMap,
    Json(payload): Json<SaveConfigWrapper>,
) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
    let actor = audit::resolve_admin_actor(&state, &headers).await;
    let mut new_config = payload.config;
    let existing_config = config::load_app_config().map_err(|e| {
        (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(ErrorResponse { error: e }),
        )
    })?;
    let mut warnings: Vec<&'static str> = Vec::new();
    if new_config.proxy.api_key.trim().is_empty() {
        new_config.proxy.api_key = existing_config.proxy.api_key.clone();
        warnings.push("proxy.api_key_preserved_from_existing");
    }
    if let Err(errors) = crate::modules::system::validation::validate_app_config(&new_config) {
        let message = errors
            .iter()
            .map(|e| e.to_string())
            .collect::<Vec<_>>()
            .join("\n");
        return Err((
            StatusCode::BAD_REQUEST,
            Json(ErrorResponse { error: message }),
        ));
    }

    config::save_app_config(&new_config).map_err(|e| {
        (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(ErrorResponse { error: e }),
        )
    })?;
    state.config.apply_proxy_config(&new_config.proxy).await;
    state
        .core
        .upstream
        .set_google_runtime_config(
            new_config.proxy.google.clone(),
            new_config.proxy.debug_logging.clone(),
        )
        .await;
    state
        .core
        .token_manager
        .update_sticky_config(new_config.proxy.scheduling.clone())
        .await;
    state
        .core
        .token_manager
        .update_session_binding_persistence(new_config.proxy.persist_session_bindings);
    state
        .core
        .token_manager
        .update_compliance_config(new_config.proxy.compliance.clone())
        .await;
    if let Some(account_id) = new_config.proxy.preferred_account_id.clone() {
        state
            .core
            .token_manager
            .set_preferred_account(Some(account_id))
            .await;
    } else {
        state.core.token_manager.set_preferred_account(None).await;
    }
    audit::log_admin_audit(
        "save_config",
        &actor,
        serde_json::json!({
            "before": audit::summarize_proxy_config(&existing_config.proxy),
            "after": audit::summarize_proxy_config(&new_config.proxy),
            "warnings": warnings,
        }),
    );

    Ok(Json(serde_json::json!({
        "ok": true,
        "saved": true,
        "message": "Config updated",
        "warnings": warnings
    })))
}
pub(crate) async fn admin_get_proxy_pool_config(
    State(state): State<AdminState>,
) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
    let config = state.runtime.proxy_pool_state.read().await;
    Ok(Json(config.clone()))
}

fn proxy_pool_runtime_snapshot(
    config: &crate::proxy::config::ProxyPoolConfig,
) -> serde_json::Value {
    let total = config.proxies.len();
    let enabled = config.proxies.iter().filter(|p| p.enabled).count();
    serde_json::json!({
        "strategy": config.strategy,
        "enabled": config.enabled,
        "auto_failover": config.auto_failover,
        "allow_shared_proxy_fallback": config.allow_shared_proxy_fallback,
        "require_proxy_for_account_requests": config.require_proxy_for_account_requests,
        "health_check_interval": config.health_check_interval,
        "proxies_total": total,
        "proxies_enabled": enabled
    })
}

pub(crate) async fn admin_get_proxy_pool_strategy(
    State(state): State<AdminState>,
) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
    let config = state.runtime.proxy_pool_state.read().await;
    Ok(Json(proxy_pool_runtime_snapshot(&config)))
}

#[derive(Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) struct UpdateProxyPoolStrategyRequest {
    #[serde(
        default,
        alias = "proxySelectionStrategy",
        alias = "proxy_selection_strategy"
    )]
    strategy: Option<crate::proxy::config::ProxySelectionStrategy>,
}

pub(crate) async fn admin_update_proxy_pool_strategy(
    State(state): State<AdminState>,
    headers: HeaderMap,
    Json(payload): Json<UpdateProxyPoolStrategyRequest>,
) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
    let patch = config_patch::patch_proxy_config(
        &state,
        &headers,
        config_patch::RuntimeApplyPolicy::HotAppliedWhenSafe,
        |proxy| {
            let strategy = payload
                .strategy
                .clone()
                .ok_or_else(|| "strategy is required".to_string())?;
            proxy.proxy_pool.strategy = strategy;
            Ok(())
        },
    )
    .await?;

    {
        let mut runtime_cfg = state.runtime.proxy_pool_state.write().await;
        runtime_cfg.strategy = patch.after.proxy_pool.strategy.clone();
    }
    let runtime_cfg = state.runtime.proxy_pool_state.read().await;

    audit::log_admin_audit(
        "update_proxy_pool_strategy",
        &patch.actor,
        serde_json::json!({
            "before": {
                "strategy": patch.before.proxy_pool.strategy
            },
            "after": {
                "strategy": runtime_cfg.strategy
            }
        }),
    );

    Ok(Json(serde_json::json!({
        "ok": true,
        "saved": true,
        "message": "Proxy pool strategy updated",
        "runtime_apply": patch.runtime_apply_result(true),
        "proxy_pool": proxy_pool_runtime_snapshot(&runtime_cfg)
    })))
}

pub(crate) async fn admin_get_proxy_pool_runtime(
    State(state): State<AdminState>,
) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
    let config = state.runtime.proxy_pool_state.read().await;
    Ok(Json(proxy_pool_runtime_snapshot(&config)))
}

#[derive(Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) struct UpdateProxyPoolRuntimeRequest {
    #[serde(default, alias = "poolEnabled", alias = "pool_enabled")]
    enabled: Option<bool>,
    #[serde(default, alias = "autoFailover")]
    auto_failover: Option<bool>,
    #[serde(
        default,
        alias = "allowSharedProxyFallback",
        alias = "allow_shared_proxy_fallback"
    )]
    allow_shared_proxy_fallback: Option<bool>,
    #[serde(
        default,
        alias = "requireProxyForAccountRequests",
        alias = "require_proxy_for_account_requests"
    )]
    require_proxy_for_account_requests: Option<bool>,
    #[serde(
        default,
        alias = "healthCheckInterval",
        alias = "healthCheckIntervalSeconds"
    )]
    health_check_interval: Option<u64>,
}

pub(crate) async fn admin_update_proxy_pool_runtime(
    State(state): State<AdminState>,
    headers: HeaderMap,
    Json(payload): Json<UpdateProxyPoolRuntimeRequest>,
) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
    let patch = config_patch::patch_proxy_config(
        &state,
        &headers,
        config_patch::RuntimeApplyPolicy::HotAppliedWhenSafe,
        |proxy| {
            if payload.enabled.is_none()
                && payload.auto_failover.is_none()
                && payload.allow_shared_proxy_fallback.is_none()
                && payload.require_proxy_for_account_requests.is_none()
                && payload.health_check_interval.is_none()
            {
                return Err(
                    "At least one of enabled, auto_failover, allow_shared_proxy_fallback, require_proxy_for_account_requests, health_check_interval must be provided".to_string(),
                );
            }

            if let Some(enabled) = payload.enabled {
                proxy.proxy_pool.enabled = enabled;
            }
            if let Some(auto_failover) = payload.auto_failover {
                proxy.proxy_pool.auto_failover = auto_failover;
            }
            if let Some(allow_shared_proxy_fallback) = payload.allow_shared_proxy_fallback {
                proxy.proxy_pool.allow_shared_proxy_fallback = allow_shared_proxy_fallback;
            }
            if let Some(require_proxy_for_account_requests) =
                payload.require_proxy_for_account_requests
            {
                proxy.proxy_pool.require_proxy_for_account_requests =
                    require_proxy_for_account_requests;
            }
            if let Some(health_check_interval) = payload.health_check_interval {
                proxy.proxy_pool.health_check_interval = health_check_interval;
            }
            Ok(())
        },
    )
    .await?;

    let after_runtime;
    {
        let mut runtime_cfg = state.runtime.proxy_pool_state.write().await;
        if let Some(enabled) = payload.enabled {
            runtime_cfg.enabled = enabled;
        }
        if let Some(auto_failover) = payload.auto_failover {
            runtime_cfg.auto_failover = auto_failover;
        }
        if let Some(allow_shared_proxy_fallback) = payload.allow_shared_proxy_fallback {
            runtime_cfg.allow_shared_proxy_fallback = allow_shared_proxy_fallback;
        }
        if let Some(require_proxy_for_account_requests) = payload.require_proxy_for_account_requests
        {
            runtime_cfg.require_proxy_for_account_requests = require_proxy_for_account_requests;
        }
        if let Some(health_check_interval) = payload.health_check_interval {
            runtime_cfg.health_check_interval = health_check_interval;
        }
        after_runtime = proxy_pool_runtime_snapshot(&runtime_cfg);
    }

    audit::log_admin_audit(
        "update_proxy_pool_runtime",
        &patch.actor,
        serde_json::json!({
            "before": proxy_pool_runtime_snapshot(&patch.before.proxy_pool),
            "after": after_runtime
        }),
    );

    Ok(Json(serde_json::json!({
        "ok": true,
        "saved": true,
        "message": "Proxy pool runtime config updated",
        "runtime_apply": patch.runtime_apply_result(true),
        "proxy_pool": after_runtime
    })))
}

pub(crate) async fn admin_get_all_account_bindings(
    State(state): State<AdminState>,
) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
    let bindings = state.runtime.proxy_pool_manager.get_all_bindings_snapshot();
    Ok(Json(bindings))
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct BindAccountProxyRequest {
    account_id: String,
    proxy_id: String,
}

pub(crate) async fn admin_bind_account_proxy(
    State(state): State<AdminState>,
    Json(payload): Json<BindAccountProxyRequest>,
) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
    state
        .runtime
        .proxy_pool_manager
        .bind_account_to_proxy(payload.account_id, payload.proxy_id)
        .await
        .map_err(|e| {
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ErrorResponse { error: e }),
            )
        })?;
    Ok(StatusCode::OK)
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct UnbindAccountProxyRequest {
    account_id: String,
}

pub(crate) async fn admin_unbind_account_proxy(
    State(state): State<AdminState>,
    Json(payload): Json<UnbindAccountProxyRequest>,
) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
    state
        .runtime
        .proxy_pool_manager
        .unbind_account_proxy(payload.account_id)
        .await;
    Ok(StatusCode::OK)
}
pub(crate) async fn admin_get_account_proxy_binding(
    State(state): State<AdminState>,
    Path(account_id): Path<String>,
) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
    let binding = state
        .runtime
        .proxy_pool_manager
        .get_account_binding(&account_id);
    Ok(Json(binding))
}
pub(crate) async fn admin_trigger_proxy_health_check(
    State(state): State<AdminState>,
) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
    state
        .runtime
        .proxy_pool_manager
        .health_check()
        .await
        .map_err(|e| {
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ErrorResponse { error: e }),
            )
        })?;
    let config = state.runtime.proxy_pool_state.read().await;
    Ok(Json(serde_json::json!({
        "success": true,
        "message": "Health check completed",
        "proxies": config.proxies,
    })))
}