pub const NON_CHAT_MARKERS: &[&str] = &[
"embedding",
"embed",
"bge-",
"gte-",
"e5-",
"rerank",
"whisper",
"sensevoice",
"cosyvoice",
"tts",
"asr",
"stable-diffusion",
"sdxl",
"kolors",
"flux",
"image",
"dall-e", "seedream", "seedance", "t2i", "i2v", "t2v", "img2video", "cogvideo", "hailuo", "speech",
"transcribe",
"captioner",
"ocr",
"moderation",
"guard",
];
pub const NON_CHAT_PREFIXES: &[&str] = &["lora/"];
pub fn is_chat_model_id(id: &str) -> bool {
let s = id.trim().to_ascii_lowercase();
if s.is_empty() {
return false;
}
if NON_CHAT_PREFIXES.iter().any(|p| s.starts_with(p)) {
return false;
}
!NON_CHAT_MARKERS.iter().any(|m| s.contains(m))
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct CleanedModels {
pub models: Vec<String>,
pub dropped: usize,
pub dropped_models: Vec<String>,
}
pub fn clean_fetched_models<I, S>(ids: I) -> CleanedModels
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mut unique: Vec<String> = Vec::new();
for id in ids {
let t = id.as_ref().trim();
if t.is_empty() {
continue;
}
if !unique.iter().any(|x| x == t) {
unique.push(t.to_string());
}
}
let kept: Vec<String> = unique
.iter()
.filter(|s| is_chat_model_id(s))
.cloned()
.collect();
if kept.is_empty() {
return CleanedModels {
models: unique,
dropped: 0,
dropped_models: Vec::new(),
};
}
let dropped_models: Vec<String> = unique
.into_iter()
.filter(|s| !is_chat_model_id(s))
.collect();
CleanedModels {
models: kept,
dropped: dropped_models.len(),
dropped_models,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keeps_chat_models_and_drops_others() {
let r = clean_fetched_models([
"deepseek-flash",
"BAAI/bge-large-zh-v1.5",
"Qwen/Qwen3-VL-32B-Instruct", "FunAudioLLM/CosyVoice2-0.5B",
"Kwai-Kolors/Kolors",
]);
assert_eq!(
r.models,
vec!["deepseek-flash", "Qwen/Qwen3-VL-32B-Instruct"]
);
assert_eq!(r.dropped, 3);
assert_eq!(
r.dropped_models,
vec![
"BAAI/bge-large-zh-v1.5",
"FunAudioLLM/CosyVoice2-0.5B",
"Kwai-Kolors/Kolors"
],
"被滤掉的 id 要原样带回,顺序同端点"
);
}
#[test]
fn dedups_before_filtering() {
let r = clean_fetched_models(["gpt-4o", " gpt-4o ", "gpt-4o"]);
assert_eq!(r.models, vec!["gpt-4o"]);
assert_eq!(r.dropped, 0);
}
#[test]
fn returns_original_when_everything_filtered() {
let r = clean_fetched_models(["bge-m3", "text-embedding-3-large"]);
assert_eq!(r.models.len(), 2, "宁可摆出原始清单,也不能给空下拉");
assert_eq!(r.dropped, 0);
assert!(r.dropped_models.is_empty(), "已放回 models,不能再算一遍");
}
#[test]
fn multimodal_chat_models_are_not_dropped() {
assert!(is_chat_model_id("Qwen/Qwen3-VL-32B-Instruct"));
assert!(is_chat_model_id("Qwen3-Omni-30B-A3B-Instruct"));
assert!(!is_chat_model_id("Qwen3-Omni-30B-A3B-Captioner"));
assert!(!is_chat_model_id("Qwen/Qwen-Image-Edit-2509"));
}
#[test]
fn non_chat_presets_are_filtered() {
use crate::kind::Kind;
let mut leaked = Vec::new();
let mut hurt = Vec::new();
for p in crate::preset::presets() {
let ids = p.models.iter().map(|m| m.value).chain([p.model]);
for id in ids.filter(|id| !id.is_empty()) {
match (p.kind, is_chat_model_id(id)) {
(Kind::Chat, false) => hurt.push(format!("{}: {id}", p.key)),
(Kind::Chat, true) => {}
(_, true) => leaked.push(format!("{}: {id}", p.key)),
(_, false) => {}
}
}
}
assert!(
leaked.is_empty(),
"非对话模型漏进了对话清单,补特征词:{leaked:#?}"
);
assert!(hurt.is_empty(), "对话模型被误滤,特征词太宽:{hurt:#?}");
}
#[test]
fn new_markers_do_not_hurt_chat_models() {
for id in [
"doubao-seed-1-6-250615", "gpt-4o-audio-preview", "gpt-4o-realtime-preview",
"Qwen/Qwen2.5-VL-72B-Instruct",
"glm-4.5v",
] {
assert!(is_chat_model_id(id), "{id} 是对话模型,不能被滤掉");
}
for id in ["gpt-4o-transcribe", "gpt-4o-mini-transcribe"] {
assert!(!is_chat_model_id(id), "{id} 是语音转写模型");
}
assert!(is_chat_model_id("gpt-4o-search-preview"));
}
}