kernel/discovery/
modality_hints.rs1use std::collections::BTreeSet;
8use std::path::Path;
9
10use crate::records::{Capability, ExecutionMode, JsonValue, Modality};
11use crate::resolution::pipelines::{PipelineFamilyRegistry, diffusers_pipeline_class};
12
13#[derive(Debug, Clone, PartialEq)]
15pub struct Hint {
16 pub modality: Option<Modality>,
18 pub capabilities: Vec<Capability>,
20 pub execution: ExecutionMode,
22 pub context_length: Option<i64>,
24 pub quantization: Option<String>,
26}
27
28impl Hint {
29 fn new(modality: Modality, capabilities: Vec<Capability>, execution: ExecutionMode) -> Self {
30 Self {
31 modality: Some(modality),
32 capabilities,
33 execution,
34 context_length: None,
35 quantization: None,
36 }
37 }
38
39 pub fn unknown(execution: ExecutionMode) -> Self {
42 Self {
43 modality: None,
44 capabilities: Vec::new(),
45 execution,
46 context_length: None,
47 quantization: None,
48 }
49 }
50}
51
52pub fn speech_hint() -> Hint {
54 Hint::new(
55 Modality::speech(),
56 vec![Capability::speak()],
57 ExecutionMode::Stream,
58 )
59}
60
61pub fn audio_hint() -> Hint {
63 Hint::new(
64 Modality::audio(),
65 vec![Capability::transcribe()],
66 ExecutionMode::Stream,
67 )
68}
69
70pub fn text_hint() -> Hint {
72 Hint::new(
73 Modality::text(),
74 vec![Capability::chat(), Capability::complete()],
75 ExecutionMode::Stream,
76 )
77}
78
79pub fn embedding_hint() -> Hint {
81 Hint::new(
82 Modality::embedding(),
83 vec![Capability::embed()],
84 ExecutionMode::Stream,
85 )
86}
87
88pub fn reranker_hint() -> Hint {
92 Hint::new(Modality::text(), Vec::new(), ExecutionMode::Stream)
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum SentenceTransformersLayout {
98 Embedder,
100 CrossEncoder,
102}
103
104pub fn sentence_transformers_layout(dir: &Path) -> Option<SentenceTransformersLayout> {
108 let config = dir.join("config_sentence_transformers.json");
109 let declared = std::fs::read(&config)
110 .ok()
111 .and_then(|bytes| serde_json::from_slice::<JsonValue>(&bytes).ok())
112 .and_then(|json| {
113 json.as_object()?
114 .get("model_type")
115 .and_then(JsonValue::as_str)
116 .map(str::to_owned)
117 });
118 if declared.as_deref() == Some("CrossEncoder") {
119 return Some(SentenceTransformersLayout::CrossEncoder);
120 }
121 (config.exists() || dir.join("1_Pooling").exists())
122 .then_some(SentenceTransformersLayout::Embedder)
123}
124
125pub fn vision_chat_hint() -> Hint {
127 Hint::new(
128 Modality::text(),
129 vec![
130 Capability::chat(),
131 Capability::complete(),
132 Capability::see(),
133 ],
134 ExecutionMode::Stream,
135 )
136}
137
138pub fn gguf_hint() -> Hint {
140 text_hint()
141}
142
143pub fn whisper_bin_hint() -> Hint {
145 audio_hint()
146}
147
148pub fn from_model_index(path: &Path) -> Hint {
152 if let Some(class) = diffusers_pipeline_class(path)
153 && let Some(family) = PipelineFamilyRegistry::shared().family(&class)
154 {
155 return Hint {
156 modality: Some(family.modality.clone()),
157 capabilities: family.capabilities.clone(),
158 execution: ExecutionMode::Job,
159 context_length: None,
160 quantization: None,
161 };
162 }
163 Hint::unknown(ExecutionMode::Job)
164}
165
166const VISION_LANGUAGE_ARCHITECTURES: [&str; 5] =
169 ["Llava", "Qwen2VL", "Idefics", "PaliGemma", "Mllama"];
170
171pub fn from_config_json(path: &Path) -> Option<Hint> {
174 let bytes = std::fs::read(path).ok()?;
175 let json = serde_json::from_slice::<JsonValue>(&bytes).ok()?;
176 from_config(&json)
177}
178
179pub fn from_config(json: &JsonValue) -> Option<Hint> {
184 let object = json.as_object()?;
185 let architectures: Vec<&str> = object
186 .get("architectures")
187 .and_then(JsonValue::as_array)
188 .map(|items| items.iter().filter_map(JsonValue::as_str).collect())
189 .unwrap_or_default();
190
191 let context_length = ["max_position_embeddings", "n_positions", "max_seq_len"]
194 .into_iter()
195 .find_map(|key| object.get(key).and_then(JsonValue::as_i64))
196 .filter(|value| *value > 0);
197 let quantization = object
200 .get("quantization")
201 .and_then(JsonValue::as_object)
202 .and_then(|block| block.get("bits"))
203 .and_then(JsonValue::as_i64)
204 .filter(|bits| *bits > 0)
205 .map(|bits| format!("{bits}bit"));
206
207 if object.contains_key("vision_config")
208 && architectures.iter().any(|architecture| {
209 architecture.ends_with("ForConditionalGeneration")
210 || VISION_LANGUAGE_ARCHITECTURES
211 .iter()
212 .any(|marker| architecture.contains(marker))
213 })
214 {
215 return Some(with_facts(
216 vision_chat_hint(),
217 context_length,
218 quantization.clone(),
219 ));
220 }
221
222 for architecture in &architectures {
223 if let Some(hint) = architecture_hint(architecture) {
224 return Some(with_facts(hint, context_length, quantization.clone()));
225 }
226 }
227
228 let keys: BTreeSet<&str> = object.keys().map(String::as_str).collect();
229 config_key_hint(&keys).map(|hint| with_facts(hint, context_length, quantization))
230}
231
232fn with_facts(mut hint: Hint, context_length: Option<i64>, quantization: Option<String>) -> Hint {
233 hint.context_length = context_length;
234 hint.quantization = quantization;
235 hint
236}
237
238fn architecture_hint(architecture: &str) -> Option<Hint> {
241 const SPEECH: [&str; 6] = ["Kokoro", "StyleTTS", "Bark", "ParlerTTS", "Vits", "Xtts"];
242 const EMBEDDING: [&str; 5] = [
243 "BertModel",
244 "NomicBertModel",
245 "ModernBertModel",
246 "XLMRobertaModel",
247 "MPNetModel",
248 ];
249 if SPEECH.iter().any(|marker| architecture.contains(marker)) {
250 return Some(speech_hint());
251 }
252 if architecture.contains("Whisper") {
253 return Some(audio_hint());
254 }
255 if EMBEDDING.iter().any(|marker| architecture.contains(marker)) {
256 return Some(embedding_hint());
257 }
258 if architecture.contains("LMHead") || architecture.ends_with("ForCausalLM") {
259 return Some(text_hint());
260 }
261 None
262}
263
264fn config_key_hint(keys: &BTreeSet<&str>) -> Option<Hint> {
267 if keys.contains("istftnet") || keys.contains("plbert") {
268 return Some(speech_hint());
269 }
270 if keys.contains("style_dim") && keys.contains("n_mels") {
271 return Some(speech_hint());
272 }
273 None
274}