mirror of
https://github.com/warpdotdev/warp.git
synced 2026-05-06 15:22:21 +08:00
Select new default when a model is disabled (#10085)
We have some logic for selecting a new default model if the user's current model is no longer available. However, this logic doesn't account when a model is _disabled_, which is different from when a model is _removed_. This fixes our logic to account for model disablement.
This commit is contained in:
@@ -19,6 +19,8 @@ use crate::{
|
||||
workspaces::user_workspaces::{UserWorkspaces, UserWorkspacesEvent},
|
||||
};
|
||||
|
||||
use ai::api_keys::{ApiKeyManager, ApiKeyManagerEvent};
|
||||
|
||||
use super::execution_profiles::profiles::AIExecutionProfilesModel;
|
||||
|
||||
pub use ai::LLMId;
|
||||
@@ -26,8 +28,6 @@ pub use ai::LLMId;
|
||||
/// Checks if a user's' API key is being used for the given provider.
|
||||
/// Returns `true` if BYO API key is enabled and a key exists for the provider.
|
||||
pub fn is_using_api_key_for_provider(provider: &LLMProvider, app: &AppContext) -> bool {
|
||||
use ai::api_keys::ApiKeyManager;
|
||||
|
||||
let api_keys = UserWorkspaces::as_ref(app)
|
||||
.is_byo_api_key_enabled()
|
||||
.then(|| ApiKeyManager::as_ref(app).keys().clone());
|
||||
@@ -74,6 +74,23 @@ impl DisableReason {
|
||||
DisableReason::Unavailable => "This model is unavailable.",
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` when this disable reason means the user cannot use the model
|
||||
/// and we should clear their stored preference.
|
||||
///
|
||||
/// `RequiresUpgrade` is BYOK-aware: if the user has a BYO API key for the
|
||||
/// model's provider (`has_byok_key = true`), the server will still accept
|
||||
/// the request, so we keep the selection.
|
||||
///
|
||||
/// `OutOfRequests` and `ProviderOutage` are transient and expected to
|
||||
/// resolve without user action, so we preserve the selection.
|
||||
fn should_clear_preference(&self, has_byok_key: bool) -> bool {
|
||||
match self {
|
||||
DisableReason::AdminDisabled | DisableReason::Unavailable => true,
|
||||
DisableReason::RequiresUpgrade => !has_byok_key,
|
||||
DisableReason::OutOfRequests | DisableReason::ProviderOutage => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
@@ -353,6 +370,17 @@ impl AvailableLLMs {
|
||||
self.choices.iter().find(|info| info.id == *id)
|
||||
}
|
||||
|
||||
/// Returns the info for the given id only if the model is usable (present
|
||||
/// and not effectively disabled for the current user).
|
||||
fn usable_info_for_id(&self, id: &LLMId, app: &AppContext) -> Option<&LLMInfo> {
|
||||
self.info_for_id(id).filter(|info| {
|
||||
let has_byok_key = is_using_api_key_for_provider(&info.provider, app);
|
||||
info.disable_reason
|
||||
.as_ref()
|
||||
.is_none_or(|reason| !reason.should_clear_preference(has_byok_key))
|
||||
})
|
||||
}
|
||||
|
||||
fn default_llm_info(&self) -> &LLMInfo {
|
||||
self.info_for_id(&self.default_id)
|
||||
.expect("Default LLM ID must be present in choices")
|
||||
@@ -555,6 +583,15 @@ impl LLMPreferences {
|
||||
}
|
||||
});
|
||||
|
||||
// Re-reconcile disabled model preferences when BYOK keys change, since
|
||||
// RequiresUpgrade models may become usable or unusable.
|
||||
ctx.subscribe_to_model(
|
||||
&ApiKeyManager::handle(ctx),
|
||||
|me, _event: &ApiKeyManagerEvent, ctx| {
|
||||
me.reconcile_disabled_model_preferences(ctx);
|
||||
},
|
||||
);
|
||||
|
||||
let base_llm_for_terminal_view = HashMap::new();
|
||||
|
||||
let me = Self {
|
||||
@@ -945,65 +982,7 @@ impl LLMPreferences {
|
||||
}
|
||||
}
|
||||
|
||||
// Clear any model selections where the model is no longer supported,
|
||||
// and clear orphaned context window limits for non-configurable models.
|
||||
let profiles_model = AIExecutionProfilesModel::handle(ctx);
|
||||
profiles_model.update(ctx, |profiles, ctx| {
|
||||
for profile_id in profiles.get_all_profile_ids() {
|
||||
if let Some(profile) = profiles.get_profile_by_id(profile_id, ctx) {
|
||||
let profile_data = profile.data();
|
||||
let preferred_base_model = profile_data.base_model.clone();
|
||||
let effective_base_model_id = preferred_base_model
|
||||
.as_ref()
|
||||
.unwrap_or(&self.models_by_feature.agent_mode.default_id);
|
||||
let effective_base_model_info = self
|
||||
.models_by_feature
|
||||
.agent_mode
|
||||
.info_for_id(effective_base_model_id);
|
||||
let effective_base_model_missing = effective_base_model_info.is_none();
|
||||
let effective_base_model_is_configurable = effective_base_model_info
|
||||
.is_some_and(|info| info.context_window.is_configurable);
|
||||
let has_context_window_limit = profile_data.context_window_limit.is_some();
|
||||
|
||||
if preferred_base_model.is_some() && effective_base_model_missing {
|
||||
profiles.set_base_model(profile_id, None, ctx);
|
||||
}
|
||||
if has_context_window_limit
|
||||
&& (effective_base_model_missing || !effective_base_model_is_configurable)
|
||||
{
|
||||
profiles.set_context_window_limit(profile_id, None, ctx);
|
||||
}
|
||||
if let Some(preferred_llm_id) = &profile.data().coding_model {
|
||||
if self
|
||||
.models_by_feature
|
||||
.coding
|
||||
.info_for_id(preferred_llm_id)
|
||||
.is_none()
|
||||
{
|
||||
profiles.set_coding_model(profile_id, None, ctx);
|
||||
}
|
||||
}
|
||||
if let Some(preferred_llm_id) = &profile.data().cli_agent_model {
|
||||
if self
|
||||
.get_cli_agent_available()
|
||||
.info_for_id(preferred_llm_id)
|
||||
.is_none()
|
||||
{
|
||||
profiles.set_cli_agent_model(profile_id, None, ctx);
|
||||
}
|
||||
}
|
||||
if let Some(preferred_llm_id) = &profile.data().computer_use_model {
|
||||
if self
|
||||
.get_computer_use_available()
|
||||
.info_for_id(preferred_llm_id)
|
||||
.is_none()
|
||||
{
|
||||
profiles.set_computer_use_model(profile_id, None, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
self.reconcile_disabled_model_preferences(ctx);
|
||||
|
||||
let new_choices =
|
||||
get_new_agent_mode_choices(&old.agent_mode, &self.models_by_feature.agent_mode);
|
||||
@@ -1024,6 +1003,72 @@ impl LLMPreferences {
|
||||
ctx.emit(LLMPreferencesEvent::UpdatedAvailableLLMs);
|
||||
}
|
||||
|
||||
/// Clear any model selections where the model is no longer supported
|
||||
/// or effectively disabled, and clear orphaned context window limits
|
||||
/// for non-configurable or unusable models.
|
||||
///
|
||||
/// Called both when the model list is refreshed from the server and when
|
||||
/// BYOK API keys change (since `RequiresUpgrade` usability is BYOK-aware).
|
||||
fn reconcile_disabled_model_preferences(&self, ctx: &mut ModelContext<Self>) {
|
||||
let profiles_model = AIExecutionProfilesModel::handle(ctx);
|
||||
profiles_model.update(ctx, |profiles, ctx| {
|
||||
for profile_id in profiles.get_all_profile_ids() {
|
||||
if let Some(profile) = profiles.get_profile_by_id(profile_id, ctx) {
|
||||
let profile_data = profile.data();
|
||||
let preferred_base_model = profile_data.base_model.clone();
|
||||
let effective_base_model_id = preferred_base_model
|
||||
.as_ref()
|
||||
.unwrap_or(&self.models_by_feature.agent_mode.default_id);
|
||||
let effective_base_model_usable = self
|
||||
.models_by_feature
|
||||
.agent_mode
|
||||
.usable_info_for_id(effective_base_model_id, ctx);
|
||||
let effective_base_model_unusable = effective_base_model_usable.is_none();
|
||||
let effective_base_model_is_configurable = effective_base_model_usable
|
||||
.is_some_and(|info| info.context_window.is_configurable);
|
||||
let has_context_window_limit = profile_data.context_window_limit.is_some();
|
||||
|
||||
if preferred_base_model.is_some() && effective_base_model_unusable {
|
||||
profiles.set_base_model(profile_id, None, ctx);
|
||||
}
|
||||
if has_context_window_limit
|
||||
&& (effective_base_model_unusable || !effective_base_model_is_configurable)
|
||||
{
|
||||
profiles.set_context_window_limit(profile_id, None, ctx);
|
||||
}
|
||||
if let Some(preferred_llm_id) = &profile.data().coding_model {
|
||||
if self
|
||||
.models_by_feature
|
||||
.coding
|
||||
.usable_info_for_id(preferred_llm_id, ctx)
|
||||
.is_none()
|
||||
{
|
||||
profiles.set_coding_model(profile_id, None, ctx);
|
||||
}
|
||||
}
|
||||
if let Some(preferred_llm_id) = &profile.data().cli_agent_model {
|
||||
if self
|
||||
.get_cli_agent_available()
|
||||
.usable_info_for_id(preferred_llm_id, ctx)
|
||||
.is_none()
|
||||
{
|
||||
profiles.set_cli_agent_model(profile_id, None, ctx);
|
||||
}
|
||||
}
|
||||
if let Some(preferred_llm_id) = &profile.data().computer_use_model {
|
||||
if self
|
||||
.get_computer_use_available()
|
||||
.usable_info_for_id(preferred_llm_id, ctx)
|
||||
.is_none()
|
||||
{
|
||||
profiles.set_computer_use_model(profile_id, None, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn vision_supported(&self, app: &AppContext, terminal_view_id: Option<EntityId>) -> bool {
|
||||
self.get_active_base_model(app, terminal_view_id)
|
||||
.vision_supported
|
||||
|
||||
@@ -1,5 +1,46 @@
|
||||
use super::*;
|
||||
|
||||
// -- DisableReason::should_clear_preference tests --
|
||||
|
||||
#[test]
|
||||
fn should_clear_preference_admin_disabled() {
|
||||
// AdminDisabled always clears, regardless of BYOK status.
|
||||
assert!(DisableReason::AdminDisabled.should_clear_preference(false));
|
||||
assert!(DisableReason::AdminDisabled.should_clear_preference(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_clear_preference_unavailable() {
|
||||
assert!(DisableReason::Unavailable.should_clear_preference(false));
|
||||
assert!(DisableReason::Unavailable.should_clear_preference(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_not_clear_preference_out_of_requests() {
|
||||
// Transient — never clears.
|
||||
assert!(!DisableReason::OutOfRequests.should_clear_preference(false));
|
||||
assert!(!DisableReason::OutOfRequests.should_clear_preference(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_not_clear_preference_provider_outage() {
|
||||
// Transient — never clears.
|
||||
assert!(!DisableReason::ProviderOutage.should_clear_preference(false));
|
||||
assert!(!DisableReason::ProviderOutage.should_clear_preference(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_clear_preference_requires_upgrade_without_byok() {
|
||||
// No BYOK key → server will reject → clear.
|
||||
assert!(DisableReason::RequiresUpgrade.should_clear_preference(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_not_clear_preference_requires_upgrade_with_byok() {
|
||||
// BYOK key present → server allows → keep.
|
||||
assert!(!DisableReason::RequiresUpgrade.should_clear_preference(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn llm_info_deserializes_without_base_model_name() {
|
||||
let raw = r#"{
|
||||
|
||||
Reference in New Issue
Block a user