1use 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#[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#[derive(Debug, Deserialize)]
26pub struct ModeUpdate {
27 pub mode: String,
28}
29
30#[derive(Debug, Deserialize)]
32pub struct AutonomyUpdate {
33 pub level: String,
34}
35
36#[derive(Debug, Deserialize)]
38pub struct VerifyModelRequest {
39 pub provider: String,
40 pub model: String,
41}
42
43pub 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
53async fn get_config(State(state): State<AppState>) -> Result<Json<serde_json::Value>, WebError> {
55 let config = state.config().await;
56
57 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 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
102async 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
137async 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
166async 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
195async 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
232async 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}