assistant_settings.rs

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