assistant_settings.rs

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