gephyr 1.16.18

Gephyr is a headless local AI relay/proxy API handling OpenAI, Claude, and Gemini-compatible APIs
Documentation
use crate::proxy::admin::ErrorResponse;
use crate::proxy::state::AdminState;
use axum::{
    extract::State,
    http::StatusCode,
    response::{IntoResponse, Json},
};
use serde::Serialize;
use std::path::PathBuf;

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct OperatorEncryptionStatus {
    env_key_set: bool,
    machine_uid_available: bool,
    prerequisites_ok: bool,
    prerequisites_error: Option<String>,
    encrypted_secrets_seen: u64,
    decrypt_failures: u64,
    first_failure: Option<OperatorDecryptFailure>,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct OperatorDecryptFailure {
    account_id: String,
    email: String,
    field: String,
    error: String,
    file: String,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct OperatorAccountsStatus {
    index_accounts: usize,
    current_account_id: Option<String>,
    files_seen: u64,
    parse_failures: u64,
    disabled: u64,
    proxy_disabled: u64,
    missing_device_profile: u64,
    missing_refresh_token: u64,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct OperatorProxyPoolStatus {
    enabled: bool,
    require_proxy_for_account_requests: bool,
    proxies_total: usize,
    proxies_enabled: usize,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct OperatorOAuthConfigStatus {
    client_id_set: bool,
    client_secret_set: bool,
    allowed_domains_set: bool,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct OperatorStatusResponse {
    timestamp: String,
    version: String,
    data_dir: String,
    runtime_port: u16,
    runtime_running: bool,
    tls_backend: String,
    oauth: OperatorOAuthConfigStatus,
    oauth_flow: crate::modules::auth::oauth_server::OAuthFlowStatusSnapshot,
    proxy_pool: OperatorProxyPoolStatus,
    accounts: OperatorAccountsStatus,
    encryption: OperatorEncryptionStatus,
}

fn env_var_set(name: &str) -> bool {
    std::env::var(name)
        .ok()
        .map(|v| !v.trim().is_empty())
        .unwrap_or(false)
}

fn as_string_field(value: &serde_json::Value, key: &str) -> Option<String> {
    value
        .get(key)
        .and_then(|v| v.as_str())
        .map(|s| s.to_string())
}

fn token_field(value: &serde_json::Value, field: &str) -> Option<String> {
    value
        .get("token")
        .and_then(|t| t.get(field))
        .and_then(|v| v.as_str())
        .map(|s| s.to_string())
}

fn bool_field(value: &serde_json::Value, key: &str) -> bool {
    value.get(key).and_then(|v| v.as_bool()).unwrap_or(false)
}

fn has_device_profile(value: &serde_json::Value) -> bool {
    match value.get("device_profile") {
        Some(serde_json::Value::Object(obj)) => !obj.is_empty(),
        _ => false,
    }
}

async fn scan_accounts_for_operator_status(
    accounts_dir: &PathBuf,
) -> Result<(OperatorAccountsStatus, OperatorEncryptionStatus), String> {
    let mut accounts_status = OperatorAccountsStatus {
        index_accounts: 0,
        current_account_id: None,
        files_seen: 0,
        parse_failures: 0,
        disabled: 0,
        proxy_disabled: 0,
        missing_device_profile: 0,
        missing_refresh_token: 0,
    };

    let mut enc_status = OperatorEncryptionStatus {
        env_key_set: env_var_set("ENCRYPTION_KEY"),
        machine_uid_available: machine_uid::get().is_ok(),
        prerequisites_ok: true,
        prerequisites_error: None,
        encrypted_secrets_seen: 0,
        decrypt_failures: 0,
        first_failure: None,
    };

    if let Err(e) = crate::utils::crypto::validate_encryption_key_prerequisites() {
        enc_status.prerequisites_ok = false;
        enc_status.prerequisites_error = Some(e);
    }

    if let Ok(index) = crate::modules::auth::account::load_account_index() {
        accounts_status.index_accounts = index.accounts.len();
        accounts_status.current_account_id = index.current_account_id;
    }

    if !accounts_dir.exists() {
        return Ok((accounts_status, enc_status));
    }

    let entries = std::fs::read_dir(accounts_dir)
        .map_err(|e| format!("failed_to_read_accounts_dir: {}", e))?;
    for entry in entries {
        let entry = match entry {
            Ok(v) => v,
            Err(e) => {
                accounts_status.parse_failures += 1;
                tracing::warn!("operator_status_accounts_dir_entry_read_failed: {}", e);
                continue;
            }
        };
        let path = entry.path();
        if path.extension().and_then(|s| s.to_str()) != Some("json") {
            continue;
        }
        accounts_status.files_seen += 1;

        let content = match std::fs::read_to_string(&path) {
            Ok(v) => v,
            Err(e) => {
                accounts_status.parse_failures += 1;
                tracing::warn!("operator_status_account_file_read_failed {:?}: {}", path, e);
                continue;
            }
        };
        let value: serde_json::Value = match serde_json::from_str(&content) {
            Ok(v) => v,
            Err(e) => {
                accounts_status.parse_failures += 1;
                tracing::warn!(
                    "operator_status_account_file_parse_failed {:?}: {}",
                    path,
                    e
                );
                continue;
            }
        };

        if bool_field(&value, "disabled") {
            accounts_status.disabled += 1;
        }
        if bool_field(&value, "proxy_disabled") {
            accounts_status.proxy_disabled += 1;
        }
        if !has_device_profile(&value) {
            accounts_status.missing_device_profile += 1;
        }

        let refresh_token = token_field(&value, "refresh_token").unwrap_or_default();
        if refresh_token.trim().is_empty() {
            accounts_status.missing_refresh_token += 1;
        }

        let account_id = as_string_field(&value, "id").unwrap_or_else(|| "<unknown>".to_string());
        let email = as_string_field(&value, "email").unwrap_or_else(|| "<unknown>".to_string());
        for (field, raw_opt) in [
            ("token.access_token", token_field(&value, "access_token")),
            ("token.refresh_token", token_field(&value, "refresh_token")),
        ] {
            let Some(raw) = raw_opt else {
                continue;
            };
            if crate::utils::crypto::is_probably_encrypted_secret(&raw) {
                enc_status.encrypted_secrets_seen += 1;
            }
            if let Err(e) = crate::utils::crypto::preflight_verify_decryptable_secret(&raw) {
                enc_status.decrypt_failures += 1;
                if enc_status.first_failure.is_none() {
                    enc_status.first_failure = Some(OperatorDecryptFailure {
                        account_id: account_id.clone(),
                        email: email.clone(),
                        field: field.to_string(),
                        error: e,
                        file: path.to_string_lossy().to_string(),
                    });
                }
            }
        }
    }

    Ok((accounts_status, enc_status))
}

pub(crate) async fn admin_get_operator_status(
    State(state): State<AdminState>,
) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
    let data_dir = crate::modules::auth::account::get_data_dir().map_err(|e| {
        (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(ErrorResponse { error: e }),
        )
    })?;
    let accounts_dir = data_dir.join("accounts");

    let (accounts, encryption) = scan_accounts_for_operator_status(&accounts_dir)
        .await
        .map_err(|e| {
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ErrorResponse { error: e }),
            )
        })?;

    let proxy_pool_cfg = state.runtime.proxy_pool_state.read().await.clone();
    let proxy_pool = OperatorProxyPoolStatus {
        enabled: proxy_pool_cfg.enabled,
        require_proxy_for_account_requests: proxy_pool_cfg.require_proxy_for_account_requests,
        proxies_total: proxy_pool_cfg.proxies.len(),
        proxies_enabled: proxy_pool_cfg.proxies.iter().filter(|p| p.enabled).count(),
    };

    let oauth = OperatorOAuthConfigStatus {
        client_id_set: env_var_set("GOOGLE_OAUTH_CLIENT_ID"),
        client_secret_set: env_var_set("GOOGLE_OAUTH_CLIENT_SECRET"),
        allowed_domains_set: env_var_set("ALLOWED_GOOGLE_DOMAINS"),
    };
    let oauth_flow = crate::modules::auth::oauth_server::get_oauth_flow_status();

    let is_running = { *state.runtime.is_running.read().await };
    let resp = OperatorStatusResponse {
        timestamp: chrono::Utc::now().to_rfc3339(),
        version: env!("CARGO_PKG_VERSION").to_string(),
        data_dir: data_dir.to_string_lossy().to_string(),
        runtime_port: state.runtime.port,
        runtime_running: is_running,
        tls_backend: crate::utils::http::tls_backend_name().to_string(),
        oauth,
        oauth_flow,
        proxy_pool,
        accounts,
        encryption,
    };
    Ok(Json(resp))
}