assistant_settings.rs

  1use std::fmt;
  2
  3pub use anthropic::Model as AnthropicModel;
  4use gpui::Pixels;
  5pub use open_ai::Model as OpenAiModel;
  6use schemars::{
  7    schema::{InstanceType, Metadata, Schema, SchemaObject},
  8    JsonSchema,
  9};
 10use serde::{
 11    de::{self, Visitor},
 12    Deserialize, Deserializer, Serialize, Serializer,
 13};
 14use settings::{Settings, SettingsSources};
 15
 16#[derive(Clone, Debug, Default, PartialEq)]
 17pub enum ZedDotDevModel {
 18    Gpt3Point5Turbo,
 19    Gpt4,
 20    Gpt4Turbo,
 21    #[default]
 22    Gpt4Omni,
 23    Claude3Opus,
 24    Claude3Sonnet,
 25    Claude3Haiku,
 26    Custom(String),
 27}
 28
 29impl Serialize for ZedDotDevModel {
 30    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
 31    where
 32        S: Serializer,
 33    {
 34        serializer.serialize_str(self.id())
 35    }
 36}
 37
 38impl<'de> Deserialize<'de> for ZedDotDevModel {
 39    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
 40    where
 41        D: Deserializer<'de>,
 42    {
 43        struct ZedDotDevModelVisitor;
 44
 45        impl<'de> Visitor<'de> for ZedDotDevModelVisitor {
 46            type Value = ZedDotDevModel;
 47
 48            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
 49                formatter.write_str("a string for a ZedDotDevModel variant or a custom model")
 50            }
 51
 52            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
 53            where
 54                E: de::Error,
 55            {
 56                match value {
 57                    "gpt-3.5-turbo" => Ok(ZedDotDevModel::Gpt3Point5Turbo),
 58                    "gpt-4" => Ok(ZedDotDevModel::Gpt4),
 59                    "gpt-4-turbo-preview" => Ok(ZedDotDevModel::Gpt4Turbo),
 60                    "gpt-4o" => Ok(ZedDotDevModel::Gpt4Omni),
 61                    _ => Ok(ZedDotDevModel::Custom(value.to_owned())),
 62                }
 63            }
 64        }
 65
 66        deserializer.deserialize_str(ZedDotDevModelVisitor)
 67    }
 68}
 69
 70impl JsonSchema for ZedDotDevModel {
 71    fn schema_name() -> String {
 72        "ZedDotDevModel".to_owned()
 73    }
 74
 75    fn json_schema(_generator: &mut schemars::gen::SchemaGenerator) -> Schema {
 76        let variants = vec![
 77            "gpt-3.5-turbo".to_owned(),
 78            "gpt-4".to_owned(),
 79            "gpt-4-turbo-preview".to_owned(),
 80            "gpt-4o".to_owned(),
 81        ];
 82        Schema::Object(SchemaObject {
 83            instance_type: Some(InstanceType::String.into()),
 84            enum_values: Some(variants.into_iter().map(|s| s.into()).collect()),
 85            metadata: Some(Box::new(Metadata {
 86                title: Some("ZedDotDevModel".to_owned()),
 87                default: Some(serde_json::json!("gpt-4-turbo-preview")),
 88                examples: vec![
 89                    serde_json::json!("gpt-3.5-turbo"),
 90                    serde_json::json!("gpt-4"),
 91                    serde_json::json!("gpt-4-turbo-preview"),
 92                    serde_json::json!("custom-model-name"),
 93                ],
 94                ..Default::default()
 95            })),
 96            ..Default::default()
 97        })
 98    }
 99}
100
101impl ZedDotDevModel {
102    pub fn id(&self) -> &str {
103        match self {
104            Self::Gpt3Point5Turbo => "gpt-3.5-turbo",
105            Self::Gpt4 => "gpt-4",
106            Self::Gpt4Turbo => "gpt-4-turbo-preview",
107            Self::Gpt4Omni => "gpt-4o",
108            Self::Claude3Opus => "claude-3-opus",
109            Self::Claude3Sonnet => "claude-3-sonnet",
110            Self::Claude3Haiku => "claude-3-haiku",
111            Self::Custom(id) => id,
112        }
113    }
114
115    pub fn display_name(&self) -> &str {
116        match self {
117            Self::Gpt3Point5Turbo => "GPT 3.5 Turbo",
118            Self::Gpt4 => "GPT 4",
119            Self::Gpt4Turbo => "GPT 4 Turbo",
120            Self::Gpt4Omni => "GPT 4 Omni",
121            Self::Claude3Opus => "Claude 3 Opus",
122            Self::Claude3Sonnet => "Claude 3 Sonnet",
123            Self::Claude3Haiku => "Claude 3 Haiku",
124            Self::Custom(id) => id.as_str(),
125        }
126    }
127
128    pub fn max_token_count(&self) -> usize {
129        match self {
130            Self::Gpt3Point5Turbo => 2048,
131            Self::Gpt4 => 4096,
132            Self::Gpt4Turbo | Self::Gpt4Omni => 128000,
133            Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3Haiku => 200000,
134            Self::Custom(_) => 4096, // TODO: Make this configurable
135        }
136    }
137}
138
139#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
140#[serde(rename_all = "snake_case")]
141pub enum AssistantDockPosition {
142    Left,
143    #[default]
144    Right,
145    Bottom,
146}
147
148#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
149#[serde(tag = "name", rename_all = "snake_case")]
150pub enum AssistantProvider {
151    #[serde(rename = "zed.dev")]
152    ZedDotDev {
153        #[serde(default)]
154        default_model: ZedDotDevModel,
155    },
156    #[serde(rename = "openai")]
157    OpenAi {
158        #[serde(default)]
159        default_model: OpenAiModel,
160        #[serde(default = "open_ai_url")]
161        api_url: String,
162        #[serde(default)]
163        low_speed_timeout_in_seconds: Option<u64>,
164    },
165    #[serde(rename = "anthropic")]
166    Anthropic {
167        #[serde(default)]
168        default_model: AnthropicModel,
169        #[serde(default = "anthropic_api_url")]
170        api_url: String,
171        #[serde(default)]
172        low_speed_timeout_in_seconds: Option<u64>,
173    },
174}
175
176impl Default for AssistantProvider {
177    fn default() -> Self {
178        Self::ZedDotDev {
179            default_model: ZedDotDevModel::default(),
180        }
181    }
182}
183
184fn open_ai_url() -> String {
185    open_ai::OPEN_AI_API_URL.to_string()
186}
187
188fn anthropic_api_url() -> String {
189    anthropic::ANTHROPIC_API_URL.to_string()
190}
191
192#[derive(Default, Debug, Deserialize, Serialize)]
193pub struct AssistantSettings {
194    pub enabled: bool,
195    pub button: bool,
196    pub dock: AssistantDockPosition,
197    pub default_width: Pixels,
198    pub default_height: Pixels,
199    pub provider: AssistantProvider,
200}
201
202/// Assistant panel settings
203#[derive(Clone, Serialize, Deserialize, Debug)]
204#[serde(untagged)]
205pub enum AssistantSettingsContent {
206    Versioned(VersionedAssistantSettingsContent),
207    Legacy(LegacyAssistantSettingsContent),
208}
209
210impl JsonSchema for AssistantSettingsContent {
211    fn schema_name() -> String {
212        VersionedAssistantSettingsContent::schema_name()
213    }
214
215    fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> Schema {
216        VersionedAssistantSettingsContent::json_schema(gen)
217    }
218
219    fn is_referenceable() -> bool {
220        VersionedAssistantSettingsContent::is_referenceable()
221    }
222}
223
224impl Default for AssistantSettingsContent {
225    fn default() -> Self {
226        Self::Versioned(VersionedAssistantSettingsContent::default())
227    }
228}
229
230impl AssistantSettingsContent {
231    fn upgrade(&self) -> AssistantSettingsContentV1 {
232        match self {
233            AssistantSettingsContent::Versioned(settings) => match settings {
234                VersionedAssistantSettingsContent::V1(settings) => settings.clone(),
235            },
236            AssistantSettingsContent::Legacy(settings) => AssistantSettingsContentV1 {
237                enabled: None,
238                button: settings.button,
239                dock: settings.dock,
240                default_width: settings.default_width,
241                default_height: settings.default_height,
242                provider: if let Some(open_ai_api_url) = settings.openai_api_url.as_ref() {
243                    Some(AssistantProvider::OpenAi {
244                        default_model: settings.default_open_ai_model.clone().unwrap_or_default(),
245                        api_url: open_ai_api_url.clone(),
246                        low_speed_timeout_in_seconds: None,
247                    })
248                } else {
249                    settings.default_open_ai_model.clone().map(|open_ai_model| {
250                        AssistantProvider::OpenAi {
251                            default_model: open_ai_model,
252                            api_url: open_ai_url(),
253                            low_speed_timeout_in_seconds: None,
254                        }
255                    })
256                },
257            },
258        }
259    }
260
261    pub fn set_dock(&mut self, dock: AssistantDockPosition) {
262        match self {
263            AssistantSettingsContent::Versioned(settings) => match settings {
264                VersionedAssistantSettingsContent::V1(settings) => {
265                    settings.dock = Some(dock);
266                }
267            },
268            AssistantSettingsContent::Legacy(settings) => {
269                settings.dock = Some(dock);
270            }
271        }
272    }
273}
274
275#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
276#[serde(tag = "version")]
277pub enum VersionedAssistantSettingsContent {
278    #[serde(rename = "1")]
279    V1(AssistantSettingsContentV1),
280}
281
282impl Default for VersionedAssistantSettingsContent {
283    fn default() -> Self {
284        Self::V1(AssistantSettingsContentV1 {
285            enabled: None,
286            button: None,
287            dock: None,
288            default_width: None,
289            default_height: None,
290            provider: None,
291        })
292    }
293}
294
295#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
296pub struct AssistantSettingsContentV1 {
297    /// Whether the Assistant is enabled.
298    ///
299    /// Default: true
300    enabled: Option<bool>,
301    /// Whether to show the assistant panel button in the status bar.
302    ///
303    /// Default: true
304    button: Option<bool>,
305    /// Where to dock the assistant.
306    ///
307    /// Default: right
308    dock: Option<AssistantDockPosition>,
309    /// Default width in pixels when the assistant is docked to the left or right.
310    ///
311    /// Default: 640
312    default_width: Option<f32>,
313    /// Default height in pixels when the assistant is docked to the bottom.
314    ///
315    /// Default: 320
316    default_height: Option<f32>,
317    /// The provider of the assistant service.
318    ///
319    /// This can either be the internal `zed.dev` service or an external `openai` service,
320    /// each with their respective default models and configurations.
321    provider: Option<AssistantProvider>,
322}
323
324#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
325pub struct LegacyAssistantSettingsContent {
326    /// Whether to show the assistant panel button in the status bar.
327    ///
328    /// Default: true
329    pub button: Option<bool>,
330    /// Where to dock the assistant.
331    ///
332    /// Default: right
333    pub dock: Option<AssistantDockPosition>,
334    /// Default width in pixels when the assistant is docked to the left or right.
335    ///
336    /// Default: 640
337    pub default_width: Option<f32>,
338    /// Default height in pixels when the assistant is docked to the bottom.
339    ///
340    /// Default: 320
341    pub default_height: Option<f32>,
342    /// The default OpenAI model to use when creating new contexts.
343    ///
344    /// Default: gpt-4-1106-preview
345    pub default_open_ai_model: Option<OpenAiModel>,
346    /// OpenAI API base URL to use when creating new contexts.
347    ///
348    /// Default: https://api.openai.com/v1
349    pub openai_api_url: Option<String>,
350}
351
352impl Settings for AssistantSettings {
353    const KEY: Option<&'static str> = Some("assistant");
354
355    type FileContent = AssistantSettingsContent;
356
357    fn load(
358        sources: SettingsSources<Self::FileContent>,
359        _: &mut gpui::AppContext,
360    ) -> anyhow::Result<Self> {
361        let mut settings = AssistantSettings::default();
362
363        for value in sources.defaults_and_customizations() {
364            let value = value.upgrade();
365            merge(&mut settings.enabled, value.enabled);
366            merge(&mut settings.button, value.button);
367            merge(&mut settings.dock, value.dock);
368            merge(
369                &mut settings.default_width,
370                value.default_width.map(Into::into),
371            );
372            merge(
373                &mut settings.default_height,
374                value.default_height.map(Into::into),
375            );
376            if let Some(provider) = value.provider.clone() {
377                match (&mut settings.provider, provider) {
378                    (
379                        AssistantProvider::ZedDotDev { default_model },
380                        AssistantProvider::ZedDotDev {
381                            default_model: default_model_override,
382                        },
383                    ) => {
384                        *default_model = default_model_override;
385                    }
386                    (
387                        AssistantProvider::OpenAi {
388                            default_model,
389                            api_url,
390                            low_speed_timeout_in_seconds,
391                        },
392                        AssistantProvider::OpenAi {
393                            default_model: default_model_override,
394                            api_url: api_url_override,
395                            low_speed_timeout_in_seconds: low_speed_timeout_in_seconds_override,
396                        },
397                    ) => {
398                        *default_model = default_model_override;
399                        *api_url = api_url_override;
400                        *low_speed_timeout_in_seconds = low_speed_timeout_in_seconds_override;
401                    }
402                    (merged, provider_override) => {
403                        *merged = provider_override;
404                    }
405                }
406            }
407        }
408
409        Ok(settings)
410    }
411}
412
413fn merge<T: Copy>(target: &mut T, value: Option<T>) {
414    if let Some(value) = value {
415        *target = value;
416    }
417}
418
419#[cfg(test)]
420mod tests {
421    use gpui::{AppContext, UpdateGlobal};
422    use settings::SettingsStore;
423
424    use super::*;
425
426    #[gpui::test]
427    fn test_deserialize_assistant_settings(cx: &mut AppContext) {
428        let store = settings::SettingsStore::test(cx);
429        cx.set_global(store);
430
431        // Settings default to gpt-4-turbo.
432        AssistantSettings::register(cx);
433        assert_eq!(
434            AssistantSettings::get_global(cx).provider,
435            AssistantProvider::OpenAi {
436                default_model: OpenAiModel::FourOmni,
437                api_url: open_ai_url(),
438                low_speed_timeout_in_seconds: None,
439            }
440        );
441
442        // Ensure backward-compatibility.
443        SettingsStore::update_global(cx, |store, cx| {
444            store
445                .set_user_settings(
446                    r#"{
447                        "assistant": {
448                            "openai_api_url": "test-url",
449                        }
450                    }"#,
451                    cx,
452                )
453                .unwrap();
454        });
455        assert_eq!(
456            AssistantSettings::get_global(cx).provider,
457            AssistantProvider::OpenAi {
458                default_model: OpenAiModel::FourOmni,
459                api_url: "test-url".into(),
460                low_speed_timeout_in_seconds: None,
461            }
462        );
463        SettingsStore::update_global(cx, |store, cx| {
464            store
465                .set_user_settings(
466                    r#"{
467                        "assistant": {
468                            "default_open_ai_model": "gpt-4-0613"
469                        }
470                    }"#,
471                    cx,
472                )
473                .unwrap();
474        });
475        assert_eq!(
476            AssistantSettings::get_global(cx).provider,
477            AssistantProvider::OpenAi {
478                default_model: OpenAiModel::Four,
479                api_url: open_ai_url(),
480                low_speed_timeout_in_seconds: None,
481            }
482        );
483
484        // The new version supports setting a custom model when using zed.dev.
485        SettingsStore::update_global(cx, |store, cx| {
486            store
487                .set_user_settings(
488                    r#"{
489                        "assistant": {
490                            "version": "1",
491                            "provider": {
492                                "name": "zed.dev",
493                                "default_model": "custom"
494                            }
495                        }
496                    }"#,
497                    cx,
498                )
499                .unwrap();
500        });
501        assert_eq!(
502            AssistantSettings::get_global(cx).provider,
503            AssistantProvider::ZedDotDev {
504                default_model: ZedDotDevModel::Custom("custom".into())
505            }
506        );
507    }
508}