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}