1use oino_ai::Model;
4
5#[must_use]
7pub fn format_token_count(count: u64) -> String {
8 let scaled = |value: u64, unit: u64, suffix: &str| {
9 if value.is_multiple_of(unit) {
10 format!("{}{suffix}", value / unit)
11 } else {
12 let tenths = (value * 10 + unit / 2) / unit;
14 format!("{}.{}{suffix}", tenths / 10, tenths % 10)
15 }
16 };
17 if count >= 1_000_000 {
18 scaled(count, 1_000_000, "M")
19 } else if count >= 1_000 {
20 scaled(count, 1_000, "K")
21 } else {
22 count.to_string()
23 }
24}
25
26fn fuzzy_matches(text: &str, query: &str) -> bool {
29 let text = text.to_lowercase();
30 query.split_whitespace().all(|token| {
31 let mut chars = text.chars();
32 token
33 .to_lowercase()
34 .chars()
35 .all(|wanted| chars.any(|c| c == wanted))
36 })
37}
38
39#[must_use]
41pub fn list_models_output(models: &[Model], search: Option<&str>, no_models: &str) -> String {
42 if models.is_empty() {
43 return format!("{no_models}\n");
44 }
45 let mut filtered: Vec<&Model> = models
46 .iter()
47 .filter(|m| search.is_none_or(|q| fuzzy_matches(&format!("{} {}", m.provider, m.id), q)))
48 .collect();
49 if filtered.is_empty() {
50 return format!("No models matching \"{}\"\n", search.unwrap_or_default());
51 }
52 filtered.sort_by(|a, b| {
53 (
54 a.provider.to_lowercase(),
55 &a.provider,
56 a.id.to_lowercase(),
57 &a.id,
58 )
59 .cmp(&(
60 b.provider.to_lowercase(),
61 &b.provider,
62 b.id.to_lowercase(),
63 &b.id,
64 ))
65 });
66 let header = [
67 "provider", "model", "context", "max-out", "thinking", "images",
68 ]
69 .map(str::to_owned);
70 let rows: Vec<[String; 6]> = filtered
71 .iter()
72 .map(|m| {
73 [
74 m.provider.clone(),
75 m.id.clone(),
76 format_token_count(m.context_window),
77 format_token_count(m.max_tokens),
78 if m.reasoning { "yes" } else { "no" }.to_owned(),
79 if m.input.contains(&oino_ai::InputModality::Image) {
80 "yes"
81 } else {
82 "no"
83 }
84 .to_owned(),
85 ]
86 })
87 .collect();
88 let widths: Vec<usize> = (0..6)
89 .map(|c| {
90 rows.iter()
91 .map(|r| r[c].chars().count())
92 .chain([header[c].len()])
93 .max()
94 .unwrap_or(0)
95 })
96 .collect();
97 let line = |cells: &[String; 6]| {
98 let padded: Vec<String> = cells
99 .iter()
100 .zip(&widths)
101 .map(|(cell, width)| format!("{cell:<width$}"))
102 .collect();
103 padded.join(" ")
104 };
105 let mut out = String::new();
106 for cells in std::iter::once(&header).chain(rows.iter()) {
107 out.push_str(&line(cells));
108 out.push('\n');
109 }
110 out
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 #[test]
118 fn token_counts() {
119 assert_eq!(format_token_count(200_000), "200K");
120 assert_eq!(format_token_count(1_000_000), "1M");
121 assert_eq!(format_token_count(1_500), "1.5K");
122 assert_eq!(format_token_count(128_000), "128K");
123 assert_eq!(format_token_count(999), "999");
124 }
125
126 #[test]
127 fn fuzzy_tokens() {
128 assert!(fuzzy_matches("faux faux-1", "fx 1"));
129 assert!(!fuzzy_matches("faux faux-1", "zz"));
130 }
131}