1use serde::{Deserialize, Serialize};
10
11use crate::domain::Model;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
19#[serde(rename_all = "snake_case")]
20pub enum ModelSortBy {
21 #[default]
23 AddedAt,
24 Name,
26 ParamCount,
28 LatestTgTps,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
34#[serde(rename_all = "snake_case")]
35pub enum SortOrder {
36 #[default]
38 Desc,
39 Asc,
41}
42
43#[derive(Debug, Clone, Default, Serialize, Deserialize)]
57pub struct ModelListQuery {
58 #[serde(default)]
60 pub sort_by: ModelSortBy,
61 #[serde(default)]
63 pub order: SortOrder,
64 pub min_params: Option<f64>,
66 pub max_params: Option<f64>,
68 pub min_context: Option<f64>,
70 pub max_context: Option<f64>,
72 pub quantizations: Option<Vec<String>>,
76 pub tags: Option<Vec<String>>,
78 pub min_speed: Option<f64>,
81 pub max_speed: Option<f64>,
84}
85
86#[must_use]
114pub fn apply_query(mut models: Vec<Model>, query: &ModelListQuery) -> Vec<Model> {
115 models.retain(|m| matches_query(m, query));
116
117 match (query.sort_by, query.order) {
118 (ModelSortBy::Name, SortOrder::Asc) => {
119 models.sort_by(|a, b| a.name.cmp(&b.name));
120 }
121 (ModelSortBy::Name, SortOrder::Desc) => {
122 models.sort_by(|a, b| b.name.cmp(&a.name));
123 }
124 (ModelSortBy::ParamCount, SortOrder::Asc) => {
125 models.sort_by(|a, b| {
126 a.param_count_b
127 .partial_cmp(&b.param_count_b)
128 .unwrap_or(std::cmp::Ordering::Equal)
129 });
130 }
131 (ModelSortBy::ParamCount, SortOrder::Desc) => {
132 models.sort_by(|a, b| {
133 b.param_count_b
134 .partial_cmp(&a.param_count_b)
135 .unwrap_or(std::cmp::Ordering::Equal)
136 });
137 }
138 (ModelSortBy::LatestTgTps, SortOrder::Asc) => {
139 models.sort_by(|a, b| cmp_tps_asc(tps(a), tps(b)));
140 }
141 (ModelSortBy::LatestTgTps, SortOrder::Desc) => {
142 models.sort_by(|a, b| cmp_tps_desc(tps(a), tps(b)));
143 }
144 (ModelSortBy::AddedAt, SortOrder::Asc) => {
145 models.sort_by_key(|a| a.added_at);
146 }
147 (ModelSortBy::AddedAt, SortOrder::Desc) => {
148 models.sort_by_key(|b| std::cmp::Reverse(b.added_at));
149 }
150 }
151
152 models
153}
154
155fn matches_query(m: &Model, query: &ModelListQuery) -> bool {
157 if let Some(min) = query.min_params {
159 if m.param_count_b < min {
160 return false;
161 }
162 }
163 if let Some(max) = query.max_params {
164 if m.param_count_b > max {
165 return false;
166 }
167 }
168
169 if let Some(ctx) = m.context_length {
171 #[allow(clippy::cast_precision_loss)]
172 let ctx_f = ctx as f64;
173 if let Some(min) = query.min_context {
174 if ctx_f < min {
175 return false;
176 }
177 }
178 if let Some(max) = query.max_context {
179 if ctx_f > max {
180 return false;
181 }
182 }
183 }
184
185 if let Some(quants) = &query.quantizations {
187 if !quants.is_empty() {
188 match &m.quantization {
189 Some(q) if quants.contains(q) => {}
190 _ => return false,
191 }
192 }
193 }
194
195 if let Some(tags) = &query.tags {
197 if !tags.is_empty() && !tags.iter().all(|t| m.tags.contains(t)) {
198 return false;
199 }
200 }
201
202 let speed_active = query.min_speed.is_some() || query.max_speed.is_some();
204 if speed_active {
205 match m.benchmark_summary.as_ref().and_then(|s| s.latest_tg_tps) {
206 None => return false,
207 Some(v) => {
208 if query.min_speed.is_some_and(|min| v < min) {
209 return false;
210 }
211 if query.max_speed.is_some_and(|max| v > max) {
212 return false;
213 }
214 }
215 }
216 }
217
218 true
219}
220
221fn tps(m: &Model) -> Option<f64> {
223 m.benchmark_summary.as_ref()?.latest_tg_tps
224}
225
226fn cmp_tps_asc(a: Option<f64>, b: Option<f64>) -> std::cmp::Ordering {
228 match (a, b) {
229 (Some(a), Some(b)) => a.partial_cmp(&b).unwrap_or(std::cmp::Ordering::Equal),
230 (None, None) => std::cmp::Ordering::Equal,
231 (None, Some(_)) => std::cmp::Ordering::Greater,
232 (Some(_), None) => std::cmp::Ordering::Less,
233 }
234}
235
236fn cmp_tps_desc(a: Option<f64>, b: Option<f64>) -> std::cmp::Ordering {
238 match (a, b) {
239 (Some(a), Some(b)) => b.partial_cmp(&a).unwrap_or(std::cmp::Ordering::Equal),
240 (None, None) => std::cmp::Ordering::Equal,
241 (None, Some(_)) => std::cmp::Ordering::Greater,
242 (Some(_), None) => std::cmp::Ordering::Less,
243 }
244}
245
246#[cfg(test)]
251mod tests {
252 use super::*;
253 use crate::ModelCapabilities;
254 use crate::domain::benchmark::ModelBenchmarkSummary;
255 use chrono::Utc;
256 use std::collections::HashMap;
257 use std::path::PathBuf;
258
259 fn make_model(id: i64, name: &str, params: f64) -> Model {
260 Model {
261 id,
262 name: name.to_string(),
263 model_key: String::new(),
264 file_path: PathBuf::from(format!("/models/{name}.gguf")),
265 param_count_b: params,
266 architecture: None,
267 quantization: None,
268 context_length: None,
269 expert_count: None,
270 expert_used_count: None,
271 expert_shared_count: None,
272 metadata: HashMap::new(),
273 added_at: Utc::now(),
274 hf_repo_id: None,
275 hf_commit_sha: None,
276 hf_filename: None,
277 download_date: None,
278 last_update_check: None,
279 tags: vec![],
280 capabilities: ModelCapabilities::default(),
281 inference_defaults: None,
282 defaults_origin: None,
283 server_defaults: None,
284 benchmark_summary: None,
285 }
286 }
287
288 fn with_quant(mut m: Model, quant: &str) -> Model {
289 m.quantization = Some(quant.to_string());
290 m
291 }
292
293 fn with_tags(mut m: Model, tags: &[&str]) -> Model {
294 m.tags = tags.iter().map(ToString::to_string).collect();
295 m
296 }
297
298 fn with_tps(mut m: Model, tps: f64) -> Model {
299 use chrono::Utc;
300 m.benchmark_summary = Some(ModelBenchmarkSummary {
301 model_id: m.id,
302 best_tg_tps: Some(tps),
303 best_pp_tps: None,
304 latest_tg_tps: Some(tps),
305 latest_pp_tps: None,
306 latest_backend: None,
307 perf_run_count: 1,
308 compare_run_count: 0,
309 last_benchmarked_at: Utc::now(),
310 updated_at: Utc::now(),
311 });
312 m
313 }
314
315 fn models() -> Vec<Model> {
316 vec![
317 with_quant(make_model(1, "alpha", 7.0), "Q4_K_M"),
318 with_quant(make_model(2, "beta", 13.0), "Q8_0"),
319 with_quant(make_model(3, "gamma", 70.0), "Q4_K_M"),
320 ]
321 }
322
323 #[test]
324 fn default_query_preserves_all_models() {
325 let result = apply_query(models(), &ModelListQuery::default());
326 assert_eq!(result.len(), 3);
327 }
328
329 #[test]
330 fn param_range_filters_correctly() {
331 let query = ModelListQuery {
332 min_params: Some(8.0),
333 max_params: Some(20.0),
334 ..Default::default()
335 };
336 let result = apply_query(models(), &query);
337 assert_eq!(result.len(), 1);
338 assert_eq!(result[0].name, "beta");
339 }
340
341 #[test]
342 fn quantization_filter_keeps_matching() {
343 let query = ModelListQuery {
344 quantizations: Some(vec!["Q4_K_M".to_string()]),
345 ..Default::default()
346 };
347 let result = apply_query(models(), &query);
348 assert_eq!(result.len(), 2);
349 assert!(
350 result
351 .iter()
352 .all(|m| m.quantization.as_deref() == Some("Q4_K_M"))
353 );
354 }
355
356 #[test]
357 fn tag_filter_uses_and_semantics() {
358 let tagged = vec![
359 with_tags(make_model(1, "a", 7.0), &["chat", "code"]),
360 with_tags(make_model(2, "b", 13.0), &["chat"]),
361 with_tags(make_model(3, "c", 70.0), &["code"]),
362 ];
363 let query = ModelListQuery {
364 tags: Some(vec!["chat".to_string(), "code".to_string()]),
365 ..Default::default()
366 };
367 let result = apply_query(tagged, &query);
368 assert_eq!(result.len(), 1);
369 assert_eq!(result[0].name, "a");
370 }
371
372 #[test]
373 fn speed_filter_excludes_models_without_benchmark() {
374 let ms = vec![
375 with_tps(make_model(1, "fast", 7.0), 80.0),
376 make_model(2, "no-bench", 13.0), with_tps(make_model(3, "slow", 70.0), 10.0),
378 ];
379 let query = ModelListQuery {
380 min_speed: Some(50.0),
381 ..Default::default()
382 };
383 let result = apply_query(ms, &query);
384 assert_eq!(result.len(), 1);
385 assert_eq!(result[0].name, "fast");
386 }
387
388 #[test]
389 fn sort_by_name_asc() {
390 let query = ModelListQuery {
391 sort_by: ModelSortBy::Name,
392 order: SortOrder::Asc,
393 ..Default::default()
394 };
395 let result = apply_query(models(), &query);
396 assert_eq!(result[0].name, "alpha");
397 assert_eq!(result[1].name, "beta");
398 assert_eq!(result[2].name, "gamma");
399 }
400
401 #[test]
402 fn sort_by_tps_desc_puts_none_last() {
403 let ms = vec![
404 with_tps(make_model(1, "fast", 7.0), 80.0),
405 make_model(2, "no-bench", 13.0),
406 with_tps(make_model(3, "slow", 70.0), 10.0),
407 ];
408 let query = ModelListQuery {
409 sort_by: ModelSortBy::LatestTgTps,
410 order: SortOrder::Desc,
411 ..Default::default()
412 };
413 let result = apply_query(ms, &query);
414 assert_eq!(result[0].name, "fast");
415 assert_eq!(result[1].name, "slow");
416 assert_eq!(result[2].name, "no-bench");
417 }
418
419 #[test]
420 fn empty_quantizations_vec_passes_all() {
421 let query = ModelListQuery {
422 quantizations: Some(vec![]),
423 ..Default::default()
424 };
425 let result = apply_query(models(), &query);
426 assert_eq!(result.len(), 3);
427 }
428}