diff --git a/Cargo.lock b/Cargo.lock index e72eaa951236711f57817931237319e9c0c5d6a5..b6ef656a735bc3ce386d4b26bee8ec3d3c6be284 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8859,6 +8859,7 @@ dependencies = [ "credentials_provider", "deepseek", "editor", + "extension", "extension_host", "fs", "futures 0.3.31", diff --git a/crates/extension/src/extension_host_proxy.rs b/crates/extension/src/extension_host_proxy.rs index 70b2da14b47d3ab3ddcc9ecc0c8de94524e89461..9a5fdf1207808620617e8ed352b5e8515fb5b62e 100644 --- a/crates/extension/src/extension_host_proxy.rs +++ b/crates/extension/src/extension_host_proxy.rs @@ -414,9 +414,17 @@ impl ExtensionLanguageModelProviderProxy for ExtensionHostProxy { cx: &mut App, ) { let Some(proxy) = self.language_model_provider_proxy.read().clone() else { + log::warn!( + "ExtensionHostProxy::register_language_model_provider: no proxy set for provider {}", + provider_id + ); return; }; + log::info!( + "ExtensionHostProxy::register_language_model_provider: delegating to proxy for {}", + provider_id + ); proxy.register_language_model_provider(provider_id, register_fn, cx) } diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 97aafe8ebd81e8d284429a69edc88c60de8ed677..c7f26da8e1d566f9582e127411022ce1ca0440dc 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -101,8 +101,17 @@ const LEGACY_LLM_EXTENSION_IDS: &[&str] = &[ /// This migration only runs once per provider - we track which providers have been /// migrated in `migrated_llm_providers` to avoid overriding user preferences. fn migrate_legacy_llm_provider_env_var(manifest: &ExtensionManifest, cx: &mut App) { + log::info!( + "migrate_legacy_llm_provider_env_var called for extension: {}", + manifest.id + ); + // Only apply migration to known legacy LLM extensions if !LEGACY_LLM_EXTENSION_IDS.contains(&manifest.id.as_ref()) { + log::info!( + " skipping - not a legacy LLM extension (known: {:?})", + LEGACY_LLM_EXTENSION_IDS + ); return; } @@ -122,7 +131,15 @@ fn migrate_legacy_llm_provider_env_var(manifest: &ExtensionManifest, cx: &mut Ap .migrated_llm_providers .contains(full_provider_id.as_ref()); + log::info!( + " provider {}: env_var={}, already_migrated={}", + full_provider_id, + env_var_name, + already_migrated + ); + if already_migrated { + log::info!(" skipping - already migrated"); continue; } @@ -131,6 +148,8 @@ fn migrate_legacy_llm_provider_env_var(manifest: &ExtensionManifest, cx: &mut Ap .map(|v| !v.is_empty()) .unwrap_or(false); + log::info!(" env_var_is_set: {}", env_var_is_set); + // Mark as migrated regardless of whether we enable env var reading let should_enable_env_var = env_var_is_set; settings::update_settings_file(::global(cx), cx, { @@ -697,28 +716,114 @@ impl ExtensionStore { /// This can be used to make certain functionality provided by extensions /// available out-of-the-box. pub fn auto_install_extensions(&mut self, cx: &mut Context) { + log::info!("auto_install_extensions called"); + if cfg!(test) { + log::info!("auto_install_extensions: skipping because cfg!(test)"); return; } let extension_settings = ExtensionSettings::get_global(cx); + log::info!( + "auto_install_extensions: settings has {} extensions: {:?}", + extension_settings.auto_install_extensions.len(), + extension_settings + .auto_install_extensions + .keys() + .collect::>() + ); + let extensions_to_install = extension_settings .auto_install_extensions .keys() - .filter(|extension_id| extension_settings.should_auto_install(extension_id)) + .filter(|extension_id| { + let should = extension_settings.should_auto_install(extension_id); + log::info!(" {} should_auto_install: {}", extension_id, should); + should + }) .filter(|extension_id| { let is_already_installed = self .extension_index .extensions .contains_key(extension_id.as_ref()); - !is_already_installed && !SUPPRESSED_EXTENSIONS.contains(&extension_id.as_ref()) + let dominated = SUPPRESSED_EXTENSIONS.contains(&extension_id.as_ref()); + log::info!( + " {} is_already_installed: {}, suppressed: {}", + extension_id, + is_already_installed, + dominated + ); + !is_already_installed && !dominated }) .cloned() .collect::>(); + log::info!( + "auto_install_extensions: will install {:?}", + extensions_to_install + ); + cx.spawn(async move |this, cx| { for extension_id in extensions_to_install { + // HACK: In debug builds, check if extension exists locally in repo's extensions/ dir + // and install as dev extension instead of fetching from registry. + // This allows testing unpublished extensions. + #[cfg(debug_assertions)] + { + let local_extension_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .parent() + .unwrap() + .join("extensions") + .join(extension_id.as_ref()); + + if local_extension_path.exists() { + log::info!( + "Auto-installing local dev extension: {} from {:?}", + extension_id, + local_extension_path + ); + + // Force-remove existing extension directory if it exists and isn't a symlink + // This handles the case where the extension was previously installed from the registry + if let Some(installed_dir) = this + .update(cx, |this, _cx| this.installed_dir.clone()) + .ok() + { + let existing_path = installed_dir.join(extension_id.as_ref()); + if existing_path.exists() { + let metadata = std::fs::symlink_metadata(&existing_path); + let is_symlink = metadata.map(|m| m.is_symlink()).unwrap_or(false); + if !is_symlink { + log::info!( + "Removing existing non-dev extension directory: {:?}", + existing_path + ); + if let Err(e) = std::fs::remove_dir_all(&existing_path) { + log::error!( + "Failed to remove existing extension directory {:?}: {}", + existing_path, + e + ); + } + } + } + } + + if let Some(task) = this + .update(cx, |this, cx| { + this.install_dev_extension(local_extension_path, cx) + }) + .ok() + { + task.await.log_err(); + } + continue; + } + } + this.update(cx, |this, cx| { this.install_latest_extension(extension_id.clone(), cx); }) @@ -1139,6 +1244,11 @@ impl ExtensionStore { this.update(cx, |this, cx| this.reload(None, cx))?.await; this.update(cx, |this, cx| { + // Run migration for legacy LLM provider env vars + if let Some(manifest) = this.extension_manifest_for_id(&extension_id) { + migrate_legacy_llm_provider_env_var(&manifest, cx); + } + cx.emit(Event::ExtensionInstalled(extension_id.clone())); if let Some(events) = ExtensionEvents::try_global(cx) && let Some(manifest) = this.extension_manifest_for_id(&extension_id) @@ -1696,9 +1806,19 @@ impl ExtensionStore { } // Register LLM providers + log::info!( + "Extension {} has {} LLM providers to register", + manifest.id, + llm_providers_with_models.len() + ); for llm_provider in llm_providers_with_models { let provider_id: Arc = format!("{}:{}", manifest.id, llm_provider.provider_info.id).into(); + log::info!( + "Registering LLM provider {} with {} models", + provider_id, + llm_provider.models.len() + ); let wasm_ext = extension.as_ref().clone(); let pinfo = llm_provider.provider_info.clone(); let mods = llm_provider.models.clone(); diff --git a/crates/extension_host/src/wasm_host/llm_provider.rs b/crates/extension_host/src/wasm_host/llm_provider.rs index d3bc0ca036f6ed171aeae5de543a9af975555924..dff8834956cd834e6fb9f27e94c47f2aae0d6f5b 100644 --- a/crates/extension_host/src/wasm_host/llm_provider.rs +++ b/crates/extension_host/src/wasm_host/llm_provider.rs @@ -770,7 +770,8 @@ impl LanguageModel for ExtensionLanguageModel { } fn name(&self) -> LanguageModelName { - LanguageModelName::from(self.model_info.name.clone()) + // HACK: Add "(Extension)" prefix to help distinguish extension models during debugging + LanguageModelName::from(format!("(Extension) {}", self.model_info.name)) } fn provider_id(&self) -> LanguageModelProviderId { diff --git a/crates/language_model/src/registry.rs b/crates/language_model/src/registry.rs index bb563b14d70d4b938c739bdc29bdd46f38fa05b4..2b5d54dbf38d9573cf2cecd9961942a9e86bd27d 100644 --- a/crates/language_model/src/registry.rs +++ b/crates/language_model/src/registry.rs @@ -157,6 +157,11 @@ impl LanguageModelRegistry { cx: &mut Context, ) { let id = provider.id(); + log::info!( + "LanguageModelRegistry::register_provider: {} (name: {})", + id, + provider.name() + ); let subscription = provider.subscribe(cx, { let id = id.clone(); @@ -196,8 +201,22 @@ impl LanguageModelRegistry { /// Returns providers, filtering out hidden built-in providers. pub fn visible_providers(&self) -> Vec> { - self.providers() - .into_iter() + let all = self.providers(); + log::info!( + "LanguageModelRegistry::visible_providers called, all_providers={}, installed_llm_extension_ids={:?}", + all.len(), + self.installed_llm_extension_ids + ); + for p in &all { + let hidden = self.should_hide_provider(&p.id()); + log::info!( + " provider {} (id: {}): hidden={}", + p.name(), + p.id(), + hidden + ); + } + all.into_iter() .filter(|p| !self.should_hide_provider(&p.id())) .collect() } diff --git a/crates/language_models/Cargo.toml b/crates/language_models/Cargo.toml index 0bde1d6666d5446ceb6d6caad5d5a17d14b84413..4aaf9dcec5d33a8625297ffba98e1dbbc1c57fa8 100644 --- a/crates/language_models/Cargo.toml +++ b/crates/language_models/Cargo.toml @@ -28,6 +28,7 @@ convert_case.workspace = true copilot.workspace = true credentials_provider.workspace = true deepseek = { workspace = true, features = ["schemars"] } +extension.workspace = true extension_host.workspace = true fs.workspace = true futures.workspace = true diff --git a/crates/language_models/src/extension.rs b/crates/language_models/src/extension.rs index ed26bfb511684fb0e338408e8cb6b81dd786c314..b18d4e996e726def3311a6872e867a53f7ca26b6 100644 --- a/crates/language_models/src/extension.rs +++ b/crates/language_models/src/extension.rs @@ -1,5 +1,10 @@ +use ::extension::{ + ExtensionHostProxy, ExtensionLanguageModelProviderProxy, LanguageModelProviderRegistration, +}; use collections::HashMap; -use std::sync::LazyLock; +use gpui::{App, Entity}; +use language_model::{LanguageModelProviderId, LanguageModelRegistry}; +use std::sync::{Arc, LazyLock}; /// Maps built-in provider IDs to their corresponding extension IDs. /// When an extension with this ID is installed, the built-in provider should be hidden. @@ -9,7 +14,7 @@ static BUILTIN_TO_EXTENSION_MAP: LazyLock> = map.insert("anthropic", "anthropic"); map.insert("openai", "openai"); map.insert("google", "google-ai"); - map.insert("open_router", "open-router"); + map.insert("openrouter", "open-router"); map.insert("copilot_chat", "copilot-chat"); map }); @@ -18,3 +23,48 @@ static BUILTIN_TO_EXTENSION_MAP: LazyLock> = pub fn extension_for_builtin_provider(provider_id: &str) -> Option<&'static str> { BUILTIN_TO_EXTENSION_MAP.get(provider_id).copied() } + +/// Proxy that registers extension language model providers with the LanguageModelRegistry. +pub struct LanguageModelProviderRegistryProxy { + registry: Entity, +} + +impl LanguageModelProviderRegistryProxy { + pub fn new(registry: Entity) -> Self { + Self { registry } + } +} + +impl ExtensionLanguageModelProviderProxy for LanguageModelProviderRegistryProxy { + fn register_language_model_provider( + &self, + provider_id: Arc, + register_fn: LanguageModelProviderRegistration, + cx: &mut App, + ) { + log::info!( + "LanguageModelProviderRegistryProxy::register_language_model_provider called for: {}", + provider_id + ); + // The register_fn closure will call registry.register_provider internally + register_fn(cx); + } + + fn unregister_language_model_provider(&self, provider_id: Arc, cx: &mut App) { + self.registry.update(cx, |registry, cx| { + registry.unregister_provider(LanguageModelProviderId::from(provider_id), cx); + }); + } +} + +/// Initialize the extension language model provider proxy. +/// This must be called BEFORE extension_host::init to ensure the proxy is available +/// when extensions try to register their language model providers. +pub fn init_proxy(cx: &mut App) { + let proxy = ExtensionHostProxy::default_global(cx); + let registry = LanguageModelRegistry::global(cx); + log::info!( + "language_models::extension::init_proxy: registering LanguageModelProviderRegistryProxy" + ); + proxy.register_language_model_provider_proxy(LanguageModelProviderRegistryProxy::new(registry)); +} diff --git a/crates/language_models/src/language_models.rs b/crates/language_models/src/language_models.rs index 2444a5e68c381556e32041ff9adc934f2ef7a5cf..b93d5b4b5757b3cef21ecabfedd22d49631cb5f4 100644 --- a/crates/language_models/src/language_models.rs +++ b/crates/language_models/src/language_models.rs @@ -14,7 +14,7 @@ pub mod provider; mod settings; pub mod ui; -pub use crate::extension::extension_for_builtin_provider; +pub use crate::extension::{extension_for_builtin_provider, init_proxy as init_extension_proxy}; pub use crate::google_ai_api_key::api_key_for_gemini_cli; use crate::provider::anthropic::AnthropicLanguageModelProvider; use crate::provider::bedrock::BedrockLanguageModelProvider; diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 7751e6cb0118e3590488600ca2601645d6657fb7..283f25125be161d1eadc18e216f7679d3d2b6d61 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -554,6 +554,11 @@ pub fn main() { dap_adapters::init(cx); auto_update_ui::init(cx); reliability::init(client.clone(), cx); + // Initialize the language model registry first, then set up the extension proxy + // BEFORE extension_host::init so that extensions can register their LLM providers + // when they load. + language_model::init(app_state.client.clone(), cx); + language_models::init_extension_proxy(cx); extension_host::init( extension_host_proxy.clone(), app_state.fs.clone(), @@ -579,7 +584,6 @@ pub fn main() { cx, ); supermaven::init(app_state.client.clone(), cx); - language_model::init(app_state.client.clone(), cx); language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx); acp_tools::init(cx); edit_prediction_ui::init(cx);