Allow multiple API keys

Richard Feldman created

Change summary

crates/extension/src/extension_manifest.rs              |   5 
crates/extension_host/src/extension_host.rs             |  69 +-
crates/extension_host/src/extension_settings.rs         |  10 
crates/extension_host/src/wasm_host/llm_provider.rs     | 341 +++++-----
crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs |  77 +-
crates/settings/src/settings_content/extension.rs       |   6 
extensions/anthropic/extension.toml                     |   2 
extensions/google-ai/extension.toml                     |   2 
extensions/openai/extension.toml                        |   2 
extensions/openrouter/extension.toml                    |   2 
10 files changed, 262 insertions(+), 254 deletions(-)

Detailed changes

crates/extension/src/extension_manifest.rs 🔗

@@ -330,9 +330,10 @@ pub struct LanguageModelManifestEntry {
 pub struct LanguageModelAuthConfig {
     /// Human-readable name for the credential shown in the UI input field (e.g. "API Key", "Access Token").
     pub credential_label: Option<String>,
-    /// Environment variable name for the API key (if env var auth supported).
+    /// Environment variable names for the API key (if env var auth supported).
+    /// Multiple env vars can be specified; they will be checked in order.
     #[serde(default)]
-    pub env_var: Option<String>,
+    pub env_vars: Option<Vec<String>>,
     /// OAuth configuration for web-based authentication flows.
     #[serde(default)]
     pub oauth: Option<OAuthConfig>,

crates/extension_host/src/extension_host.rs 🔗

@@ -99,7 +99,7 @@ const LEGACY_LLM_EXTENSION_IDS: &[&str] = &[
 /// Migrates legacy LLM provider extensions by auto-enabling env var reading
 /// if the env var is currently present in the environment.
 ///
-/// This is idempotent: if the provider is already in `allowed_env_var_providers`,
+/// This is idempotent: if the env var is already in `allowed_env_vars`,
 /// we skip. This means if a user explicitly removes it, it will be re-added on
 /// next launch if the env var is still set - but that's predictable behavior.
 fn migrate_legacy_llm_provider_env_var(manifest: &ExtensionManifest, cx: &mut App) {
@@ -113,48 +113,51 @@ fn migrate_legacy_llm_provider_env_var(manifest: &ExtensionManifest, cx: &mut Ap
         let Some(auth_config) = &provider_entry.auth else {
             continue;
         };
-        let Some(env_var_name) = &auth_config.env_var else {
+        let Some(env_vars) = &auth_config.env_vars else {
             continue;
         };
 
-        let full_provider_id: Arc<str> = format!("{}:{}", manifest.id, provider_id).into();
+        let full_provider_id = format!("{}:{}", manifest.id, provider_id);
 
-        // Check if the env var is present and non-empty
-        let env_var_is_set = std::env::var(env_var_name)
-            .map(|v| !v.is_empty())
-            .unwrap_or(false);
+        // For each env var, check if it's set and enable it if so
+        for env_var_name in env_vars {
+            let env_var_is_set = std::env::var(env_var_name)
+                .map(|v| !v.is_empty())
+                .unwrap_or(false);
 
-        // If env var isn't set, no need to do anything
-        if !env_var_is_set {
-            continue;
-        }
+            if !env_var_is_set {
+                continue;
+            }
 
-        // Check if already enabled in settings
-        let already_enabled = ExtensionSettings::get_global(cx)
-            .allowed_env_var_providers
-            .contains(full_provider_id.as_ref());
+            let settings_key: Arc<str> = format!("{}:{}", full_provider_id, env_var_name).into();
 
-        if already_enabled {
-            continue;
-        }
+            // Check if already enabled in settings
+            let already_enabled = ExtensionSettings::get_global(cx)
+                .allowed_env_vars
+                .contains(settings_key.as_ref());
 
-        // Enable env var reading since the env var is set
-        settings::update_settings_file(<dyn fs::Fs>::global(cx), cx, {
-            let full_provider_id = full_provider_id.clone();
-            move |settings, _| {
-                let providers = settings
-                    .extension
-                    .allowed_env_var_providers
-                    .get_or_insert_with(Vec::new);
+            if already_enabled {
+                continue;
+            }
 
-                if !providers
-                    .iter()
-                    .any(|id| id.as_ref() == full_provider_id.as_ref())
-                {
-                    providers.push(full_provider_id);
+            // Enable env var reading since the env var is set
+            settings::update_settings_file(<dyn fs::Fs>::global(cx), cx, {
+                let settings_key = settings_key.clone();
+                move |settings, _| {
+                    let allowed = settings
+                        .extension
+                        .allowed_env_vars
+                        .get_or_insert_with(Vec::new);
+
+                    if !allowed
+                        .iter()
+                        .any(|id| id.as_ref() == settings_key.as_ref())
+                    {
+                        allowed.push(settings_key);
+                    }
                 }
-            }
-        });
+            });
+        }
     }
 }
 

crates/extension_host/src/extension_settings.rs 🔗

@@ -17,9 +17,9 @@ pub struct ExtensionSettings {
     pub auto_update_extensions: HashMap<Arc<str>, bool>,
     pub granted_capabilities: Vec<ExtensionCapability>,
     /// The extension language model providers that are allowed to read API keys
-    /// from environment variables. Each entry is a provider ID in the format
-    /// "extension_id:provider_id".
-    pub allowed_env_var_providers: HashSet<Arc<str>>,
+    /// from environment variables. Each entry is in the format
+    /// "extension_id:provider_id:ENV_VAR_NAME".
+    pub allowed_env_vars: HashSet<Arc<str>>,
 }
 
 impl ExtensionSettings {
@@ -64,9 +64,9 @@ impl Settings for ExtensionSettings {
                     }
                 })
                 .collect(),
-            allowed_env_var_providers: content
+            allowed_env_vars: content
                 .extension
-                .allowed_env_var_providers
+                .allowed_env_vars
                 .clone()
                 .unwrap_or_default()
                 .into_iter()

crates/extension_host/src/wasm_host/llm_provider.rs 🔗

@@ -1,5 +1,6 @@
 use crate::ExtensionSettings;
 use crate::wasm_host::WasmExtension;
+use collections::HashSet;
 
 use crate::wasm_host::wit::{
     LlmCompletionEvent, LlmCompletionRequest, LlmImageData, LlmMessageContent, LlmMessageRole,
@@ -46,8 +47,10 @@ pub struct ExtensionLanguageModelProvider {
 pub struct ExtensionLlmProviderState {
     is_authenticated: bool,
     available_models: Vec<LlmModelInfo>,
-    env_var_allowed: bool,
-    api_key_from_env: bool,
+    /// Set of env var names that are allowed to be read for this provider.
+    allowed_env_vars: HashSet<String>,
+    /// If authenticated via env var, which one was used.
+    env_var_name_used: Option<String>,
 }
 
 impl EventEmitter<()> for ExtensionLlmProviderState {}
@@ -63,31 +66,40 @@ impl ExtensionLanguageModelProvider {
         cx: &mut App,
     ) -> Self {
         let provider_id_string = format!("{}:{}", extension.manifest.id, provider_info.id);
-        let env_var_allowed = ExtensionSettings::get_global(cx)
-            .allowed_env_var_providers
-            .contains(provider_id_string.as_str());
-
-        let (is_authenticated, api_key_from_env) =
-            if env_var_allowed && auth_config.as_ref().is_some_and(|c| c.env_var.is_some()) {
-                let env_var_name = auth_config.as_ref().unwrap().env_var.as_ref().unwrap();
-                if let Ok(value) = std::env::var(env_var_name) {
-                    if !value.is_empty() {
-                        (true, true)
-                    } else {
-                        (is_authenticated, false)
-                    }
-                } else {
-                    (is_authenticated, false)
+
+        // Build set of allowed env vars for this provider
+        let settings = ExtensionSettings::get_global(cx);
+        let mut allowed_env_vars = HashSet::default();
+        if let Some(env_vars) = auth_config.as_ref().and_then(|c| c.env_vars.as_ref()) {
+            for env_var_name in env_vars {
+                let key = format!("{}:{}", provider_id_string, env_var_name);
+                if settings.allowed_env_vars.contains(key.as_str()) {
+                    allowed_env_vars.insert(env_var_name.clone());
                 }
-            } else {
-                (is_authenticated, false)
-            };
+            }
+        }
+
+        // Check if any allowed env var is set
+        let env_var_name_used = allowed_env_vars.iter().find_map(|env_var_name| {
+            if let Ok(value) = std::env::var(env_var_name) {
+                if !value.is_empty() {
+                    return Some(env_var_name.clone());
+                }
+            }
+            None
+        });
+
+        let is_authenticated = if env_var_name_used.is_some() {
+            true
+        } else {
+            is_authenticated
+        };
 
         let state = cx.new(|_| ExtensionLlmProviderState {
             is_authenticated,
             available_models: models,
-            env_var_allowed,
-            api_key_from_env,
+            allowed_env_vars,
+            env_var_name_used,
         });
 
         Self {
@@ -184,18 +196,19 @@ impl LanguageModelProvider for ExtensionLanguageModelProvider {
             return true;
         }
 
-        // Also check env var dynamically (in case migration happened after provider creation)
+        // Also check env var dynamically (in case settings changed after provider creation)
         if let Some(ref auth_config) = self.auth_config {
-            if let Some(ref env_var_name) = auth_config.env_var {
+            if let Some(ref env_vars) = auth_config.env_vars {
                 let provider_id_string = self.provider_id_string();
-                let env_var_allowed = ExtensionSettings::get_global(cx)
-                    .allowed_env_var_providers
-                    .contains(provider_id_string.as_str());
-
-                if env_var_allowed {
-                    if let Ok(value) = std::env::var(env_var_name) {
-                        if !value.is_empty() {
-                            return true;
+                let settings = ExtensionSettings::get_global(cx);
+
+                for env_var_name in env_vars {
+                    let key = format!("{}:{}", provider_id_string, env_var_name);
+                    if settings.allowed_env_vars.contains(key.as_str()) {
+                        if let Ok(value) = std::env::var(env_var_name) {
+                            if !value.is_empty() {
+                                return true;
+                            }
                         }
                     }
                 }
@@ -409,11 +422,11 @@ impl ExtensionProviderConfigurationView {
         let state = self.state.clone();
 
         // Check if we should use env var (already set in state during provider construction)
-        let api_key_from_env = self.state.read(cx).api_key_from_env;
+        let using_env_var = self.state.read(cx).env_var_name_used.is_some();
 
         cx.spawn(async move |this, cx| {
             // If using env var, we're already authenticated
-            if api_key_from_env {
+            if using_env_var {
                 this.update(cx, |this, cx| {
                     this.loading_credentials = false;
                     cx.notify();
@@ -448,76 +461,72 @@ impl ExtensionProviderConfigurationView {
         .detach();
     }
 
-    fn toggle_env_var_permission(&mut self, cx: &mut Context<Self>) {
-        let full_provider_id: Arc<str> = self.full_provider_id.clone().into();
-        let env_var_name = match &self.auth_config {
-            Some(config) => config.env_var.clone(),
-            None => return,
-        };
+    fn toggle_env_var_permission(&mut self, env_var_name: String, cx: &mut Context<Self>) {
+        let full_provider_id = self.full_provider_id.clone();
+        let settings_key: Arc<str> = format!("{}:{}", full_provider_id, env_var_name).into();
 
         let state = self.state.clone();
-        let currently_allowed = self.state.read(cx).env_var_allowed;
+        let currently_allowed = self.state.read(cx).allowed_env_vars.contains(&env_var_name);
 
         // Update settings file
-        settings::update_settings_file(<dyn fs::Fs>::global(cx), cx, move |settings, _| {
-            let providers = settings
-                .extension
-                .allowed_env_var_providers
-                .get_or_insert_with(Vec::new);
-
-            if currently_allowed {
-                providers.retain(|id| id.as_ref() != full_provider_id.as_ref());
-            } else {
-                if !providers
-                    .iter()
-                    .any(|id| id.as_ref() == full_provider_id.as_ref())
-                {
-                    providers.push(full_provider_id.clone());
+        settings::update_settings_file(<dyn fs::Fs>::global(cx), cx, {
+            let settings_key = settings_key.clone();
+            move |settings, _| {
+                let allowed = settings
+                    .extension
+                    .allowed_env_vars
+                    .get_or_insert_with(Vec::new);
+
+                if currently_allowed {
+                    allowed.retain(|id| id.as_ref() != settings_key.as_ref());
+                } else {
+                    if !allowed
+                        .iter()
+                        .any(|id| id.as_ref() == settings_key.as_ref())
+                    {
+                        allowed.push(settings_key.clone());
+                    }
                 }
             }
         });
 
         // Update local state
         let new_allowed = !currently_allowed;
-        let new_from_env = if new_allowed {
-            if let Some(var_name) = &env_var_name {
-                if let Ok(value) = std::env::var(var_name) {
-                    !value.is_empty()
-                } else {
-                    false
-                }
-            } else {
-                false
-            }
-        } else {
-            false
-        };
+        let env_var_name_clone = env_var_name.clone();
 
         state.update(cx, |state, cx| {
-            state.env_var_allowed = new_allowed;
-            state.api_key_from_env = new_from_env;
-            if new_from_env {
-                state.is_authenticated = true;
+            if new_allowed {
+                state.allowed_env_vars.insert(env_var_name_clone.clone());
+                // Check if this env var is set and update env_var_name_used
+                if let Ok(value) = std::env::var(&env_var_name_clone) {
+                    if !value.is_empty() && state.env_var_name_used.is_none() {
+                        state.env_var_name_used = Some(env_var_name_clone);
+                        state.is_authenticated = true;
+                    }
+                }
+            } else {
+                state.allowed_env_vars.remove(&env_var_name_clone);
+                // If this was the env var being used, clear it and find another
+                if state.env_var_name_used.as_ref() == Some(&env_var_name_clone) {
+                    state.env_var_name_used = state.allowed_env_vars.iter().find_map(|var| {
+                        if let Ok(value) = std::env::var(var) {
+                            if !value.is_empty() {
+                                return Some(var.clone());
+                            }
+                        }
+                        None
+                    });
+                    if state.env_var_name_used.is_none() {
+                        // No env var auth available, need to check keychain
+                        state.is_authenticated = false;
+                    }
+                }
             }
             cx.notify();
         });
 
-        // If env var is being enabled, clear any stored keychain credentials
-        // so there's only one source of truth for the API key
-        if new_allowed {
-            let credential_key = self.credential_key.clone();
-            let credentials_provider = <dyn CredentialsProvider>::global(cx);
-            cx.spawn(async move |_this, cx| {
-                credentials_provider
-                    .delete_credentials(&credential_key, cx)
-                    .await
-                    .log_err();
-            })
-            .detach();
-        }
-
-        // If env var is being disabled, reload credentials from keychain
-        if !new_allowed {
+        // If all env vars are being disabled, reload credentials from keychain
+        if !new_allowed && self.state.read(cx).allowed_env_vars.is_empty() {
             self.reload_keychain_credentials(cx);
         }
 
@@ -760,8 +769,8 @@ impl gpui::Render for ExtensionProviderConfigurationView {
     fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let is_loading = self.loading_settings || self.loading_credentials;
         let is_authenticated = self.is_authenticated(cx);
-        let env_var_allowed = self.state.read(cx).env_var_allowed;
-        let api_key_from_env = self.state.read(cx).api_key_from_env;
+        let allowed_env_vars = self.state.read(cx).allowed_env_vars.clone();
+        let env_var_name_used = self.state.read(cx).env_var_name_used.clone();
         let has_oauth = self.has_oauth_config();
         let has_api_key = self.has_api_key_config();
 
@@ -780,95 +789,83 @@ impl gpui::Render for ExtensionProviderConfigurationView {
             content = content.child(MarkdownElement::new(markdown.clone(), style));
         }
 
-        // Render env var checkbox if the extension specifies an env var
+        // Render env var checkboxes - one for each env var the extension declares
         if let Some(auth_config) = &self.auth_config {
-            if let Some(env_var_name) = &auth_config.env_var {
-                let env_var_name = env_var_name.clone();
-                let checkbox_label =
-                    format!("Read API key from {} environment variable", env_var_name);
-
-                content = content.child(
-                    h_flex()
-                        .gap_2()
-                        .child(
-                            ui::Checkbox::new("env-var-permission", env_var_allowed.into())
-                                .on_click(cx.listener(|this, _, _window, cx| {
-                                    this.toggle_env_var_permission(cx);
-                                })),
-                        )
-                        .child(Label::new(checkbox_label).size(LabelSize::Small)),
-                );
+            if let Some(env_vars) = &auth_config.env_vars {
+                for env_var_name in env_vars {
+                    let is_allowed = allowed_env_vars.contains(env_var_name);
+                    let checkbox_label =
+                        format!("Read API key from {} environment variable", env_var_name);
+                    let env_var_for_click = env_var_name.clone();
 
-                // Show status if env var is allowed
-                if env_var_allowed {
-                    if api_key_from_env {
-                        let tooltip_label = format!(
-                            "To reset this API key, unset the {} environment variable.",
-                            env_var_name
-                        );
-                        content = content.child(
-                            h_flex()
-                                .mt_0p5()
-                                .p_1()
-                                .justify_between()
-                                .rounded_md()
-                                .border_1()
-                                .border_color(cx.theme().colors().border)
-                                .bg(cx.theme().colors().background)
-                                .child(
-                                    h_flex()
-                                        .flex_1()
-                                        .min_w_0()
-                                        .gap_1()
-                                        .child(
-                                            ui::Icon::new(ui::IconName::Check)
-                                                .color(Color::Success),
-                                        )
-                                        .child(
-                                            Label::new(format!(
-                                                "API key set in {} environment variable",
-                                                env_var_name
-                                            ))
-                                            .truncate(),
-                                        ),
-                                )
-                                .child(
-                                    ui::Button::new("reset-key", "Reset Key")
-                                        .label_size(LabelSize::Small)
-                                        .icon(ui::IconName::Undo)
-                                        .icon_size(ui::IconSize::Small)
-                                        .icon_color(Color::Muted)
-                                        .icon_position(ui::IconPosition::Start)
-                                        .disabled(true)
-                                        .tooltip(ui::Tooltip::text(tooltip_label)),
-                                ),
-                        );
-                        return content.into_any_element();
-                    } else {
-                        content = content.child(
-                            h_flex()
-                                .gap_2()
-                                .child(
-                                    ui::Icon::new(ui::IconName::Warning)
-                                        .color(Color::Warning)
-                                        .size(ui::IconSize::Small),
+                    content = content.child(
+                        h_flex()
+                            .gap_2()
+                            .child(
+                                ui::Checkbox::new(
+                                    SharedString::from(format!("env-var-{}", env_var_name)),
+                                    is_allowed.into(),
                                 )
-                                .child(
-                                    Label::new(format!(
-                                        "{} is not set or empty. You can set it and restart Zed, or use another authentication method below.",
-                                        env_var_name
-                                    ))
-                                    .color(Color::Warning)
-                                    .size(LabelSize::Small),
-                                ),
-                        );
-                    }
+                                .on_click(cx.listener(
+                                    move |this, _, _window, cx| {
+                                        this.toggle_env_var_permission(
+                                            env_var_for_click.clone(),
+                                            cx,
+                                        );
+                                    },
+                                )),
+                            )
+                            .child(Label::new(checkbox_label).size(LabelSize::Small)),
+                    );
+                }
+
+                // Show status if any env var is being used
+                if let Some(used_var) = &env_var_name_used {
+                    let tooltip_label = format!(
+                        "To reset this API key, unset the {} environment variable.",
+                        used_var
+                    );
+                    content = content.child(
+                        h_flex()
+                            .mt_0p5()
+                            .p_1()
+                            .justify_between()
+                            .rounded_md()
+                            .border_1()
+                            .border_color(cx.theme().colors().border)
+                            .bg(cx.theme().colors().background)
+                            .child(
+                                h_flex()
+                                    .flex_1()
+                                    .min_w_0()
+                                    .gap_1()
+                                    .child(ui::Icon::new(ui::IconName::Check).color(Color::Success))
+                                    .child(
+                                        Label::new(format!(
+                                            "API key set in {} environment variable",
+                                            used_var
+                                        ))
+                                        .truncate(),
+                                    ),
+                            )
+                            .child(
+                                ui::Button::new("reset-key", "Reset Key")
+                                    .label_size(LabelSize::Small)
+                                    .icon(ui::IconName::Undo)
+                                    .icon_size(ui::IconSize::Small)
+                                    .icon_color(Color::Muted)
+                                    .icon_position(ui::IconPosition::Start)
+                                    .disabled(true)
+                                    .tooltip(ui::Tooltip::text(tooltip_label)),
+                            ),
+                    );
+                    return content.into_any_element();
                 }
             }
         }
 
         // If authenticated, show success state with sign out option
-        if is_authenticated && !api_key_from_env {
+        if is_authenticated && env_var_name_used.is_none() {
             let reset_label = if has_oauth && !has_api_key {
                 "Sign Out"
             } else {
@@ -915,7 +912,7 @@ impl gpui::Render for ExtensionProviderConfigurationView {
         }
 
         // Not authenticated - show available auth options
-        if !api_key_from_env {
+        if env_var_name_used.is_none() {
             // Render OAuth sign-in button if configured
             if has_oauth {
                 let oauth_config = self.oauth_config();

crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs 🔗

@@ -1115,38 +1115,44 @@ impl llm_provider::Host for WasmState {
     async fn get_credential(&mut self, provider_id: String) -> wasmtime::Result<Option<String>> {
         let extension_id = self.manifest.id.clone();
 
-        // Check if this provider has an env var configured and if the user has allowed it
-        let env_var_name = self
+        // Check if this provider has env vars configured and if the user has allowed any of them
+        let env_vars = self
             .manifest
             .language_model_providers
             .get(&Arc::<str>::from(provider_id.as_str()))
             .and_then(|entry| entry.auth.as_ref())
-            .and_then(|auth| auth.env_var.clone());
-
-        if let Some(env_var_name) = env_var_name {
-            let full_provider_id: Arc<str> = format!("{}:{}", extension_id, provider_id).into();
-            // Read settings dynamically to get current allowed_env_var_providers
-            let is_allowed = self
-                .on_main_thread({
-                    let full_provider_id = full_provider_id.clone();
-                    move |cx| {
-                        async move {
-                            cx.update(|cx| {
-                                crate::extension_settings::ExtensionSettings::get_global(cx)
-                                    .allowed_env_var_providers
-                                    .contains(&full_provider_id)
-                            })
+            .and_then(|auth| auth.env_vars.clone());
+
+        if let Some(env_vars) = env_vars {
+            let full_provider_id = format!("{}:{}", extension_id, provider_id);
+
+            // Check each env var to see if it's allowed and set
+            for env_var_name in &env_vars {
+                let settings_key: Arc<str> =
+                    format!("{}:{}", full_provider_id, env_var_name).into();
+
+                let is_allowed = self
+                    .on_main_thread({
+                        let settings_key = settings_key.clone();
+                        move |cx| {
+                            async move {
+                                cx.update(|cx| {
+                                    crate::extension_settings::ExtensionSettings::get_global(cx)
+                                        .allowed_env_vars
+                                        .contains(&settings_key)
+                                })
+                            }
+                            .boxed_local()
                         }
-                        .boxed_local()
-                    }
-                })
-                .await
-                .unwrap_or(false);
+                    })
+                    .await
+                    .unwrap_or(false);
 
-            if is_allowed {
-                if let Ok(value) = env::var(&env_var_name) {
-                    if !value.is_empty() {
-                        return Ok(Some(value));
+                if is_allowed {
+                    if let Ok(value) = env::var(env_var_name) {
+                        if !value.is_empty() {
+                            return Ok(Some(value));
+                        }
                     }
                 }
             }
@@ -1220,9 +1226,11 @@ impl llm_provider::Host for WasmState {
         let mut allowed_provider_id: Option<Arc<str>> = None;
         for (provider_id, provider_entry) in &self.manifest.language_model_providers {
             if let Some(auth_config) = &provider_entry.auth {
-                if auth_config.env_var.as_deref() == Some(&name) {
-                    allowed_provider_id = Some(provider_id.clone());
-                    break;
+                if let Some(env_vars) = &auth_config.env_vars {
+                    if env_vars.iter().any(|v| v == &name) {
+                        allowed_provider_id = Some(provider_id.clone());
+                        break;
+                    }
                 }
             }
         }
@@ -1237,18 +1245,17 @@ impl llm_provider::Host for WasmState {
             return Ok(None);
         };
 
-        // Check if the user has allowed this provider to read env vars
-        // Read settings dynamically to get current allowed_env_var_providers
-        let full_provider_id: Arc<str> = format!("{}:{}", extension_id, provider_id).into();
+        // Check if the user has allowed this specific env var
+        let settings_key: Arc<str> = format!("{}:{}:{}", extension_id, provider_id, name).into();
         let is_allowed = self
             .on_main_thread({
-                let full_provider_id = full_provider_id.clone();
+                let settings_key = settings_key.clone();
                 move |cx| {
                     async move {
                         cx.update(|cx| {
                             crate::extension_settings::ExtensionSettings::get_global(cx)
-                                .allowed_env_var_providers
-                                .contains(&full_provider_id)
+                                .allowed_env_vars
+                                .contains(&settings_key)
                         })
                     }
                     .boxed_local()

crates/settings/src/settings_content/extension.rs 🔗

@@ -21,11 +21,11 @@ pub struct ExtensionSettingsContent {
     /// The capabilities granted to extensions.
     pub granted_extension_capabilities: Option<Vec<ExtensionCapabilityContent>>,
     /// Extension language model providers that are allowed to read API keys from
-    /// environment variables. Each entry is a provider ID in the format
-    /// "extension_id:provider_id" (e.g., "openai:openai").
+    /// environment variables. Each entry is in the format
+    /// "extension_id:provider_id:ENV_VAR_NAME" (e.g., "google-ai:google-ai:GEMINI_API_KEY").
     ///
     /// Default: []
-    pub allowed_env_var_providers: Option<Vec<Arc<str>>>,
+    pub allowed_env_vars: Option<Vec<Arc<str>>>,
 }
 
 /// A capability for an extension.

extensions/anthropic/extension.toml 🔗

@@ -10,4 +10,4 @@ repository = "https://github.com/zed-industries/zed"
 name = "Anthropic"
 
 [language_model_providers.anthropic.auth]
-env_var = "ANTHROPIC_API_KEY"
+env_vars = ["ANTHROPIC_API_KEY"]

extensions/google-ai/extension.toml 🔗

@@ -10,4 +10,4 @@ repository = "https://github.com/zed-industries/zed"
 name = "Google AI"
 
 [language_model_providers.google-ai.auth]
-env_var = "GEMINI_API_KEY"
+env_vars = ["GEMINI_API_KEY", "GOOGLE_AI_API_KEY"]

extensions/openai/extension.toml 🔗

@@ -10,4 +10,4 @@ repository = "https://github.com/zed-industries/zed"
 name = "OpenAI"
 
 [language_model_providers.openai.auth]
-env_var = "OPENAI_API_KEY"
+env_vars = ["OPENAI_API_KEY"]

extensions/openrouter/extension.toml 🔗

@@ -10,4 +10,4 @@ repository = "https://github.com/zed-industries/zed"
 name = "OpenRouter"
 
 [language_model_providers.openrouter.auth]
-env_var = "OPENROUTER_API_KEY"
+env_vars = ["OPENROUTER_API_KEY"]