Skip to content

Commit f9e409c

Browse files
committed
Add model configuration modal for server connections
- Implement a new modal to configure remote model settings - Allow enabling/disabling specific models for each server connection - Update preferences and store to persist model configuration - Add new icons for settings and trash actions in connection settings - Modify remote model handling to respect user-configured model status
1 parent 73e9a4e commit f9e409c

11 files changed

+514
-61
lines changed

resources/images/settings_icon.png

7.48 KB
Loading

resources/images/trash_icon.png

4.07 KB
Loading

src/chat/model_selector_list.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ impl ModelSelectorList {
171171
}
172172

173173
let remote_models_vector: Vec<_> =
174-
store.chats.remote_models.values().cloned().collect();
174+
store.chats.get_remote_models_list(true).iter().cloned().collect();
175175

176176
for (i, remote_model) in remote_models_vector.iter().enumerate() {
177177
let item_id = LiveId(10_000 + i as u64).into();

src/chat/prompt_input.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ impl WidgetMatchEvent for PromptInput {
244244
// Add remote models
245245
for (idx, remote_model) in store
246246
.chats
247-
.get_remote_models_list()
247+
.get_remote_models_list(true)
248248
.iter()
249249
.filter(|m| terms.iter().all(|t| m.name.to_lowercase().contains(t)))
250250
.enumerate()

src/data/chats/mod.rs

+77-11
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use std::{cell::RefCell, path::PathBuf};
1616

1717
use super::filesystem::setup_chats_folder;
1818
use super::moly_client::MolyClient;
19+
use super::preferences::{Preferences, ServerModel};
1920
use super::remote_servers::{OpenAIClient, OpenAIServerResponse, RemoteModel, RemoteModelId};
2021

2122
#[derive(Clone, Debug)]
@@ -62,7 +63,7 @@ pub struct Chats {
6263
pub mofa_servers: HashMap<MofaServerId, MofaServer>,
6364
pub available_agents: HashMap<AgentId, MofaAgent>,
6465

65-
pub remote_models: HashMap<ModelID, RemoteModel>,
66+
pub remote_models: HashMap<RemoteModelId, RemoteModel>,
6667
pub openai_servers: HashMap<String, OpenAIClient>,
6768

6869
/// Set it thru `set_current_chat` method to trigger side effects.
@@ -179,7 +180,7 @@ impl Chats {
179180
}
180181
}
181182
Some(ChatEntityId::RemoteModel(model_id)) => {
182-
if let Some(openai_client) = self.get_client_for_remote_model(&model_id.0) {
183+
if let Some(openai_client) = self.get_client_for_remote_model(&model_id) {
183184
chat.cancel_remote_model_interaction(&openai_client);
184185
} else {
185186
error!("No openai client found for model: {}", model_id.0);
@@ -337,15 +338,60 @@ impl Chats {
337338
}
338339
}
339340

340-
pub fn handle_openai_server_connection_result(&mut self, result: OpenAIServerResponse) {
341+
pub fn handle_openai_server_connection_result(
342+
&mut self,
343+
result: OpenAIServerResponse,
344+
preferences: &mut Preferences
345+
) {
341346
match result {
342-
OpenAIServerResponse::Connected(address, models) => {
347+
OpenAIServerResponse::Connected(address, fetched_models) => {
348+
// Merge with preferences
349+
if let Some(conn) = preferences
350+
.server_connections
351+
.iter_mut()
352+
.find(|sc| sc.address == address)
353+
{
354+
// For each newly fetched model
355+
for rm in &fetched_models {
356+
if let Some(_existing) =
357+
conn.models.iter_mut().find(|m| m.name == rm.name)
358+
{
359+
// Keep existing enabled status
360+
} else {
361+
// Insert new model with default enabled
362+
conn.models.push(ServerModel {
363+
name: rm.name.clone(),
364+
enabled: true,
365+
});
366+
}
367+
}
368+
// Remove models that no longer appear
369+
conn.models.retain(|m| {
370+
fetched_models.iter().any(|rm| rm.name == m.name)
371+
});
372+
373+
preferences.save();
374+
}
375+
376+
// Now store them in memory
377+
for mut remote_model in fetched_models {
378+
// Look up preference
379+
let user_pref_enabled = preferences
380+
.server_connections
381+
.iter()
382+
.find(|sc| sc.address == address)
383+
.and_then(|sc| sc.models.iter().find(|m| m.name == remote_model.name))
384+
.map(|m| m.enabled)
385+
.unwrap_or(true);
386+
remote_model.enabled = user_pref_enabled;
387+
388+
self.remote_models
389+
.insert(remote_model.id.clone(), remote_model);
390+
}
391+
392+
// Mark the server as connected
343393
if let Some(server) = self.openai_servers.get_mut(&address) {
344394
server.connection_status = ServerConnectionStatus::Connected;
345-
346-
for remote_model in models {
347-
self.remote_models.insert(remote_model.id.0.clone(), remote_model);
348-
}
349395
}
350396
}
351397
OpenAIServerResponse::Unavailable(address) => {
@@ -376,6 +422,12 @@ impl Chats {
376422
self.available_agents.retain(|_, agent| agent.server_id.0 != address);
377423
}
378424

425+
/// Removes a OpenAI server from the list of available servers.
426+
pub fn remove_openai_server(&mut self, address: &str) {
427+
self.openai_servers.remove(address);
428+
self.remote_models.retain(|_, model| model.server_id.0 != address);
429+
}
430+
379431
/// Retrieves the corresponding MofaClient for an agent
380432
pub fn get_client_for_agent(&self, agent_id: &AgentId) -> Option<&MofaClient> {
381433
self.available_agents.get(agent_id)
@@ -384,7 +436,7 @@ impl Chats {
384436
}
385437

386438
/// Retrieves the corresponding OpenAIClient for a remote model
387-
pub fn get_client_for_remote_model(&self, model_id: &ModelID) -> Option<&OpenAIClient> {
439+
pub fn get_client_for_remote_model(&self, model_id: &RemoteModelId) -> Option<&OpenAIClient> {
388440
self.remote_models.get(model_id)
389441
.and_then(|model| self.openai_servers.get(&model.server_id.0))
390442
}
@@ -397,11 +449,25 @@ impl Chats {
397449
}
398450

399451
/// Helper method for components that need a sorted vector of remote models
400-
pub fn get_remote_models_list(&self) -> Vec<RemoteModel> {
452+
pub fn get_remote_models_list(&self, exclude_disabled: bool) -> Vec<RemoteModel> {
401453
let mut models: Vec<_> = self.remote_models.values().cloned().collect();
402454
models.sort_by(|a, b| a.name.cmp(&b.name));
455+
if exclude_disabled {
456+
models.retain(|model| model.enabled);
457+
}
403458
models
404459
}
460+
461+
/// Returns a list of remote models for a given server address.
462+
pub fn get_remote_models_list_for_server(&self, server_id: &str) -> Vec<RemoteModel> {
463+
// TODO: we should make this more efficient by using a map of server_id -> models
464+
// instead of iterating over all models.
465+
self.remote_models.values()
466+
.filter(|model| model.server_id.0 == server_id)
467+
.cloned()
468+
.collect()
469+
}
470+
405471
/// Tests the connection to a MoFa server by requesting /v1/models.
406472
///
407473
/// The connection status is updated at the App level based on the actions dispatched.
@@ -473,7 +539,7 @@ impl Chats {
473539
}
474540

475541
pub fn get_remote_model_or_placeholder(&self, model_id: &RemoteModelId) -> &RemoteModel {
476-
self.remote_models.get(&model_id.0).unwrap_or(&self.unknown_remote_model)
542+
self.remote_models.get(model_id).unwrap_or(&self.unknown_remote_model)
477543
}
478544
}
479545

src/data/preferences.rs

+38
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,20 @@ use crate::settings::connection_settings::ProviderType;
1010

1111
const PREFERENCES_FILENAME: &str = "preferences.json";
1212

13+
#[derive(Serialize, Deserialize, Debug, Clone)]
14+
pub struct ServerModel {
15+
pub name: String,
16+
pub enabled: bool,
17+
}
18+
1319
#[derive(Serialize, Deserialize, Debug, Clone)]
1420
pub struct ServerConnection {
1521
pub address: String,
1622
pub provider: ProviderType,
1723
#[serde(default)]
1824
pub api_key: Option<String>,
25+
#[serde(default)]
26+
pub models: Vec<ServerModel>,
1927
}
2028

2129
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
@@ -86,6 +94,7 @@ impl Preferences {
8694
address,
8795
provider,
8896
api_key,
97+
models: vec![], // start empty; populate later if needed
8998
});
9099
self.save();
91100
}
@@ -95,6 +104,35 @@ impl Preferences {
95104
.retain(|sc| sc.address != address);
96105
self.save();
97106
}
107+
108+
/// Refresh or insert a model in the server's model list.
109+
pub fn _ensure_server_model_exists(&mut self, address: &str, model_name: &str) {
110+
if let Some(conn) = self.server_connections.iter_mut().find(|sc| sc.address == address) {
111+
let already_exists = conn.models.iter().any(|m| m.name == model_name);
112+
if !already_exists {
113+
conn.models.push(ServerModel {
114+
name: model_name.to_string(),
115+
enabled: true,
116+
});
117+
}
118+
}
119+
}
120+
121+
/// Update the enabled/disabled status of a model for a specific server
122+
pub fn update_model_status(&mut self, address: &str, model_name: &str, enabled: bool) {
123+
if let Some(conn) = self.server_connections.iter_mut().find(|sc| sc.address == address) {
124+
if let Some(model) = conn.models.iter_mut().find(|m| m.name == model_name) {
125+
model.enabled = enabled;
126+
} else {
127+
// If not found, add it
128+
conn.models.push(ServerModel {
129+
name: model_name.to_string(),
130+
enabled,
131+
});
132+
}
133+
}
134+
self.save();
135+
}
98136
}
99137

100138
fn preferences_path() -> PathBuf {

src/data/remote_servers.rs

+5-2
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ fn should_include_model(url: &str, model_id: &str) -> bool {
5252
pub struct RemoteModelId(pub String);
5353

5454
impl RemoteModelId {
55-
pub fn new(agent_name: &str, server_address: &str) -> Self {
55+
pub fn from_model_and_server(agent_name: &str, server_address: &str) -> Self {
5656
RemoteModelId(format!("{}-{}", agent_name, server_address))
5757
}
5858
}
@@ -74,6 +74,7 @@ pub struct RemoteModel {
7474
pub name: String,
7575
pub description: String,
7676
pub server_id: RemoteServerId,
77+
pub enabled: bool,
7778
}
7879

7980
impl RemoteModel {
@@ -86,6 +87,7 @@ impl RemoteModel {
8687
description: "This model is not currently reachable, its information is not available"
8788
.to_string(),
8889
server_id: RemoteServerId("Unknown".to_string()),
90+
enabled: true,
8991
}
9092
}
9193
}
@@ -250,10 +252,11 @@ impl OpenAIClient {
250252
let models: Vec<RemoteModel> = models.data.into_iter()
251253
.filter(|model| should_include_model(&url, &model.id))
252254
.map(|model| RemoteModel {
253-
id: RemoteModelId::new(&model.id, &url),
255+
id: RemoteModelId::from_model_and_server(&model.id, &url),
254256
name: model.id,
255257
description: format!("OpenAI {} model", model.object),
256258
server_id: RemoteServerId(url.clone()),
259+
enabled: true,
257260
})
258261
.collect();
259262
tx.send(OpenAIServerResponse::Connected(url, models)).unwrap();

src/data/store.rs

+36-21
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ use anyhow::Result;
1212
use chrono::{DateTime, Utc};
1313
use makepad_widgets::{Action, ActionDefaultRef, DefaultNone};
1414

15+
use super::chats::ServerType;
16+
use crate::settings::connection_settings::ProviderType;
1517
use moly_mofa::MofaServerResponse;
1618
use moly_protocol::data::{Author, DownloadedFile, File, FileID, Model, ModelID, PendingDownload};
17-
use crate::settings::connection_settings::ProviderType;
18-
use super::chats::ServerType;
1919

20+
#[allow(dead_code)]
2021
const DEFAULT_MOFA_ADDRESS: &str = "http://localhost:8000";
2122

2223
#[derive(Clone, DefaultNone, Debug)]
@@ -69,12 +70,12 @@ impl Default for Store {
6970
impl Store {
7071
pub fn new() -> Self {
7172
let preferences = Preferences::load();
72-
73+
7374
let server_port = std::env::var("MOLY_SERVER_PORT")
7475
.ok()
7576
.and_then(|p| p.parse::<u16>().ok())
7677
.unwrap_or(8765);
77-
78+
7879
let moly_client = MolyClient::new(format!("http://localhost:{}", server_port));
7980

8081
let mut store = Self {
@@ -92,9 +93,9 @@ impl Store {
9293
store.init_current_chat();
9394

9495
store.search.load_featured_models();
95-
store
96-
.chats
97-
.register_mofa_server(DEFAULT_MOFA_ADDRESS.to_string());
96+
// store
97+
// .chats
98+
// .register_mofa_server(DEFAULT_MOFA_ADDRESS.to_string());
9899

99100
store.load_preference_connections();
100101

@@ -183,8 +184,8 @@ impl Store {
183184
}
184185
ChatEntityId::RemoteModel(model_id) => {
185186
if let (Some(client), Some(model)) = (
186-
self.chats.get_client_for_remote_model(&model_id.0),
187-
self.chats.remote_models.get(&model_id.0),
187+
self.chats.get_client_for_remote_model(&model_id),
188+
self.chats.remote_models.get(&model_id),
188189
) {
189190
chat.send_message_to_remote_model(prompt, model, &client);
190191
} else {
@@ -241,7 +242,7 @@ impl Store {
241242
Some(ChatEntityId::RemoteModel(model_id)) => self
242243
.chats
243244
.remote_models
244-
.get(&model_id.0)
245+
.get(&model_id)
245246
.map(|m| m.name.clone()),
246247
None => {
247248
// Fallback to loaded model if exists
@@ -384,8 +385,9 @@ impl Store {
384385
}
385386
MoFaTestServerAction::Failure(address) => {
386387
if let Some(addr) = address {
387-
self.chats
388-
.handle_mofa_server_connection_result(MofaServerResponse::Unavailable(addr));
388+
self.chats.handle_mofa_server_connection_result(
389+
MofaServerResponse::Unavailable(addr),
390+
);
389391
}
390392
}
391393
_ => (),
@@ -395,28 +397,41 @@ impl Store {
395397
pub fn handle_openai_test_server_action(&mut self, action: OpenAiTestServerAction) {
396398
match action {
397399
OpenAiTestServerAction::Success(address, models) => {
398-
self.chats.handle_openai_server_connection_result(OpenAIServerResponse::Connected(address, models));
399-
},
400-
OpenAiTestServerAction::Failure(address) => {
401-
if let Some(addr) = address {
402-
self.chats.handle_openai_server_connection_result(OpenAIServerResponse::Unavailable(addr));
403-
}
404-
},
405-
OpenAiTestServerAction::None => (),
400+
// Merge fetched models with existing preferences
401+
self.chats.handle_openai_server_connection_result(
402+
OpenAIServerResponse::Connected(address, models),
403+
&mut self.preferences,
404+
);
405+
406+
}
407+
OpenAiTestServerAction::Failure(address_opt) => {
408+
if let Some(addr) = address_opt {
409+
eprintln!("Failed to connect to OpenAI-compatible server at {}", addr);
410+
self.chats.handle_openai_server_connection_result(
411+
OpenAIServerResponse::Unavailable(addr),
412+
&mut self.preferences,
413+
);
414+
};
415+
}
416+
OpenAiTestServerAction::None => {}
406417
}
407418
}
408419

420+
// The existing code that loads from preferences and calls register_server
409421
fn load_preference_connections(&mut self) {
410422
for conn in &self.preferences.server_connections {
411423
match conn.provider {
412424
ProviderType::OpenAIAPI => {
425+
// Registering will do a test & fetch, we'll merge the models with preferences with
426+
// the fetched models returned by the test.
413427
self.chats.register_server(ServerType::OpenAI {
414428
address: conn.address.clone(),
415429
api_key: conn.api_key.clone().unwrap_or_default(),
416430
});
417431
}
418432
ProviderType::MoFa => {
419-
self.chats.register_server(ServerType::Mofa(conn.address.clone()));
433+
self.chats
434+
.register_server(ServerType::Mofa(conn.address.clone()));
420435
}
421436
}
422437
}

0 commit comments

Comments
 (0)