assistant_settings.rs

  1use std::sync::Arc;
  2
  3use ::open_ai::Model as OpenAiModel;
  4use anthropic::Model as AnthropicModel;
  5use deepseek::Model as DeepseekModel;
  6use feature_flags::FeatureFlagAppExt;
  7use gpui::{App, Pixels};
  8use language_model::{CloudModel, LanguageModel};
  9use lmstudio::Model as LmStudioModel;
 10use ollama::Model as OllamaModel;
 11use schemars::{schema::Schema, JsonSchema};
 12use serde::{Deserialize, Serialize};
 13use settings::{Settings, SettingsSources};
 14
 15#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
 16#[serde(rename_all = "snake_case")]
 17pub enum AssistantDockPosition {
 18    Left,
 19    #[default]
 20    Right,
 21    Bottom,
 22}
 23
 24#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
 25#[serde(tag = "name", rename_all = "snake_case")]
 26pub enum AssistantProviderContentV1 {
 27    #[serde(rename = "zed.dev")]
 28    ZedDotDev { default_model: Option<CloudModel> },
 29    #[serde(rename = "openai")]
 30    OpenAi {
 31        default_model: Option<OpenAiModel>,
 32        api_url: Option<String>,
 33        available_models: Option<Vec<OpenAiModel>>,
 34    },
 35    #[serde(rename = "anthropic")]
 36    Anthropic {
 37        default_model: Option<AnthropicModel>,
 38        api_url: Option<String>,
 39    },
 40    #[serde(rename = "ollama")]
 41    Ollama {
 42        default_model: Option<OllamaModel>,
 43        api_url: Option<String>,
 44    },
 45    #[serde(rename = "lmstudio")]
 46    LmStudio {
 47        default_model: Option<LmStudioModel>,
 48        api_url: Option<String>,
 49    },
 50    #[serde(rename = "deepseek")]
 51    DeepSeek {
 52        default_model: Option<DeepseekModel>,
 53        api_url: Option<String>,
 54    },
 55}
 56
 57#[derive(Debug, Default)]
 58pub struct AssistantSettings {
 59    pub enabled: bool,
 60    pub button: bool,
 61    pub dock: AssistantDockPosition,
 62    pub default_width: Pixels,
 63    pub default_height: Pixels,
 64    pub default_model: LanguageModelSelection,
 65    pub inline_alternatives: Vec<LanguageModelSelection>,
 66    pub using_outdated_settings_version: bool,
 67    pub enable_experimental_live_diffs: bool,
 68}
 69
 70impl AssistantSettings {
 71    pub fn are_live_diffs_enabled(&self, cx: &App) -> bool {
 72        cx.is_staff() || self.enable_experimental_live_diffs
 73    }
 74}
 75
 76/// Assistant panel settings
 77#[derive(Clone, Serialize, Deserialize, Debug)]
 78#[serde(untagged)]
 79pub enum AssistantSettingsContent {
 80    Versioned(VersionedAssistantSettingsContent),
 81    Legacy(LegacyAssistantSettingsContent),
 82}
 83
 84impl JsonSchema for AssistantSettingsContent {
 85    fn schema_name() -> String {
 86        VersionedAssistantSettingsContent::schema_name()
 87    }
 88
 89    fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> Schema {
 90        VersionedAssistantSettingsContent::json_schema(gen)
 91    }
 92
 93    fn is_referenceable() -> bool {
 94        VersionedAssistantSettingsContent::is_referenceable()
 95    }
 96}
 97
 98impl Default for AssistantSettingsContent {
 99    fn default() -> Self {
100        Self::Versioned(VersionedAssistantSettingsContent::default())
101    }
102}
103
104impl AssistantSettingsContent {
105    pub fn is_version_outdated(&self) -> bool {
106        match self {
107            AssistantSettingsContent::Versioned(settings) => match settings {
108                VersionedAssistantSettingsContent::V1(_) => true,
109                VersionedAssistantSettingsContent::V2(_) => false,
110            },
111            AssistantSettingsContent::Legacy(_) => true,
112        }
113    }
114
115    fn upgrade(&self) -> AssistantSettingsContentV2 {
116        match self {
117            AssistantSettingsContent::Versioned(settings) => match settings {
118                VersionedAssistantSettingsContent::V1(settings) => AssistantSettingsContentV2 {
119                    enabled: settings.enabled,
120                    button: settings.button,
121                    dock: settings.dock,
122                    default_width: settings.default_width,
123                    default_height: settings.default_width,
124                    default_model: settings
125                        .provider
126                        .clone()
127                        .and_then(|provider| match provider {
128                            AssistantProviderContentV1::ZedDotDev { default_model } => {
129                                default_model.map(|model| LanguageModelSelection {
130                                    provider: "zed.dev".to_string(),
131                                    model: model.id().to_string(),
132                                })
133                            }
134                            AssistantProviderContentV1::OpenAi { default_model, .. } => {
135                                default_model.map(|model| LanguageModelSelection {
136                                    provider: "openai".to_string(),
137                                    model: model.id().to_string(),
138                                })
139                            }
140                            AssistantProviderContentV1::Anthropic { default_model, .. } => {
141                                default_model.map(|model| LanguageModelSelection {
142                                    provider: "anthropic".to_string(),
143                                    model: model.id().to_string(),
144                                })
145                            }
146                            AssistantProviderContentV1::Ollama { default_model, .. } => {
147                                default_model.map(|model| LanguageModelSelection {
148                                    provider: "ollama".to_string(),
149                                    model: model.id().to_string(),
150                                })
151                            }
152                            AssistantProviderContentV1::LmStudio { default_model, .. } => {
153                                default_model.map(|model| LanguageModelSelection {
154                                    provider: "lmstudio".to_string(),
155                                    model: model.id().to_string(),
156                                })
157                            }
158                            AssistantProviderContentV1::DeepSeek { default_model, .. } => {
159                                default_model.map(|model| LanguageModelSelection {
160                                    provider: "deepseek".to_string(),
161                                    model: model.id().to_string(),
162                                })
163                            }
164                        }),
165                    inline_alternatives: None,
166                    enable_experimental_live_diffs: None,
167                },
168                VersionedAssistantSettingsContent::V2(settings) => settings.clone(),
169            },
170            AssistantSettingsContent::Legacy(settings) => AssistantSettingsContentV2 {
171                enabled: None,
172                button: settings.button,
173                dock: settings.dock,
174                default_width: settings.default_width,
175                default_height: settings.default_height,
176                default_model: Some(LanguageModelSelection {
177                    provider: "openai".to_string(),
178                    model: settings
179                        .default_open_ai_model
180                        .clone()
181                        .unwrap_or_default()
182                        .id()
183                        .to_string(),
184                }),
185                inline_alternatives: None,
186                enable_experimental_live_diffs: None,
187            },
188        }
189    }
190
191    pub fn set_dock(&mut self, dock: AssistantDockPosition) {
192        match self {
193            AssistantSettingsContent::Versioned(settings) => match settings {
194                VersionedAssistantSettingsContent::V1(settings) => {
195                    settings.dock = Some(dock);
196                }
197                VersionedAssistantSettingsContent::V2(settings) => {
198                    settings.dock = Some(dock);
199                }
200            },
201            AssistantSettingsContent::Legacy(settings) => {
202                settings.dock = Some(dock);
203            }
204        }
205    }
206
207    pub fn set_model(&mut self, language_model: Arc<dyn LanguageModel>) {
208        let model = language_model.id().0.to_string();
209        let provider = language_model.provider_id().0.to_string();
210
211        match self {
212            AssistantSettingsContent::Versioned(settings) => match settings {
213                VersionedAssistantSettingsContent::V1(settings) => match provider.as_ref() {
214                    "zed.dev" => {
215                        log::warn!("attempted to set zed.dev model on outdated settings");
216                    }
217                    "anthropic" => {
218                        let api_url = match &settings.provider {
219                            Some(AssistantProviderContentV1::Anthropic { api_url, .. }) => {
220                                api_url.clone()
221                            }
222                            _ => None,
223                        };
224                        settings.provider = Some(AssistantProviderContentV1::Anthropic {
225                            default_model: AnthropicModel::from_id(&model).ok(),
226                            api_url,
227                        });
228                    }
229                    "ollama" => {
230                        let api_url = match &settings.provider {
231                            Some(AssistantProviderContentV1::Ollama { api_url, .. }) => {
232                                api_url.clone()
233                            }
234                            _ => None,
235                        };
236                        settings.provider = Some(AssistantProviderContentV1::Ollama {
237                            default_model: Some(ollama::Model::new(&model, None, None)),
238                            api_url,
239                        });
240                    }
241                    "lmstudio" => {
242                        let api_url = match &settings.provider {
243                            Some(AssistantProviderContentV1::LmStudio { api_url, .. }) => {
244                                api_url.clone()
245                            }
246                            _ => None,
247                        };
248                        settings.provider = Some(AssistantProviderContentV1::LmStudio {
249                            default_model: Some(lmstudio::Model::new(&model, None, None)),
250                            api_url,
251                        });
252                    }
253                    "openai" => {
254                        let (api_url, available_models) = match &settings.provider {
255                            Some(AssistantProviderContentV1::OpenAi {
256                                api_url,
257                                available_models,
258                                ..
259                            }) => (api_url.clone(), available_models.clone()),
260                            _ => (None, None),
261                        };
262                        settings.provider = Some(AssistantProviderContentV1::OpenAi {
263                            default_model: OpenAiModel::from_id(&model).ok(),
264                            api_url,
265                            available_models,
266                        });
267                    }
268                    "deepseek" => {
269                        let api_url = match &settings.provider {
270                            Some(AssistantProviderContentV1::DeepSeek { api_url, .. }) => {
271                                api_url.clone()
272                            }
273                            _ => None,
274                        };
275                        settings.provider = Some(AssistantProviderContentV1::DeepSeek {
276                            default_model: DeepseekModel::from_id(&model).ok(),
277                            api_url,
278                        });
279                    }
280                    _ => {}
281                },
282                VersionedAssistantSettingsContent::V2(settings) => {
283                    settings.default_model = Some(LanguageModelSelection { provider, model });
284                }
285            },
286            AssistantSettingsContent::Legacy(settings) => {
287                if let Ok(model) = OpenAiModel::from_id(&language_model.id().0) {
288                    settings.default_open_ai_model = Some(model);
289                }
290            }
291        }
292    }
293}
294
295#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
296#[serde(tag = "version")]
297pub enum VersionedAssistantSettingsContent {
298    #[serde(rename = "1")]
299    V1(AssistantSettingsContentV1),
300    #[serde(rename = "2")]
301    V2(AssistantSettingsContentV2),
302}
303
304impl Default for VersionedAssistantSettingsContent {
305    fn default() -> Self {
306        Self::V2(AssistantSettingsContentV2 {
307            enabled: None,
308            button: None,
309            dock: None,
310            default_width: None,
311            default_height: None,
312            default_model: None,
313            inline_alternatives: None,
314            enable_experimental_live_diffs: None,
315        })
316    }
317}
318
319#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
320pub struct AssistantSettingsContentV2 {
321    /// Whether the Assistant is enabled.
322    ///
323    /// Default: true
324    enabled: Option<bool>,
325    /// Whether to show the assistant panel button in the status bar.
326    ///
327    /// Default: true
328    button: Option<bool>,
329    /// Where to dock the assistant.
330    ///
331    /// Default: right
332    dock: Option<AssistantDockPosition>,
333    /// Default width in pixels when the assistant is docked to the left or right.
334    ///
335    /// Default: 640
336    default_width: Option<f32>,
337    /// Default height in pixels when the assistant is docked to the bottom.
338    ///
339    /// Default: 320
340    default_height: Option<f32>,
341    /// The default model to use when creating new chats.
342    default_model: Option<LanguageModelSelection>,
343    /// Additional models with which to generate alternatives when performing inline assists.
344    inline_alternatives: Option<Vec<LanguageModelSelection>>,
345    /// Enable experimental live diffs in the assistant panel.
346    ///
347    /// Default: false
348    enable_experimental_live_diffs: Option<bool>,
349}
350
351#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
352pub struct LanguageModelSelection {
353    #[schemars(schema_with = "providers_schema")]
354    pub provider: String,
355    pub model: String,
356}
357
358fn providers_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
359    schemars::schema::SchemaObject {
360        enum_values: Some(vec![
361            "anthropic".into(),
362            "bedrock".into(),
363            "google".into(),
364            "lmstudio".into(),
365            "ollama".into(),
366            "openai".into(),
367            "zed.dev".into(),
368            "copilot_chat".into(),
369            "deepseek".into(),
370        ]),
371        ..Default::default()
372    }
373    .into()
374}
375
376impl Default for LanguageModelSelection {
377    fn default() -> Self {
378        Self {
379            provider: "openai".to_string(),
380            model: "gpt-4".to_string(),
381        }
382    }
383}
384
385#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
386pub struct AssistantSettingsContentV1 {
387    /// Whether the Assistant is enabled.
388    ///
389    /// Default: true
390    enabled: Option<bool>,
391    /// Whether to show the assistant panel button in the status bar.
392    ///
393    /// Default: true
394    button: Option<bool>,
395    /// Where to dock the assistant.
396    ///
397    /// Default: right
398    dock: Option<AssistantDockPosition>,
399    /// Default width in pixels when the assistant is docked to the left or right.
400    ///
401    /// Default: 640
402    default_width: Option<f32>,
403    /// Default height in pixels when the assistant is docked to the bottom.
404    ///
405    /// Default: 320
406    default_height: Option<f32>,
407    /// The provider of the assistant service.
408    ///
409    /// This can be "openai", "anthropic", "ollama", "lmstudio", "deepseek", "zed.dev"
410    /// each with their respective default models and configurations.
411    provider: Option<AssistantProviderContentV1>,
412}
413
414#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
415pub struct LegacyAssistantSettingsContent {
416    /// Whether to show the assistant panel button in the status bar.
417    ///
418    /// Default: true
419    pub button: Option<bool>,
420    /// Where to dock the assistant.
421    ///
422    /// Default: right
423    pub dock: Option<AssistantDockPosition>,
424    /// Default width in pixels when the assistant is docked to the left or right.
425    ///
426    /// Default: 640
427    pub default_width: Option<f32>,
428    /// Default height in pixels when the assistant is docked to the bottom.
429    ///
430    /// Default: 320
431    pub default_height: Option<f32>,
432    /// The default OpenAI model to use when creating new chats.
433    ///
434    /// Default: gpt-4-1106-preview
435    pub default_open_ai_model: Option<OpenAiModel>,
436    /// OpenAI API base URL to use when creating new chats.
437    ///
438    /// Default: <https://api.openai.com/v1>
439    pub openai_api_url: Option<String>,
440}
441
442impl Settings for AssistantSettings {
443    const KEY: Option<&'static str> = Some("assistant");
444
445    const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
446
447    type FileContent = AssistantSettingsContent;
448
449    fn load(
450        sources: SettingsSources<Self::FileContent>,
451        _: &mut gpui::App,
452    ) -> anyhow::Result<Self> {
453        let mut settings = AssistantSettings::default();
454
455        for value in sources.defaults_and_customizations() {
456            if value.is_version_outdated() {
457                settings.using_outdated_settings_version = true;
458            }
459
460            let value = value.upgrade();
461            merge(&mut settings.enabled, value.enabled);
462            merge(&mut settings.button, value.button);
463            merge(&mut settings.dock, value.dock);
464            merge(
465                &mut settings.default_width,
466                value.default_width.map(Into::into),
467            );
468            merge(
469                &mut settings.default_height,
470                value.default_height.map(Into::into),
471            );
472            merge(&mut settings.default_model, value.default_model);
473            merge(&mut settings.inline_alternatives, value.inline_alternatives);
474            merge(
475                &mut settings.enable_experimental_live_diffs,
476                value.enable_experimental_live_diffs,
477            );
478        }
479
480        Ok(settings)
481    }
482}
483
484fn merge<T>(target: &mut T, value: Option<T>) {
485    if let Some(value) = value {
486        *target = value;
487    }
488}
489
490#[cfg(test)]
491mod tests {
492    use fs::Fs;
493    use gpui::{ReadGlobal, TestAppContext};
494
495    use super::*;
496
497    #[gpui::test]
498    async fn test_deserialize_assistant_settings_with_version(cx: &mut TestAppContext) {
499        let fs = fs::FakeFs::new(cx.executor().clone());
500        fs.create_dir(paths::settings_file().parent().unwrap())
501            .await
502            .unwrap();
503
504        cx.update(|cx| {
505            let test_settings = settings::SettingsStore::test(cx);
506            cx.set_global(test_settings);
507            AssistantSettings::register(cx);
508        });
509
510        cx.update(|cx| {
511            assert!(!AssistantSettings::get_global(cx).using_outdated_settings_version);
512            assert_eq!(
513                AssistantSettings::get_global(cx).default_model,
514                LanguageModelSelection {
515                    provider: "zed.dev".into(),
516                    model: "claude-3-5-sonnet-latest".into(),
517                }
518            );
519        });
520
521        cx.update(|cx| {
522            settings::SettingsStore::global(cx).update_settings_file::<AssistantSettings>(
523                fs.clone(),
524                |settings, _| {
525                    *settings = AssistantSettingsContent::Versioned(
526                        VersionedAssistantSettingsContent::V2(AssistantSettingsContentV2 {
527                            default_model: Some(LanguageModelSelection {
528                                provider: "test-provider".into(),
529                                model: "gpt-99".into(),
530                            }),
531                            inline_alternatives: None,
532                            enabled: None,
533                            button: None,
534                            dock: None,
535                            default_width: None,
536                            default_height: None,
537                            enable_experimental_live_diffs: None,
538                        }),
539                    )
540                },
541            );
542        });
543
544        cx.run_until_parked();
545
546        let raw_settings_value = fs.load(paths::settings_file()).await.unwrap();
547        assert!(raw_settings_value.contains(r#""version": "2""#));
548
549        #[derive(Debug, Deserialize)]
550        struct AssistantSettingsTest {
551            assistant: AssistantSettingsContent,
552        }
553
554        let assistant_settings: AssistantSettingsTest =
555            serde_json_lenient::from_str(&raw_settings_value).unwrap();
556
557        assert!(!assistant_settings.assistant.is_version_outdated());
558    }
559}