mermaid_cli/tui/state/
model.rs1use std::sync::Arc;
6use tokio::sync::RwLock;
7
8use crate::models::{Model, ModelConfig, ReasoningCapability, ReasoningLevel};
9
10pub struct ModelState {
12 pub model: Arc<RwLock<Box<dyn Model>>>,
13 pub model_id: String,
14 pub model_name: String,
15 pub vision_supported: Option<bool>,
20 pub supported_reasoning: ReasoningCapability,
26 pub base_config: ModelConfig,
29}
30
31impl ModelState {
32 pub fn new(model: Box<dyn Model>, model_id: String, base_config: ModelConfig) -> Self {
33 let model_name = model.name().to_string();
34 let supported_reasoning = model.capabilities().supports_reasoning.clone();
35 Self {
36 model: Arc::new(RwLock::new(model)),
37 model_id,
38 model_name,
39 vision_supported: None,
40 supported_reasoning,
41 base_config,
42 }
43 }
44
45 pub fn model_ref(&self) -> &Arc<RwLock<Box<dyn Model>>> {
47 &self.model
48 }
49
50 pub fn cycle_reasoning(&mut self) -> ReasoningLevel {
57 let next = match self.base_config.reasoning {
58 ReasoningLevel::None => ReasoningLevel::Low,
59 ReasoningLevel::Low => ReasoningLevel::Medium,
60 ReasoningLevel::Medium => ReasoningLevel::High,
61 ReasoningLevel::High => ReasoningLevel::Max,
62 ReasoningLevel::Max => ReasoningLevel::None,
63 ReasoningLevel::Minimal => ReasoningLevel::Low,
66 ReasoningLevel::XHigh => ReasoningLevel::None,
70 };
71 self.base_config.reasoning = next;
72 next
73 }
74
75 pub fn set_reasoning(&mut self, level: ReasoningLevel) {
77 self.base_config.reasoning = level;
78 }
79
80 pub fn build_config(&self) -> ModelConfig {
83 let mut config = self.base_config.clone();
84 config.model = self.model_id.clone();
85 config
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 struct StubModel;
96
97 #[async_trait::async_trait]
98 impl Model for StubModel {
99 async fn chat(
100 &self,
101 _messages: &[crate::models::ChatMessage],
102 _config: &ModelConfig,
103 _stream_callback: Option<crate::models::StreamCallback>,
104 ) -> crate::models::Result<crate::models::ModelResponse> {
105 unimplemented!("stub")
106 }
107 fn name(&self) -> &str {
108 "stub"
109 }
110 fn capabilities(&self) -> &crate::models::ModelCapabilities {
111 use std::sync::OnceLock;
112 static CAPS: OnceLock<crate::models::ModelCapabilities> = OnceLock::new();
113 CAPS.get_or_init(crate::models::ModelCapabilities::ollama_default)
114 }
115 async fn list_models(&self) -> crate::models::Result<Vec<String>> {
116 Ok(vec![])
117 }
118 }
119
120 fn make_state(initial: ReasoningLevel) -> ModelState {
121 let base = ModelConfig {
122 reasoning: initial,
123 ..Default::default()
124 };
125 ModelState::new(Box::new(StubModel), "stub/model".to_string(), base)
126 }
127
128 #[test]
129 fn cycle_reasoning_visits_5_stops_starting_from_none() {
130 let mut state = make_state(ReasoningLevel::None);
131 assert_eq!(state.cycle_reasoning(), ReasoningLevel::Low);
132 assert_eq!(state.cycle_reasoning(), ReasoningLevel::Medium);
133 assert_eq!(state.cycle_reasoning(), ReasoningLevel::High);
134 assert_eq!(state.cycle_reasoning(), ReasoningLevel::Max);
135 assert_eq!(state.cycle_reasoning(), ReasoningLevel::None);
136 }
137
138 #[test]
139 fn cycle_reasoning_after_minimal_lands_on_low() {
140 let mut state = make_state(ReasoningLevel::Minimal);
144 assert_eq!(state.cycle_reasoning(), ReasoningLevel::Low);
145 }
146
147 #[test]
148 fn cycle_reasoning_after_xhigh_lands_on_none() {
149 let mut state = make_state(ReasoningLevel::XHigh);
153 assert_eq!(state.cycle_reasoning(), ReasoningLevel::None);
154 }
155
156 #[test]
157 fn set_reasoning_updates_base_config() {
158 let mut state = make_state(ReasoningLevel::Medium);
159 state.set_reasoning(ReasoningLevel::Max);
160 assert_eq!(state.base_config.reasoning, ReasoningLevel::Max);
161 }
162
163 #[test]
164 fn build_config_propagates_reasoning_from_base() {
165 let mut state = make_state(ReasoningLevel::High);
166 let config = state.build_config();
167 assert_eq!(config.reasoning, ReasoningLevel::High);
168 state.cycle_reasoning();
171 assert_eq!(config.reasoning, ReasoningLevel::High);
172 assert_eq!(state.base_config.reasoning, ReasoningLevel::Max);
173 }
174
175 #[test]
179 fn model_state_caches_supported_reasoning_at_construction() {
180 let state = make_state(ReasoningLevel::Medium);
181 let expected = crate::models::ModelCapabilities::ollama_default().supports_reasoning;
185 assert_eq!(state.supported_reasoning, expected);
186 }
187}