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}