ai: Show "API key configured for {URL}" for non-default urls (#38170)

Michael Sloan created

Followup to #38163, also makes some changes intended to be included in
that PR.

Release Notes:

- N/A

Change summary

crates/language_models/src/provider/anthropic.rs          | 17 ++-
crates/language_models/src/provider/deepseek.rs           | 34 ++++++--
crates/language_models/src/provider/google.rs             | 11 ++
crates/language_models/src/provider/mistral.rs            | 13 ++-
crates/language_models/src/provider/open_ai.rs            | 15 ++-
crates/language_models/src/provider/open_ai_compatible.rs |  6 
crates/language_models/src/provider/open_router.rs        | 21 ++++-
crates/language_models/src/provider/vercel.rs             | 15 ++-
crates/language_models/src/provider/x_ai.rs               | 11 ++
9 files changed, 100 insertions(+), 43 deletions(-)

Detailed changes

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

@@ -1,8 +1,8 @@
 use crate::api_key::ApiKeyState;
 use crate::ui::InstructionListItem;
 use anthropic::{
-    AnthropicError, AnthropicModelMode, ContentDelta, Event, ResponseContent, ToolResultContent,
-    ToolResultPart, Usage,
+    ANTHROPIC_API_URL, AnthropicError, AnthropicModelMode, ContentDelta, Event, ResponseContent,
+    ToolResultContent, ToolResultPart, Usage,
 };
 use anyhow::{Result, anyhow};
 use collections::{BTreeMap, HashMap};
@@ -27,7 +27,7 @@ use std::sync::{Arc, LazyLock};
 use strum::IntoEnumIterator;
 use theme::ThemeSettings;
 use ui::{Icon, IconName, List, Tooltip, prelude::*};
-use util::ResultExt;
+use util::{ResultExt, truncate_and_trailoff};
 use zed_env_vars::{EnvVar, env_var};
 
 const PROVIDER_ID: LanguageModelProviderId = language_model::ANTHROPIC_PROVIDER_ID;
@@ -162,7 +162,7 @@ impl AnthropicLanguageModelProvider {
     fn api_url(cx: &App) -> SharedString {
         let api_url = &Self::settings(cx).api_url;
         if api_url.is_empty() {
-            anthropic::ANTHROPIC_API_URL.into()
+            ANTHROPIC_API_URL.into()
         } else {
             SharedString::new(api_url.as_str())
         }
@@ -1045,9 +1045,14 @@ impl Render for ConfigurationView {
                         .gap_1()
                         .child(Icon::new(IconName::Check).color(Color::Success))
                         .child(Label::new(if env_var_set {
-                            format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable.")
+                            format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable")
                         } else {
-                            "API key configured.".to_string()
+                            let api_url = AnthropicLanguageModelProvider::api_url(cx);
+                            if api_url == ANTHROPIC_API_URL {
+                                "API key configured".to_string()
+                            } else {
+                                format!("API key configured for {}", truncate_and_trailoff(&api_url, 32))
+                            }
                         })),
                 )
                 .child(

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

@@ -1,5 +1,6 @@
 use anyhow::{Result, anyhow};
 use collections::{BTreeMap, HashMap};
+use deepseek::DEEPSEEK_API_URL;
 use editor::{Editor, EditorElement, EditorStyle};
 use futures::Stream;
 use futures::{FutureExt, StreamExt, future, future::BoxFuture, stream::BoxStream};
@@ -23,7 +24,7 @@ use std::str::FromStr;
 use std::sync::{Arc, LazyLock};
 use theme::ThemeSettings;
 use ui::{Icon, IconName, List, prelude::*};
-use util::ResultExt;
+use util::{ResultExt, truncate_and_trailoff};
 use zed_env_vars::{EnvVar, env_var};
 
 use crate::{api_key::ApiKeyState, ui::InstructionListItem};
@@ -70,13 +71,13 @@ impl State {
     }
 
     fn set_api_key(&mut self, api_key: Option<String>, cx: &mut Context<Self>) -> Task<Result<()>> {
-        let api_url = SharedString::new(DeepSeekLanguageModelProvider::api_url(cx));
+        let api_url = DeepSeekLanguageModelProvider::api_url(cx);
         self.api_key_state
             .store(api_url, api_key, |this| &mut this.api_key_state, cx)
     }
 
     fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
-        let api_url = SharedString::new(DeepSeekLanguageModelProvider::api_url(cx));
+        let api_url = DeepSeekLanguageModelProvider::api_url(cx);
         self.api_key_state.load_if_needed(
             api_url,
             &API_KEY_ENV_VAR,
@@ -90,7 +91,7 @@ impl DeepSeekLanguageModelProvider {
     pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut App) -> Self {
         let state = cx.new(|cx| {
             cx.observe_global::<SettingsStore>(|this: &mut State, cx| {
-                let api_url = SharedString::new(Self::api_url(cx));
+                let api_url = Self::api_url(cx);
                 this.api_key_state.handle_url_change(
                     api_url,
                     &API_KEY_ENV_VAR,
@@ -101,7 +102,7 @@ impl DeepSeekLanguageModelProvider {
             })
             .detach();
             State {
-                api_key_state: ApiKeyState::new(SharedString::new(Self::api_url(cx))),
+                api_key_state: ApiKeyState::new(Self::api_url(cx)),
             }
         });
 
@@ -122,8 +123,13 @@ impl DeepSeekLanguageModelProvider {
         &crate::AllLanguageModelSettings::get_global(cx).deepseek
     }
 
-    fn api_url(cx: &App) -> &str {
-        &Self::settings(cx).api_url
+    fn api_url(cx: &App) -> SharedString {
+        let api_url = &Self::settings(cx).api_url;
+        if api_url.is_empty() {
+            DEEPSEEK_API_URL.into()
+        } else {
+            SharedString::new(api_url.as_str())
+        }
     }
 }
 
@@ -222,7 +228,7 @@ impl DeepSeekLanguageModel {
 
         let Ok((api_key, api_url)) = self.state.read_with(cx, |state, cx| {
             let api_url = DeepSeekLanguageModelProvider::api_url(cx);
-            (state.api_key_state.key(api_url), api_url.to_string())
+            (state.api_key_state.key(&api_url), api_url)
         }) else {
             return future::ready(Err(anyhow!("App state dropped"))).boxed();
         };
@@ -691,9 +697,17 @@ impl Render for ConfigurationView {
                         .gap_1()
                         .child(Icon::new(IconName::Check).color(Color::Success))
                         .child(Label::new(if env_var_set {
-                            format!("API key set in {API_KEY_ENV_VAR_NAME}")
+                            format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable")
                         } else {
-                            "API key configured".to_string()
+                            let api_url = DeepSeekLanguageModelProvider::api_url(cx);
+                            if api_url == DEEPSEEK_API_URL {
+                                "API key configured".to_string()
+                            } else {
+                                format!(
+                                    "API key configured for {}",
+                                    truncate_and_trailoff(&api_url, 32)
+                                )
+                            }
                         })),
                 )
                 .child(

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

@@ -33,7 +33,7 @@ use std::sync::{
 use strum::IntoEnumIterator;
 use theme::ThemeSettings;
 use ui::{Icon, IconName, List, Tooltip, prelude::*};
-use util::ResultExt;
+use util::{ResultExt, truncate_and_trailoff};
 use zed_env_vars::EnvVar;
 
 use crate::api_key::ApiKey;
@@ -930,9 +930,14 @@ impl Render for ConfigurationView {
                         .gap_1()
                         .child(Icon::new(IconName::Check).color(Color::Success))
                         .child(Label::new(if env_var_set {
-                            format!("API key set in {GEMINI_API_KEY_VAR_NAME} environment variable.")
+                            format!("API key set in {} environment variable", API_KEY_ENV_VAR.name)
                         } else {
-                            "API key configured.".to_string()
+                            let api_url = GoogleLanguageModelProvider::api_url(cx);
+                            if api_url == google_ai::API_URL {
+                                "API key configured".to_string()
+                            } else {
+                                format!("API key configured for {}", truncate_and_trailoff(&api_url, 32))
+                            }
                         })),
                 )
                 .child(

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

@@ -14,7 +14,7 @@ use language_model::{
     LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolUse, MessageContent,
     RateLimiter, Role, StopReason, TokenUsage,
 };
-use mistral::StreamResponse;
+use mistral::{MISTRAL_API_URL, StreamResponse};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsStore};
@@ -25,7 +25,7 @@ use std::sync::{Arc, LazyLock};
 use strum::IntoEnumIterator;
 use theme::ThemeSettings;
 use ui::{Icon, IconName, List, Tooltip, prelude::*};
-use util::ResultExt;
+use util::{ResultExt, truncate_and_trailoff};
 use zed_env_vars::{EnvVar, env_var};
 
 use crate::{api_key::ApiKeyState, ui::InstructionListItem};
@@ -871,9 +871,14 @@ impl Render for ConfigurationView {
                         .gap_1()
                         .child(Icon::new(IconName::Check).color(Color::Success))
                         .child(Label::new(if env_var_set {
-                            format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable.")
+                            format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable")
                         } else {
-                            "API key configured.".to_string()
+                            let api_url = MistralLanguageModelProvider::api_url(cx);
+                            if api_url == MISTRAL_API_URL {
+                                "API key configured".to_string()
+                            } else {
+                                format!("API key configured for {}", truncate_and_trailoff(&api_url, 32))
+                            }
                         })),
                 )
                 .child(

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

@@ -12,7 +12,9 @@ use language_model::{
     RateLimiter, Role, StopReason, TokenUsage,
 };
 use menu;
-use open_ai::{ImageUrl, Model, ReasoningEffort, ResponseStreamEvent, stream_completion};
+use open_ai::{
+    ImageUrl, Model, OPEN_AI_API_URL, ReasoningEffort, ResponseStreamEvent, stream_completion,
+};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsStore};
@@ -22,7 +24,7 @@ use std::sync::{Arc, LazyLock};
 use strum::IntoEnumIterator;
 use ui::{ElevationIndex, List, Tooltip, prelude::*};
 use ui_input::SingleLineInput;
-use util::ResultExt;
+use util::{ResultExt, truncate_and_trailoff};
 use zed_env_vars::{EnvVar, env_var};
 
 use crate::{api_key::ApiKeyState, ui::InstructionListItem};
@@ -818,9 +820,14 @@ impl Render for ConfigurationView {
                         .gap_1()
                         .child(Icon::new(IconName::Check).color(Color::Success))
                         .child(Label::new(if env_var_set {
-                            format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable.")
+                            format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable")
                         } else {
-                            "API key configured.".to_string()
+                            let api_url = OpenAiLanguageModelProvider::api_url(cx);
+                            if api_url == OPEN_AI_API_URL {
+                                "API key configured".to_string()
+                            } else {
+                                format!("API key configured for {}", truncate_and_trailoff(&api_url, 32))
+                            }
                         })),
                 )
                 .child(

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

@@ -17,7 +17,7 @@ use settings::{Settings, SettingsStore};
 use std::sync::Arc;
 use ui::{ElevationIndex, Tooltip, prelude::*};
 use ui_input::SingleLineInput;
-use util::ResultExt;
+use util::{ResultExt, truncate_and_trailoff};
 use zed_env_vars::EnvVar;
 
 use crate::api_key::ApiKeyState;
@@ -488,9 +488,9 @@ impl Render for ConfigurationView {
                         .gap_1()
                         .child(Icon::new(IconName::Check).color(Color::Success))
                         .child(Label::new(if env_var_set {
-                            format!("API key set in {env_var_name} environment variable.")
+                            format!("API key set in {env_var_name} environment variable")
                         } else {
-                            "API key configured.".to_string()
+                            format!("API key configured for {}", truncate_and_trailoff(&state.settings.api_url, 32))
                         })),
                 )
                 .child(

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

@@ -14,7 +14,8 @@ use language_model::{
     LanguageModelToolUse, MessageContent, RateLimiter, Role, StopReason, TokenUsage,
 };
 use open_router::{
-    Model, ModelMode as OpenRouterModelMode, Provider, ResponseStreamEvent, list_models,
+    Model, ModelMode as OpenRouterModelMode, OPEN_ROUTER_API_URL, Provider, ResponseStreamEvent,
+    list_models,
 };
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
@@ -24,7 +25,7 @@ use std::str::FromStr as _;
 use std::sync::{Arc, LazyLock};
 use theme::ThemeSettings;
 use ui::{Icon, IconName, List, Tooltip, prelude::*};
-use util::ResultExt;
+use util::{ResultExt, truncate_and_trailoff};
 use zed_env_vars::{EnvVar, env_var};
 
 use crate::{api_key::ApiKeyState, ui::InstructionListItem};
@@ -199,7 +200,12 @@ impl OpenRouterLanguageModelProvider {
     }
 
     fn api_url(cx: &App) -> SharedString {
-        SharedString::new(Self::settings(cx).api_url.as_str())
+        let api_url = &Self::settings(cx).api_url;
+        if api_url.is_empty() {
+            OPEN_ROUTER_API_URL.into()
+        } else {
+            SharedString::new(api_url.as_str())
+        }
     }
 
     fn create_language_model(&self, model: open_router::Model) -> Arc<dyn LanguageModel> {
@@ -904,9 +910,14 @@ impl Render for ConfigurationView {
                         .gap_1()
                         .child(Icon::new(IconName::Check).color(Color::Success))
                         .child(Label::new(if env_var_set {
-                            format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable.")
+                            format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable")
                         } else {
-                            "API key configured.".to_string()
+                            let api_url = OpenRouterLanguageModelProvider::api_url(cx);
+                            if api_url == OPEN_ROUTER_API_URL {
+                                "API key configured".to_string()
+                            } else {
+                                format!("API key configured for {}", truncate_and_trailoff(&api_url, 32))
+                            }
                         })),
                 )
                 .child(

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

@@ -17,8 +17,8 @@ use std::sync::{Arc, LazyLock};
 use strum::IntoEnumIterator;
 use ui::{ElevationIndex, List, Tooltip, prelude::*};
 use ui_input::SingleLineInput;
-use util::ResultExt;
-use vercel::Model;
+use util::{ResultExt, truncate_and_trailoff};
+use vercel::{Model, VERCEL_API_URL};
 use zed_env_vars::{EnvVar, env_var};
 
 use crate::{api_key::ApiKeyState, ui::InstructionListItem};
@@ -114,7 +114,7 @@ impl VercelLanguageModelProvider {
     fn api_url(cx: &App) -> SharedString {
         let api_url = &Self::settings(cx).api_url;
         if api_url.is_empty() {
-            vercel::VERCEL_API_URL.into()
+            VERCEL_API_URL.into()
         } else {
             SharedString::new(api_url.as_str())
         }
@@ -502,9 +502,14 @@ impl Render for ConfigurationView {
                         .gap_1()
                         .child(Icon::new(IconName::Check).color(Color::Success))
                         .child(Label::new(if env_var_set {
-                            format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable.")
+                            format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable")
                         } else {
-                            "API key configured.".to_string()
+                            let api_url = VercelLanguageModelProvider::api_url(cx);
+                            if api_url == VERCEL_API_URL {
+                                "API key configured".to_string()
+                            } else {
+                                format!("API key configured for {}", truncate_and_trailoff(&api_url, 32))
+                            }
                         })),
                 )
                 .child(

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

@@ -17,7 +17,7 @@ use std::sync::{Arc, LazyLock};
 use strum::IntoEnumIterator;
 use ui::{ElevationIndex, List, Tooltip, prelude::*};
 use ui_input::SingleLineInput;
-use util::ResultExt;
+use util::{ResultExt, truncate_and_trailoff};
 use x_ai::{Model, XAI_API_URL};
 use zed_env_vars::{EnvVar, env_var};
 
@@ -496,9 +496,14 @@ impl Render for ConfigurationView {
                         .gap_1()
                         .child(Icon::new(IconName::Check).color(Color::Success))
                         .child(Label::new(if env_var_set {
-                            format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable.")
+                            format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable")
                         } else {
-                            "API key configured.".to_string()
+                            let api_url = XAiLanguageModelProvider::api_url(cx);
+                            if api_url == XAI_API_URL {
+                                "API key configured".to_string()
+                            } else {
+                                format!("API key configured for {}", truncate_and_trailoff(&api_url, 32))
+                            }
                         })),
                 )
                 .child(