first pass of merging in warp (doesn't build)
This commit is contained in:
+647
-128
@@ -1,61 +1,59 @@
|
||||
use galaxy_core::ui::icons::Icon;
|
||||
use galaxy_core::user_preferences::GetUserPreferences;
|
||||
use galaxyui::{AppContext, Entity, EntityId, ModelContext, SingletonEntity};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
use ai::api_keys::{ApiKeyManager, ApiKeyManagerEvent, CustomEndpoint, CustomEndpointModel};
|
||||
pub use ai::LLMId;
|
||||
use parking_lot::FairMutex;
|
||||
use serde::{de, Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
auth::{
|
||||
auth_manager::{AuthManager, AuthManagerEvent},
|
||||
AuthStateProvider,
|
||||
},
|
||||
network::{NetworkStatus, NetworkStatusEvent, NetworkStatusKind},
|
||||
report_error,
|
||||
server::server_api::ServerApiProvider,
|
||||
settings::ai::{
|
||||
AISettings, AISettingsChangedEvent, BedrockModelConfig, OpenAIModelConfig,
|
||||
OpenAIProviderConfig,
|
||||
},
|
||||
workspaces::user_workspaces::{UserWorkspaces, UserWorkspacesEvent},
|
||||
};
|
||||
|
||||
use settings::Setting;
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxy_core::ui::icons::Icon;
|
||||
use galaxy_core::user_preferences::GetUserPreferences;
|
||||
use warp_multi_agent_api as api;
|
||||
use galaxyui::{AppContext, Entity, EntityId, ModelContext, SingletonEntity};
|
||||
|
||||
use super::custom_model_routers::{self, CustomModelRouter, ModelConfigError};
|
||||
use super::execution_profiles::profiles::AIExecutionProfilesModel;
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use super::bedrock::models::get_effective_models;
|
||||
|
||||
pub use ai::LLMId;
|
||||
use crate::auth::auth_manager::{AuthManager, AuthManagerEvent};
|
||||
use crate::auth::AuthStateProvider;
|
||||
use crate::network::{NetworkStatus, NetworkStatusEvent, NetworkStatusKind};
|
||||
use crate::report_error;
|
||||
use crate::server::server_api::ServerApiProvider;
|
||||
use crate::user_config::{WarpConfig, WarpConfigUpdateEvent};
|
||||
use crate::workspaces::user_workspaces::{UserWorkspaces, UserWorkspacesEvent};
|
||||
|
||||
/// 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.
|
||||
/// For xAI, a connected Grok subscription counts: its OAuth access token is
|
||||
/// sent like a BYO key (see `ApiKeyManager::api_keys_for_request`).
|
||||
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());
|
||||
if !UserWorkspaces::as_ref(app).is_byo_api_key_enabled(app) {
|
||||
return false;
|
||||
}
|
||||
let manager = ApiKeyManager::as_ref(app);
|
||||
|
||||
match provider {
|
||||
LLMProvider::OpenAI => api_keys.is_some_and(|keys| keys.openai.is_some()),
|
||||
LLMProvider::Anthropic => api_keys.is_some_and(|keys| keys.anthropic.is_some()),
|
||||
LLMProvider::Google => api_keys.is_some_and(|keys| keys.google.is_some()),
|
||||
LLMProvider::Bedrock => true,
|
||||
LLMProvider::LiteLLM => true,
|
||||
_ => false,
|
||||
LLMProvider::OpenAI => manager.keys().openai.is_some(),
|
||||
LLMProvider::Anthropic => manager.keys().anthropic.is_some(),
|
||||
LLMProvider::Google => manager.keys().google.is_some(),
|
||||
LLMProvider::Xai => manager.grok_tokens().is_some(),
|
||||
LLMProvider::Unknown => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn should_show_bedrock_icon_for_model(llm: &LLMInfo, app: &AppContext) -> bool {
|
||||
UserWorkspaces::as_ref(app).is_aws_bedrock_credentials_enabled(app)
|
||||
&& llm
|
||||
.host_configs
|
||||
.get(&LLMModelHost::AwsBedrock)
|
||||
.is_some_and(|config| config.enabled)
|
||||
}
|
||||
|
||||
/// Key for cached LLM metadata in user preferences.
|
||||
///
|
||||
/// Note: this key used to store a single [`AvailableLLMs`]
|
||||
/// but was migrated to store a full [`ModelsByFeature`].
|
||||
pub const MODELS_BY_FEATURE_CACHE_KEY: &str = "AvailableLLMs";
|
||||
const CUSTOM_ENDPOINT_USAGE_FALLBACK_LABEL: &str = "Custom endpoint";
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct LLMUsageMetadata {
|
||||
@@ -85,6 +83,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)]
|
||||
@@ -119,6 +134,17 @@ impl LLMProvider {
|
||||
LLMProvider::Unknown => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Human-readable provider name for user-facing copy.
|
||||
pub fn display_name(&self) -> &'static str {
|
||||
match self {
|
||||
LLMProvider::OpenAI => "OpenAI",
|
||||
LLMProvider::Anthropic => "Anthropic",
|
||||
LLMProvider::Google => "Google",
|
||||
LLMProvider::Xai => "xAI",
|
||||
LLMProvider::Unknown => "this provider",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The host where an LLM can be routed to.
|
||||
@@ -126,6 +152,8 @@ impl LLMProvider {
|
||||
pub enum LLMModelHost {
|
||||
DirectApi,
|
||||
AwsBedrock,
|
||||
CustomEndpoint,
|
||||
GeminiEnterprise,
|
||||
#[serde(other)]
|
||||
Unknown,
|
||||
}
|
||||
@@ -137,6 +165,18 @@ pub struct RoutingHostConfig {
|
||||
pub model_routing_host: LLMModelHost,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct LLMContextWindow {
|
||||
#[serde(default)]
|
||||
pub is_configurable: bool,
|
||||
#[serde(default)]
|
||||
pub min: u32,
|
||||
#[serde(default)]
|
||||
pub max: u32,
|
||||
#[serde(default)]
|
||||
pub default_max: u32,
|
||||
}
|
||||
|
||||
/// Metadata about an LLM.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
pub struct LLMInfo {
|
||||
@@ -152,6 +192,7 @@ pub struct LLMInfo {
|
||||
pub provider: LLMProvider,
|
||||
pub host_configs: HashMap<LLMModelHost, RoutingHostConfig>,
|
||||
pub discount_percentage: Option<f32>,
|
||||
pub context_window: LLMContextWindow,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for LLMInfo {
|
||||
@@ -197,6 +238,8 @@ impl<'de> Deserialize<'de> for LLMInfo {
|
||||
host_configs: HostConfigsWire,
|
||||
#[serde(default)]
|
||||
discount_percentage: Option<f32>,
|
||||
#[serde(default)]
|
||||
context_window: LLMContextWindow,
|
||||
}
|
||||
|
||||
let wire = WireLLMInfo::deserialize(deserializer)?;
|
||||
@@ -231,6 +274,7 @@ impl<'de> Deserialize<'de> for LLMInfo {
|
||||
spec: wire.spec,
|
||||
host_configs,
|
||||
discount_percentage: wire.discount_percentage,
|
||||
context_window: wire.context_window,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -252,6 +296,12 @@ pub fn dedupe_model_display_names<'a>(
|
||||
impl LLMInfo {
|
||||
/// Returns the display name for the LLM, to be used in the LLM selector menu.
|
||||
pub fn menu_display_name(&self) -> String {
|
||||
// Custom model routers carry a routing/source description that belongs in
|
||||
// the sidecar detail panel, not inline in the chip label. Appending it
|
||||
// here would produce a redundant "(Routes by … · …)" suffix.
|
||||
if custom_model_routers::is_custom_router_id(self.id.as_str()) {
|
||||
return self.display_name.clone();
|
||||
}
|
||||
// Base label includes optional description in parentheses
|
||||
match &self.description {
|
||||
// This is a temporary implementation that won't scale well for longer
|
||||
@@ -297,6 +347,7 @@ impl LLMInfo {
|
||||
provider: LLMProvider::Unknown,
|
||||
host_configs: HashMap::new(),
|
||||
discount_percentage: None,
|
||||
context_window: LLMContextWindow::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -352,6 +403,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")
|
||||
@@ -422,6 +484,7 @@ fn default_computer_use_llms() -> AvailableLLMs {
|
||||
provider: LLMProvider::Unknown,
|
||||
host_configs: HashMap::new(),
|
||||
discount_percentage: None,
|
||||
context_window: LLMContextWindow::default(),
|
||||
}],
|
||||
preferred_codex_model_id: None,
|
||||
}
|
||||
@@ -448,6 +511,7 @@ impl Default for ModelsByFeature {
|
||||
provider: LLMProvider::Unknown,
|
||||
host_configs: HashMap::new(),
|
||||
discount_percentage: None,
|
||||
context_window: LLMContextWindow::default(),
|
||||
}],
|
||||
preferred_codex_model_id: None,
|
||||
},
|
||||
@@ -469,6 +533,7 @@ impl Default for ModelsByFeature {
|
||||
provider: LLMProvider::Unknown,
|
||||
host_configs: HashMap::new(),
|
||||
discount_percentage: None,
|
||||
context_window: LLMContextWindow::default(),
|
||||
}],
|
||||
preferred_codex_model_id: None,
|
||||
},
|
||||
@@ -490,6 +555,7 @@ impl Default for ModelsByFeature {
|
||||
provider: LLMProvider::Unknown,
|
||||
host_configs: HashMap::new(),
|
||||
discount_percentage: None,
|
||||
context_window: LLMContextWindow::default(),
|
||||
}],
|
||||
preferred_codex_model_id: None,
|
||||
}),
|
||||
@@ -515,17 +581,23 @@ pub struct LLMPreferences {
|
||||
models_by_feature: ModelsByFeature,
|
||||
last_update: Option<AvailableLLMsUpdate>,
|
||||
base_llm_for_terminal_view: HashMap<EntityId, LLMId>,
|
||||
/// Maps model IDs from OpenAI-compatible providers to their client configs.
|
||||
/// Used by `resolve_provider_config` to route requests to the correct endpoint.
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
openai_provider_routing: HashMap<String, super::openai::client::OpenAIClientConfig>,
|
||||
/// Synthetic `LLMInfo` entries built from the user's `ApiKeyManager.custom_endpoints` so
|
||||
/// custom models surface in the model picker and resolve through `info_for_id` lookups.
|
||||
/// Each entry's `id` is the model's `config_key` (UUID), which is also what flows out to
|
||||
/// `Request.Settings.custom_model_providers.providers[*].models[*].config_key`.
|
||||
///
|
||||
/// Rebuilt from scratch on every `ApiKeyManagerEvent::KeysUpdated`, so adds, edits, and
|
||||
/// removals all immediately propagate to the picker.
|
||||
custom_llms: Vec<LLMInfo>,
|
||||
/// All custom model routers, including both local and cloud-backed.
|
||||
custom_model_routers: Vec<CustomModelRouter>,
|
||||
}
|
||||
|
||||
impl LLMPreferences {
|
||||
pub fn new(ctx: &mut ModelContext<Self>) -> Self {
|
||||
let models_by_feature = get_cached_models(ctx).unwrap_or_default();
|
||||
|
||||
ctx.subscribe_to_model(&NetworkStatus::handle(ctx), |me, event, ctx| {
|
||||
ctx.subscribe_to_model(&NetworkStatus::handle(ctx), |me, _, event, ctx| {
|
||||
if let NetworkStatusEvent::NetworkStatusChanged {
|
||||
new_status: NetworkStatusKind::Online,
|
||||
} = event
|
||||
@@ -538,52 +610,60 @@ impl LLMPreferences {
|
||||
// available LLMs query to the general workspace metadata query which is polled
|
||||
// and hooked up to workspace changes. For that to work, each user would need to
|
||||
// have a personal workspace. This is a stop-gap.
|
||||
ctx.subscribe_to_model(&AuthManager::handle(ctx), |me, event, ctx| {
|
||||
ctx.subscribe_to_model(&AuthManager::handle(ctx), |me, _, event, ctx| {
|
||||
if let AuthManagerEvent::AuthComplete = event {
|
||||
me.refresh_authed_models(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
ctx.subscribe_to_model(&UserWorkspaces::handle(ctx), |me, event, ctx| {
|
||||
ctx.subscribe_to_model(&UserWorkspaces::handle(ctx), |me, _, event, ctx| {
|
||||
if let UserWorkspacesEvent::TeamsChanged = event {
|
||||
me.sanitize_disabled_custom_model_preferences(ctx);
|
||||
me.refresh_authed_models(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
ctx.subscribe_to_model(&AISettings::handle(ctx), |me, event, ctx| {
|
||||
if matches!(
|
||||
event,
|
||||
AISettingsChangedEvent::BedrockEnabled { .. }
|
||||
| AISettingsChangedEvent::BedrockModels { .. }
|
||||
| AISettingsChangedEvent::BedrockCrossRegionInference { .. }
|
||||
| AISettingsChangedEvent::BedrockRegion { .. }
|
||||
) {
|
||||
me.inject_bedrock_models(ctx);
|
||||
// Re-reconcile disabled model preferences when BYOK keys change, since
|
||||
// RequiresUpgrade models may become usable or unusable.
|
||||
// Also rebuild `custom_llms` so adds/edits/removals to the user's custom endpoints
|
||||
// immediately flow through to the model picker.
|
||||
ctx.subscribe_to_model(
|
||||
&ApiKeyManager::handle(ctx),
|
||||
|me, _, _event: &ApiKeyManagerEvent, ctx| {
|
||||
me.rebuild_custom_llms(ctx);
|
||||
me.reconcile_disabled_model_preferences(ctx);
|
||||
ctx.emit(LLMPreferencesEvent::UpdatedAvailableLLMs);
|
||||
}
|
||||
if matches!(
|
||||
event,
|
||||
AISettingsChangedEvent::OpenAIEnabled { .. }
|
||||
| AISettingsChangedEvent::OpenAIModels { .. }
|
||||
| AISettingsChangedEvent::OpenAIBaseUrl { .. }
|
||||
| AISettingsChangedEvent::OpenAIProviders { .. }
|
||||
) {
|
||||
me.inject_openai_models(ctx);
|
||||
ctx.emit(LLMPreferencesEvent::UpdatedAvailableLLMs);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Rebuild custom model routers whenever the local `model_configs/` directory
|
||||
// changes, and reconcile any now-stale local selection.
|
||||
if FeatureFlag::CustomModelRouters.is_enabled() {
|
||||
ctx.subscribe_to_model(&WarpConfig::handle(ctx), |me, _, event, ctx| {
|
||||
if matches!(event, WarpConfigUpdateEvent::ModelConfigs) {
|
||||
me.rebuild_custom_model_routers(ctx);
|
||||
me.reconcile_stale_custom_router_selection(ctx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let base_llm_for_terminal_view = HashMap::new();
|
||||
let custom_llms = build_custom_llm_infos(ApiKeyManager::as_ref(ctx).keys());
|
||||
|
||||
let mut me = Self {
|
||||
models_by_feature,
|
||||
last_update: None,
|
||||
base_llm_for_terminal_view,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
openai_provider_routing: HashMap::new(),
|
||||
custom_llms,
|
||||
custom_model_routers: Vec::new(),
|
||||
};
|
||||
|
||||
// Seed from any already-loaded local config (the async load emits
|
||||
// `ModelConfigs` shortly after startup to populate fully).
|
||||
if FeatureFlag::CustomModelRouters.is_enabled() {
|
||||
me.rebuild_custom_model_routers(ctx);
|
||||
}
|
||||
|
||||
// In agent mode eval builds, eagerly kick off a fetch of the model list from the server
|
||||
// so that it's available by the time test steps like `set_preferred_agent_mode_llm` run.
|
||||
// In production, this is handled reactively (on auth complete, network online, etc.)
|
||||
@@ -961,7 +1041,13 @@ impl LLMPreferences {
|
||||
if let Some(terminal_view_id) = terminal_view_id {
|
||||
let raw_override = self.base_llm_for_terminal_view.get(&terminal_view_id);
|
||||
if let Some(llm_id) = raw_override {
|
||||
if let Some(llm_info) = self.models_by_feature.agent_mode.info_for_id(llm_id) {
|
||||
if let Some(llm_info) = Self::server_info_for_id_router_gated(
|
||||
&self.models_by_feature.agent_mode,
|
||||
llm_id,
|
||||
)
|
||||
.or_else(|| self.custom_llm_info_for_id_if_enabled(llm_id, app))
|
||||
.or_else(|| self.custom_router_llm_info_for_id_if_enabled(llm_id))
|
||||
{
|
||||
return llm_info;
|
||||
}
|
||||
}
|
||||
@@ -973,7 +1059,11 @@ impl LLMPreferences {
|
||||
.data()
|
||||
.base_model
|
||||
.clone()
|
||||
.and_then(|id| self.models_by_feature.agent_mode.info_for_id(&id))
|
||||
.and_then(|id| {
|
||||
Self::server_info_for_id_router_gated(&self.models_by_feature.agent_mode, &id)
|
||||
.or_else(|| self.custom_llm_info_for_id_if_enabled(&id, app))
|
||||
.or_else(|| self.custom_router_llm_info_for_id_if_enabled(&id))
|
||||
})
|
||||
.unwrap_or_else(|| self.models_by_feature.agent_mode.default_llm_info())
|
||||
}
|
||||
|
||||
@@ -997,33 +1087,76 @@ impl LLMPreferences {
|
||||
.data()
|
||||
.coding_model
|
||||
.clone()
|
||||
.and_then(|id| self.models_by_feature.coding.info_for_id(&id))
|
||||
.and_then(|id| {
|
||||
Self::server_info_for_id_router_gated(&self.models_by_feature.coding, &id)
|
||||
.or_else(|| self.custom_llm_info_for_id_if_enabled(&id, app))
|
||||
.or_else(|| self.custom_router_llm_info_for_id_if_enabled(&id))
|
||||
})
|
||||
.unwrap_or_else(|| self.models_by_feature.coding.default_llm_info())
|
||||
}
|
||||
|
||||
/// Resolves `id` against a server-provided model list, but hides cloud/team
|
||||
/// custom routers when the custom-router feature flag is off. Mirrors the
|
||||
/// gating applied to local routers (see
|
||||
/// [`Self::custom_router_llm_info_for_id_if_enabled`]) so the whole
|
||||
/// custom-router feature is controlled by a single client flag.
|
||||
fn server_info_for_id_router_gated<'a>(
|
||||
available: &'a AvailableLLMs,
|
||||
id: &LLMId,
|
||||
) -> Option<&'a LLMInfo> {
|
||||
let info = available.info_for_id(id)?;
|
||||
if !FeatureFlag::CustomModelRouters.is_enabled()
|
||||
&& custom_model_routers::is_cloud_custom_router_id(info.id.as_str())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(info)
|
||||
}
|
||||
|
||||
/// Returns the set of LLMs available for Agent Mode use.
|
||||
pub fn get_base_llm_choices_for_agent_mode(&self) -> impl Iterator<Item = &LLMInfo> {
|
||||
pub fn get_base_llm_choices_for_agent_mode(
|
||||
&self,
|
||||
app: &AppContext,
|
||||
) -> impl Iterator<Item = &LLMInfo> {
|
||||
// Don't show admin-disabled models in the dropdown
|
||||
let routers_enabled = FeatureFlag::CustomModelRouters.is_enabled();
|
||||
self.models_by_feature
|
||||
.agent_mode
|
||||
.choices
|
||||
.iter()
|
||||
.filter(|llm| !matches!(llm.disable_reason, Some(DisableReason::AdminDisabled)))
|
||||
// Gate cloud/team routers behind the same flag as local routers so
|
||||
// the entire custom-router feature is controlled by one flag.
|
||||
.filter(move |llm| {
|
||||
routers_enabled || !custom_model_routers::is_cloud_custom_router_id(llm.id.as_str())
|
||||
})
|
||||
.chain(self.custom_llm_choices(app))
|
||||
.chain(self.custom_router_choices())
|
||||
}
|
||||
|
||||
/// Returns the set of LLMs available for coding.
|
||||
pub fn get_coding_llm_choices(&self) -> impl Iterator<Item = &LLMInfo> {
|
||||
pub fn get_coding_llm_choices(&self, app: &AppContext) -> impl Iterator<Item = &LLMInfo> {
|
||||
// Don't show admin-disabled models in the dropdown
|
||||
let routers_enabled = FeatureFlag::CustomModelRouters.is_enabled();
|
||||
self.models_by_feature
|
||||
.coding
|
||||
.choices
|
||||
.iter()
|
||||
.filter(|llm| !matches!(llm.disable_reason, Some(DisableReason::AdminDisabled)))
|
||||
// Gate cloud/team routers behind the same flag as local routers.
|
||||
.filter(move |llm| {
|
||||
routers_enabled || !custom_model_routers::is_cloud_custom_router_id(llm.id.as_str())
|
||||
})
|
||||
.chain(self.custom_llm_choices(app))
|
||||
.chain(self.custom_router_choices())
|
||||
}
|
||||
|
||||
/// Returns the set of LLMs available for CLI agent.
|
||||
pub fn get_cli_agent_llm_choices(&self) -> impl Iterator<Item = &LLMInfo> {
|
||||
self.get_cli_agent_available().choices.iter()
|
||||
pub fn get_cli_agent_llm_choices(&self, app: &AppContext) -> impl Iterator<Item = &LLMInfo> {
|
||||
self.get_cli_agent_available()
|
||||
.choices
|
||||
.iter()
|
||||
.chain(self.custom_llm_choices(app))
|
||||
}
|
||||
|
||||
/// Returns the `LLMInfo` for the CLI agent model.
|
||||
@@ -1039,7 +1172,11 @@ impl LLMPreferences {
|
||||
.data()
|
||||
.cli_agent_model
|
||||
.clone()
|
||||
.and_then(|id| available.info_for_id(&id))
|
||||
.and_then(|id| {
|
||||
available
|
||||
.info_for_id(&id)
|
||||
.or_else(|| self.custom_llm_info_for_id_if_enabled(&id, app))
|
||||
})
|
||||
.unwrap_or_else(|| available.default_llm_info())
|
||||
}
|
||||
|
||||
@@ -1094,8 +1231,309 @@ impl LLMPreferences {
|
||||
}
|
||||
|
||||
/// Returns metadata about an LLM, if the client knows about it.
|
||||
/// Falls back to the user's custom-endpoint LLMs when the id isn't a server-known model
|
||||
/// id (e.g. when it's a `config_key` UUID).
|
||||
pub fn get_llm_info(&self, id: &LLMId) -> Option<&LLMInfo> {
|
||||
self.models_by_feature.info_for_id(id)
|
||||
self.models_by_feature
|
||||
.info_for_id(id)
|
||||
.or_else(|| self.custom_llm_info_for_id(id))
|
||||
.or_else(|| self.custom_router_llm_info_for_id(id))
|
||||
}
|
||||
|
||||
/// Resolves an `LLMId` against the user's custom-endpoint LLMs.
|
||||
/// Returns `None` if the id isn't a known custom model `config_key`.
|
||||
pub fn custom_llm_info_for_id(&self, id: &LLMId) -> Option<&LLMInfo> {
|
||||
self.custom_llms.iter().find(|info| info.id == *id)
|
||||
}
|
||||
|
||||
/// Footer label for custom endpoint usage keyed by the request config_key.
|
||||
/// The synthetic custom LLMInfo already owns alias-or-name display semantics.
|
||||
pub fn custom_endpoint_usage_display_label(&self, config_key: &str) -> String {
|
||||
let config_key = LLMId::from(config_key);
|
||||
self.custom_llm_info_for_id(&config_key)
|
||||
.map(|info| info.display_name.as_str())
|
||||
.map(str::to_string)
|
||||
.unwrap_or_else(|| CUSTOM_ENDPOINT_USAGE_FALLBACK_LABEL.to_string())
|
||||
}
|
||||
|
||||
fn custom_llm_info_for_id_if_enabled(&self, id: &LLMId, app: &AppContext) -> Option<&LLMInfo> {
|
||||
Self::custom_inference_enabled(app)
|
||||
.then(|| self.custom_llm_info_for_id(id))
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// Iterator over the user's custom-endpoint LLMs, gated on the feature flag and entitlement.
|
||||
pub fn custom_llm_choices(&self, app: &AppContext) -> std::slice::Iter<'_, LLMInfo> {
|
||||
if Self::custom_inference_enabled(app) {
|
||||
self.custom_llms.iter()
|
||||
} else {
|
||||
// Empty slice with a matching element type so the return type stays consistent
|
||||
// across both branches.
|
||||
(&[] as &[LLMInfo]).iter()
|
||||
}
|
||||
}
|
||||
|
||||
fn custom_inference_enabled(app: &AppContext) -> bool {
|
||||
FeatureFlag::CustomInferenceEndpoints.is_enabled()
|
||||
&& UserWorkspaces::as_ref(app).is_custom_inference_enabled(app)
|
||||
}
|
||||
|
||||
/// Resolves a custom model router by its `config_key`/`LLMId`.
|
||||
pub fn custom_model_router_for_id(&self, id: &LLMId) -> Option<&CustomModelRouter> {
|
||||
self.custom_model_routers.iter().find(|m| m.llm_id() == *id)
|
||||
}
|
||||
|
||||
fn custom_router_llm_info_for_id(&self, id: &LLMId) -> Option<&LLMInfo> {
|
||||
self.custom_model_routers
|
||||
.iter()
|
||||
.find(|m| m.info.id == *id)
|
||||
.map(|m| &m.info)
|
||||
}
|
||||
|
||||
fn custom_router_llm_info_for_id_if_enabled(&self, id: &LLMId) -> Option<&LLMInfo> {
|
||||
FeatureFlag::CustomModelRouters
|
||||
.is_enabled()
|
||||
.then(|| self.custom_router_llm_info_for_id(id))
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// Iterator over the custom router picker entries, gated on the feature flag.
|
||||
/// Mirrors [`Self::custom_llm_choices`].
|
||||
pub fn custom_router_choices(&self) -> impl Iterator<Item = &LLMInfo> {
|
||||
let enabled = FeatureFlag::CustomModelRouters.is_enabled();
|
||||
self.custom_model_routers
|
||||
.iter()
|
||||
.filter(move |_| enabled)
|
||||
.map(|m| &m.info)
|
||||
}
|
||||
|
||||
/// Builds the custom_model_routers registry for an outbound request.
|
||||
pub fn custom_model_routers_for_request(
|
||||
&self,
|
||||
base_id: &LLMId,
|
||||
coding_id: &LLMId,
|
||||
) -> api::request::settings::CustomModelRouters {
|
||||
let mut models = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
for id in [base_id, coding_id] {
|
||||
if let Some(entry) = self.custom_router_proto_entry(id) {
|
||||
if seen.insert(entry.config_key.clone()) {
|
||||
models.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
api::request::settings::CustomModelRouters { routers: models }
|
||||
}
|
||||
|
||||
/// Returns the proto registry entry for a local custom-router id, or `None`
|
||||
/// if `id` is not a known local router.
|
||||
fn custom_router_proto_entry(
|
||||
&self,
|
||||
id: &LLMId,
|
||||
) -> Option<api::request::settings::custom_model_routers::CustomModelRouter> {
|
||||
self.custom_model_router_for_id(id).map(|m| m.to_proto())
|
||||
}
|
||||
|
||||
/// Rebuilds `custom_model_routers` from the `model_configs/` directory,
|
||||
/// then notifies subscribers.
|
||||
///
|
||||
/// Routers whose targets include an unknown model are excluded and a
|
||||
/// warning is logged. The check uses the currently loaded model list
|
||||
/// (server-fetched + cached), so it is best-effort at startup before
|
||||
/// the server responds.
|
||||
fn rebuild_custom_model_routers(&mut self, ctx: &mut ModelContext<Self>) {
|
||||
let local = WarpConfig::as_ref(ctx).custom_model_routers().clone();
|
||||
|
||||
let mut deduped = Vec::with_capacity(local.len());
|
||||
let mut seen = HashSet::new();
|
||||
for model in local {
|
||||
if seen.insert(model.config_key()) {
|
||||
deduped.push(model);
|
||||
}
|
||||
}
|
||||
let mut validation_errors: Vec<ModelConfigError> = Vec::new();
|
||||
deduped.retain(|router| {
|
||||
let unknown: Vec<&str> = router
|
||||
.all_targets()
|
||||
.into_iter()
|
||||
.filter(|id| self.get_llm_info(&LLMId::from(*id)).is_none())
|
||||
.collect();
|
||||
if unknown.is_empty() {
|
||||
return true;
|
||||
}
|
||||
let error_message = format!("unknown target model(s): {}", unknown.join(", "));
|
||||
log::warn!(
|
||||
"Custom model router '{}': {} — excluding from picker",
|
||||
router.info.display_name,
|
||||
error_message,
|
||||
);
|
||||
validation_errors.push(ModelConfigError {
|
||||
file_name: router
|
||||
.source_path
|
||||
.as_ref()
|
||||
.and_then(|p| p.file_name())
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or(router.info.display_name.as_str())
|
||||
.to_owned(),
|
||||
file_path: router.source_path.clone().unwrap_or_default(),
|
||||
error_message,
|
||||
});
|
||||
false
|
||||
});
|
||||
if !validation_errors.is_empty() {
|
||||
WarpConfig::handle(ctx).update(ctx, |_, ctx| {
|
||||
ctx.emit(WarpConfigUpdateEvent::ModelConfigErrors(validation_errors));
|
||||
});
|
||||
}
|
||||
|
||||
// vision is supported only when every concrete target model supports it.
|
||||
for router in &mut deduped {
|
||||
router.info.vision_supported = router.all_targets().iter().all(|id| {
|
||||
self.get_llm_info(&LLMId::from(*id))
|
||||
.is_some_and(|info| info.vision_supported)
|
||||
});
|
||||
}
|
||||
|
||||
self.custom_model_routers = deduped;
|
||||
ctx.emit(LLMPreferencesEvent::UpdatedAvailableLLMs);
|
||||
}
|
||||
|
||||
/// Resets any persisted *local* custom-router selection that no longer resolves
|
||||
/// to a loaded definition, so a deleted/invalid local config falls back to the
|
||||
/// default model and the visible selection updates. Scoped to local
|
||||
/// ids so a cloud selection isn't reset by a local reload.
|
||||
fn reconcile_stale_custom_router_selection(&mut self, ctx: &mut ModelContext<Self>) {
|
||||
let valid_local: HashSet<LLMId> = self
|
||||
.custom_model_routers
|
||||
.iter()
|
||||
.map(|m| m.llm_id())
|
||||
.collect();
|
||||
|
||||
let mut updated_agent_mode = false;
|
||||
let mut updated_coding = false;
|
||||
|
||||
self.base_llm_for_terminal_view.retain(|_, id| {
|
||||
let stale = custom_model_routers::is_local_custom_router_id(id.as_str())
|
||||
&& !valid_local.contains(&*id);
|
||||
updated_agent_mode |= stale;
|
||||
!stale
|
||||
});
|
||||
|
||||
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles, ctx| {
|
||||
for profile_id in profiles.get_all_profile_ids() {
|
||||
let Some(profile) = profiles.get_profile_by_id(profile_id, ctx) else {
|
||||
continue;
|
||||
};
|
||||
let profile_data = profile.data();
|
||||
let base_stale = profile_data.base_model.as_ref().is_some_and(|id| {
|
||||
custom_model_routers::is_local_custom_router_id(id.as_str())
|
||||
&& !valid_local.contains(id)
|
||||
});
|
||||
if base_stale {
|
||||
profiles.set_base_model(profile_id, None, ctx);
|
||||
profiles.set_context_window_limit(profile_id, None, ctx);
|
||||
updated_agent_mode = true;
|
||||
}
|
||||
let coding_stale = profile_data.coding_model.as_ref().is_some_and(|id| {
|
||||
custom_model_routers::is_local_custom_router_id(id.as_str())
|
||||
&& !valid_local.contains(id)
|
||||
});
|
||||
if coding_stale {
|
||||
profiles.set_coding_model(profile_id, None, ctx);
|
||||
updated_coding = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if updated_agent_mode {
|
||||
self.trigger_snapshot_save(ctx);
|
||||
ctx.emit(LLMPreferencesEvent::UpdatedActiveAgentModeLLM);
|
||||
}
|
||||
if updated_coding {
|
||||
ctx.emit(LLMPreferencesEvent::UpdatedActiveCodingLLM);
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads the user's current `ApiKeyManager.custom_endpoints` and replaces `custom_llms`
|
||||
/// with synthetic `LLMInfo`s. Called on every `ApiKeyManagerEvent::KeysUpdated`, so adds,
|
||||
/// edits, and removals all propagate immediately.
|
||||
fn rebuild_custom_llms(&mut self, app: &AppContext) {
|
||||
self.custom_llms = build_custom_llm_infos(ApiKeyManager::as_ref(app).keys());
|
||||
}
|
||||
|
||||
fn sanitize_disabled_custom_model_preferences(&mut self, ctx: &mut ModelContext<Self>) {
|
||||
if Self::custom_inference_enabled(ctx) || self.custom_llms.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let custom_ids: HashSet<_> = self
|
||||
.custom_llms
|
||||
.iter()
|
||||
.map(|info| info.id.clone())
|
||||
.collect();
|
||||
let mut updated_agent_mode = false;
|
||||
let mut updated_coding = false;
|
||||
let mut updated_other = false;
|
||||
|
||||
self.base_llm_for_terminal_view.retain(|_, id| {
|
||||
let keep = !custom_ids.contains(id);
|
||||
updated_agent_mode |= !keep;
|
||||
keep
|
||||
});
|
||||
|
||||
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles, ctx| {
|
||||
for profile_id in profiles.get_all_profile_ids() {
|
||||
let Some(profile) = profiles.get_profile_by_id(profile_id, ctx) else {
|
||||
continue;
|
||||
};
|
||||
let profile_data = profile.data();
|
||||
|
||||
if profile_data
|
||||
.base_model
|
||||
.as_ref()
|
||||
.is_some_and(|id| custom_ids.contains(id))
|
||||
{
|
||||
profiles.set_base_model(profile_id, None, ctx);
|
||||
profiles.set_context_window_limit(profile_id, None, ctx);
|
||||
updated_agent_mode = true;
|
||||
}
|
||||
if profile_data
|
||||
.coding_model
|
||||
.as_ref()
|
||||
.is_some_and(|id| custom_ids.contains(id))
|
||||
{
|
||||
profiles.set_coding_model(profile_id, None, ctx);
|
||||
updated_coding = true;
|
||||
}
|
||||
if profile_data
|
||||
.cli_agent_model
|
||||
.as_ref()
|
||||
.is_some_and(|id| custom_ids.contains(id))
|
||||
{
|
||||
profiles.set_cli_agent_model(profile_id, None, ctx);
|
||||
updated_other = true;
|
||||
}
|
||||
if profile_data
|
||||
.computer_use_model
|
||||
.as_ref()
|
||||
.is_some_and(|id| custom_ids.contains(id))
|
||||
{
|
||||
profiles.set_computer_use_model(profile_id, None, ctx);
|
||||
updated_other = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if updated_agent_mode {
|
||||
self.trigger_snapshot_save(ctx);
|
||||
ctx.emit(LLMPreferencesEvent::UpdatedActiveAgentModeLLM);
|
||||
}
|
||||
if updated_coding {
|
||||
ctx.emit(LLMPreferencesEvent::UpdatedActiveCodingLLM);
|
||||
}
|
||||
if updated_other {
|
||||
ctx.emit(LLMPreferencesEvent::UpdatedAvailableLLMs);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the default base model as a fallback.
|
||||
@@ -1315,55 +1753,15 @@ impl LLMPreferences {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
self.inject_bedrock_models(ctx);
|
||||
self.reconcile_disabled_model_preferences(ctx);
|
||||
|
||||
// Clear any model selections where the model is no longer supported.
|
||||
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) {
|
||||
if let Some(preferred_llm_id) = &profile.data().base_model {
|
||||
if self
|
||||
.models_by_feature
|
||||
.agent_mode
|
||||
.info_for_id(preferred_llm_id)
|
||||
.is_none()
|
||||
{
|
||||
profiles.set_base_model(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// Re-evaluate custom model routers now that the server catalog is fresh.
|
||||
// A router that was excluded at startup (because its target wasn't in the
|
||||
// cached catalog) is reconsidered here with the authoritative model list.
|
||||
if FeatureFlag::CustomModelRouters.is_enabled() {
|
||||
self.rebuild_custom_model_routers(ctx);
|
||||
self.reconcile_stale_custom_router_selection(ctx);
|
||||
}
|
||||
|
||||
let new_choices =
|
||||
get_new_agent_mode_choices(&old.agent_mode, &self.models_by_feature.agent_mode);
|
||||
@@ -1384,6 +1782,81 @@ 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)
|
||||
.or_else(|| {
|
||||
self.custom_llm_info_for_id_if_enabled(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)
|
||||
.or_else(|| {
|
||||
self.custom_llm_info_for_id_if_enabled(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)
|
||||
.or_else(|| {
|
||||
self.custom_llm_info_for_id_if_enabled(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
|
||||
@@ -1443,6 +1916,52 @@ fn get_new_agent_mode_choices(
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Builds synthetic [`LLMInfo`]s from the user's persisted custom endpoints.
|
||||
///
|
||||
/// One entry per `CustomEndpointModel`. The display label is the **alias** when present,
|
||||
/// falling back to the raw model name. The `id` is the model's `config_key`, which is
|
||||
/// also what flows out to `Request.Settings.custom_model_providers` so the server can map
|
||||
/// a `ModelConfig.{base,coding,cli_agent,computer_use_agent}` selection back to the
|
||||
/// user-provided endpoint.
|
||||
///
|
||||
/// Endpoints with empty URL or API key, and models with empty name or config_key, are
|
||||
/// skipped — they shouldn't surface in the picker until the user finishes configuring them.
|
||||
fn build_custom_llm_infos(keys: &ai::api_keys::ApiKeys) -> Vec<LLMInfo> {
|
||||
keys.custom_endpoints
|
||||
.iter()
|
||||
.filter(|ep| !ep.url.trim().is_empty() && !ep.api_key.is_empty())
|
||||
.flat_map(|endpoint| {
|
||||
endpoint
|
||||
.models
|
||||
.iter()
|
||||
.filter(|m| !m.name.trim().is_empty() && !m.config_key.is_empty())
|
||||
.map(move |model| custom_llm_info_from(endpoint, model))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn custom_llm_info_from(endpoint: &CustomEndpoint, model: &CustomEndpointModel) -> LLMInfo {
|
||||
let label = model.display_label().to_owned();
|
||||
LLMInfo {
|
||||
display_name: label.clone(),
|
||||
base_model_name: label,
|
||||
id: model.config_key.clone().into(),
|
||||
reasoning_level: None,
|
||||
usage_metadata: LLMUsageMetadata {
|
||||
request_multiplier: 1,
|
||||
credit_multiplier: None,
|
||||
},
|
||||
description: Some(format!("Custom · {}", endpoint.name)),
|
||||
disable_reason: None,
|
||||
vision_supported: true,
|
||||
spec: None,
|
||||
provider: LLMProvider::Unknown,
|
||||
host_configs: HashMap::new(),
|
||||
discount_percentage: None,
|
||||
context_window: LLMContextWindow::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the last cached LLM metadata.
|
||||
fn get_cached_models(app: &mut AppContext) -> Option<ModelsByFeature> {
|
||||
let value = app
|
||||
|
||||
Reference in New Issue
Block a user