unigateway 1.2.1

Lightweight, local-first LLM gateway for developers. A stable, single-binary unified entry point for all your AI tools and models.
use anyhow::{Context, Result, bail};
use std::path::Path;

use crate::config::{GatewayState, ModeKey, ModeProvider, ModeView, build_mode_views};
use crate::types::AppConfig;

pub(crate) fn mask_key(key: &str) -> String {
    if key.len() <= 8 {
        return "****".to_string();
    }
    format!("{}{}", &key[..4], &key[key.len() - 4..])
}

pub(crate) fn format_i64_limit(limit: Option<i64>) -> String {
    limit
        .map(|value| value.to_string())
        .unwrap_or_else(|| "unlimited".to_string())
}

pub(crate) fn format_f64_limit(limit: Option<f64>) -> String {
    limit
        .map(|value| value.to_string())
        .unwrap_or_else(|| "unlimited".to_string())
}

pub(crate) fn user_bind_address(bind: &str) -> String {
    let Some((host, port)) = bind.rsplit_once(':') else {
        return bind.to_string();
    };

    let host = match host {
        "0.0.0.0" | "::" | "[::]" => "127.0.0.1",
        _ => host,
    };
    format!("{host}:{port}")
}

pub(crate) fn user_openai_base_url(bind_override: Option<&str>) -> String {
    let bind = bind_override
        .map(ToOwned::to_owned)
        .unwrap_or_else(|| AppConfig::from_env().bind);
    format!("http://{}/v1", user_bind_address(&bind))
}

pub(crate) fn user_anthropic_base_url(bind_override: Option<&str>) -> String {
    let bind = bind_override
        .map(ToOwned::to_owned)
        .unwrap_or_else(|| AppConfig::from_env().bind);
    format!("http://{}", user_bind_address(&bind))
}

pub(crate) async fn load_mode_views(config_path: &str) -> Result<Vec<ModeView>> {
    let state = GatewayState::load(Path::new(config_path)).await?;
    let default_mode = state.get_default_mode().await.unwrap_or_default();
    let guard = state.inner.read().await;
    Ok(build_mode_views(&guard.file, &default_mode))
}

pub(crate) fn supported_protocols(mode: &ModeView) -> Vec<&'static str> {
    let mut protocols = Vec::new();
    if mode
        .providers
        .iter()
        .any(|provider| provider.is_enabled && provider.provider_type == "openai")
    {
        protocols.push("openai");
    }
    if mode
        .providers
        .iter()
        .any(|provider| provider.is_enabled && provider.provider_type == "anthropic")
    {
        protocols.push("anthropic");
    }
    protocols
}

pub(crate) fn mode_providers_for<'a>(mode: &'a ModeView, protocol: &str) -> Vec<&'a ModeProvider> {
    mode.providers
        .iter()
        .filter(|provider| provider.is_enabled && provider.provider_type == protocol)
        .collect()
}

pub(crate) fn effective_default_mode_id(modes: &[ModeView]) -> Option<&str> {
    modes
        .iter()
        .find(|mode| mode.is_default)
        .map(|mode| mode.id.as_str())
        .or_else(|| {
            modes
                .iter()
                .find(|mode| mode.id == "default")
                .map(|mode| mode.id.as_str())
        })
        .or_else(|| {
            if modes.len() == 1 {
                Some(modes[0].id.as_str())
            } else {
                None
            }
        })
}

pub(crate) fn select_mode<'a>(
    modes: &'a [ModeView],
    requested_mode: Option<&str>,
) -> Result<&'a ModeView> {
    if modes.is_empty() {
        bail!("No modes configured. Run `ug guide` first.")
    }

    if let Some(mode) = requested_mode {
        return modes
            .iter()
            .find(|candidate| candidate.id == mode)
            .with_context(|| format!("mode '{}' not found", mode));
    }

    if let Some(default_mode_id) = effective_default_mode_id(modes) {
        return modes
            .iter()
            .find(|candidate| candidate.id == default_mode_id)
            .with_context(|| format!("default mode '{}' not found", default_mode_id));
    }

    let ids = modes
        .iter()
        .map(|mode| mode.id.as_str())
        .collect::<Vec<_>>()
        .join(", ");
    bail!("multiple modes configured ({ids}); use --mode")
}

pub(crate) fn provider_default_model<'a>(provider: &'a ModeProvider, fallback: &'a str) -> &'a str {
    provider
        .default_model
        .as_deref()
        .filter(|model| !model.is_empty())
        .unwrap_or(fallback)
}

pub(crate) fn pick_mode_key(mode: &ModeView) -> Result<String> {
    let selected: Option<&ModeKey> = mode
        .keys
        .iter()
        .find(|key| key.is_active)
        .or_else(|| mode.keys.first());

    selected.map(|key| key.key.clone()).with_context(|| {
        format!(
            "mode '{}' has no API key; create one with `ug create-api-key`",
            mode.id
        )
    })
}

pub(crate) fn pick_mode_protocol<'a>(
    mode: &'a ModeView,
    requested: Option<&str>,
) -> Result<&'a str> {
    let protocols = supported_protocols(mode);
    if protocols.is_empty() {
        bail!("mode '{}' has no supported providers", mode.id);
    }

    if let Some(requested) = requested {
        let requested = requested.trim().to_ascii_lowercase();
        return protocols
            .into_iter()
            .find(|protocol| *protocol == requested)
            .with_context(|| {
                format!(
                    "mode '{}' does not support protocol '{}'; available: {}",
                    mode.id,
                    requested,
                    supported_protocols(mode).join(", ")
                )
            });
    }

    Ok(protocols[0])
}

pub async fn list_modes(config_path: &str, json: bool) -> Result<()> {
    let modes = load_mode_views(config_path).await?;
    if json {
        println!("{}", serde_json::to_string_pretty(&modes)?);
        return Ok(());
    }

    if modes.is_empty() {
        println!("No modes configured. Run `ug guide` first.");
        return Ok(());
    }

    let default_mode = effective_default_mode_id(&modes).map(ToOwned::to_owned);

    println!("Modes:");
    for mode in modes {
        let protocols = supported_protocols(&mode);
        println!(
            "  - {}{} ({}) routing={} providers={} keys={} protocols={}",
            mode.id,
            if default_mode.as_deref() == Some(mode.id.as_str()) {
                " [default]"
            } else {
                ""
            },
            mode.name,
            mode.routing_strategy,
            mode.providers.len(),
            mode.keys.iter().filter(|key| key.is_active).count(),
            if protocols.is_empty() {
                "none".to_string()
            } else {
                protocols.join(", ")
            }
        );
    }
    Ok(())
}

pub async fn show_mode(config_path: &str, mode_id: &str, json: bool) -> Result<()> {
    let modes = load_mode_views(config_path).await?;
    let default_mode = effective_default_mode_id(&modes).map(ToOwned::to_owned);
    let mode = select_mode(&modes, Some(mode_id))?;

    if json {
        println!("{}", serde_json::to_string_pretty(&mode)?);
        return Ok(());
    }

    println!("Mode: {}", mode.id);
    println!("Name: {}", mode.name);
    println!(
        "Default: {}",
        default_mode.as_deref() == Some(mode.id.as_str())
    );
    println!("Routing: {}", mode.routing_strategy);

    let protocols = supported_protocols(mode);
    println!(
        "Protocols: {}",
        if protocols.is_empty() {
            "none".to_string()
        } else {
            protocols.join(", ")
        }
    );

    if mode.providers.is_empty() {
        println!("Providers: none");
    } else {
        println!("Providers:");
        for provider in &mode.providers {
            println!(
                "  - name={} type={} enabled={} priority={} endpoint={} default_model={} base_url={}",
                provider.name,
                provider.provider_type,
                provider.is_enabled,
                provider.priority,
                provider.endpoint_id.as_deref().unwrap_or("-"),
                provider.default_model.as_deref().unwrap_or("-"),
                provider.base_url.as_deref().unwrap_or("(default)"),
            );
        }
    }

    if mode.keys.is_empty() {
        println!("API Keys: none");
    } else {
        println!("API Keys:");
        for key in &mode.keys {
            println!(
                "  - key={} active={} quota={} qps={} concurrency={}",
                mask_key(&key.key),
                key.is_active,
                format_i64_limit(key.quota_limit),
                format_f64_limit(key.qps_limit),
                format_i64_limit(key.concurrency_limit),
            );
        }
    }

    Ok(())
}

pub async fn use_mode(config_path: &str, mode_id: &str) -> Result<()> {
    let state = GatewayState::load(Path::new(config_path)).await?;
    state.set_default_mode(mode_id).await?;
    state.persist_if_dirty().await?;
    println!("Default mode set to '{}'", mode_id);
    Ok(())
}