Skip to main content

rustyclaw_core/gateway/
model_handler.rs

1//! Model handler — gateway-side model tool dispatch.
2//!
3//! Handles model_* tool calls by interacting with the shared ModelRegistry.
4
5use serde_json::{Value, json};
6use tracing::instrument;
7
8use crate::models::{CostTier, SharedModelRegistry, TaskComplexity};
9
10/// Check if a tool name is a model tool.
11pub 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/// Execute a model tool call.
19#[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
35/// List available models.
36async 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
100/// Enable a model.
101async 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
118/// Disable a model.
119async 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
136/// Set the active model.
137async 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    // Check if model exists and is usable
146    {
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
168/// Get model recommendation for task complexity.
169async 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
221// ── Helpers ─────────────────────────────────────────────────────────────────
222
223fn 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
232/// Generate system prompt section for model selection guidance.
233pub async fn generate_model_prompt_section(model_registry: &SharedModelRegistry) -> String {
234    let registry = model_registry.read().await;
235    crate::models::generate_subagent_guidance(&registry)
236}