返回 CodeWhale
modeldb.rs
根目录 / crates / tui / src / commands / groups / core / modeldb.rs
1 //! `/modeldb` command — browse the factual model reference database.
2 //!
3 //! Opens a read-only pager listing each catalog model's stated attributes:
4 //! provider + kind, the model id verbatim, context window, max output,
5 //! modality (text vs multimodal), and price. This is labels only — it never
6 //! selects, routes, or tiers a model (#3205, #2300). Attributes the catalog
7 //! does not state render as `unknown`, never guessed.
8
9 use codewhale_config::model_reference::ModelReferenceDatabase;
10 use ratatui::style::{Modifier, Style};
11 use ratatui::text::Line;
12
13 use crate::commands::traits::{CommandInfo, RegisterCommand};
14 use crate::localization::MessageId;
15 use crate::tui::app::App;
16 use crate::tui::pager::PagerView;
17
18 use super::CommandResult;
19
20 pub(in crate::commands) const COMMAND_INFO: CommandInfo = CommandInfo {
21 name: "modeldb",
22 aliases: &["model-reference", "modelref"],
23 usage: "/modeldb",
24 description_id: MessageId::CmdModelDbDescription,
25 };
26
27 pub(in crate::commands) struct ModelDbCmd;
28
29 impl RegisterCommand for ModelDbCmd {
30 fn info() -> &'static CommandInfo {
31 &COMMAND_INFO
32 }
33
34 fn execute(app: &mut App, _arg: Option<&str>) -> CommandResult {
35 let db = ModelReferenceDatabase::bundled();
36 let title = format!(
37 "Model Reference — {} offerings · {} providers",
38 db.len(),
39 db.providers().len()
40 );
41 app.view_stack
42 .push(PagerView::new(title, reference_lines(&db)));
43 CommandResult::ok()
44 }
45 }
46
47 /// Render the reference database as aligned, browsable pager lines.
48 ///
49 /// Cards are grouped under a provider/kind header (the database is already
50 /// sorted by `(provider, model id)`), so each row only needs the model-scoped
51 /// columns. Column widths are computed across the whole table for stable
52 /// alignment.
53 fn reference_lines(db: &ModelReferenceDatabase) -> Vec<Line<'static>> {
54 let bold = Style::default().add_modifier(Modifier::BOLD);
55 let dim = Style::default().add_modifier(Modifier::DIM);
56
57 let mut lines: Vec<Line<'static>> = Vec::new();
58 lines.push(Line::styled(
59 "Bundled curated model reference catalog.".to_string(),
60 bold,
61 ));
62 lines.push(Line::styled(
63 "Attributes are stated facts; \"unknown\" means the catalog did not state it (never guessed)."
64 .to_string(),
65 dim,
66 ));
67 lines.push(Line::from(String::new()));
68
69 if db.is_empty() {
70 lines.push(Line::from("(no models in catalog)".to_string()));
71 return lines;
72 }
73
74 let cards = db.cards();
75 let id_w = cards
76 .iter()
77 .map(|card| card.model_id.chars().count())
78 .chain(std::iter::once("MODEL ID".len()))
79 .max()
80 .unwrap_or(8)
81 .clamp(8, 46);
82 let ctx_w = cards
83 .iter()
84 .map(|card| card.context_window_label().chars().count())
85 .chain(std::iter::once("CTX".len()))
86 .max()
87 .unwrap_or(3);
88 let out_w = cards
89 .iter()
90 .map(|card| card.max_output_label().chars().count())
91 .chain(std::iter::once("MAX OUT".len()))
92 .max()
93 .unwrap_or(7);
94 // "multimodal" (10) is the widest possible label and exceeds "MODALITY".
95 let mod_w = "multimodal".len();
96
97 lines.push(Line::styled(
98 format!(
99 " {} {} {} {} {}",
100 pad("MODEL ID", id_w),
101 pad("CTX", ctx_w),
102 pad("MAX OUT", out_w),
103 pad("MODALITY", mod_w),
104 "PRICE (USD/Mtok)"
105 ),
106 bold,
107 ));
108
109 let mut current_provider: Option<&str> = None;
110 for card in cards {
111 if current_provider != Some(card.provider.as_str()) {
112 lines.push(Line::from(String::new()));
113 lines.push(Line::styled(
114 format!(
115 "{} · kind: {}",
116 card.provider,
117 card.provider_kind_label()
118 ),
119 bold,
120 ));
121 current_provider = Some(card.provider.as_str());
122 }
123 lines.push(Line::from(format!(
124 " {} {} {} {} {}",
125 pad(&truncate_to(&card.model_id, id_w), id_w),
126 pad(&card.context_window_label(), ctx_w),
127 pad(&card.max_output_label(), out_w),
128 pad(card.modality.as_str(), mod_w),
129 card.price_label(),
130 )));
131 }
132
133 lines
134 }
135
136 /// Left-justify `s` to `width` display columns (counted by `char`).
137 fn pad(s: &str, width: usize) -> String {
138 let len = s.chars().count();
139 if len >= width {
140 s.to_string()
141 } else {
142 format!("{s}{}", " ".repeat(width - len))
143 }
144 }
145
146 /// Truncate `s` to at most `width` chars, marking elision with `…`.
147 fn truncate_to(s: &str, width: usize) -> String {
148 let count = s.chars().count();
149 if count <= width {
150 return s.to_string();
151 }
152 if width <= 1 {
153 return s.chars().take(width).collect();
154 }
155 let mut truncated: String = s.chars().take(width - 1).collect();
156 truncated.push('…');
157 truncated
158 }
159
160 #[cfg(test)]
161 mod tests {
162 use super::*;
163
164 fn rendered(db: &ModelReferenceDatabase) -> Vec<String> {
165 reference_lines(db)
166 .iter()
167 .map(|line| {
168 line.spans
169 .iter()
170 .map(|span| span.content.as_ref())
171 .collect::<String>()
172 })
173 .collect()
174 }
175
176 #[test]
177 fn bundled_reference_lists_models_with_factual_columns() {
178 let db = ModelReferenceDatabase::bundled();
179 let text = rendered(&db).join("\n");
180
181 // Legend states the honesty contract.
182 assert!(text.contains("never guessed"));
183 // Column key present.
184 assert!(text.contains("MODEL ID"));
185 assert!(text.contains("MODALITY"));
186 assert!(text.contains("PRICE (USD/Mtok)"));
187 // A provider header and a verbatim model id row.
188 assert!(text.contains("kind: deepseek"));
189 assert!(text.contains("deepseek-v4-pro"));
190 // Stated modality and an honest unknown price both appear.
191 assert!(text.contains("text"));
192 assert!(text.contains("unknown"));
193 // A priced row surfaces a concrete rate.
194 assert!(text.contains("$0.30 / $1.20 per Mtok"));
195 }
196
197 #[test]
198 fn empty_database_renders_placeholder_not_a_crash() {
199 let db = ModelReferenceDatabase::from_offerings(&[]);
200 let text = rendered(&db).join("\n");
201 assert!(text.contains("(no models in catalog)"));
202 }
203
204 #[test]
205 fn pad_and_truncate_are_width_safe() {
206 assert_eq!(pad("ab", 5), "ab ");
207 assert_eq!(pad("abcde", 3), "abcde");
208 assert_eq!(truncate_to("short", 10), "short");
209 assert_eq!(truncate_to("abcdefghij", 5), "abcd…");
210 }
211 }
212
212 lines RUST