use serde::Serialize;
use super::{presets, ProviderPreset};
use crate::kind::Kind;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Vendor {
pub id: &'static str,
pub label: &'static str,
pub group_key: &'static str,
pub group_label: &'static str,
pub kinds: Vec<Kind>,
pub preset_keys: Vec<&'static str>,
pub host: Option<&'static str>,
pub apply_url: Option<&'static str>,
pub is_local: bool,
}
fn host_of(base_url: Option<&'static str>) -> Option<&'static str> {
let url = base_url?;
let rest = url
.strip_prefix("https://")
.or_else(|| url.strip_prefix("http://"))?;
Some(match rest.find('/') {
Some(i) => &rest[..i],
None => rest,
})
}
pub fn vendors(allow_kinds: &[Kind]) -> Vec<Vendor> {
vendors_in(presets(), allow_kinds)
}
pub fn vendors_in(list: &[ProviderPreset], allow_kinds: &[Kind]) -> Vec<Vendor> {
let mut out: Vec<Vendor> = Vec::new();
for p in list {
if !allow_kinds.contains(&p.kind) {
continue;
}
match out.iter_mut().find(|v| v.id == p.vendor_id) {
Some(v) => {
if !v.kinds.contains(&p.kind) {
v.kinds.push(p.kind);
}
v.preset_keys.push(p.key);
}
None => out.push(Vendor {
id: p.vendor_id,
label: p.label,
group_key: p.group_key,
group_label: p.group_label,
kinds: vec![p.kind],
preset_keys: vec![p.key],
host: host_of(p.base_url),
apply_url: p.apply_url,
is_local: p.is_local,
}),
}
}
out
}
pub fn vendors_all() -> Vec<Vendor> {
vendors(ALL_KINDS)
}
const ALL_KINDS: &[Kind] = &[
Kind::Chat,
#[cfg(feature = "image")]
Kind::Image,
#[cfg(feature = "video")]
Kind::Video,
#[cfg(feature = "tts")]
Kind::Tts,
];
pub fn presets_of_vendor(vendor_id: &str) -> impl Iterator<Item = &'static ProviderPreset> + '_ {
presets().iter().filter(move |p| p.vendor_id == vendor_id)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn vendor_ids_consistent() {
for p in presets() {
let host = host_of(p.base_url);
for q in presets_of_vendor(p.vendor_id) {
let qh = host_of(q.base_url);
if let (Some(a), Some(b)) = (host, qh) {
assert_eq!(
a, b,
"vendor_id={} 下 {} 与 {} 的 host 不一致({} vs {})",
p.vendor_id, p.key, q.key, a, b
);
}
}
}
}
#[test]
fn vendors_filtered_by_kind() {
let all = vendors(&[Kind::Chat]);
assert!(!all.is_empty(), "chat 应当有厂商");
for v in &all {
assert!(!v.preset_keys.is_empty(), "{} 没有任何预置", v.id);
assert!(v.kinds.contains(&Kind::Chat));
}
assert!(vendors(&[]).is_empty());
}
#[test]
fn local_vendors_are_marked() {
let v = vendors(&[Kind::Chat]);
let ollama = v.iter().find(|x| x.id == "ollama").expect("应有 ollama");
assert!(ollama.is_local);
assert!(ollama.apply_url.is_none(), "本地服务不需要申请密钥");
let ds = v
.iter()
.find(|x| x.id == "deepseek")
.expect("应有 deepseek");
assert!(!ds.is_local);
assert!(ds.apply_url.is_some(), "云端服务应给申请入口");
}
#[test]
fn host_extraction() {
assert_eq!(
host_of(Some("https://api.deepseek.com/v1")),
Some("api.deepseek.com")
);
assert_eq!(
host_of(Some("http://localhost:11434/v1")),
Some("localhost:11434")
);
assert_eq!(
host_of(Some("https://open.bigmodel.cn/api/paas/v4")),
Some("open.bigmodel.cn")
);
assert_eq!(host_of(None), None);
}
}