Add `CredentialsProvider` to silence keychain prompts in development (#25266)

Marshall Bowers created

This PR adds a new `CredentialsProvider` trait that abstracts over
interacting with the system keychain.

We had previously introduced a version of this scoped just to Zed auth
in https://github.com/zed-industries/zed/pull/11505.

However, after landing https://github.com/zed-industries/zed/pull/25123,
we now have a similar issue with the credentials for language model
providers that are also stored in the keychain (and thus also produce a
spam of popups when running a development build of Zed).

This PR takes the existing approach and makes it more generic, such that
we can use it everywhere that we need to read/store credentials in the
keychain.

There are still two credential provider implementations:

- `KeychainCredentialsProvider` will interact with the system keychain
(using the existing GPUI APIs)
- `DevelopmentCredentialsProvider` will use a local file on the file
system

We only use the `DevelopmentCredentialsProvider` when:

1. We are running a development build of Zed
2. The `ZED_DEVELOPMENT_AUTH` environment variable is set
- I am considering removing the need for this and making it the default,
but that will be explored in a follow-up PR.

Release Notes:

- N/A

Change summary

Cargo.lock                                              |  16 
Cargo.toml                                              |   4 
crates/client/Cargo.toml                                |   1 
crates/client/src/client.rs                             | 220 +++-------
crates/credentials_provider/Cargo.toml                  |  21 +
crates/credentials_provider/LICENSE-GPL                 |   1 
crates/credentials_provider/src/credentials_provider.rs | 187 +++++++++
crates/language_models/Cargo.toml                       |   1 
crates/language_models/src/provider/anthropic.rs        |  36 +
crates/language_models/src/provider/deepseek.rs         |  32 +
crates/language_models/src/provider/google.rs           |  42 +-
crates/language_models/src/provider/mistral.rs          |  31 +
crates/language_models/src/provider/open_ai.rs          |  32 +
crates/zed/Cargo.toml                                   |   1 
crates/zed/src/main.rs                                  |   2 
15 files changed, 401 insertions(+), 226 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -2619,6 +2619,7 @@ dependencies = [
  "clock",
  "cocoa 0.26.0",
  "collections",
+ "credentials_provider",
  "feature_flags",
  "futures 0.3.31",
  "gpui",
@@ -3477,6 +3478,19 @@ dependencies = [
  "crc",
 ]
 
+[[package]]
+name = "credentials_provider"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "futures 0.3.31",
+ "gpui",
+ "paths",
+ "release_channel",
+ "serde",
+ "serde_json",
+]
+
 [[package]]
 name = "criterion"
 version = "0.5.1"
@@ -6985,6 +6999,7 @@ dependencies = [
  "client",
  "collections",
  "copilot",
+ "credentials_provider",
  "deepseek",
  "editor",
  "feature_flags",
@@ -16682,6 +16697,7 @@ dependencies = [
  "command_palette_hooks",
  "component_preview",
  "copilot",
+ "credentials_provider",
  "db",
  "diagnostics",
  "editor",

Cargo.toml 🔗

@@ -16,6 +16,7 @@ members = [
     "crates/auto_update",
     "crates/auto_update_ui",
     "crates/breadcrumbs",
+    "crates/buffer_diff",
     "crates/call",
     "crates/channel",
     "crates/cli",
@@ -31,10 +32,10 @@ members = [
     "crates/context_server",
     "crates/context_server_settings",
     "crates/copilot",
+    "crates/credentials_provider",
     "crates/db",
     "crates/deepseek",
     "crates/diagnostics",
-    "crates/buffer_diff",
     "crates/docs_preprocessor",
     "crates/editor",
     "crates/evals",
@@ -233,6 +234,7 @@ component_preview = { path = "crates/component_preview" }
 context_server = { path = "crates/context_server" }
 context_server_settings = { path = "crates/context_server_settings" }
 copilot = { path = "crates/copilot" }
+credentials_provider = { path = "crates/credentials_provider" }
 db = { path = "crates/db" }
 deepseek = { path = "crates/deepseek" }
 diagnostics = { path = "crates/diagnostics" }

crates/client/Cargo.toml 🔗

@@ -22,6 +22,7 @@ async-tungstenite = { workspace = true, features = ["async-std", "async-tls"] }
 chrono = { workspace = true, features = ["serde"] }
 clock.workspace = true
 collections.workspace = true
+credentials_provider.workspace = true
 feature_flags.workspace = true
 futures.workspace = true
 gpui.workspace = true

crates/client/src/client.rs 🔗

@@ -15,6 +15,7 @@ use async_tungstenite::tungstenite::{
 };
 use chrono::{DateTime, Utc};
 use clock::SystemClock;
+use credentials_provider::CredentialsProvider;
 use futures::{
     channel::oneshot, future::BoxFuture, AsyncReadExt, FutureExt, SinkExt, Stream, StreamExt,
     TryFutureExt as _, TryStreamExt,
@@ -57,14 +58,6 @@ static ZED_SERVER_URL: LazyLock<Option<String>> =
     LazyLock::new(|| std::env::var("ZED_SERVER_URL").ok());
 static ZED_RPC_URL: LazyLock<Option<String>> = LazyLock::new(|| std::env::var("ZED_RPC_URL").ok());
 
-/// An environment variable whose presence indicates that the development auth
-/// provider should be used.
-///
-/// Only works in development. Setting this environment variable in other release
-/// channels is a no-op.
-pub static ZED_DEVELOPMENT_AUTH: LazyLock<bool> = LazyLock::new(|| {
-    std::env::var("ZED_DEVELOPMENT_AUTH").map_or(false, |value| !value.is_empty())
-});
 pub static IMPERSONATE_LOGIN: LazyLock<Option<String>> = LazyLock::new(|| {
     std::env::var("ZED_IMPERSONATE")
         .ok()
@@ -193,7 +186,7 @@ pub struct Client {
     peer: Arc<Peer>,
     http: Arc<HttpClientWithUrl>,
     telemetry: Arc<Telemetry>,
-    credentials_provider: Arc<dyn CredentialsProvider + Send + Sync + 'static>,
+    credentials_provider: ClientCredentialsProvider,
     state: RwLock<ClientState>,
     handler_set: parking_lot::Mutex<ProtoMessageHandlerSet>,
 
@@ -304,16 +297,46 @@ impl Credentials {
     }
 }
 
-/// A provider for [`Credentials`].
-///
-/// Used to abstract over reading and writing credentials to some form of
-/// persistence (like the system keychain).
-trait CredentialsProvider {
+pub struct ClientCredentialsProvider {
+    provider: Arc<dyn CredentialsProvider>,
+}
+
+impl ClientCredentialsProvider {
+    pub fn new(cx: &App) -> Self {
+        Self {
+            provider: <dyn CredentialsProvider>::global(cx),
+        }
+    }
+
+    fn server_url(&self, cx: &AsyncApp) -> Result<String> {
+        cx.update(|cx| ClientSettings::get_global(cx).server_url.clone())
+    }
+
     /// Reads the credentials from the provider.
     fn read_credentials<'a>(
         &'a self,
         cx: &'a AsyncApp,
-    ) -> Pin<Box<dyn Future<Output = Option<Credentials>> + 'a>>;
+    ) -> Pin<Box<dyn Future<Output = Option<Credentials>> + 'a>> {
+        async move {
+            if IMPERSONATE_LOGIN.is_some() {
+                return None;
+            }
+
+            let server_url = self.server_url(cx).ok()?;
+            let (user_id, access_token) = self
+                .provider
+                .read_credentials(&server_url, cx)
+                .await
+                .log_err()
+                .flatten()?;
+
+            Some(Credentials {
+                user_id: user_id.parse().ok()?,
+                access_token: String::from_utf8(access_token).ok()?,
+            })
+        }
+        .boxed_local()
+    }
 
     /// Writes the credentials to the provider.
     fn write_credentials<'a>(
@@ -321,13 +344,32 @@ trait CredentialsProvider {
         user_id: u64,
         access_token: String,
         cx: &'a AsyncApp,
-    ) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>>;
+    ) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
+        async move {
+            let server_url = self.server_url(cx)?;
+            self.provider
+                .write_credentials(
+                    &server_url,
+                    &user_id.to_string(),
+                    access_token.as_bytes(),
+                    cx,
+                )
+                .await
+        }
+        .boxed_local()
+    }
 
     /// Deletes the credentials from the provider.
     fn delete_credentials<'a>(
         &'a self,
         cx: &'a AsyncApp,
-    ) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>>;
+    ) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
+        async move {
+            let server_url = self.server_url(cx)?;
+            self.provider.delete_credentials(&server_url, cx).await
+        }
+        .boxed_local()
+    }
 }
 
 impl Default for ClientState {
@@ -484,27 +526,12 @@ impl Client {
         http: Arc<HttpClientWithUrl>,
         cx: &mut App,
     ) -> Arc<Self> {
-        let use_zed_development_auth = match ReleaseChannel::try_global(cx) {
-            Some(ReleaseChannel::Dev) => *ZED_DEVELOPMENT_AUTH,
-            Some(ReleaseChannel::Nightly | ReleaseChannel::Preview | ReleaseChannel::Stable)
-            | None => false,
-        };
-
-        let credentials_provider: Arc<dyn CredentialsProvider + Send + Sync + 'static> =
-            if use_zed_development_auth {
-                Arc::new(DevelopmentCredentialsProvider {
-                    path: paths::config_dir().join("development_auth"),
-                })
-            } else {
-                Arc::new(KeychainCredentialsProvider)
-            };
-
         Arc::new(Self {
             id: AtomicU64::new(0),
             peer: Peer::new(0),
             telemetry: Telemetry::new(clock, http.clone(), cx),
             http,
-            credentials_provider,
+            credentials_provider: ClientCredentialsProvider::new(cx),
             state: Default::default(),
             handler_set: Default::default(),
 
@@ -842,8 +869,7 @@ impl Client {
                     Ok(conn) => {
                         self.state.write().credentials = Some(credentials.clone());
                         if !read_from_provider && IMPERSONATE_LOGIN.is_none() {
-                                self.credentials_provider.write_credentials(credentials.user_id, credentials.access_token, cx).await.log_err();
-
+                            self.credentials_provider.write_credentials(credentials.user_id, credentials.access_token, cx).await.log_err();
                         }
 
                         futures::select_biased! {
@@ -1588,130 +1614,6 @@ impl ProtoClient for Client {
     }
 }
 
-#[derive(Serialize, Deserialize)]
-struct DevelopmentCredentials {
-    user_id: u64,
-    access_token: String,
-}
-
-/// A credentials provider that stores credentials in a local file.
-///
-/// This MUST only be used in development, as this is not a secure way of storing
-/// credentials on user machines.
-///
-/// Its existence is purely to work around the annoyance of having to constantly
-/// re-allow access to the system keychain when developing Zed.
-struct DevelopmentCredentialsProvider {
-    path: PathBuf,
-}
-
-impl CredentialsProvider for DevelopmentCredentialsProvider {
-    fn read_credentials<'a>(
-        &'a self,
-        _cx: &'a AsyncApp,
-    ) -> Pin<Box<dyn Future<Output = Option<Credentials>> + 'a>> {
-        async move {
-            if IMPERSONATE_LOGIN.is_some() {
-                return None;
-            }
-
-            let json = std::fs::read(&self.path).log_err()?;
-
-            let credentials: DevelopmentCredentials = serde_json::from_slice(&json).log_err()?;
-
-            Some(Credentials {
-                user_id: credentials.user_id,
-                access_token: credentials.access_token,
-            })
-        }
-        .boxed_local()
-    }
-
-    fn write_credentials<'a>(
-        &'a self,
-        user_id: u64,
-        access_token: String,
-        _cx: &'a AsyncApp,
-    ) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
-        async move {
-            let json = serde_json::to_string(&DevelopmentCredentials {
-                user_id,
-                access_token,
-            })?;
-
-            std::fs::write(&self.path, json)?;
-
-            Ok(())
-        }
-        .boxed_local()
-    }
-
-    fn delete_credentials<'a>(
-        &'a self,
-        _cx: &'a AsyncApp,
-    ) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
-        async move { Ok(std::fs::remove_file(&self.path)?) }.boxed_local()
-    }
-}
-
-/// A credentials provider that stores credentials in the system keychain.
-struct KeychainCredentialsProvider;
-
-impl CredentialsProvider for KeychainCredentialsProvider {
-    fn read_credentials<'a>(
-        &'a self,
-        cx: &'a AsyncApp,
-    ) -> Pin<Box<dyn Future<Output = Option<Credentials>> + 'a>> {
-        async move {
-            if IMPERSONATE_LOGIN.is_some() {
-                return None;
-            }
-
-            let (user_id, access_token) = cx
-                .update(|cx| cx.read_credentials(&ClientSettings::get_global(cx).server_url))
-                .log_err()?
-                .await
-                .log_err()??;
-
-            Some(Credentials {
-                user_id: user_id.parse().ok()?,
-                access_token: String::from_utf8(access_token).ok()?,
-            })
-        }
-        .boxed_local()
-    }
-
-    fn write_credentials<'a>(
-        &'a self,
-        user_id: u64,
-        access_token: String,
-        cx: &'a AsyncApp,
-    ) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
-        async move {
-            cx.update(move |cx| {
-                cx.write_credentials(
-                    &ClientSettings::get_global(cx).server_url,
-                    &user_id.to_string(),
-                    access_token.as_bytes(),
-                )
-            })?
-            .await
-        }
-        .boxed_local()
-    }
-
-    fn delete_credentials<'a>(
-        &'a self,
-        cx: &'a AsyncApp,
-    ) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
-        async move {
-            cx.update(move |cx| cx.delete_credentials(&ClientSettings::get_global(cx).server_url))?
-                .await
-        }
-        .boxed_local()
-    }
-}
-
 /// prefix for the zed:// url scheme
 pub const ZED_URL_SCHEME: &str = "zed";
 

crates/credentials_provider/Cargo.toml 🔗

@@ -0,0 +1,21 @@
+[package]
+name = "credentials_provider"
+version = "0.1.0"
+edition.workspace = true
+publish.workspace = true
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/credentials_provider.rs"
+
+[dependencies]
+anyhow.workspace = true
+futures.workspace = true
+gpui.workspace = true
+paths.workspace = true
+release_channel.workspace = true
+serde.workspace = true
+serde_json.workspace = true

crates/credentials_provider/src/credentials_provider.rs 🔗

@@ -0,0 +1,187 @@
+use std::collections::HashMap;
+use std::future::Future;
+use std::path::PathBuf;
+use std::pin::Pin;
+use std::sync::{Arc, LazyLock};
+
+use anyhow::Result;
+use futures::FutureExt as _;
+use gpui::{App, AsyncApp};
+use release_channel::ReleaseChannel;
+
+/// An environment variable whose presence indicates that the development auth
+/// provider should be used.
+///
+/// Only works in development. Setting this environment variable in other release
+/// channels is a no-op.
+pub static ZED_DEVELOPMENT_AUTH: LazyLock<bool> = LazyLock::new(|| {
+    std::env::var("ZED_DEVELOPMENT_AUTH").map_or(false, |value| !value.is_empty())
+});
+
+/// A provider for credentials.
+///
+/// Used to abstract over reading and writing credentials to some form of
+/// persistence (like the system keychain).
+pub trait CredentialsProvider: Send + Sync {
+    /// Reads the credentials from the provider.
+    fn read_credentials<'a>(
+        &'a self,
+        url: &'a str,
+        cx: &'a AsyncApp,
+    ) -> Pin<Box<dyn Future<Output = Result<Option<(String, Vec<u8>)>>> + 'a>>;
+
+    /// Writes the credentials to the provider.
+    fn write_credentials<'a>(
+        &'a self,
+        url: &'a str,
+        username: &'a str,
+        password: &'a [u8],
+        cx: &'a AsyncApp,
+    ) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>>;
+
+    /// Deletes the credentials from the provider.
+    fn delete_credentials<'a>(
+        &'a self,
+        url: &'a str,
+        cx: &'a AsyncApp,
+    ) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>>;
+}
+
+impl dyn CredentialsProvider {
+    /// Returns the global [`CredentialsProvider`].
+    pub fn global(cx: &App) -> Arc<Self> {
+        // The `CredentialsProvider` trait has `Send + Sync` bounds on it, so it
+        // seems like this is a false positive from Clippy.
+        #[allow(clippy::arc_with_non_send_sync)]
+        Self::new(cx)
+    }
+
+    fn new(cx: &App) -> Arc<Self> {
+        let use_development_backend = match ReleaseChannel::try_global(cx) {
+            Some(ReleaseChannel::Dev) => *ZED_DEVELOPMENT_AUTH,
+            Some(ReleaseChannel::Nightly | ReleaseChannel::Preview | ReleaseChannel::Stable)
+            | None => false,
+        };
+
+        if use_development_backend {
+            Arc::new(DevelopmentCredentialsProvider::new())
+        } else {
+            Arc::new(KeychainCredentialsProvider)
+        }
+    }
+}
+
+/// A credentials provider that stores credentials in the system keychain.
+struct KeychainCredentialsProvider;
+
+impl CredentialsProvider for KeychainCredentialsProvider {
+    fn read_credentials<'a>(
+        &'a self,
+        url: &'a str,
+        cx: &'a AsyncApp,
+    ) -> Pin<Box<dyn Future<Output = Result<Option<(String, Vec<u8>)>>> + 'a>> {
+        async move { cx.update(|cx| cx.read_credentials(url))?.await }.boxed_local()
+    }
+
+    fn write_credentials<'a>(
+        &'a self,
+        url: &'a str,
+        username: &'a str,
+        password: &'a [u8],
+        cx: &'a AsyncApp,
+    ) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
+        async move {
+            cx.update(move |cx| cx.write_credentials(url, username, password))?
+                .await
+        }
+        .boxed_local()
+    }
+
+    fn delete_credentials<'a>(
+        &'a self,
+        url: &'a str,
+        cx: &'a AsyncApp,
+    ) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
+        async move { cx.update(move |cx| cx.delete_credentials(url))?.await }.boxed_local()
+    }
+}
+
+/// A credentials provider that stores credentials in a local file.
+///
+/// This MUST only be used in development, as this is not a secure way of storing
+/// credentials on user machines.
+///
+/// Its existence is purely to work around the annoyance of having to constantly
+/// re-allow access to the system keychain when developing Zed.
+struct DevelopmentCredentialsProvider {
+    path: PathBuf,
+}
+
+impl DevelopmentCredentialsProvider {
+    fn new() -> Self {
+        let path = paths::config_dir().join("development_credentials");
+
+        Self { path }
+    }
+
+    fn load_credentials(&self) -> Result<HashMap<String, (String, Vec<u8>)>> {
+        let json = std::fs::read(&self.path)?;
+        let credentials: HashMap<String, (String, Vec<u8>)> = serde_json::from_slice(&json)?;
+
+        Ok(credentials)
+    }
+
+    fn save_credentials(&self, credentials: &HashMap<String, (String, Vec<u8>)>) -> Result<()> {
+        let json = serde_json::to_string(credentials)?;
+        std::fs::write(&self.path, json)?;
+
+        Ok(())
+    }
+}
+
+impl CredentialsProvider for DevelopmentCredentialsProvider {
+    fn read_credentials<'a>(
+        &'a self,
+        url: &'a str,
+        _cx: &'a AsyncApp,
+    ) -> Pin<Box<dyn Future<Output = Result<Option<(String, Vec<u8>)>>> + 'a>> {
+        async move {
+            Ok(self
+                .load_credentials()
+                .unwrap_or_default()
+                .get(url)
+                .cloned())
+        }
+        .boxed_local()
+    }
+
+    fn write_credentials<'a>(
+        &'a self,
+        url: &'a str,
+        username: &'a str,
+        password: &'a [u8],
+        _cx: &'a AsyncApp,
+    ) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
+        async move {
+            let mut credentials = self.load_credentials().unwrap_or_default();
+            credentials.insert(url.to_string(), (username.to_string(), password.to_vec()));
+
+            self.save_credentials(&credentials)
+        }
+        .boxed_local()
+    }
+
+    fn delete_credentials<'a>(
+        &'a self,
+        url: &'a str,
+        _cx: &'a AsyncApp,
+    ) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
+        async move {
+            let mut credentials = self.load_credentials()?;
+            credentials.remove(url);
+
+            self.save_credentials(&credentials)
+        }
+        .boxed_local()
+    }
+}

crates/language_models/Cargo.toml 🔗

@@ -16,6 +16,7 @@ anthropic = { workspace = true, features = ["schemars"] }
 anyhow.workspace = true
 client.workspace = true
 collections.workspace = true
+credentials_provider.workspace = true
 copilot = { workspace = true, features = ["schemars"] }
 deepseek = { workspace = true, features = ["schemars"] }
 editor.workspace = true

crates/language_models/src/provider/anthropic.rs 🔗

@@ -2,6 +2,7 @@ use crate::AllLanguageModelSettings;
 use anthropic::{AnthropicError, ContentDelta, Event, ResponseContent};
 use anyhow::{anyhow, Context as _, Result};
 use collections::{BTreeMap, HashMap};
+use credentials_provider::CredentialsProvider;
 use editor::{Editor, EditorElement, EditorStyle};
 use futures::Stream;
 use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt, TryStreamExt as _};
@@ -70,10 +71,16 @@ pub struct State {
 
 impl State {
     fn reset_api_key(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
-        let delete_credentials =
-            cx.delete_credentials(&AllLanguageModelSettings::get_global(cx).anthropic.api_url);
+        let credentials_provider = <dyn CredentialsProvider>::global(cx);
+        let api_url = AllLanguageModelSettings::get_global(cx)
+            .anthropic
+            .api_url
+            .clone();
         cx.spawn(|this, mut cx| async move {
-            delete_credentials.await.ok();
+            credentials_provider
+                .delete_credentials(&api_url, &cx)
+                .await
+                .ok();
             this.update(&mut cx, |this, cx| {
                 this.api_key = None;
                 this.api_key_from_env = false;
@@ -83,16 +90,16 @@ impl State {
     }
 
     fn set_api_key(&mut self, api_key: String, cx: &mut Context<Self>) -> Task<Result<()>> {
-        let write_credentials = cx.write_credentials(
-            AllLanguageModelSettings::get_global(cx)
-                .anthropic
-                .api_url
-                .as_str(),
-            "Bearer",
-            api_key.as_bytes(),
-        );
+        let credentials_provider = <dyn CredentialsProvider>::global(cx);
+        let api_url = AllLanguageModelSettings::get_global(cx)
+            .anthropic
+            .api_url
+            .clone();
         cx.spawn(|this, mut cx| async move {
-            write_credentials.await?;
+            credentials_provider
+                .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx)
+                .await
+                .ok();
 
             this.update(&mut cx, |this, cx| {
                 this.api_key = Some(api_key);
@@ -110,6 +117,7 @@ impl State {
             return Task::ready(Ok(()));
         }
 
+        let credentials_provider = <dyn CredentialsProvider>::global(cx);
         let api_url = AllLanguageModelSettings::get_global(cx)
             .anthropic
             .api_url
@@ -119,8 +127,8 @@ impl State {
             let (api_key, from_env) = if let Ok(api_key) = std::env::var(ANTHROPIC_API_KEY_VAR) {
                 (api_key, true)
             } else {
-                let (_, api_key) = cx
-                    .update(|cx| cx.read_credentials(&api_url))?
+                let (_, api_key) = credentials_provider
+                    .read_credentials(&api_url, &cx)
                     .await?
                     .ok_or(AuthenticateError::CredentialsNotFound)?;
                 (

crates/language_models/src/provider/deepseek.rs 🔗

@@ -1,5 +1,6 @@
 use anyhow::{anyhow, Context as _, Result};
 use collections::BTreeMap;
+use credentials_provider::CredentialsProvider;
 use editor::{Editor, EditorElement, EditorStyle};
 use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
 use gpui::{
@@ -57,10 +58,16 @@ impl State {
     }
 
     fn reset_api_key(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
-        let settings = &AllLanguageModelSettings::get_global(cx).deepseek;
-        let delete_credentials = cx.delete_credentials(&settings.api_url);
+        let credentials_provider = <dyn CredentialsProvider>::global(cx);
+        let api_url = AllLanguageModelSettings::get_global(cx)
+            .deepseek
+            .api_url
+            .clone();
         cx.spawn(|this, mut cx| async move {
-            delete_credentials.await.log_err();
+            credentials_provider
+                .delete_credentials(&api_url, &cx)
+                .await
+                .log_err();
             this.update(&mut cx, |this, cx| {
                 this.api_key = None;
                 this.api_key_from_env = false;
@@ -70,12 +77,15 @@ impl State {
     }
 
     fn set_api_key(&mut self, api_key: String, cx: &mut Context<Self>) -> Task<Result<()>> {
-        let settings = &AllLanguageModelSettings::get_global(cx).deepseek;
-        let write_credentials =
-            cx.write_credentials(&settings.api_url, "Bearer", api_key.as_bytes());
-
+        let credentials_provider = <dyn CredentialsProvider>::global(cx);
+        let api_url = AllLanguageModelSettings::get_global(cx)
+            .deepseek
+            .api_url
+            .clone();
         cx.spawn(|this, mut cx| async move {
-            write_credentials.await?;
+            credentials_provider
+                .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx)
+                .await?;
             this.update(&mut cx, |this, cx| {
                 this.api_key = Some(api_key);
                 cx.notify();
@@ -88,17 +98,17 @@ impl State {
             return Task::ready(Ok(()));
         }
 
+        let credentials_provider = <dyn CredentialsProvider>::global(cx);
         let api_url = AllLanguageModelSettings::get_global(cx)
             .deepseek
             .api_url
             .clone();
-
         cx.spawn(|this, mut cx| async move {
             let (api_key, from_env) = if let Ok(api_key) = std::env::var(DEEPSEEK_API_KEY_VAR) {
                 (api_key, true)
             } else {
-                let (_, api_key) = cx
-                    .update(|cx| cx.read_credentials(&api_url))?
+                let (_, api_key) = credentials_provider
+                    .read_credentials(&api_url, &cx)
                     .await?
                     .ok_or(AuthenticateError::CredentialsNotFound)?;
                 (

crates/language_models/src/provider/google.rs 🔗

@@ -1,5 +1,6 @@
 use anyhow::{anyhow, Context as _, Result};
 use collections::BTreeMap;
+use credentials_provider::CredentialsProvider;
 use editor::{Editor, EditorElement, EditorStyle};
 use futures::{future::BoxFuture, FutureExt, StreamExt};
 use google_ai::stream_generate_content;
@@ -59,10 +60,16 @@ impl State {
     }
 
     fn reset_api_key(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
-        let delete_credentials =
-            cx.delete_credentials(&AllLanguageModelSettings::get_global(cx).google.api_url);
+        let credentials_provider = <dyn CredentialsProvider>::global(cx);
+        let api_url = AllLanguageModelSettings::get_global(cx)
+            .google
+            .api_url
+            .clone();
         cx.spawn(|this, mut cx| async move {
-            delete_credentials.await.ok();
+            credentials_provider
+                .delete_credentials(&api_url, &cx)
+                .await
+                .log_err();
             this.update(&mut cx, |this, cx| {
                 this.api_key = None;
                 this.api_key_from_env = false;
@@ -72,12 +79,15 @@ impl State {
     }
 
     fn set_api_key(&mut self, api_key: String, cx: &mut Context<Self>) -> Task<Result<()>> {
-        let settings = &AllLanguageModelSettings::get_global(cx).google;
-        let write_credentials =
-            cx.write_credentials(&settings.api_url, "Bearer", api_key.as_bytes());
-
+        let credentials_provider = <dyn CredentialsProvider>::global(cx);
+        let api_url = AllLanguageModelSettings::get_global(cx)
+            .google
+            .api_url
+            .clone();
         cx.spawn(|this, mut cx| async move {
-            write_credentials.await?;
+            credentials_provider
+                .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx)
+                .await?;
             this.update(&mut cx, |this, cx| {
                 this.api_key = Some(api_key);
                 cx.notify();
@@ -90,6 +100,7 @@ impl State {
             return Task::ready(Ok(()));
         }
 
+        let credentials_provider = <dyn CredentialsProvider>::global(cx);
         let api_url = AllLanguageModelSettings::get_global(cx)
             .google
             .api_url
@@ -99,8 +110,8 @@ impl State {
             let (api_key, from_env) = if let Ok(api_key) = std::env::var(GOOGLE_AI_API_KEY_VAR) {
                 (api_key, true)
             } else {
-                let (_, api_key) = cx
-                    .update(|cx| cx.read_credentials(&api_url))?
+                let (_, api_key) = credentials_provider
+                    .read_credentials(&api_url, &cx)
                     .await?
                     .ok_or(AuthenticateError::CredentialsNotFound)?;
                 (
@@ -208,16 +219,7 @@ impl LanguageModelProvider for GoogleLanguageModelProvider {
     }
 
     fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>> {
-        let state = self.state.clone();
-        let delete_credentials =
-            cx.delete_credentials(&AllLanguageModelSettings::get_global(cx).google.api_url);
-        cx.spawn(|mut cx| async move {
-            delete_credentials.await.log_err();
-            state.update(&mut cx, |this, cx| {
-                this.api_key = None;
-                cx.notify();
-            })
-        })
+        self.state.update(cx, |state, cx| state.reset_api_key(cx))
     }
 }
 

crates/language_models/src/provider/mistral.rs 🔗

@@ -1,5 +1,6 @@
 use anyhow::{anyhow, Context as _, Result};
 use collections::BTreeMap;
+use credentials_provider::CredentialsProvider;
 use editor::{Editor, EditorElement, EditorStyle};
 use futures::{future::BoxFuture, FutureExt, StreamExt};
 use gpui::{
@@ -62,10 +63,16 @@ impl State {
     }
 
     fn reset_api_key(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
-        let settings = &AllLanguageModelSettings::get_global(cx).mistral;
-        let delete_credentials = cx.delete_credentials(&settings.api_url);
+        let credentials_provider = <dyn CredentialsProvider>::global(cx);
+        let api_url = AllLanguageModelSettings::get_global(cx)
+            .mistral
+            .api_url
+            .clone();
         cx.spawn(|this, mut cx| async move {
-            delete_credentials.await.log_err();
+            credentials_provider
+                .delete_credentials(&api_url, &cx)
+                .await
+                .log_err();
             this.update(&mut cx, |this, cx| {
                 this.api_key = None;
                 this.api_key_from_env = false;
@@ -75,12 +82,15 @@ impl State {
     }
 
     fn set_api_key(&mut self, api_key: String, cx: &mut Context<Self>) -> Task<Result<()>> {
-        let settings = &AllLanguageModelSettings::get_global(cx).mistral;
-        let write_credentials =
-            cx.write_credentials(&settings.api_url, "Bearer", api_key.as_bytes());
-
+        let credentials_provider = <dyn CredentialsProvider>::global(cx);
+        let api_url = AllLanguageModelSettings::get_global(cx)
+            .mistral
+            .api_url
+            .clone();
         cx.spawn(|this, mut cx| async move {
-            write_credentials.await?;
+            credentials_provider
+                .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx)
+                .await?;
             this.update(&mut cx, |this, cx| {
                 this.api_key = Some(api_key);
                 cx.notify();
@@ -93,6 +103,7 @@ impl State {
             return Task::ready(Ok(()));
         }
 
+        let credentials_provider = <dyn CredentialsProvider>::global(cx);
         let api_url = AllLanguageModelSettings::get_global(cx)
             .mistral
             .api_url
@@ -101,8 +112,8 @@ impl State {
             let (api_key, from_env) = if let Ok(api_key) = std::env::var(MISTRAL_API_KEY_VAR) {
                 (api_key, true)
             } else {
-                let (_, api_key) = cx
-                    .update(|cx| cx.read_credentials(&api_url))?
+                let (_, api_key) = credentials_provider
+                    .read_credentials(&api_url, &cx)
                     .await?
                     .ok_or(AuthenticateError::CredentialsNotFound)?;
                 (

crates/language_models/src/provider/open_ai.rs 🔗

@@ -1,5 +1,6 @@
 use anyhow::{anyhow, Context as _, Result};
 use collections::BTreeMap;
+use credentials_provider::CredentialsProvider;
 use editor::{Editor, EditorElement, EditorStyle};
 use futures::{future::BoxFuture, FutureExt, StreamExt};
 use gpui::{
@@ -63,10 +64,16 @@ impl State {
     }
 
     fn reset_api_key(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
-        let settings = &AllLanguageModelSettings::get_global(cx).openai;
-        let delete_credentials = cx.delete_credentials(&settings.api_url);
+        let credentials_provider = <dyn CredentialsProvider>::global(cx);
+        let api_url = AllLanguageModelSettings::get_global(cx)
+            .openai
+            .api_url
+            .clone();
         cx.spawn(|this, mut cx| async move {
-            delete_credentials.await.log_err();
+            credentials_provider
+                .delete_credentials(&api_url, &cx)
+                .await
+                .log_err();
             this.update(&mut cx, |this, cx| {
                 this.api_key = None;
                 this.api_key_from_env = false;
@@ -76,12 +83,16 @@ impl State {
     }
 
     fn set_api_key(&mut self, api_key: String, cx: &mut Context<Self>) -> Task<Result<()>> {
-        let settings = &AllLanguageModelSettings::get_global(cx).openai;
-        let write_credentials =
-            cx.write_credentials(&settings.api_url, "Bearer", api_key.as_bytes());
-
+        let credentials_provider = <dyn CredentialsProvider>::global(cx);
+        let api_url = AllLanguageModelSettings::get_global(cx)
+            .openai
+            .api_url
+            .clone();
         cx.spawn(|this, mut cx| async move {
-            write_credentials.await?;
+            credentials_provider
+                .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx)
+                .await
+                .log_err();
             this.update(&mut cx, |this, cx| {
                 this.api_key = Some(api_key);
                 cx.notify();
@@ -94,6 +105,7 @@ impl State {
             return Task::ready(Ok(()));
         }
 
+        let credentials_provider = <dyn CredentialsProvider>::global(cx);
         let api_url = AllLanguageModelSettings::get_global(cx)
             .openai
             .api_url
@@ -102,8 +114,8 @@ impl State {
             let (api_key, from_env) = if let Ok(api_key) = std::env::var(OPENAI_API_KEY_VAR) {
                 (api_key, true)
             } else {
-                let (_, api_key) = cx
-                    .update(|cx| cx.read_credentials(&api_url))?
+                let (_, api_key) = credentials_provider
+                    .read_credentials(&api_url, &cx)
                     .await?
                     .ok_or(AuthenticateError::CredentialsNotFound)?;
                 (

crates/zed/Cargo.toml 🔗

@@ -41,6 +41,7 @@ command_palette.workspace = true
 command_palette_hooks.workspace = true
 component_preview.workspace = true
 copilot.workspace = true
+credentials_provider.workspace = true
 db.workspace = true
 diagnostics.workspace = true
 editor.workspace = true

crates/zed/src/main.rs 🔗

@@ -723,7 +723,7 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
 
 async fn authenticate(client: Arc<Client>, cx: &AsyncApp) -> Result<()> {
     if stdout_is_a_pty() {
-        if *client::ZED_DEVELOPMENT_AUTH {
+        if *credentials_provider::ZED_DEVELOPMENT_AUTH {
             client.authenticate_and_connect(true, cx).await?;
         } else if client::IMPERSONATE_LOGIN.is_some() {
             client.authenticate_and_connect(false, cx).await?;