From 26a77f9100b85baed914efa0f6ad72cac3cd7bd3 Mon Sep 17 00:00:00 2001 From: "zed-zippy[bot]" <234243425+zed-zippy[bot]@users.noreply.github.com> Date: Thu, 5 Feb 2026 22:57:06 +0000 Subject: [PATCH] Fix Codestral API key credentials URL mismatch (#48513) (cherry-pick to preview) (#48533) Cherry-pick of #48513 to preview ---- Closes #46506 Release Notes: - Fixed an issue where the codestral URL used for credentials would be different than the one used for requests causing authentication errors Co-authored-by: Ben Kunkle --- Cargo.lock | 5 +- crates/codestral/Cargo.toml | 3 +- crates/codestral/src/codestral.rs | 76 ++++++++++++------- .../src/edit_prediction_button.rs | 9 +-- .../language_models/src/provider/mistral.rs | 42 +--------- crates/mistral/src/mistral.rs | 1 - crates/settings_ui/Cargo.toml | 2 +- .../pages/edit_prediction_provider_setup.rs | 9 ++- crates/zed/src/zed.rs | 1 - .../zed/src/zed/edit_prediction_registry.rs | 7 +- 10 files changed, 67 insertions(+), 88 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5f43827c13c88024a8939048d22e3c92076fe1a0..4f27a87a85dabca6399c5412ce84c2ccd57ba3c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3301,9 +3301,8 @@ dependencies = [ "http_client", "icons", "language", - "language_models", + "language_model", "log", - "mistral", "serde", "serde_json", "text", @@ -15205,6 +15204,7 @@ dependencies = [ "assets", "bm25", "client", + "codestral", "component", "copilot", "copilot_ui", @@ -15218,7 +15218,6 @@ dependencies = [ "heck 0.5.0", "itertools 0.14.0", "language", - "language_models", "log", "menu", "node_runtime", diff --git a/crates/codestral/Cargo.toml b/crates/codestral/Cargo.toml index 0036e9df0f89bef0e4fec6ca48951549a5d55bb4..2addcf110a7c8194538523077d09af9d5104bd0d 100644 --- a/crates/codestral/Cargo.toml +++ b/crates/codestral/Cargo.toml @@ -17,9 +17,8 @@ gpui.workspace = true http_client.workspace = true icons.workspace = true language.workspace = true -language_models.workspace = true +language_model.workspace = true log.workspace = true -mistral.workspace = true serde.workspace = true serde_json.workspace = true text.workspace = true diff --git a/crates/codestral/src/codestral.rs b/crates/codestral/src/codestral.rs index 0720673ff784b68145cc28f8245ee5147db5779a..59a274b7b2c278957bb30264a95d012692091242 100644 --- a/crates/codestral/src/codestral.rs +++ b/crates/codestral/src/codestral.rs @@ -4,15 +4,15 @@ use edit_prediction_types::{ EditPrediction, EditPredictionDelegate, EditPredictionDismissReason, EditPredictionIconSet, }; use futures::AsyncReadExt; -use gpui::{App, Context, Entity, Task}; +use gpui::{App, AppContext as _, Context, Entity, Global, SharedString, Task}; use http_client::HttpClient; use icons::IconName; use language::{ Anchor, Buffer, BufferSnapshot, EditPreview, ToPoint, language_settings::all_language_settings, }; -use language_models::MistralLanguageModelProvider; -use mistral::CODESTRAL_API_URL; +use language_model::{ApiKeyState, AuthenticateError, EnvVar, env_var}; use serde::{Deserialize, Serialize}; + use std::{ ops::Range, sync::Arc, @@ -20,8 +20,50 @@ use std::{ }; use text::{OffsetRangeExt as _, ToOffset}; +pub const CODESTRAL_API_URL: &str = "https://codestral.mistral.ai"; pub const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(150); +static CODESTRAL_API_KEY_ENV_VAR: std::sync::LazyLock = env_var!("CODESTRAL_API_KEY"); + +struct GlobalCodestralApiKey(Entity); + +impl Global for GlobalCodestralApiKey {} + +pub fn codestral_api_key_state(cx: &mut App) -> Entity { + if let Some(global) = cx.try_global::() { + return global.0.clone(); + } + let entity = + cx.new(|cx| ApiKeyState::new(codestral_api_url(cx), CODESTRAL_API_KEY_ENV_VAR.clone())); + cx.set_global(GlobalCodestralApiKey(entity.clone())); + entity +} + +pub fn codestral_api_key(cx: &App) -> Option> { + let url = codestral_api_url(cx); + cx.try_global::()? + .0 + .read(cx) + .key(&url) +} + +pub fn load_codestral_api_key(cx: &mut App) -> Task> { + let api_url = codestral_api_url(cx); + codestral_api_key_state(cx).update(cx, |key_state, cx| { + key_state.load_if_needed(api_url, |s| s, cx) + }) +} + +pub fn codestral_api_url(cx: &App) -> SharedString { + all_language_settings(None, cx) + .edit_predictions + .codestral + .api_url + .clone() + .unwrap_or_else(|| CODESTRAL_API_URL.to_string()) + .into() +} + /// Represents a completion that has been received and processed from Codestral. /// This struct maintains the state needed to interpolate the completion as the user types. #[derive(Clone)] @@ -59,21 +101,8 @@ impl CodestralEditPredictionDelegate { } } - pub fn has_api_key(cx: &App) -> bool { - Self::api_key(cx).is_some() - } - - /// This is so we can immediately show Codestral as a provider users can - /// switch to in the edit prediction menu, if the API has been added - pub fn ensure_api_key_loaded(http_client: Arc, cx: &mut App) { - MistralLanguageModelProvider::global(http_client, cx) - .load_codestral_api_key(cx) - .detach(); - } - - fn api_key(cx: &App) -> Option> { - MistralLanguageModelProvider::try_global(cx) - .and_then(|provider| provider.codestral_api_key(CODESTRAL_API_URL, cx)) + pub fn ensure_api_key_loaded(cx: &mut App) { + load_codestral_api_key(cx).detach(); } /// Uses Codestral's Fill-in-the-Middle API for code completion. @@ -180,7 +209,7 @@ impl EditPredictionDelegate for CodestralEditPredictionDelegate { } fn is_enabled(&self, _buffer: &Entity, _cursor_position: Anchor, cx: &App) -> bool { - Self::api_key(cx).is_some() + codestral_api_key(cx).is_some() } fn is_refreshing(&self, _cx: &App) -> bool { @@ -196,7 +225,7 @@ impl EditPredictionDelegate for CodestralEditPredictionDelegate { ) { log::debug!("Codestral: Refresh called (debounce: {})", debounce); - let Some(api_key) = Self::api_key(cx) else { + let Some(api_key) = codestral_api_key(cx) else { log::warn!("Codestral: No API key configured, skipping refresh"); return; }; @@ -221,12 +250,7 @@ impl EditPredictionDelegate for CodestralEditPredictionDelegate { .clone() .unwrap_or_else(|| "codestral-latest".to_string()); let max_tokens = settings.edit_predictions.codestral.max_tokens; - let api_url = settings - .edit_predictions - .codestral - .api_url - .clone() - .unwrap_or_else(|| CODESTRAL_API_URL.to_string()); + let api_url = codestral_api_url(cx).to_string(); self.pending_request = Some(cx.spawn(async move |this, cx| { if debounce { diff --git a/crates/edit_prediction_ui/src/edit_prediction_button.rs b/crates/edit_prediction_ui/src/edit_prediction_button.rs index 1456aac401d1d453d967a84763c5740bf1180c09..8835dd5507dc9deccb57ad4f4ba15d8af017bfd3 100644 --- a/crates/edit_prediction_ui/src/edit_prediction_button.rs +++ b/crates/edit_prediction_ui/src/edit_prediction_button.rs @@ -1,7 +1,7 @@ use anyhow::Result; use client::{Client, UserStore, zed_urls}; use cloud_llm_client::UsageLimit; -use codestral::CodestralEditPredictionDelegate; +use codestral::{self, CodestralEditPredictionDelegate}; use copilot::Status; use edit_prediction::{EditPredictionStore, Zeta2FeatureFlag}; use edit_prediction_types::EditPredictionDelegateHandle; @@ -287,7 +287,7 @@ impl Render for EditPredictionButton { EditPredictionProvider::Codestral => { let enabled = self.editor_enabled.unwrap_or(true); - let has_api_key = CodestralEditPredictionDelegate::has_api_key(cx); + let has_api_key = codestral::codestral_api_key(cx).is_some(); let this = cx.weak_entity(); let file = self.file.clone(); let language = self.language.clone(); @@ -600,7 +600,6 @@ impl EditPredictionButton { fs: Arc, user_store: Entity, popover_menu_handle: PopoverMenuHandle, - client: Arc, project: Entity, cx: &mut Context, ) -> Self { @@ -630,7 +629,7 @@ impl EditPredictionButton { }) .detach(); - CodestralEditPredictionDelegate::ensure_api_key_loaded(client.http_client(), cx); + CodestralEditPredictionDelegate::ensure_api_key_loaded(cx); Self { editor_subscription: None, @@ -1493,7 +1492,7 @@ pub fn get_available_providers(cx: &mut App) -> Vec { } } - if CodestralEditPredictionDelegate::has_api_key(cx) { + if codestral::codestral_api_key(cx).is_some() { providers.push(EditPredictionProvider::Codestral); } diff --git a/crates/language_models/src/provider/mistral.rs b/crates/language_models/src/provider/mistral.rs index c8c34d7d2942ca1b42613d8733dc2219800bd66c..0fa4755ed544b2026b1ce17accf32f712c6026c5 100644 --- a/crates/language_models/src/provider/mistral.rs +++ b/crates/language_models/src/provider/mistral.rs @@ -11,7 +11,7 @@ use language_model::{ LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolUse, MessageContent, RateLimiter, Role, StopReason, TokenUsage, env_var, }; -pub use mistral::{CODESTRAL_API_URL, MISTRAL_API_URL, StreamResponse}; +pub use mistral::{MISTRAL_API_URL, StreamResponse}; pub use settings::MistralAvailableModel as AvailableModel; use settings::{Settings, SettingsStore}; use std::collections::HashMap; @@ -29,9 +29,6 @@ const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new( const API_KEY_ENV_VAR_NAME: &str = "MISTRAL_API_KEY"; static API_KEY_ENV_VAR: LazyLock = env_var!(API_KEY_ENV_VAR_NAME); -const CODESTRAL_API_KEY_ENV_VAR_NAME: &str = "CODESTRAL_API_KEY"; -static CODESTRAL_API_KEY_ENV_VAR: LazyLock = env_var!(CODESTRAL_API_KEY_ENV_VAR_NAME); - #[derive(Default, Clone, Debug, PartialEq)] pub struct MistralSettings { pub api_url: String, @@ -45,20 +42,6 @@ pub struct MistralLanguageModelProvider { pub struct State { api_key_state: ApiKeyState, - codestral_api_key_state: Entity, -} - -pub fn codestral_api_key(cx: &mut App) -> Entity { - // IMPORTANT: - // Do not store `Entity` handles in process-wide statics (e.g. `OnceLock`). - // - // `Entity` is tied to a particular `App`/entity-map context. Caching it globally can - // cause panics like "used a entity with the wrong context" when tests (or multiple apps) - // create distinct `App` instances in the same process. - // - // If we want a per-process singleton, store plain data (e.g. env var names) and create - // the entity per-App instead. - cx.new(|_| ApiKeyState::new(CODESTRAL_API_URL.into(), CODESTRAL_API_KEY_ENV_VAR.clone())) } impl State { @@ -77,15 +60,6 @@ impl State { self.api_key_state .load_if_needed(api_url, |this| &mut this.api_key_state, cx) } - - fn authenticate_codestral( - &mut self, - cx: &mut Context, - ) -> Task> { - self.codestral_api_key_state.update(cx, |state, cx| { - state.load_if_needed(CODESTRAL_API_URL.into(), |state| state, cx) - }) - } } struct GlobalMistralLanguageModelProvider(Arc); @@ -112,7 +86,6 @@ impl MistralLanguageModelProvider { .detach(); State { api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()), - codestral_api_key_state: codestral_api_key(cx), } }); @@ -121,19 +94,6 @@ impl MistralLanguageModelProvider { cx.global::().0.clone() } - pub fn load_codestral_api_key(&self, cx: &mut App) -> Task> { - self.state - .update(cx, |state, cx| state.authenticate_codestral(cx)) - } - - pub fn codestral_api_key(&self, url: &str, cx: &App) -> Option> { - self.state - .read(cx) - .codestral_api_key_state - .read(cx) - .key(url) - } - fn create_language_model(&self, model: mistral::Model) -> Arc { Arc::new(MistralLanguageModel { id: LanguageModelId::from(model.id().to_string()), diff --git a/crates/mistral/src/mistral.rs b/crates/mistral/src/mistral.rs index 2fa8a2cedaee01daa1452ade35b20c440055b7fc..04e641c23b5387966fe8228e4bc13aa27758e5b2 100644 --- a/crates/mistral/src/mistral.rs +++ b/crates/mistral/src/mistral.rs @@ -7,7 +7,6 @@ use std::convert::TryFrom; use strum::EnumIter; pub const MISTRAL_API_URL: &str = "https://api.mistral.ai/v1"; -pub const CODESTRAL_API_URL: &str = "https://codestral.mistral.ai"; #[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)] #[serde(rename_all = "lowercase")] diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml index 2e8ce3aa25211d602bd4311d9d3a27bd5b5bd950..f497abc19c423379ed0f86f30f804f1a3e204920 100644 --- a/crates/settings_ui/Cargo.toml +++ b/crates/settings_ui/Cargo.toml @@ -21,6 +21,7 @@ agent_settings.workspace = true anyhow.workspace = true bm25 = "2.3.2" component.workspace = true +codestral.workspace = true copilot.workspace = true copilot_ui.workspace = true edit_prediction.workspace = true @@ -32,7 +33,6 @@ fuzzy.workspace = true gpui.workspace = true heck.workspace = true itertools.workspace = true -language_models.workspace = true language.workspace = true log.workspace = true menu.workspace = true diff --git a/crates/settings_ui/src/pages/edit_prediction_provider_setup.rs b/crates/settings_ui/src/pages/edit_prediction_provider_setup.rs index b88a355d6779d0273b291b8b31a5041268d7e416..b2d35ad5baf5e4ee5fabd13ac0a799152cdeead5 100644 --- a/crates/settings_ui/src/pages/edit_prediction_provider_setup.rs +++ b/crates/settings_ui/src/pages/edit_prediction_provider_setup.rs @@ -1,3 +1,4 @@ +use codestral::{CODESTRAL_API_URL, codestral_api_key_state, codestral_api_url}; use edit_prediction::{ ApiKeyState, mercury::{MERCURY_CREDENTIALS_URL, mercury_api_token}, @@ -6,7 +7,7 @@ use edit_prediction::{ use edit_prediction_ui::{get_available_providers, set_completion_provider}; use gpui::{Entity, ScrollHandle, prelude::*}; use language::language_settings::AllLanguageSettings; -use language_models::provider::mistral::{CODESTRAL_API_URL, codestral_api_key}; + use settings::Settings as _; use ui::{ButtonLink, ConfiguredApiCard, ContextMenu, DropdownMenu, DropdownStyle, prelude::*}; use workspace::AppState; @@ -69,8 +70,8 @@ pub(crate) fn render_edit_prediction_setup_page( IconName::AiMistral, "Codestral", "https://console.mistral.ai/codestral".into(), - codestral_api_key(cx), - |cx| language_models::MistralLanguageModelProvider::api_url(cx), + codestral_api_key_state(cx), + |cx| codestral_api_url(cx), Some( settings_window .render_sub_page_items_section( @@ -174,7 +175,7 @@ fn render_api_key_provider( cx: &mut Context, ) -> impl IntoElement { let weak_page = cx.weak_entity(); - _ = window.use_keyed_state(title, cx, |_, cx| { + _ = window.use_keyed_state(current_url(cx), cx, |_, cx| { let task = api_key_state.update(cx, |key_state, cx| { key_state.load_if_needed(current_url(cx), |state| state, cx) }); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 68303c30f9b12744944a2ee2e555a8e4ca18e369..f6789ae2b3fe254b6cdaf0185ea4875f92d1fe94 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -407,7 +407,6 @@ pub fn initialize_workspace( app_state.fs.clone(), app_state.user_store.clone(), edit_prediction_menu_handle.clone(), - app_state.client.clone(), workspace.project().clone(), cx, ) diff --git a/crates/zed/src/zed/edit_prediction_registry.rs b/crates/zed/src/zed/edit_prediction_registry.rs index ff4234ec2c5a0ecb1e12d336d26e13bd9ca564d9..2347e27ccaa9ee5a94a9db4d262607ce126c3e57 100644 --- a/crates/zed/src/zed/edit_prediction_registry.rs +++ b/crates/zed/src/zed/edit_prediction_registry.rs @@ -1,5 +1,5 @@ use client::{Client, UserStore}; -use codestral::CodestralEditPredictionDelegate; +use codestral::{CodestralEditPredictionDelegate, load_codestral_api_key}; use collections::HashMap; use copilot::CopilotEditPredictionDelegate; use edit_prediction::{ZedEditPredictionDelegate, Zeta2FeatureFlag}; @@ -7,7 +7,7 @@ use editor::Editor; use feature_flags::FeatureFlagAppExt; use gpui::{AnyWindowHandle, App, AppContext as _, Context, Entity, WeakEntity}; use language::language_settings::{EditPredictionProvider, all_language_settings}; -use language_models::MistralLanguageModelProvider; + use settings::{EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME, SettingsStore}; use std::{cell::RefCell, rc::Rc, sync::Arc}; use supermaven::{Supermaven, SupermavenEditPredictionDelegate}; @@ -111,8 +111,7 @@ fn assign_edit_prediction_providers( cx: &mut App, ) { if provider == EditPredictionProvider::Codestral { - let mistral = MistralLanguageModelProvider::global(client.http_client(), cx); - mistral.load_codestral_api_key(cx).detach(); + load_codestral_api_key(cx).detach(); } for (editor, window) in editors.borrow().iter() { _ = window.update(cx, |_window, window, cx| {