libnoa 0.4.0

AI-native distributed version control
Documentation
use std::collections::HashMap;

#[derive(Debug, Clone, Default)]
pub struct ProviderIdentity {
    pub display_name: Option<String>,
    pub provider_id: String,
}

#[derive(Debug, Clone, Default)]
pub struct ProviderMap {
    pub by_model: HashMap<String, ProviderIdentity>,
}

impl ProviderMap {
    pub fn builtin() -> Self {
        let mut by_model: HashMap<String, ProviderIdentity> = HashMap::new();
        let mut add = |model_prefix: &str, display: &str, provider: &str| {
            by_model.insert(
                model_prefix.to_string(),
                ProviderIdentity {
                    display_name: Some(display.to_string()),
                    provider_id: provider.to_string(),
                },
            );
        };
        add("glm", "GLM", "zhipuai.cn");
        add("deepseek", "Deepseek", "deepseek.com");
        add("claude", "Claude", "anthropic.com");
        add("gpt", "GPT", "openai.com");
        add("o1", "OpenAI", "openai.com");
        add("o3", "OpenAI", "openai.com");
        add("o4", "OpenAI", "openai.com");
        add("qwen", "Qwen", "dashscope.aliyuncs.com");
        add("gemini", "Gemini", "google.com");
        add("llama", "Llama", "meta.com");
        add("mistral", "Mistral", "mistral.ai");
        add("moonshot", "Kimi", "moonshot.cn");
        add("kimi", "Kimi", "moonshot.cn");
        add("yi", "Yi", "01.ai");
        add("baichuan", "Baichuan", "baichuan-ai.com");
        add("spark", "Spark", "xfyun.cn");
        add("ernie", "ERNIE", "baidu.com");
        add("hunyuan", "Hunyuan", "tencent.com");
        add("doubao", "Doubao", "volcengine.com");
        add("ark", "Doubao", "volcengine.com");
        Self { by_model }
    }

    pub fn merge_aporia(&mut self, providers: &[AporiaProviderEntry]) {
        for p in providers {
            if p.model.is_empty() {
                continue;
            }
            let provider_id = if !p.website_domain.is_empty() {
                p.website_domain.clone()
            } else {
                tracing::debug!(
                    model = %p.model,
                    provider = %p.name,
                    "aporia provider has no website_domain; skipping co-author attribution (website_domain is mandatory)"
                );
                continue;
            };
            let display = derive_display_name(&p.model, &provider_id);
            self.by_model.insert(
                p.model.clone(),
                ProviderIdentity {
                    display_name: Some(display),
                    provider_id,
                },
            );
        }
    }
}

#[derive(Debug, Clone)]
pub struct AporiaProviderEntry {
    pub name: String,
    pub model: String,
    pub endpoint: String,
    pub website_domain: String,
}

pub fn resolve_provider(model_id: &str, map: &ProviderMap) -> Option<ProviderIdentity> {
    let lower = model_id.to_ascii_lowercase();
    if let Some(id) = map
        .by_model
        .get(model_id)
        .or_else(|| map.by_model.get(&lower))
    {
        return Some(id.clone());
    }
    for (key, id) in &map.by_model {
        if lower.starts_with(key) {
            return Some(id.clone());
        }
    }
    None
}

pub fn endpoint_to_provider_id(endpoint: &str) -> Option<String> {
    let host = endpoint
        .split("://")
        .nth(1)
        .unwrap_or(endpoint)
        .split('/')
        .next()?
        .split(':')
        .next()?;
    let host = host.to_ascii_lowercase();
    let bare = host.trim_start_matches("api.").trim_start_matches("www.");
    let third_party = [
        "opencode.ai",
        "openrouter.ai",
        "jdcloud.com",
        "together.xyz",
        "together.ai",
        "fireworks.ai",
        "groq.com",
        "replicate.com",
        "anyscale.com",
        "lepton.ai",
        "siliconflow.cn",
        "volcengine.com",
        "dashscope.aliyuncs.com",
        "bigmodel.cn",
    ];
    if third_party.contains(&bare) {
        return Some(bare.to_string());
    }
    let first_party = [
        "deepseek.com",
        "anthropic.com",
        "openai.com",
        "google.com",
        "googleapis.com",
        "zhipuai.cn",
        "mistral.ai",
        "meta.com",
        "moonshot.cn",
        "01.ai",
        "baichuan-ai.com",
        "xfyun.cn",
        "baidu.com",
        "tencent.com",
        "dashscope.aliyuncs.com",
    ];
    if first_party.contains(&bare) {
        return Some(normalize_first_party(bare));
    }
    if bare == "bigmodel.cn" {
        return Some("zhipuai.cn".to_string());
    }
    None
}

fn normalize_first_party(host: &str) -> String {
    match host {
        "googleapis.com" => "google.com".to_string(),
        "dashscope.aliyuncs.com" => "dashscope.aliyuncs.com".to_string(),
        other => other.to_string(),
    }
}

pub fn derive_provider_id_from_model(model_id: &str) -> Option<String> {
    let lower = model_id.to_ascii_lowercase();
    let prefixes: &[(&str, &str)] = &[
        ("glm", "zhipuai.cn"),
        ("deepseek", "deepseek.com"),
        ("claude", "anthropic.com"),
        ("gpt", "openai.com"),
        ("qwen", "dashscope.aliyuncs.com"),
        ("gemini", "google.com"),
        ("llama", "meta.com"),
        ("mistral", "mistral.ai"),
        ("mixtral", "mistral.ai"),
        ("moonshot", "moonshot.cn"),
        ("kimi", "moonshot.cn"),
        ("yi", "01.ai"),
        ("baichuan", "baichuan-ai.com"),
        ("spark", "xfyun.cn"),
        ("ernie", "baidu.com"),
        ("wenxin", "baidu.com"),
        ("hunyuan", "tencent.com"),
        ("doubao", "volcengine.com"),
    ];
    for (prefix, provider) in prefixes {
        if lower.starts_with(prefix) {
            return Some((*provider).to_string());
        }
    }
    None
}

pub fn derive_display_name(model_id: &str, provider_id: &str) -> String {
    let lower = model_id.to_ascii_lowercase();
    let brand = match provider_id {
        "zhipuai.cn" => "GLM",
        "deepseek.com" => "Deepseek",
        "anthropic.com" => "Claude",
        "openai.com" => {
            if lower.starts_with("o1") || lower.starts_with("o3") || lower.starts_with("o4") {
                "OpenAI"
            } else {
                "GPT"
            }
        }
        "google.com" => "Gemini",
        "meta.com" => "Llama",
        "mistral.ai" => "Mistral",
        "moonshot.cn" => "Kimi",
        "01.ai" => "Yi",
        "baichuan-ai.com" => "Baichuan",
        "xfyun.cn" => "Spark",
        "baidu.com" => "ERNIE",
        "tencent.com" => "Hunyuan",
        "volcengine.com" => "Doubao",
        "dashscope.aliyuncs.com" => "Qwen",
        _ => "",
    };
    let rest_raw = if !brand.is_empty() {
        lower.strip_prefix(&lower.chars().take(brand.len()).collect::<String>())
    } else {
        None
    };
    let rest = rest_raw.unwrap_or(&lower);
    let rest = rest.trim_start_matches('-').trim_start_matches('_');
    let mut parts: Vec<String> = Vec::new();
    if !brand.is_empty() {
        parts.push(brand.to_string());
    }
    for chunk in rest.split(['-', '_', '.']) {
        if chunk.is_empty() {
            continue;
        }
        if let Ok(num) = chunk.parse::<f64>() {
            parts.push(format_trimmed_number(num));
        } else {
            parts.push(capitalize_chunk(chunk));
        }
    }
    if parts.is_empty() {
        capitalize_chunk(model_id)
    } else {
        parts.join(" ")
    }
}

fn format_trimmed_number(n: f64) -> String {
    let s = format!("{}", n);
    s
}

fn capitalize_chunk(chunk: &str) -> String {
    let mut out = String::with_capacity(chunk.len());
    for (i, c) in chunk.chars().enumerate() {
        if i == 0 {
            out.extend(c.to_uppercase());
        } else {
            out.extend(c.to_lowercase());
        }
    }
    if out.is_empty() {
        chunk.to_string()
    } else {
        out
    }
}