Distinguish between missing models and registries in error messages (#32678)

Consolidates configuration error handling by moving the error type and
logic from assistant_context_editor to language_model::registry.

The registry now provides a single method to check for configuration
errors, making the error handling more consistent across the agent panel
and context editor.

This also now checks if the issue is that we don't have any providers,
or if we just can't find the model.

Previously, an incorrect model name showed up as having no providers,
which is very confusing.

Release Notes:

- N/A
This commit is contained in:
Ben Brandt 2025-06-13 12:31:52 +02:00 committed by GitHub
parent fc7c106b2a
commit 9427833fdf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 218 additions and 207 deletions

View file

@ -10,9 +10,9 @@ use serde::{Deserialize, Serialize};
use agent_settings::{AgentDockPosition, AgentSettings, CompletionMode, DefaultView}; use agent_settings::{AgentDockPosition, AgentSettings, CompletionMode, DefaultView};
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use assistant_context_editor::{ use assistant_context_editor::{
AgentPanelDelegate, AssistantContext, ConfigurationError, ContextEditor, ContextEvent, AgentPanelDelegate, AssistantContext, ContextEditor, ContextEvent, ContextSummary,
ContextSummary, SlashCommandCompletionProvider, humanize_token_count, SlashCommandCompletionProvider, humanize_token_count, make_lsp_adapter_delegate,
make_lsp_adapter_delegate, render_remaining_tokens, render_remaining_tokens,
}; };
use assistant_slash_command::SlashCommandWorkingSet; use assistant_slash_command::SlashCommandWorkingSet;
use assistant_tool::ToolWorkingSet; use assistant_tool::ToolWorkingSet;
@ -29,7 +29,8 @@ use gpui::{
}; };
use language::LanguageRegistry; use language::LanguageRegistry;
use language_model::{ use language_model::{
LanguageModelProviderTosView, LanguageModelRegistry, RequestUsage, ZED_CLOUD_PROVIDER_ID, ConfigurationError, LanguageModelProviderTosView, LanguageModelRegistry, RequestUsage,
ZED_CLOUD_PROVIDER_ID,
}; };
use project::{Project, ProjectPath, Worktree}; use project::{Project, ProjectPath, Worktree};
use prompt_store::{PromptBuilder, PromptStore, UserPromptId}; use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
@ -2353,24 +2354,6 @@ impl AgentPanel {
self.thread.clone().into_any_element() self.thread.clone().into_any_element()
} }
fn configuration_error(&self, cx: &App) -> Option<ConfigurationError> {
let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
return Some(ConfigurationError::NoProvider);
};
if !model.provider.is_authenticated(cx) {
return Some(ConfigurationError::ProviderNotAuthenticated);
}
if model.provider.must_accept_terms(cx) {
return Some(ConfigurationError::ProviderPendingTermsAcceptance(
model.provider,
));
}
None
}
fn render_thread_empty_state( fn render_thread_empty_state(
&self, &self,
window: &mut Window, window: &mut Window,
@ -2380,7 +2363,9 @@ impl AgentPanel {
.history_store .history_store
.update(cx, |this, cx| this.recent_entries(6, cx)); .update(cx, |this, cx| this.recent_entries(6, cx));
let configuration_error = self.configuration_error(cx); let model_registry = LanguageModelRegistry::read_global(cx);
let configuration_error =
model_registry.configuration_error(model_registry.default_model(), cx);
let no_error = configuration_error.is_none(); let no_error = configuration_error.is_none();
let focus_handle = self.focus_handle(cx); let focus_handle = self.focus_handle(cx);
@ -2397,11 +2382,7 @@ impl AgentPanel {
.justify_center() .justify_center()
.items_center() .items_center()
.gap_1() .gap_1()
.child( .child(h_flex().child(Headline::new("Welcome to the Agent Panel")))
h_flex().child(
Headline::new("Welcome to the Agent Panel")
),
)
.when(no_error, |parent| { .when(no_error, |parent| {
parent parent
.child( .child(
@ -2425,7 +2406,10 @@ impl AgentPanel {
cx, cx,
)) ))
.on_click(|_event, window, cx| { .on_click(|_event, window, cx| {
window.dispatch_action(NewThread::default().boxed_clone(), cx) window.dispatch_action(
NewThread::default().boxed_clone(),
cx,
)
}), }),
) )
.child( .child(
@ -2442,7 +2426,10 @@ impl AgentPanel {
cx, cx,
)) ))
.on_click(|_event, window, cx| { .on_click(|_event, window, cx| {
window.dispatch_action(ToggleContextPicker.boxed_clone(), cx) window.dispatch_action(
ToggleContextPicker.boxed_clone(),
cx,
)
}), }),
) )
.child( .child(
@ -2459,7 +2446,10 @@ impl AgentPanel {
cx, cx,
)) ))
.on_click(|_event, window, cx| { .on_click(|_event, window, cx| {
window.dispatch_action(ToggleModelSelector.boxed_clone(), cx) window.dispatch_action(
ToggleModelSelector.boxed_clone(),
cx,
)
}), }),
) )
.child( .child(
@ -2476,51 +2466,50 @@ impl AgentPanel {
cx, cx,
)) ))
.on_click(|_event, window, cx| { .on_click(|_event, window, cx| {
window.dispatch_action(OpenConfiguration.boxed_clone(), cx) window.dispatch_action(
OpenConfiguration.boxed_clone(),
cx,
)
}), }),
) )
}) })
.map(|parent| { .map(|parent| match configuration_error_ref {
match configuration_error_ref { Some(
Some(ConfigurationError::ProviderNotAuthenticated) err @ (ConfigurationError::ModelNotFound
| Some(ConfigurationError::NoProvider) => { | ConfigurationError::ProviderNotAuthenticated(_)
parent | ConfigurationError::NoProvider),
.child( ) => parent
h_flex().child( .child(h_flex().child(
Label::new("To start using the agent, configure at least one LLM provider.") Label::new(err.to_string()).color(Color::Muted).mb_2p5(),
.color(Color::Muted) ))
.mb_2p5() .child(
) Button::new("settings", "Configure a Provider")
) .icon(IconName::Settings)
.child( .icon_position(IconPosition::Start)
Button::new("settings", "Configure a Provider") .icon_size(IconSize::Small)
.icon(IconName::Settings) .icon_color(Color::Muted)
.icon_position(IconPosition::Start) .full_width()
.icon_size(IconSize::Small) .key_binding(KeyBinding::for_action_in(
.icon_color(Color::Muted) &OpenConfiguration,
.full_width() &focus_handle,
.key_binding(KeyBinding::for_action_in( window,
&OpenConfiguration,
&focus_handle,
window,
cx,
))
.on_click(|_event, window, cx| {
window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
}),
)
}
Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
parent.children(
provider.render_accept_terms(
LanguageModelProviderTosView::ThreadFreshStart,
cx, cx,
), ))
) .on_click(|_event, window, cx| {
} window.dispatch_action(
None => parent, OpenConfiguration.boxed_clone(),
cx,
)
}),
),
Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
parent.children(provider.render_accept_terms(
LanguageModelProviderTosView::ThreadFreshStart,
cx,
))
} }
}) None => parent,
}),
) )
}) })
.when(!recent_history.is_empty(), |parent| { .when(!recent_history.is_empty(), |parent| {
@ -2555,7 +2544,8 @@ impl AgentPanel {
&self.focus_handle(cx), &self.focus_handle(cx),
window, window,
cx, cx,
).map(|kb| kb.size(rems_from_px(12.))), )
.map(|kb| kb.size(rems_from_px(12.))),
) )
.on_click(move |_event, window, cx| { .on_click(move |_event, window, cx| {
window.dispatch_action(OpenHistory.boxed_clone(), cx); window.dispatch_action(OpenHistory.boxed_clone(), cx);
@ -2565,79 +2555,68 @@ impl AgentPanel {
.child( .child(
v_flex() v_flex()
.gap_1() .gap_1()
.children( .children(recent_history.into_iter().enumerate().map(
recent_history.into_iter().enumerate().map(|(index, entry)| { |(index, entry)| {
// TODO: Add keyboard navigation. // TODO: Add keyboard navigation.
let is_hovered = self.hovered_recent_history_item == Some(index); let is_hovered =
self.hovered_recent_history_item == Some(index);
HistoryEntryElement::new(entry.clone(), cx.entity().downgrade()) HistoryEntryElement::new(entry.clone(), cx.entity().downgrade())
.hovered(is_hovered) .hovered(is_hovered)
.on_hover(cx.listener(move |this, is_hovered, _window, cx| { .on_hover(cx.listener(
if *is_hovered { move |this, is_hovered, _window, cx| {
this.hovered_recent_history_item = Some(index); if *is_hovered {
} else if this.hovered_recent_history_item == Some(index) { this.hovered_recent_history_item = Some(index);
this.hovered_recent_history_item = None; } else if this.hovered_recent_history_item
} == Some(index)
cx.notify(); {
})) this.hovered_recent_history_item = None;
}
cx.notify();
},
))
.into_any_element() .into_any_element()
}), },
) )),
) )
.map(|parent| { .map(|parent| match configuration_error_ref {
match configuration_error_ref { Some(
Some(ConfigurationError::ProviderNotAuthenticated) err @ (ConfigurationError::ModelNotFound
| Some(ConfigurationError::NoProvider) => { | ConfigurationError::ProviderNotAuthenticated(_)
parent | ConfigurationError::NoProvider),
.child( ) => parent.child(
Banner::new() Banner::new()
.severity(ui::Severity::Warning) .severity(ui::Severity::Warning)
.child( .child(Label::new(err.to_string()).size(LabelSize::Small))
Label::new( .action_slot(
"Configure at least one LLM provider to start using the panel.", Button::new("settings", "Configure Provider")
) .style(ButtonStyle::Tinted(ui::TintColor::Warning))
.size(LabelSize::Small), .label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(
&OpenConfiguration,
&focus_handle,
window,
cx,
) )
.action_slot( .map(|kb| kb.size(rems_from_px(12.))),
Button::new("settings", "Configure Provider") )
.style(ButtonStyle::Tinted(ui::TintColor::Warning)) .on_click(|_event, window, cx| {
.label_size(LabelSize::Small) window.dispatch_action(
.key_binding( OpenConfiguration.boxed_clone(),
KeyBinding::for_action_in( cx,
&OpenConfiguration, )
&focus_handle, }),
window, ),
cx, ),
) Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
.map(|kb| kb.size(rems_from_px(12.))), parent.child(Banner::new().severity(ui::Severity::Warning).child(
) h_flex().w_full().children(provider.render_accept_terms(
.on_click(|_event, window, cx| { LanguageModelProviderTosView::ThreadtEmptyState,
window.dispatch_action( cx,
OpenConfiguration.boxed_clone(), )),
cx, ))
)
}),
),
)
}
Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
parent
.child(
Banner::new()
.severity(ui::Severity::Warning)
.child(
h_flex()
.w_full()
.children(
provider.render_accept_terms(
LanguageModelProviderTosView::ThreadtEmptyState,
cx,
),
),
),
)
}
None => parent,
} }
None => parent,
}) })
}) })
} }

View file

@ -24,6 +24,7 @@ use gpui::{
WeakEntity, Window, point, WeakEntity, Window, point,
}; };
use language::{Buffer, Point, Selection, TransactionId}; use language::{Buffer, Point, Selection, TransactionId};
use language_model::ConfigurationError;
use language_model::ConfiguredModel; use language_model::ConfiguredModel;
use language_model::{LanguageModelRegistry, report_assistant_event}; use language_model::{LanguageModelRegistry, report_assistant_event};
use multi_buffer::MultiBufferRow; use multi_buffer::MultiBufferRow;
@ -232,10 +233,9 @@ impl InlineAssistant {
return; return;
}; };
let is_authenticated = || { let configuration_error = || {
LanguageModelRegistry::read_global(cx) let model_registry = LanguageModelRegistry::read_global(cx);
.inline_assistant_model() model_registry.configuration_error(model_registry.inline_assistant_model(), cx)
.map_or(false, |model| model.provider.is_authenticated(cx))
}; };
let Some(agent_panel) = workspace.panel::<AgentPanel>(cx) else { let Some(agent_panel) = workspace.panel::<AgentPanel>(cx) else {
@ -283,20 +283,23 @@ impl InlineAssistant {
} }
}; };
if is_authenticated() { if let Some(error) = configuration_error() {
handle_assist(window, cx); if let ConfigurationError::ProviderNotAuthenticated(provider) = error {
} else { cx.spawn(async move |_, cx| {
cx.spawn_in(window, async move |_workspace, cx| { cx.update(|cx| provider.authenticate(cx))?.await?;
let Some(task) = cx.update(|_, cx| { anyhow::Ok(())
LanguageModelRegistry::read_global(cx) })
.inline_assistant_model() .detach_and_log_err(cx);
.map_or(None, |model| Some(model.provider.authenticate(cx)))
})? if configuration_error().is_none() {
else { handle_assist(window, cx);
}
} else {
cx.spawn_in(window, async move |_, cx| {
let answer = cx let answer = cx
.prompt( .prompt(
gpui::PromptLevel::Warning, gpui::PromptLevel::Warning,
"No language model provider configured", &error.to_string(),
None, None,
&["Configure", "Cancel"], &["Configure", "Cancel"],
) )
@ -310,17 +313,12 @@ impl InlineAssistant {
.ok(); .ok();
} }
} }
return Ok(()); anyhow::Ok(())
}; })
task.await?; .detach_and_log_err(cx);
anyhow::Ok(())
})
.detach_and_log_err(cx);
if is_authenticated() {
handle_assist(window, cx);
} }
} else {
handle_assist(window, cx);
} }
} }

View file

@ -39,7 +39,7 @@ use language::{
language_settings::{SoftWrap, all_language_settings}, language_settings::{SoftWrap, all_language_settings},
}; };
use language_model::{ use language_model::{
LanguageModelImage, LanguageModelProvider, LanguageModelProviderTosView, LanguageModelRegistry, ConfigurationError, LanguageModelImage, LanguageModelProviderTosView, LanguageModelRegistry,
Role, Role,
}; };
use multi_buffer::MultiBufferRow; use multi_buffer::MultiBufferRow;
@ -1887,6 +1887,8 @@ impl ContextEditor {
// value to not show the nudge. // value to not show the nudge.
let nudge = Some(false); let nudge = Some(false);
let model_registry = LanguageModelRegistry::read_global(cx);
if nudge.map_or(false, |value| value) { if nudge.map_or(false, |value| value) {
Some( Some(
h_flex() h_flex()
@ -1935,14 +1937,9 @@ impl ContextEditor {
) )
.into_any_element(), .into_any_element(),
) )
} else if let Some(configuration_error) = configuration_error(cx) { } else if let Some(configuration_error) =
let label = match configuration_error { model_registry.configuration_error(model_registry.default_model(), cx)
ConfigurationError::NoProvider => "No LLM provider selected.", {
ConfigurationError::ProviderNotAuthenticated => "LLM provider is not configured.",
ConfigurationError::ProviderPendingTermsAcceptance(_) => {
"LLM provider requires accepting the Terms of Service."
}
};
Some( Some(
h_flex() h_flex()
.px_3() .px_3()
@ -1959,7 +1956,7 @@ impl ContextEditor {
.size(IconSize::Small) .size(IconSize::Small)
.color(Color::Warning), .color(Color::Warning),
) )
.child(Label::new(label)), .child(Label::new(configuration_error.to_string())),
) )
.child( .child(
Button::new("open-configuration", "Configure Providers") Button::new("open-configuration", "Configure Providers")
@ -2034,14 +2031,19 @@ impl ContextEditor {
/// Will return false if the selected provided has a configuration error or /// Will return false if the selected provided has a configuration error or
/// if the user has not accepted the terms of service for this provider. /// if the user has not accepted the terms of service for this provider.
fn sending_disabled(&self, cx: &mut Context<'_, ContextEditor>) -> bool { fn sending_disabled(&self, cx: &mut Context<'_, ContextEditor>) -> bool {
let model = LanguageModelRegistry::read_global(cx).default_model(); let model_registry = LanguageModelRegistry::read_global(cx);
let Some(configuration_error) =
model_registry.configuration_error(model_registry.default_model(), cx)
else {
return false;
};
let has_configuration_error = configuration_error(cx).is_some(); match configuration_error {
let needs_to_accept_terms = self.show_accept_terms ConfigurationError::NoProvider
&& model | ConfigurationError::ModelNotFound
.as_ref() | ConfigurationError::ProviderNotAuthenticated(_) => true,
.map_or(false, |model| model.provider.must_accept_terms(cx)); ConfigurationError::ProviderPendingTermsAcceptance(_) => self.show_accept_terms,
has_configuration_error || needs_to_accept_terms }
} }
fn render_inject_context_menu(&self, cx: &mut Context<Self>) -> impl IntoElement { fn render_inject_context_menu(&self, cx: &mut Context<Self>) -> impl IntoElement {
@ -3180,33 +3182,6 @@ fn size_for_image(data: &RenderImage, max_size: Size<Pixels>) -> Size<Pixels> {
} }
} }
pub enum ConfigurationError {
NoProvider,
ProviderNotAuthenticated,
ProviderPendingTermsAcceptance(Arc<dyn LanguageModelProvider>),
}
fn configuration_error(cx: &App) -> Option<ConfigurationError> {
let model = LanguageModelRegistry::read_global(cx).default_model();
let is_authenticated = model
.as_ref()
.map_or(false, |model| model.provider.is_authenticated(cx));
if model.is_some() && is_authenticated {
return None;
}
if model.is_none() {
return Some(ConfigurationError::NoProvider);
}
if !is_authenticated {
return Some(ConfigurationError::ProviderNotAuthenticated);
}
None
}
pub fn humanize_token_count(count: usize) -> String { pub fn humanize_token_count(count: usize) -> String {
match count { match count {
0..=999 => count.to_string(), 0..=999 => count.to_string(),

View file

@ -5,6 +5,7 @@ use crate::{
use collections::BTreeMap; use collections::BTreeMap;
use gpui::{App, Context, Entity, EventEmitter, Global, prelude::*}; use gpui::{App, Context, Entity, EventEmitter, Global, prelude::*};
use std::{str::FromStr, sync::Arc}; use std::{str::FromStr, sync::Arc};
use thiserror::Error;
use util::maybe; use util::maybe;
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
@ -16,6 +17,34 @@ struct GlobalLanguageModelRegistry(Entity<LanguageModelRegistry>);
impl Global for GlobalLanguageModelRegistry {} impl Global for GlobalLanguageModelRegistry {}
#[derive(Error)]
pub enum ConfigurationError {
#[error("Configure at least one LLM provider to start using the panel.")]
NoProvider,
#[error("LLM Provider is not configured or does not support the configured model.")]
ModelNotFound,
#[error("{} LLM provider is not configured.", .0.name().0)]
ProviderNotAuthenticated(Arc<dyn LanguageModelProvider>),
#[error("Using the {} LLM provider requires accepting the Terms of Service.",
.0.name().0)]
ProviderPendingTermsAcceptance(Arc<dyn LanguageModelProvider>),
}
impl std::fmt::Debug for ConfigurationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NoProvider => write!(f, "NoProvider"),
Self::ModelNotFound => write!(f, "ModelNotFound"),
Self::ProviderNotAuthenticated(provider) => {
write!(f, "ProviderNotAuthenticated({})", provider.id())
}
Self::ProviderPendingTermsAcceptance(provider) => {
write!(f, "ProviderPendingTermsAcceptance({})", provider.id())
}
}
}
}
#[derive(Default)] #[derive(Default)]
pub struct LanguageModelRegistry { pub struct LanguageModelRegistry {
default_model: Option<ConfiguredModel>, default_model: Option<ConfiguredModel>,
@ -152,6 +181,36 @@ impl LanguageModelRegistry {
providers providers
} }
pub fn configuration_error(
&self,
model: Option<ConfiguredModel>,
cx: &App,
) -> Option<ConfigurationError> {
let Some(model) = model else {
if !self.has_authenticated_provider(cx) {
return Some(ConfigurationError::NoProvider);
}
return Some(ConfigurationError::ModelNotFound);
};
if !model.provider.is_authenticated(cx) {
return Some(ConfigurationError::ProviderNotAuthenticated(model.provider));
}
if model.provider.must_accept_terms(cx) {
return Some(ConfigurationError::ProviderPendingTermsAcceptance(
model.provider,
));
}
None
}
/// Check that we have at least one provider that is authenticated.
fn has_authenticated_provider(&self, cx: &App) -> bool {
self.providers.values().any(|p| p.is_authenticated(cx))
}
pub fn available_models<'a>( pub fn available_models<'a>(
&'a self, &'a self,
cx: &'a App, cx: &'a App,