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