unigateway 1.3.0

Lightweight, local-first LLM gateway for developers. A stable, single-binary unified entry point for all your AI tools and models.
use std::sync::Arc;

use serde::Serialize;
use unigateway_core::UniGatewayEngine;
use unigateway_host::host::HostEnvProvider;

use crate::config::GatewayState;
use crate::config::core_sync::sync_core_pools;

pub fn default_config_path() -> String {
    let dir = dirs::config_dir()
        .unwrap_or_else(|| std::path::PathBuf::from("."))
        .join("unigateway");
    dir.join("config.toml").to_string_lossy().into_owned()
}

#[derive(Debug, Clone)]
pub struct AppConfig {
    pub bind: String,
    pub config_path: String,
    pub admin_token: String,
    pub openai_base_url: String,
    pub openai_api_key: String,
    pub openai_model: String,
    pub anthropic_base_url: String,
    pub anthropic_api_key: String,
    pub anthropic_model: String,
}

impl AppConfig {
    pub fn from_env() -> Self {
        let bind = std::env::var("UNIGATEWAY_BIND").unwrap_or_else(|_| {
            std::env::var("PORT")
                .map(|port| format!("0.0.0.0:{port}"))
                .unwrap_or_else(|_| "127.0.0.1:3210".to_string())
        });

        Self {
            bind,
            config_path: std::env::var("UNIGATEWAY_CONFIG")
                .unwrap_or_else(|_| default_config_path()),
            admin_token: std::env::var("UNIGATEWAY_ADMIN_TOKEN").unwrap_or_default(),
            openai_base_url: std::env::var("OPENAI_BASE_URL")
                .unwrap_or_else(|_| "https://api.openai.com".to_string()),
            openai_api_key: std::env::var("OPENAI_API_KEY").unwrap_or_default(),
            openai_model: std::env::var("OPENAI_MODEL")
                .unwrap_or_else(|_| "gpt-4o-mini".to_string()),
            anthropic_base_url: std::env::var("ANTHROPIC_BASE_URL")
                .unwrap_or_else(|_| "https://api.anthropic.com".to_string()),
            anthropic_api_key: std::env::var("ANTHROPIC_API_KEY").unwrap_or_default(),
            anthropic_model: std::env::var("ANTHROPIC_MODEL")
                .unwrap_or_else(|_| "claude-3-5-sonnet-latest".to_string()),
        }
    }
}

#[derive(Clone)]
pub(crate) struct AppState {
    pub config: AppConfig,
    pub gateway: Arc<GatewayState>,
    pub core_engine: Arc<UniGatewayEngine>,
}

#[derive(Clone)]
pub(crate) struct GatewayRequestState {
    bind: String,
    openai_base_url: String,
    openai_api_key: String,
    openai_model: String,
    anthropic_base_url: String,
    anthropic_api_key: String,
    anthropic_model: String,
    gateway: Arc<GatewayState>,
    core_engine: Arc<UniGatewayEngine>,
}

#[derive(Clone)]
pub(crate) struct SystemState {
    openai_model: String,
    anthropic_model: String,
    gateway: Arc<GatewayState>,
}

impl AppState {
    pub fn new(config: AppConfig, gateway: Arc<GatewayState>) -> Self {
        let core_engine = Arc::new(
            UniGatewayEngine::builder()
                .with_builtin_http_drivers()
                .with_hooks(Arc::new(crate::telemetry::GatewayTelemetryHooks))
                .build()
                .expect("Failed to initialize core engine"),
        );

        Self {
            config,
            gateway,
            core_engine,
        }
    }

    pub fn admin_token(&self) -> &str {
        self.config.admin_token.as_str()
    }

    pub fn engine(&self) -> &UniGatewayEngine {
        self.core_engine.as_ref()
    }

    pub async fn sync_core_pools(&self) -> anyhow::Result<()> {
        sync_core_pools(self.gateway.as_ref(), self.engine()).await
    }
}

impl GatewayRequestState {
    pub fn from_app_state(state: &AppState) -> Self {
        Self {
            bind: state.config.bind.clone(),
            openai_base_url: state.config.openai_base_url.clone(),
            openai_api_key: state.config.openai_api_key.clone(),
            openai_model: state.config.openai_model.clone(),
            anthropic_base_url: state.config.anthropic_base_url.clone(),
            anthropic_api_key: state.config.anthropic_api_key.clone(),
            anthropic_model: state.config.anthropic_model.clone(),
            gateway: state.gateway.clone(),
            core_engine: state.core_engine.clone(),
        }
    }

    pub fn gateway(&self) -> &GatewayState {
        self.gateway.as_ref()
    }

    pub fn engine(&self) -> &UniGatewayEngine {
        self.core_engine.as_ref()
    }

    pub fn is_local_bind(&self) -> bool {
        self.bind.starts_with("127.0.0.1") || self.bind.starts_with("localhost")
    }

    pub fn provider_base_url(&self, provider: HostEnvProvider) -> &str {
        match provider {
            HostEnvProvider::OpenAi => self.openai_base_url.as_str(),
            HostEnvProvider::Anthropic => self.anthropic_base_url.as_str(),
        }
    }

    pub fn provider_api_key(&self, provider: HostEnvProvider) -> &str {
        match provider {
            HostEnvProvider::OpenAi => self.openai_api_key.as_str(),
            HostEnvProvider::Anthropic => self.anthropic_api_key.as_str(),
        }
    }

    pub fn provider_model(&self, provider: HostEnvProvider) -> &str {
        match provider {
            HostEnvProvider::OpenAi => self.openai_model.as_str(),
            HostEnvProvider::Anthropic => self.anthropic_model.as_str(),
        }
    }
}

impl SystemState {
    pub fn from_app_state(state: &AppState) -> Self {
        Self {
            openai_model: state.config.openai_model.clone(),
            anthropic_model: state.config.anthropic_model.clone(),
            gateway: state.gateway.clone(),
        }
    }

    pub fn gateway(&self) -> &GatewayState {
        self.gateway.as_ref()
    }

    pub fn provider_model(&self, provider: HostEnvProvider) -> &str {
        match provider {
            HostEnvProvider::OpenAi => self.openai_model.as_str(),
            HostEnvProvider::Anthropic => self.anthropic_model.as_str(),
        }
    }
}

#[derive(Serialize)]
pub(crate) struct ModelList {
    pub object: &'static str,
    pub data: Vec<ModelItem>,
}

#[derive(Serialize)]
pub(crate) struct ModelItem {
    pub id: String,
    pub object: &'static str,
    pub created: i64,
    pub owned_by: &'static str,
}