1use serde_json::{Value, json};
6use tracing::instrument;
7
8use crate::models::{CostTier, SharedModelRegistry, TaskComplexity};
9
10pub fn is_model_tool(name: &str) -> bool {
12 matches!(
13 name,
14 "model_list" | "model_enable" | "model_disable" | "model_set" | "model_recommend"
15 )
16}
17
18#[instrument(skip(model_registry, args), fields(tool = %name))]
20pub async fn execute_model_tool(
21 name: &str,
22 args: &Value,
23 model_registry: &SharedModelRegistry,
24) -> Result<String, String> {
25 match name {
26 "model_list" => exec_model_list(args, model_registry).await,
27 "model_enable" => exec_model_enable(args, model_registry).await,
28 "model_disable" => exec_model_disable(args, model_registry).await,
29 "model_set" => exec_model_set(args, model_registry).await,
30 "model_recommend" => exec_model_recommend(args, model_registry).await,
31 _ => Err(format!("Unknown model tool: {}", name)),
32 }
33}
34
35async fn exec_model_list(
37 args: &Value,
38 model_registry: &SharedModelRegistry,
39) -> Result<String, String> {
40 let tier_filter = args
41 .get("tier")
42 .and_then(|v| v.as_str())
43 .and_then(CostTier::from_str);
44 let enabled_only = args
45 .get("enabledOnly")
46 .and_then(|v| v.as_bool())
47 .unwrap_or(false);
48 let usable_only = args
49 .get("usableOnly")
50 .and_then(|v| v.as_bool())
51 .unwrap_or(false);
52
53 let registry = model_registry.read().await;
54
55 let models: Vec<_> = registry
56 .all()
57 .into_iter()
58 .filter(|m| {
59 if let Some(tier) = tier_filter {
60 if m.tier != tier {
61 return false;
62 }
63 }
64 if enabled_only && !m.enabled {
65 return false;
66 }
67 if usable_only && !m.is_usable() {
68 return false;
69 }
70 true
71 })
72 .map(|m| {
73 json!({
74 "id": m.id,
75 "provider": m.provider,
76 "name": m.name,
77 "displayName": m.display_name,
78 "tier": format!("{} {}", m.tier.emoji(), m.tier.display()),
79 "tierCode": format!("{:?}", m.tier).to_lowercase(),
80 "enabled": m.enabled,
81 "available": m.available,
82 "usable": m.is_usable(),
83 "contextWindow": m.context_window,
84 "vision": m.supports_vision,
85 "thinking": m.supports_thinking,
86 })
87 })
88 .collect();
89
90 let active = registry.active().map(|m| m.id.as_str());
91
92 Ok(json!({
93 "models": models,
94 "count": models.len(),
95 "activeModel": active,
96 })
97 .to_string())
98}
99
100async fn exec_model_enable(
102 args: &Value,
103 model_registry: &SharedModelRegistry,
104) -> Result<String, String> {
105 let model_id = parse_model_id(args)?;
106
107 let mut registry = model_registry.write().await;
108 registry.enable(&model_id)?;
109
110 Ok(json!({
111 "success": true,
112 "modelId": model_id,
113 "message": format!("Model '{}' enabled", model_id),
114 })
115 .to_string())
116}
117
118async fn exec_model_disable(
120 args: &Value,
121 model_registry: &SharedModelRegistry,
122) -> Result<String, String> {
123 let model_id = parse_model_id(args)?;
124
125 let mut registry = model_registry.write().await;
126 registry.disable(&model_id)?;
127
128 Ok(json!({
129 "success": true,
130 "modelId": model_id,
131 "message": format!("Model '{}' disabled", model_id),
132 })
133 .to_string())
134}
135
136async fn exec_model_set(
138 args: &Value,
139 model_registry: &SharedModelRegistry,
140) -> Result<String, String> {
141 let model_id = parse_model_id(args)?;
142
143 let mut registry = model_registry.write().await;
144
145 {
147 let model = registry
148 .get(&model_id)
149 .ok_or_else(|| format!("Model not found: {}", model_id))?;
150 if !model.is_usable() {
151 return Err(format!(
152 "Model '{}' is not usable (enabled: {}, available: {})",
153 model_id, model.enabled, model.available
154 ));
155 }
156 }
157
158 registry.set_active(&model_id)?;
159
160 Ok(json!({
161 "success": true,
162 "modelId": model_id,
163 "message": format!("Active model set to '{}'", model_id),
164 })
165 .to_string())
166}
167
168async fn exec_model_recommend(
170 args: &Value,
171 model_registry: &SharedModelRegistry,
172) -> Result<String, String> {
173 let complexity_str = args
174 .get("complexity")
175 .and_then(|v| v.as_str())
176 .unwrap_or("medium");
177
178 let complexity = match complexity_str.to_lowercase().as_str() {
179 "simple" => TaskComplexity::Simple,
180 "medium" => TaskComplexity::Medium,
181 "complex" => TaskComplexity::Complex,
182 "critical" => TaskComplexity::Critical,
183 _ => {
184 return Err(format!(
185 "Unknown complexity: {}. Use: simple, medium, complex, critical",
186 complexity_str
187 ));
188 }
189 };
190
191 let registry = model_registry.read().await;
192
193 let recommended = registry.recommend_for_subagent(complexity);
194
195 match recommended {
196 Some(model) => Ok(json!({
197 "complexity": complexity_str,
198 "recommendedTier": format!("{} {}", complexity.recommended_tier().emoji(), complexity.recommended_tier().display()),
199 "model": {
200 "id": model.id,
201 "displayName": model.display_name,
202 "tier": format!("{} {}", model.tier.emoji(), model.tier.display()),
203 "provider": model.provider,
204 },
205 "suggestion": format!(
206 "For {} tasks, use '{}' ({})",
207 complexity_str,
208 model.id,
209 model.tier.display()
210 ),
211 }).to_string()),
212 None => Ok(json!({
213 "complexity": complexity_str,
214 "recommendedTier": format!("{} {}", complexity.recommended_tier().emoji(), complexity.recommended_tier().display()),
215 "model": null,
216 "error": "No usable model found for this complexity level",
217 }).to_string()),
218 }
219}
220
221fn parse_model_id(args: &Value) -> Result<String, String> {
224 args.get("id")
225 .or_else(|| args.get("model"))
226 .or_else(|| args.get("modelId"))
227 .and_then(|v| v.as_str())
228 .map(|s| s.to_string())
229 .ok_or_else(|| "Missing required parameter: id (model ID)".to_string())
230}
231
232pub async fn generate_model_prompt_section(model_registry: &SharedModelRegistry) -> String {
234 let registry = model_registry.read().await;
235 crate::models::generate_subagent_guidance(®istry)
236}