AkurAI Build
Menu

Public repository / 7c142d14d18427ae50a82404fa1f44af54d36fae

AkurAI-Router / src/cli.rs

Latest change dbe1df51d5c4a984bc453b213992c6f6c3f78606 - Split admin UI into sidebar sections by Ólafur Búi Ólafsson

use std::env;
use std::path::PathBuf;

use crate::accounts;
use crate::auth;
use crate::config::{
    Config, Model, PROVIDER_CLAUDE, PROVIDER_CODEX, PROVIDER_OPENCODE_GO, Provider,
    canonical_provider_id, default_provider_auth_path, ensure_default_files,
    infer_model_provider_id, load_models, load_providers, provider_display_name, save_models,
    save_providers, write_local_env_template,
};
use crate::http::{self, Request};
use crate::util::{env_quote, random_hex};
use crate::{landing, oauth, upstream};

pub fn run() -> Result<(), String> {
    let args: Vec<String> = env::args().collect();
    let cmd = args.get(1).map(|s| s.as_str()).unwrap_or("serve");
    match cmd {
        "serve" => serve(),
        "init" => init(),
        "key" => key(&args[2..]),
        "providers" => providers(&args[2..]),
        "models" => models(&args[2..]),
        "idp" => idp(&args[2..]),
        "help" | "-h" | "--help" => {
            print_help();
            Ok(())
        }
        "version" | "--version" | "-V" => {
            println!("akurai-router {}", env!("CARGO_PKG_VERSION"));
            Ok(())
        }
        other => Err(format!(
            "unknown command `{other}`; run `akurai-router help`"
        )),
    }
}

fn serve() -> Result<(), String> {
    let cfg = Config::load()?;
    ensure_default_files(&cfg)?;
    accounts::ensure_account_files(&cfg)?;
    cfg.validate_for_serve()?;
    let addr = cfg.listen_addr.clone();
    http::serve(&addr, move |req, stream| dispatch(req, stream, &cfg))
}

fn dispatch(req: Request, stream: &mut std::net::TcpStream, cfg: &Config) {
    if req.method == "OPTIONS" {
        let _ = http::no_content(stream);
        return;
    }
    match (req.method.as_str(), req.path.as_str()) {
        ("GET", "/") => landing::landing(&req, stream, cfg),
        ("GET", "/assets/hero.png") => landing::hero(stream),
        ("GET", "/assets/router-og.png") => landing::router_og(stream),
        ("GET", "/favicon.svg") => landing::favicon(stream),
        ("GET", "/apple-touch-icon.png") => landing::apple_touch_icon(stream),
        ("GET", "/health") => {
            let _ = http::send_json(stream, 200, "{\"ok\":true,\"service\":\"akurai-router\"}");
        }
        ("GET", "/login") => oauth::login(&req, stream, cfg),
        ("GET", "/auth/callback") => oauth::callback(&req, stream, cfg),
        ("GET", "/logout") => landing::logout(&req, stream, cfg),
        ("GET", p) if p == "/admin" || p.starts_with("/admin/") => {
            landing::admin(&req, stream, cfg)
        }
        ("POST", p) if p.starts_with("/admin/") => landing::admin_post(&req, stream, cfg),
        ("GET", "/v1/models") | ("GET", "/api/v1/models") => {
            if auth::authenticate_api_key(&req, cfg).is_none() {
                let _ =
                    http::send_json(stream, 401, "{\"error\":{\"message\":\"invalid API key\"}}");
                return;
            }
            let _ = http::send_json(stream, 200, &upstream::models_json(cfg));
        }
        ("POST", "/v1/embeddings") | ("POST", "/api/v1/embeddings") => {
            let Some(actor) = auth::authenticate_api_key(&req, cfg) else {
                let _ =
                    http::send_json(stream, 401, "{\"error\":{\"message\":\"invalid API key\"}}");
                return;
            };
            upstream::forward_embeddings(&req, stream, cfg, &actor);
        }
        ("POST", "/v1/responses")
        | ("POST", "/api/v1/responses")
        | ("POST", "/responses")
        | ("POST", "/codex")
        | ("POST", "/v1/chat/completions")
        | ("POST", "/api/v1/chat/completions") => {
            let Some(actor) = auth::authenticate_api_key(&req, cfg) else {
                let _ =
                    http::send_json(stream, 401, "{\"error\":{\"message\":\"invalid API key\"}}");
                return;
            };
            upstream::forward_model(&req, stream, cfg, &actor);
        }
        _ => {
            let _ = http::send_text(stream, 404, "text/plain", "not found");
        }
    }
}

fn init() -> Result<(), String> {
    let cfg = Config::load()?;
    let path = write_local_env_template(&cfg)?;
    ensure_default_files(&cfg)?;
    accounts::ensure_account_files(&cfg)?;
    println!("wrote {}", path.display());
    Ok(())
}

fn key(args: &[String]) -> Result<(), String> {
    match args.first().map(|s| s.as_str()) {
        Some("generate") | None => {
            println!("akr_{}", random_hex(32));
            Ok(())
        }
        _ => Err("usage: akurai-router key generate".to_string()),
    }
}

fn providers(args: &[String]) -> Result<(), String> {
    let cfg = Config::load()?;
    ensure_default_files(&cfg)?;
    match args.first().map(|s| s.as_str()) {
        Some("list") | None => {
            for p in load_providers(&cfg) {
                println!(
                    "{}\t{}\t{}\t{}",
                    p.id,
                    p.name,
                    if p.enabled { "enabled" } else { "disabled" },
                    p.auth_path.display()
                );
            }
            Ok(())
        }
        Some("add") => {
            let id = canonical_provider_id(args.get(1).map(|s| s.as_str()).unwrap_or("codex"));
            if ![PROVIDER_CODEX, PROVIDER_CLAUDE, PROVIDER_OPENCODE_GO]
                .contains(&id.as_str())
            {
                return Err("provider id must be codex, claude, or opencode-go".to_string());
            }
            let auth_path = args
                .iter()
                .position(|s| s == "--auth-path")
                .and_then(|i| args.get(i + 1))
                .map(PathBuf::from)
                .unwrap_or_else(|| default_provider_auth_path(&cfg, &id));
            let mut providers = load_providers(&cfg);
            providers.retain(|p| canonical_provider_id(&p.id) != id);
            providers.push(Provider {
                id: id.clone(),
                name: provider_display_name(&id).to_string(),
                enabled: true,
                auth_path,
            });
            save_providers(&cfg, &providers)
        }
        Some("disable") => set_provider_enabled(&cfg, false, args.get(1).map(|s| s.as_str())),
        Some("enable") => set_provider_enabled(&cfg, true, args.get(1).map(|s| s.as_str())),
        _ => Err(
            "usage: akurai-router providers [list|add codex|claude|opencode-go --auth-path PATH|enable [ID]|disable [ID]]"
                .to_string(),
        ),
    }
}

fn set_provider_enabled(
    cfg: &Config,
    enabled: bool,
    provider_id: Option<&str>,
) -> Result<(), String> {
    let mut providers = load_providers(cfg);
    let target = canonical_provider_id(provider_id.unwrap_or("codex"));
    for provider in &mut providers {
        if canonical_provider_id(&provider.id) == target {
            provider.enabled = enabled;
        }
    }
    save_providers(cfg, &providers)
}

fn models(args: &[String]) -> Result<(), String> {
    let cfg = Config::load()?;
    ensure_default_files(&cfg)?;
    match args.first().map(|s| s.as_str()) {
        Some("list") | None => {
            for m in load_models(&cfg) {
                println!(
                    "{}\t{}\t{}\t{}\t{}",
                    m.id,
                    m.name,
                    m.upstream_id,
                    m.provider_id,
                    if m.enabled { "enabled" } else { "disabled" }
                );
            }
            Ok(())
        }
        Some("add") => {
            let id = args.get(1).ok_or_else(|| "model id required".to_string())?.clone();
            let name = args.get(2).cloned().unwrap_or_else(|| id.clone());
            let upstream_id = args.get(3).cloned().unwrap_or_else(|| id.clone());
            let provider_id = args
                .get(4)
                .map(|s| canonical_provider_id(s))
                .unwrap_or_else(|| infer_model_provider_id(&id));
            let mut models = load_models(&cfg);
            models.retain(|m| m.id != id);
            models.push(Model {
                id,
                name,
                upstream_id,
                provider_id,
                enabled: true,
            });
            save_models(&cfg, &models)
        }
        Some("remove") => {
            let id = args.get(1).ok_or_else(|| "model id required".to_string())?;
            let mut models = load_models(&cfg);
            models.retain(|m| &m.id != id);
            save_models(&cfg, &models)
        }
        Some("enable") | Some("disable") => {
            let enabled = args[0] == "enable";
            let id = args.get(1).ok_or_else(|| "model id required".to_string())?;
            let mut models = load_models(&cfg);
            for m in &mut models {
                if &m.id == id {
                    m.enabled = enabled;
                }
            }
            save_models(&cfg, &models)
        }
        _ => Err("usage: akurai-router models [list|add ID [NAME] [UPSTREAM] [PROVIDER]|remove ID|enable ID|disable ID]".to_string()),
    }
}

fn idp(args: &[String]) -> Result<(), String> {
    let cfg = Config::load()?;
    match args.first().map(|s| s.as_str()) {
        Some("client-json") | None => {
            println!(
                "{{\"name\":\"AkurAI Router\",\"tenant_id\":\"<tenant-id>\",\"redirect_uris\":[\"{}\"],\"grant_types\":[\"authorization_code\",\"refresh_token\"],\"scopes\":[\"openid\",\"profile\",\"email\",\"groups\"],\"first_party\":true}}",
                cfg.callback_url()
            );
            Ok(())
        }
        Some("env") => {
            println!("AKURAI_ROUTER_IDP_ISSUER={}", env_quote(&cfg.idp_issuer));
            println!(
                "AKURAI_ROUTER_IDP_CLIENT_ID={}",
                env_quote(&cfg.idp_client_id)
            );
            println!(
                "AKURAI_ROUTER_IDP_CLIENT_SECRET={}",
                env_quote(&cfg.idp_client_secret)
            );
            println!(
                "AKURAI_ROUTER_ADMIN_EMAIL={}",
                env_quote(&cfg.admin_allowed_email)
            );
            Ok(())
        }
        _ => Err("usage: akurai-router idp [client-json|env]".to_string()),
    }
}

fn print_help() {
    println!(
        r#"akurai-router {}

Usage:
  akurai-router serve
  akurai-router init
  akurai-router key generate
  akurai-router providers list
  akurai-router providers add codex --auth-path ~/.codex/auth.json
  akurai-router providers add claude --auth-path ~/.claude/.credentials.json
  akurai-router providers add opencode-go --auth-path ~/.local/share/opencode/auth.json
  akurai-router providers enable [ID]
  akurai-router providers disable [ID]
  akurai-router models list
  akurai-router models add ID [NAME] [UPSTREAM_ID] [PROVIDER_ID]
  akurai-router models remove|enable|disable ID
  akurai-router idp client-json

Environment:
  AKURAI_ROUTER_LISTEN=127.0.0.1:4219
  AKURAI_ROUTER_PUBLIC_URL=https://akurai-router.olibuijr.com
  AKURAI_ROUTER_API_KEY=akr_...
  AKURAI_ROUTER_COOKIE_SECRET=...
  AKURAI_ROUTER_CODEX_AUTH_PATH=/home/ubuntu/.codex/auth.json
  AKURAI_ROUTER_CLAUDE_AUTH_PATH=/home/olafurbui/.claude/.credentials.json
  AKURAI_ROUTER_OPENCODE_GO_AUTH_PATH=/home/olafurbui/.local/share/opencode/auth.json
  AKURAI_ROUTER_IDP_ISSUER=https://auth.olibuijr.com
  AKURAI_ROUTER_EMBEDDINGS_URL=http://127.0.0.1:8081/v1/embeddings
  AKURAI_ROUTER_EMBEDDINGS_MODEL=intfloat/multilingual-e5-small
  AKURAI_ROUTER_EMBEDDINGS_ENABLED=true
  AKURAI_ROUTER_IDP_CLIENT_ID=...
  AKURAI_ROUTER_IDP_CLIENT_SECRET=...
  AKURAI_ROUTER_ADMIN_EMAIL=olibuijr@olibuijr.com
"#,
        env!("CARGO_PKG_VERSION")
    );
}