assistant_settings.rs

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