Allow AI interactions to be proxied through Zed's server so you don't need an API key (#7367)

Co-authored-by: Antonio <antonio@zed.dev>

Resurrected this from some assistant work I did in Spring of 2023.
- [x] Resurrect streaming responses
- [x] Use streaming responses to enable AI via Zed's servers by default
(but preserve API key option for now)
- [x] Simplify protobuf
- [x] Proxy to OpenAI on zed.dev
- [x] Proxy to Gemini on zed.dev
- [x] Improve UX for switching between openAI and google models
- We current disallow cycling when setting a custom model, but we need a
better solution to keep OpenAI models available while testing the google
ones
- [x] Show remaining tokens correctly for Google models
- [x] Remove semantic index
- [x] Delete `ai` crate
- [x] Cloud front so we can ban abuse
- [x] Rate-limiting
- [x] Fix panic when using inline assistant
- [x] Double check the upgraded `AssistantSettings` are
backwards-compatible
- [x] Add hosted LLM interaction behind a `language-models` feature
flag.

Release Notes:

- We are temporarily removing the semantic index in order to redesign it
from scratch.

---------

Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Thorsten <thorsten@zed.dev>
Co-authored-by: Max <max@zed.dev>
This commit is contained in:
Nathan Sobo 2024-03-19 12:22:26 -06:00 committed by GitHub
parent 905a24079a
commit 8ae5a3b61a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
87 changed files with 3647 additions and 8937 deletions

View file

@ -1,169 +1,296 @@
use ai::providers::open_ai::{
AzureOpenAiApiVersion, OpenAiCompletionProviderKind, OPEN_AI_API_URL,
};
use anyhow::anyhow;
use std::fmt;
use gpui::Pixels;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
pub use open_ai::Model as OpenAiModel;
use schemars::{
schema::{InstanceType, Metadata, Schema, SchemaObject},
JsonSchema,
};
use serde::{
de::{self, Visitor},
Deserialize, Deserializer, Serialize, Serializer,
};
use settings::Settings;
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum OpenAiModel {
#[serde(rename = "gpt-3.5-turbo-0613")]
ThreePointFiveTurbo,
#[serde(rename = "gpt-4-0613")]
Four,
#[serde(rename = "gpt-4-1106-preview")]
FourTurbo,
#[derive(Clone, Debug, Default, PartialEq)]
pub enum ZedDotDevModel {
GptThreePointFiveTurbo,
GptFour,
#[default]
GptFourTurbo,
Custom(String),
}
impl OpenAiModel {
pub fn full_name(&self) -> &'static str {
impl Serialize for ZedDotDevModel {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.id())
}
}
impl<'de> Deserialize<'de> for ZedDotDevModel {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct ZedDotDevModelVisitor;
impl<'de> Visitor<'de> for ZedDotDevModelVisitor {
type Value = ZedDotDevModel;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a string for a ZedDotDevModel variant or a custom model")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
match value {
"gpt-3.5-turbo" => Ok(ZedDotDevModel::GptThreePointFiveTurbo),
"gpt-4" => Ok(ZedDotDevModel::GptFour),
"gpt-4-turbo-preview" => Ok(ZedDotDevModel::GptFourTurbo),
_ => Ok(ZedDotDevModel::Custom(value.to_owned())),
}
}
}
deserializer.deserialize_str(ZedDotDevModelVisitor)
}
}
impl JsonSchema for ZedDotDevModel {
fn schema_name() -> String {
"ZedDotDevModel".to_owned()
}
fn json_schema(_generator: &mut schemars::gen::SchemaGenerator) -> Schema {
let variants = vec![
"gpt-3.5-turbo".to_owned(),
"gpt-4".to_owned(),
"gpt-4-turbo-preview".to_owned(),
];
Schema::Object(SchemaObject {
instance_type: Some(InstanceType::String.into()),
enum_values: Some(variants.into_iter().map(|s| s.into()).collect()),
metadata: Some(Box::new(Metadata {
title: Some("ZedDotDevModel".to_owned()),
default: Some(serde_json::json!("gpt-4-turbo-preview")),
examples: vec![
serde_json::json!("gpt-3.5-turbo"),
serde_json::json!("gpt-4"),
serde_json::json!("gpt-4-turbo-preview"),
serde_json::json!("custom-model-name"),
],
..Default::default()
})),
..Default::default()
})
}
}
impl ZedDotDevModel {
pub fn id(&self) -> &str {
match self {
Self::ThreePointFiveTurbo => "gpt-3.5-turbo-0613",
Self::Four => "gpt-4-0613",
Self::FourTurbo => "gpt-4-1106-preview",
Self::GptThreePointFiveTurbo => "gpt-3.5-turbo",
Self::GptFour => "gpt-4",
Self::GptFourTurbo => "gpt-4-turbo-preview",
Self::Custom(id) => id,
}
}
pub fn short_name(&self) -> &'static str {
pub fn display_name(&self) -> &str {
match self {
Self::ThreePointFiveTurbo => "gpt-3.5-turbo",
Self::Four => "gpt-4",
Self::FourTurbo => "gpt-4-turbo",
}
}
pub fn cycle(&self) -> Self {
match self {
Self::ThreePointFiveTurbo => Self::Four,
Self::Four => Self::FourTurbo,
Self::FourTurbo => Self::ThreePointFiveTurbo,
Self::GptThreePointFiveTurbo => "gpt-3.5-turbo",
Self::GptFour => "gpt-4",
Self::GptFourTurbo => "gpt-4-turbo",
Self::Custom(id) => id.as_str(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum AssistantDockPosition {
Left,
#[default]
Right,
Bottom,
}
#[derive(Debug, Deserialize)]
pub struct AssistantSettings {
/// Whether to show the assistant panel button in the status bar.
pub button: bool,
/// Where to dock the assistant.
pub dock: AssistantDockPosition,
/// Default width in pixels when the assistant is docked to the left or right.
pub default_width: Pixels,
/// Default height in pixels when the assistant is docked to the bottom.
pub default_height: Pixels,
/// The default OpenAI model to use when starting new conversations.
#[deprecated = "Please use `provider.default_model` instead."]
pub default_open_ai_model: OpenAiModel,
/// OpenAI API base URL to use when starting new conversations.
#[deprecated = "Please use `provider.api_url` instead."]
pub openai_api_url: String,
/// The settings for the AI provider.
pub provider: AiProviderSettings,
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(tag = "name", rename_all = "snake_case")]
pub enum AssistantProvider {
#[serde(rename = "zed.dev")]
ZedDotDev {
#[serde(default)]
default_model: ZedDotDevModel,
},
#[serde(rename = "openai")]
OpenAi {
#[serde(default)]
default_model: OpenAiModel,
#[serde(default = "open_ai_url")]
api_url: String,
},
}
impl AssistantSettings {
pub fn provider_kind(&self) -> anyhow::Result<OpenAiCompletionProviderKind> {
match &self.provider {
AiProviderSettings::OpenAi(_) => Ok(OpenAiCompletionProviderKind::OpenAi),
AiProviderSettings::AzureOpenAi(settings) => {
let deployment_id = settings
.deployment_id
.clone()
.ok_or_else(|| anyhow!("no Azure OpenAI deployment ID"))?;
let api_version = settings
.api_version
.ok_or_else(|| anyhow!("no Azure OpenAI API version"))?;
Ok(OpenAiCompletionProviderKind::AzureOpenAi {
deployment_id,
api_version,
})
}
impl Default for AssistantProvider {
fn default() -> Self {
Self::ZedDotDev {
default_model: ZedDotDevModel::default(),
}
}
}
pub fn provider_api_url(&self) -> anyhow::Result<String> {
match &self.provider {
AiProviderSettings::OpenAi(settings) => Ok(settings
.api_url
.clone()
.unwrap_or_else(|| OPEN_AI_API_URL.to_string())),
AiProviderSettings::AzureOpenAi(settings) => settings
.api_url
.clone()
.ok_or_else(|| anyhow!("no Azure OpenAI API URL")),
}
fn open_ai_url() -> String {
"https://api.openai.com/v1".into()
}
#[derive(Default, Debug, Deserialize, Serialize)]
pub struct AssistantSettings {
pub button: bool,
pub dock: AssistantDockPosition,
pub default_width: Pixels,
pub default_height: Pixels,
pub provider: AssistantProvider,
}
/// Assistant panel settings
#[derive(Clone, Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub enum AssistantSettingsContent {
Versioned(VersionedAssistantSettingsContent),
Legacy(LegacyAssistantSettingsContent),
}
impl JsonSchema for AssistantSettingsContent {
fn schema_name() -> String {
VersionedAssistantSettingsContent::schema_name()
}
pub fn provider_model(&self) -> anyhow::Result<OpenAiModel> {
match &self.provider {
AiProviderSettings::OpenAi(settings) => {
Ok(settings.default_model.unwrap_or(OpenAiModel::FourTurbo))
}
AiProviderSettings::AzureOpenAi(settings) => {
let deployment_id = settings
.deployment_id
.as_deref()
.ok_or_else(|| anyhow!("no Azure OpenAI deployment ID"))?;
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> Schema {
VersionedAssistantSettingsContent::json_schema(gen)
}
match deployment_id {
// https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models#gpt-4-and-gpt-4-turbo-preview
"gpt-4" | "gpt-4-32k" => Ok(OpenAiModel::Four),
// https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models#gpt-35
"gpt-35-turbo" | "gpt-35-turbo-16k" | "gpt-35-turbo-instruct" => {
Ok(OpenAiModel::ThreePointFiveTurbo)
fn is_referenceable() -> bool {
VersionedAssistantSettingsContent::is_referenceable()
}
}
impl Default for AssistantSettingsContent {
fn default() -> Self {
Self::Versioned(VersionedAssistantSettingsContent::default())
}
}
impl AssistantSettingsContent {
fn upgrade(&self) -> AssistantSettingsContentV1 {
match self {
AssistantSettingsContent::Versioned(settings) => match settings {
VersionedAssistantSettingsContent::V1(settings) => settings.clone(),
},
AssistantSettingsContent::Legacy(settings) => {
if let Some(open_ai_api_url) = settings.openai_api_url.as_ref() {
AssistantSettingsContentV1 {
button: settings.button,
dock: settings.dock,
default_width: settings.default_width,
default_height: settings.default_height,
provider: Some(AssistantProvider::OpenAi {
default_model: settings
.default_open_ai_model
.clone()
.unwrap_or_default(),
api_url: open_ai_api_url.clone(),
}),
}
} else if let Some(open_ai_model) = settings.default_open_ai_model.clone() {
AssistantSettingsContentV1 {
button: settings.button,
dock: settings.dock,
default_width: settings.default_width,
default_height: settings.default_height,
provider: Some(AssistantProvider::OpenAi {
default_model: open_ai_model,
api_url: open_ai_url(),
}),
}
} else {
AssistantSettingsContentV1 {
button: settings.button,
dock: settings.dock,
default_width: settings.default_width,
default_height: settings.default_height,
provider: None,
}
_ => Err(anyhow!(
"no matching OpenAI model found for deployment ID: '{deployment_id}'"
)),
}
}
}
}
pub fn provider_model_name(&self) -> anyhow::Result<String> {
match &self.provider {
AiProviderSettings::OpenAi(settings) => Ok(settings
.default_model
.unwrap_or(OpenAiModel::FourTurbo)
.full_name()
.to_string()),
AiProviderSettings::AzureOpenAi(settings) => settings
.deployment_id
.clone()
.ok_or_else(|| anyhow!("no Azure OpenAI deployment ID")),
pub fn set_dock(&mut self, dock: AssistantDockPosition) {
match self {
AssistantSettingsContent::Versioned(settings) => match settings {
VersionedAssistantSettingsContent::V1(settings) => {
settings.dock = Some(dock);
}
},
AssistantSettingsContent::Legacy(settings) => {
settings.dock = Some(dock);
}
}
}
}
impl Settings for AssistantSettings {
const KEY: Option<&'static str> = Some("assistant");
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
#[serde(tag = "version")]
pub enum VersionedAssistantSettingsContent {
#[serde(rename = "1")]
V1(AssistantSettingsContentV1),
}
type FileContent = AssistantSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
impl Default for VersionedAssistantSettingsContent {
fn default() -> Self {
Self::V1(AssistantSettingsContentV1 {
button: None,
dock: None,
default_width: None,
default_height: None,
provider: None,
})
}
}
/// Assistant panel settings
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
pub struct AssistantSettingsContent {
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
pub struct AssistantSettingsContentV1 {
/// Whether to show the assistant panel button in the status bar.
///
/// Default: true
button: Option<bool>,
/// Where to dock the assistant.
///
/// Default: right
dock: Option<AssistantDockPosition>,
/// Default width in pixels when the assistant is docked to the left or right.
///
/// Default: 640
default_width: Option<f32>,
/// Default height in pixels when the assistant is docked to the bottom.
///
/// Default: 320
default_height: Option<f32>,
/// The provider of the assistant service.
///
/// This can either be the internal `zed.dev` service or an external `openai` service,
/// each with their respective default models and configurations.
provider: Option<AssistantProvider>,
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
pub struct LegacyAssistantSettingsContent {
/// Whether to show the assistant panel button in the status bar.
///
/// Default: true
@ -180,88 +307,164 @@ pub struct AssistantSettingsContent {
///
/// Default: 320
pub default_height: Option<f32>,
/// Deprecated: Please use `provider.default_model` instead.
/// The default OpenAI model to use when starting new conversations.
///
/// Default: gpt-4-1106-preview
#[deprecated = "Please use `provider.default_model` instead."]
pub default_open_ai_model: Option<OpenAiModel>,
/// Deprecated: Please use `provider.api_url` instead.
/// OpenAI API base URL to use when starting new conversations.
///
/// Default: https://api.openai.com/v1
#[deprecated = "Please use `provider.api_url` instead."]
pub openai_api_url: Option<String>,
/// The settings for the AI provider.
#[serde(default)]
pub provider: AiProviderSettingsContent,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AiProviderSettings {
/// The settings for the OpenAI provider.
#[serde(rename = "openai")]
OpenAi(OpenAiProviderSettings),
/// The settings for the Azure OpenAI provider.
#[serde(rename = "azure_openai")]
AzureOpenAi(AzureOpenAiProviderSettings),
}
impl Settings for AssistantSettings {
const KEY: Option<&'static str> = Some("assistant");
/// The settings for the AI provider used by the Zed Assistant.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AiProviderSettingsContent {
/// The settings for the OpenAI provider.
#[serde(rename = "openai")]
OpenAi(OpenAiProviderSettingsContent),
/// The settings for the Azure OpenAI provider.
#[serde(rename = "azure_openai")]
AzureOpenAi(AzureOpenAiProviderSettingsContent),
}
type FileContent = AssistantSettingsContent;
impl Default for AiProviderSettingsContent {
fn default() -> Self {
Self::OpenAi(OpenAiProviderSettingsContent::default())
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
let mut settings = AssistantSettings::default();
for value in [default_value].iter().chain(user_values) {
let value = value.upgrade();
merge(&mut settings.button, value.button);
merge(&mut settings.dock, value.dock);
merge(
&mut settings.default_width,
value.default_width.map(Into::into),
);
merge(
&mut settings.default_height,
value.default_height.map(Into::into),
);
if let Some(provider) = value.provider.clone() {
match (&mut settings.provider, provider) {
(
AssistantProvider::ZedDotDev { default_model },
AssistantProvider::ZedDotDev {
default_model: default_model_override,
},
) => {
*default_model = default_model_override;
}
(
AssistantProvider::OpenAi {
default_model,
api_url,
},
AssistantProvider::OpenAi {
default_model: default_model_override,
api_url: api_url_override,
},
) => {
*default_model = default_model_override;
*api_url = api_url_override;
}
(merged, provider_override) => {
*merged = provider_override;
}
}
}
}
Ok(settings)
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct OpenAiProviderSettings {
/// The OpenAI API base URL to use when starting new conversations.
pub api_url: Option<String>,
/// The default OpenAI model to use when starting new conversations.
pub default_model: Option<OpenAiModel>,
fn merge<T: Copy>(target: &mut T, value: Option<T>) {
if let Some(value) = value {
*target = value;
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
pub struct OpenAiProviderSettingsContent {
/// The OpenAI API base URL to use when starting new conversations.
///
/// Default: https://api.openai.com/v1
pub api_url: Option<String>,
/// The default OpenAI model to use when starting new conversations.
///
/// Default: gpt-4-1106-preview
pub default_model: Option<OpenAiModel>,
}
#[cfg(test)]
mod tests {
use gpui::AppContext;
use settings::SettingsStore;
#[derive(Debug, Clone, Deserialize)]
pub struct AzureOpenAiProviderSettings {
/// The Azure OpenAI API base URL to use when starting new conversations.
pub api_url: Option<String>,
/// The Azure OpenAI API version.
pub api_version: Option<AzureOpenAiApiVersion>,
/// The Azure OpenAI API deployment ID.
pub deployment_id: Option<String>,
}
use super::*;
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
pub struct AzureOpenAiProviderSettingsContent {
/// The Azure OpenAI API base URL to use when starting new conversations.
pub api_url: Option<String>,
/// The Azure OpenAI API version.
pub api_version: Option<AzureOpenAiApiVersion>,
/// The Azure OpenAI deployment ID.
pub deployment_id: Option<String>,
#[gpui::test]
fn test_deserialize_assistant_settings(cx: &mut AppContext) {
let store = settings::SettingsStore::test(cx);
cx.set_global(store);
// Settings default to gpt-4-turbo.
AssistantSettings::register(cx);
assert_eq!(
AssistantSettings::get_global(cx).provider,
AssistantProvider::OpenAi {
default_model: OpenAiModel::FourTurbo,
api_url: open_ai_url()
}
);
// Ensure backward-compatibility.
cx.update_global::<SettingsStore, _>(|store, cx| {
store
.set_user_settings(
r#"{
"assistant": {
"openai_api_url": "test-url",
}
}"#,
cx,
)
.unwrap();
});
assert_eq!(
AssistantSettings::get_global(cx).provider,
AssistantProvider::OpenAi {
default_model: OpenAiModel::FourTurbo,
api_url: "test-url".into()
}
);
cx.update_global::<SettingsStore, _>(|store, cx| {
store
.set_user_settings(
r#"{
"assistant": {
"default_open_ai_model": "gpt-4-0613"
}
}"#,
cx,
)
.unwrap();
});
assert_eq!(
AssistantSettings::get_global(cx).provider,
AssistantProvider::OpenAi {
default_model: OpenAiModel::Four,
api_url: open_ai_url()
}
);
// The new version supports setting a custom model when using zed.dev.
cx.update_global::<SettingsStore, _>(|store, cx| {
store
.set_user_settings(
r#"{
"assistant": {
"version": "1",
"provider": {
"name": "zed.dev",
"default_model": "custom"
}
}
}"#,
cx,
)
.unwrap();
});
assert_eq!(
AssistantSettings::get_global(cx).provider,
AssistantProvider::ZedDotDev {
default_model: ZedDotDevModel::Custom("custom".into())
}
);
}
}