Skip to main content

opendev_web/routes/
config.rs

1//! Configuration routes.
2
3use axum::extract::State;
4use axum::routing::{get, post};
5use axum::{Json, Router};
6use serde::Deserialize;
7
8use crate::error::WebError;
9use crate::protocol::WsMessageType;
10use crate::state::{AppState, OperationMode, WsBroadcast};
11
12/// Configuration update request.
13#[derive(Debug, Deserialize)]
14pub struct ConfigUpdate {
15    pub model_provider: Option<String>,
16    pub model: Option<String>,
17    pub model_vlm_provider: Option<String>,
18    pub model_vlm: Option<String>,
19    pub temperature: Option<f64>,
20    pub max_tokens: Option<u32>,
21    pub enable_bash: Option<bool>,
22}
23
24/// Mode update request.
25#[derive(Debug, Deserialize)]
26pub struct ModeUpdate {
27    pub mode: String,
28}
29
30/// Autonomy update request.
31#[derive(Debug, Deserialize)]
32pub struct AutonomyUpdate {
33    pub level: String,
34}
35
36/// Verify model request.
37#[derive(Debug, Deserialize)]
38pub struct VerifyModelRequest {
39    pub provider: String,
40    pub model: String,
41}
42
43/// Build the config router.
44pub fn router() -> Router<AppState> {
45    Router::new()
46        .route("/api/config", get(get_config).put(update_config))
47        .route("/api/config/mode", post(set_mode))
48        .route("/api/config/autonomy", post(set_autonomy))
49        .route("/api/config/providers", get(list_providers))
50        .route("/api/config/verify-model", post(verify_model))
51}
52
53/// Get current configuration.
54async fn get_config(State(state): State<AppState>) -> Result<Json<serde_json::Value>, WebError> {
55    let config = state.config().await;
56
57    // Mask API key
58    let masked_key = config.api_key.as_ref().map(|key| {
59        if key.len() > 8 {
60            format!("{}...{}", &key[..4], &key[key.len() - 4..])
61        } else {
62            "***".to_string()
63        }
64    });
65
66    let mode = state.mode().await;
67    let autonomy_level = state.autonomy_level().await;
68    let git_branch = state.git_branch();
69
70    // Resolve compact agent role for API consumers
71    let (compact_model, compact_provider) = config.resolve_agent_role("compact");
72    let compact_model_opt =
73        if compact_model == config.model && compact_provider == config.model_provider {
74            None
75        } else {
76            Some(&compact_model)
77        };
78    let compact_provider_opt = if compact_model_opt.is_none() {
79        None
80    } else {
81        Some(&compact_provider)
82    };
83
84    Ok(Json(serde_json::json!({
85        "model_provider": config.model_provider,
86        "model": config.model,
87        "model_vlm_provider": config.model_vlm_provider,
88        "model_vlm": config.model_vlm,
89        "model_compact_provider": compact_provider_opt,
90        "model_compact": compact_model_opt,
91        "api_key": masked_key,
92        "temperature": config.temperature,
93        "max_tokens": config.max_tokens,
94        "enable_bash": config.enable_bash,
95        "mode": mode.to_string(),
96        "autonomy_level": autonomy_level,
97        "working_dir": state.working_dir(),
98        "git_branch": git_branch,
99    })))
100}
101
102/// Update configuration.
103async fn update_config(
104    State(state): State<AppState>,
105    Json(update): Json<ConfigUpdate>,
106) -> Result<Json<serde_json::Value>, WebError> {
107    let mut config = state.config_mut().await;
108
109    if let Some(provider) = update.model_provider {
110        config.model_provider = provider;
111    }
112    if let Some(model) = update.model {
113        config.model = model;
114    }
115    if let Some(provider) = update.model_vlm_provider {
116        config.model_vlm_provider = Some(provider);
117    }
118    if let Some(model) = update.model_vlm {
119        config.model_vlm = Some(model);
120    }
121    if let Some(temp) = update.temperature {
122        config.temperature = temp;
123    }
124    if let Some(max) = update.max_tokens {
125        config.max_tokens = max;
126    }
127    if let Some(bash) = update.enable_bash {
128        config.enable_bash = bash;
129    }
130
131    Ok(Json(serde_json::json!({
132        "status": "success",
133        "message": "Configuration updated",
134    })))
135}
136
137/// Set operation mode.
138async fn set_mode(
139    State(state): State<AppState>,
140    Json(update): Json<ModeUpdate>,
141) -> Result<Json<serde_json::Value>, WebError> {
142    let mode = match update.mode.as_str() {
143        "normal" => OperationMode::Normal,
144        "plan" => OperationMode::Plan,
145        other => {
146            return Err(WebError::BadRequest(format!("Invalid mode: {}", other)));
147        }
148    };
149
150    state.set_mode(mode).await;
151
152    state.broadcast(WsBroadcast {
153        msg_type: WsMessageType::StatusUpdate.as_str().to_string(),
154        data: serde_json::json!({
155            "mode": mode.to_string(),
156            "autonomy_level": state.autonomy_level().await,
157        }),
158    });
159
160    Ok(Json(serde_json::json!({
161        "status": "success",
162        "message": format!("Mode set to {}", mode),
163    })))
164}
165
166/// Set autonomy level.
167async fn set_autonomy(
168    State(state): State<AppState>,
169    Json(update): Json<AutonomyUpdate>,
170) -> Result<Json<serde_json::Value>, WebError> {
171    let valid = ["Manual", "Semi-Auto", "Auto"];
172    if !valid.contains(&update.level.as_str()) {
173        return Err(WebError::BadRequest(format!(
174            "Invalid autonomy level: {}. Must be one of {:?}",
175            update.level, valid
176        )));
177    }
178
179    state.set_autonomy_level(update.level.clone()).await;
180
181    state.broadcast(WsBroadcast {
182        msg_type: WsMessageType::StatusUpdate.as_str().to_string(),
183        data: serde_json::json!({
184            "mode": state.mode().await.to_string(),
185            "autonomy_level": update.level,
186        }),
187    });
188
189    Ok(Json(serde_json::json!({
190        "status": "success",
191        "message": format!("Autonomy set to {}", update.level),
192    })))
193}
194
195/// List all available AI providers and their models.
196async fn list_providers(
197    State(state): State<AppState>,
198) -> Result<Json<Vec<serde_json::Value>>, WebError> {
199    let registry = state.model_registry().await;
200
201    let mut providers = Vec::new();
202    for provider_info in registry.list_providers() {
203        let models: Vec<serde_json::Value> = provider_info
204            .list_models(None)
205            .iter()
206            .map(|model_info| {
207                let ctx_k = model_info.context_length / 1000;
208                let mut description = format!("{}k context", ctx_k);
209                if model_info.recommended {
210                    description = format!("Recommended \u{2022} {}", description);
211                }
212
213                serde_json::json!({
214                    "id": model_info.id,
215                    "name": model_info.name,
216                    "description": description,
217                })
218            })
219            .collect();
220
221        providers.push(serde_json::json!({
222            "id": provider_info.id,
223            "name": provider_info.name,
224            "description": provider_info.description,
225            "models": models,
226        }));
227    }
228
229    Ok(Json(providers))
230}
231
232/// Verify that a provider/model combination is accessible.
233async fn verify_model(
234    State(state): State<AppState>,
235    Json(request): Json<VerifyModelRequest>,
236) -> Json<serde_json::Value> {
237    let registry = state.model_registry().await;
238
239    let provider = match registry.get_provider(&request.provider) {
240        Some(p) => p,
241        None => {
242            return Json(serde_json::json!({
243                "valid": false,
244                "error": format!("Unknown provider: {}", request.provider),
245            }));
246        }
247    };
248
249    if request.model.is_empty() {
250        return Json(serde_json::json!({
251            "valid": false,
252            "error": "Model name cannot be empty",
253        }));
254    }
255
256    let _model_found = registry.find_model_by_id(&request.model).is_some();
257
258    let config = state.config().await;
259    let env_var = &provider.api_key_env;
260    let has_key = if env_var.is_empty() {
261        config.api_key.is_some()
262    } else {
263        config.api_key.is_some() || std::env::var(env_var).is_ok()
264    };
265
266    if !has_key {
267        let hint = if env_var.is_empty() {
268            "No API key configured".to_string()
269        } else {
270            format!("No API key found. Set {} environment variable", env_var)
271        };
272        return Json(serde_json::json!({
273            "valid": false,
274            "error": hint,
275        }));
276    }
277
278    Json(serde_json::json!({
279        "valid": true,
280    }))
281}