language_model_selector: Authenticate all providers up front (#25123)

This PR fixes an issue where configured language model providers would
not show up unless the configuration view was opened.

The problem was that we were filtering unauthenticated language model
providers out of the language model selector, but would only
authenticate the active provider when the selector loaded.
Authenticating the rest of the providers was deferred until the
configuration view was opened for the first time.

Closes https://github.com/zed-industries/zed/issues/21821.

Release Notes:

- Fixed an issue where configured languages models were not showing up
in the language model selector until the configuration view was opened
for the first time.
This commit is contained in:
Marshall Bowers 2025-02-18 20:40:07 -05:00 committed by GitHub
parent 7a6b652ebc
commit 372aaecdb4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 57 additions and 1 deletions

1
Cargo.lock generated
View file

@ -6963,6 +6963,7 @@ dependencies = [
"feature_flags", "feature_flags",
"gpui", "gpui",
"language_model", "language_model",
"log",
"picker", "picker",
"proto", "proto",
"ui", "ui",

View file

@ -15,6 +15,7 @@ path = "src/language_model_selector.rs"
feature_flags.workspace = true feature_flags.workspace = true
gpui.workspace = true gpui.workspace = true
language_model.workspace = true language_model.workspace = true
log.workspace = true
picker.workspace = true picker.workspace = true
proto.workspace = true proto.workspace = true
ui.workspace = true ui.workspace = true

View file

@ -5,7 +5,9 @@ use gpui::{
Action, AnyElement, AnyView, App, Corner, DismissEvent, Entity, EventEmitter, FocusHandle, Action, AnyElement, AnyView, App, Corner, DismissEvent, Entity, EventEmitter, FocusHandle,
Focusable, Subscription, Task, WeakEntity, Focusable, Subscription, Task, WeakEntity,
}; };
use language_model::{LanguageModel, LanguageModelAvailability, LanguageModelRegistry}; use language_model::{
AuthenticateError, LanguageModel, LanguageModelAvailability, LanguageModelRegistry,
};
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use proto::Plan; use proto::Plan;
use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverMenuHandle, PopoverTrigger}; use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverMenuHandle, PopoverTrigger};
@ -20,6 +22,7 @@ pub struct LanguageModelSelector {
/// The task used to update the picker's matches when there is a change to /// The task used to update the picker's matches when there is a change to
/// the language model registry. /// the language model registry.
update_matches_task: Option<Task<()>>, update_matches_task: Option<Task<()>>,
_authenticate_all_providers_task: Task<()>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
} }
@ -50,6 +53,7 @@ impl LanguageModelSelector {
LanguageModelSelector { LanguageModelSelector {
picker, picker,
update_matches_task: None, update_matches_task: None,
_authenticate_all_providers_task: Self::authenticate_all_providers(cx),
_subscriptions: vec![cx.subscribe_in( _subscriptions: vec![cx.subscribe_in(
&LanguageModelRegistry::global(cx), &LanguageModelRegistry::global(cx),
window, window,
@ -80,6 +84,56 @@ impl LanguageModelSelector {
} }
} }
/// Authenticates all providers in the [`LanguageModelRegistry`].
///
/// We do this so that we can populate the language selector with all of the
/// models from the configured providers.
fn authenticate_all_providers(cx: &mut App) -> Task<()> {
let authenticate_all_providers = LanguageModelRegistry::global(cx)
.read(cx)
.providers()
.iter()
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
.collect::<Vec<_>>();
cx.spawn(|_cx| async move {
for (provider_id, provider_name, authenticate_task) in authenticate_all_providers {
if let Err(err) = authenticate_task.await {
if matches!(err, AuthenticateError::CredentialsNotFound) {
// Since we're authenticating these providers in the
// background for the purposes of populating the
// language selector, we don't care about providers
// where the credentials are not found.
} else {
// Some providers have noisy failure states that we
// don't want to spam the logs with every time the
// language model selector is initialized.
//
// Ideally these should have more clear failure modes
// that we know are safe to ignore here, like what we do
// with `CredentialsNotFound` above.
match provider_id.0.as_ref() {
"lmstudio" | "ollama" => {
// LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated".
//
// These fail noisily, so we don't log them.
}
"copilot_chat" => {
// Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors.
}
_ => {
log::error!(
"Failed to authenticate provider: {}: {err}",
provider_name.0
);
}
}
}
}
}
})
}
fn all_models(cx: &App) -> Vec<ModelInfo> { fn all_models(cx: &App) -> Vec<ModelInfo> {
LanguageModelRegistry::global(cx) LanguageModelRegistry::global(cx)
.read(cx) .read(cx)