Remove all Warp server AI model fetching — use only local providers
Galaxy should never call Warp's cloud API for AI. This is a policy requirement. All model availability is now determined exclusively by locally configured providers (Bedrock and/or OpenAI/LiteLLM). Changes: - Disable refresh_authed_models, refresh_public_models, refresh_available_models (now no-ops with debug log) - Disable on_server_update and update_feature_model_choices - Disable get_cached_models (no stale server models restored from cache) - Replace ModelsByFeature::default() with minimal placeholder that gets stripped by inject_bedrock_models/inject_openai_models - Make inject_bedrock_models strip Unknown placeholders unconditionally - Make default_llm_info() return a static fallback instead of panicking when no models are configured (prevents null reference crashes) - Add has_any_provider_models() for UI to check provider availability - Make ProviderConfig::None return a user-friendly error instead of calling Warp's cloud API (the previous fallback behavior) Safety: if no providers are enabled, the system gracefully returns an error message rather than crashing or silently calling Warp's servers.
This commit is contained in:
@@ -198,31 +198,16 @@ pub async fn generate_multi_agent_output(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ProviderConfig::None => {
|
ProviderConfig::None => {
|
||||||
let response_stream = warp_multi_agent_client::generate_multi_agent_output(
|
// No provider configured — do not fall back to Warp's cloud API.
|
||||||
server_api.base_client().as_ref(),
|
let err = Arc::new(crate::server::server_api::AIApiError::Stream {
|
||||||
&request,
|
stream_type: "none",
|
||||||
)
|
source: anyhow::anyhow!(
|
||||||
.await;
|
"No AI provider configured. Enable Bedrock or OpenAI/LiteLLM in settings."
|
||||||
match response_stream {
|
),
|
||||||
Ok(stream) => {
|
});
|
||||||
let output_stream = stream
|
let (tx, rx) = async_channel::unbounded();
|
||||||
.then(|result| async {
|
let _ = tx.send(Err(err)).await;
|
||||||
match result {
|
Ok(Box::pin(rx))
|
||||||
Ok(event) => Ok(event),
|
|
||||||
Err(error) => Err(convert_multi_agent_client_error(error).await),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.take_until(cancellation_rx);
|
|
||||||
Ok(Box::pin(output_stream))
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
let (tx, rx) = async_channel::unbounded();
|
|
||||||
let _ = tx
|
|
||||||
.send(Err(convert_multi_agent_client_error(e).await))
|
|
||||||
.await;
|
|
||||||
Ok(Box::pin(rx))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+94
-201
@@ -423,8 +423,32 @@ impl AvailableLLMs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn default_llm_info(&self) -> &LLMInfo {
|
fn default_llm_info(&self) -> &LLMInfo {
|
||||||
|
static NO_PROVIDER_FALLBACK: std::sync::LazyLock<LLMInfo> = std::sync::LazyLock::new(|| {
|
||||||
|
LLMInfo {
|
||||||
|
display_name: "No models configured".to_owned(),
|
||||||
|
base_model_name: "No models configured".to_owned(),
|
||||||
|
id: "none".to_owned().into(),
|
||||||
|
reasoning_level: None,
|
||||||
|
usage_metadata: LLMUsageMetadata {
|
||||||
|
request_multiplier: 1,
|
||||||
|
credit_multiplier: None,
|
||||||
|
},
|
||||||
|
description: Some(
|
||||||
|
"Enable Bedrock or OpenAI/LiteLLM in settings".to_string(),
|
||||||
|
),
|
||||||
|
disable_reason: Some(DisableReason::Unavailable),
|
||||||
|
vision_supported: false,
|
||||||
|
spec: None,
|
||||||
|
provider: LLMProvider::Unknown,
|
||||||
|
host_configs: HashMap::new(),
|
||||||
|
discount_percentage: None,
|
||||||
|
context_window: LLMContextWindow::default(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
self.info_for_id(&self.default_id)
|
self.info_for_id(&self.default_id)
|
||||||
.expect("Default LLM ID must be present in choices")
|
.or_else(|| self.choices.first())
|
||||||
|
.unwrap_or(&NO_PROVIDER_FALLBACK)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "integration_tests")]
|
#[cfg(feature = "integration_tests")]
|
||||||
@@ -499,74 +523,37 @@ fn default_computer_use_llms() -> AvailableLLMs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ModelsByFeature {
|
impl Default for ModelsByFeature {
|
||||||
|
/// Returns a minimal placeholder. The real model list is populated exclusively
|
||||||
|
/// by `inject_bedrock_models` and `inject_openai_models` based on local settings.
|
||||||
|
/// The placeholder entry uses `LLMProvider::Unknown` so it gets stripped by
|
||||||
|
/// `inject_bedrock_models` once real models are loaded.
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
let placeholder = || AvailableLLMs {
|
||||||
|
default_id: "placeholder".to_owned().into(),
|
||||||
|
choices: vec![LLMInfo {
|
||||||
|
display_name: "No models configured".to_owned(),
|
||||||
|
base_model_name: "No models configured".to_owned(),
|
||||||
|
id: "placeholder".to_owned().into(),
|
||||||
|
reasoning_level: None,
|
||||||
|
usage_metadata: LLMUsageMetadata {
|
||||||
|
request_multiplier: 1,
|
||||||
|
credit_multiplier: None,
|
||||||
|
},
|
||||||
|
description: Some("Enable Bedrock or OpenAI/LiteLLM in settings".to_string()),
|
||||||
|
disable_reason: None,
|
||||||
|
vision_supported: false,
|
||||||
|
spec: None,
|
||||||
|
provider: LLMProvider::Unknown,
|
||||||
|
host_configs: HashMap::new(),
|
||||||
|
discount_percentage: None,
|
||||||
|
context_window: LLMContextWindow::default(),
|
||||||
|
}],
|
||||||
|
preferred_codex_model_id: None,
|
||||||
|
};
|
||||||
Self {
|
Self {
|
||||||
agent_mode: AvailableLLMs {
|
agent_mode: placeholder(),
|
||||||
default_id: "anthropic.claude-opus-4-6[1m]".to_owned().into(),
|
coding: placeholder(),
|
||||||
choices: vec![LLMInfo {
|
cli_agent: Some(placeholder()),
|
||||||
display_name: "Claude Opus 4.6".to_owned(),
|
|
||||||
base_model_name: "Claude Opus 4.6".to_owned(),
|
|
||||||
id: "anthropic.claude-opus-4-6[1m]".to_owned().into(),
|
|
||||||
reasoning_level: None,
|
|
||||||
usage_metadata: LLMUsageMetadata {
|
|
||||||
request_multiplier: 1,
|
|
||||||
credit_multiplier: None,
|
|
||||||
},
|
|
||||||
description: None,
|
|
||||||
disable_reason: None,
|
|
||||||
vision_supported: true,
|
|
||||||
spec: None,
|
|
||||||
provider: LLMProvider::Unknown,
|
|
||||||
host_configs: HashMap::new(),
|
|
||||||
discount_percentage: None,
|
|
||||||
context_window: LLMContextWindow::default(),
|
|
||||||
}],
|
|
||||||
preferred_codex_model_id: None,
|
|
||||||
},
|
|
||||||
coding: AvailableLLMs {
|
|
||||||
default_id: "anthropic.claude-sonnet-4-6[1m]".to_owned().into(),
|
|
||||||
choices: vec![LLMInfo {
|
|
||||||
display_name: "Claude Sonnet 4.6".to_owned(),
|
|
||||||
base_model_name: "Claude Sonnet 4.6".to_owned(),
|
|
||||||
id: "anthropic.claude-sonnet-4-6[1m]".to_owned().into(),
|
|
||||||
reasoning_level: None,
|
|
||||||
usage_metadata: LLMUsageMetadata {
|
|
||||||
request_multiplier: 1,
|
|
||||||
credit_multiplier: None,
|
|
||||||
},
|
|
||||||
description: None,
|
|
||||||
disable_reason: None,
|
|
||||||
vision_supported: true,
|
|
||||||
spec: None,
|
|
||||||
provider: LLMProvider::Unknown,
|
|
||||||
host_configs: HashMap::new(),
|
|
||||||
discount_percentage: None,
|
|
||||||
context_window: LLMContextWindow::default(),
|
|
||||||
}],
|
|
||||||
preferred_codex_model_id: None,
|
|
||||||
},
|
|
||||||
cli_agent: Some(AvailableLLMs {
|
|
||||||
default_id: "anthropic.claude-haiku-4-5-20251001-v1:0".to_owned().into(),
|
|
||||||
choices: vec![LLMInfo {
|
|
||||||
display_name: "Claude Haiku 4.5".to_owned(),
|
|
||||||
base_model_name: "Claude Haiku 4.5".to_owned(),
|
|
||||||
id: "anthropic.claude-haiku-4-5-20251001-v1:0".to_owned().into(),
|
|
||||||
reasoning_level: None,
|
|
||||||
usage_metadata: LLMUsageMetadata {
|
|
||||||
request_multiplier: 1,
|
|
||||||
credit_multiplier: None,
|
|
||||||
},
|
|
||||||
description: None,
|
|
||||||
disable_reason: None,
|
|
||||||
vision_supported: false,
|
|
||||||
spec: None,
|
|
||||||
provider: LLMProvider::Unknown,
|
|
||||||
host_configs: HashMap::new(),
|
|
||||||
discount_percentage: None,
|
|
||||||
context_window: LLMContextWindow::default(),
|
|
||||||
}],
|
|
||||||
preferred_codex_model_id: None,
|
|
||||||
}),
|
|
||||||
computer_use: Some(default_computer_use_llms()),
|
computer_use: Some(default_computer_use_llms()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -765,16 +752,17 @@ impl LLMPreferences {
|
|||||||
|
|
||||||
#[cfg(not(target_family = "wasm"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
fn inject_bedrock_models(&mut self, ctx: &AppContext) {
|
fn inject_bedrock_models(&mut self, ctx: &AppContext) {
|
||||||
|
// Strip both existing Bedrock models and placeholder Unknown models.
|
||||||
self.models_by_feature
|
self.models_by_feature
|
||||||
.agent_mode
|
.agent_mode
|
||||||
.choices
|
.choices
|
||||||
.retain(|m| m.provider != LLMProvider::Bedrock);
|
.retain(|m| m.provider != LLMProvider::Bedrock && m.provider != LLMProvider::Unknown);
|
||||||
self.models_by_feature
|
self.models_by_feature
|
||||||
.coding
|
.coding
|
||||||
.choices
|
.choices
|
||||||
.retain(|m| m.provider != LLMProvider::Bedrock);
|
.retain(|m| m.provider != LLMProvider::Bedrock && m.provider != LLMProvider::Unknown);
|
||||||
if let Some(ref mut cli) = self.models_by_feature.cli_agent {
|
if let Some(ref mut cli) = self.models_by_feature.cli_agent {
|
||||||
cli.choices.retain(|m| m.provider != LLMProvider::Bedrock);
|
cli.choices.retain(|m| m.provider != LLMProvider::Bedrock && m.provider != LLMProvider::Unknown);
|
||||||
}
|
}
|
||||||
|
|
||||||
let settings = AISettings::as_ref(ctx);
|
let settings = AISettings::as_ref(ctx);
|
||||||
@@ -863,19 +851,6 @@ impl LLMPreferences {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove any placeholder/auto-routing entries now that real Bedrock models are available.
|
|
||||||
self.models_by_feature
|
|
||||||
.agent_mode
|
|
||||||
.choices
|
|
||||||
.retain(|m| m.provider != LLMProvider::Unknown);
|
|
||||||
self.models_by_feature
|
|
||||||
.coding
|
|
||||||
.choices
|
|
||||||
.retain(|m| m.provider != LLMProvider::Unknown);
|
|
||||||
if let Some(ref mut cli) = self.models_by_feature.cli_agent {
|
|
||||||
cli.choices.retain(|m| m.provider != LLMProvider::Unknown);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default agent mode to ANTHROPIC_MODEL from external config if set,
|
// Default agent mode to ANTHROPIC_MODEL from external config if set,
|
||||||
// otherwise Claude Opus 4.6, falling back to the first available model.
|
// otherwise Claude Opus 4.6, falling back to the first available model.
|
||||||
// Note: external_config already loaded above for filtering
|
// Note: external_config already loaded above for filtering
|
||||||
@@ -1781,6 +1756,17 @@ impl LLMPreferences {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the default base model as a fallback.
|
/// Returns the default base model as a fallback.
|
||||||
|
/// Returns `true` if at least one real AI provider model is configured and available.
|
||||||
|
/// When this returns `false`, agent mode should be disabled to avoid null references
|
||||||
|
/// or attempts to call unconfigured providers.
|
||||||
|
pub fn has_any_provider_models(&self) -> bool {
|
||||||
|
self.models_by_feature
|
||||||
|
.agent_mode
|
||||||
|
.choices
|
||||||
|
.iter()
|
||||||
|
.any(|m| m.provider != LLMProvider::Unknown)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_default_base_model(&self) -> &LLMInfo {
|
pub fn get_default_base_model(&self) -> &LLMInfo {
|
||||||
self.models_by_feature.agent_mode.default_llm_info()
|
self.models_by_feature.agent_mode.default_llm_info()
|
||||||
}
|
}
|
||||||
@@ -1920,110 +1906,38 @@ impl LLMPreferences {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Fetches the latest set of models from the server for the currently logged in user, and updates the model.
|
/// Fetches the latest set of models from the server for the currently logged in user, and updates the model.
|
||||||
pub fn refresh_authed_models(&self, ctx: &mut ModelContext<Self>) {
|
///
|
||||||
// Don't try to fetch auth'd models if the user is not logged in yet.
|
/// NOTE: Disabled — Galaxy uses only locally configured providers (Bedrock/LiteLLM).
|
||||||
if !AuthStateProvider::as_ref(ctx).get().is_logged_in() {
|
/// No models are fetched from Warp's cloud API.
|
||||||
return;
|
pub fn refresh_authed_models(&self, _ctx: &mut ModelContext<Self>) {
|
||||||
}
|
log::debug!("[llm] Server model fetch disabled — using local providers only");
|
||||||
|
|
||||||
let ai_api_client = ServerApiProvider::as_ref(ctx).get_ai_client();
|
|
||||||
ctx.spawn(
|
|
||||||
async move { ai_api_client.get_feature_model_choices().await },
|
|
||||||
|me, result, ctx| match result {
|
|
||||||
Ok(update) => {
|
|
||||||
if update != me.models_by_feature {
|
|
||||||
me.on_server_update(update, ctx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
report_error!(e.context("Failed to fetch LLMs from server"));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// No auth required (i.e. to populate the pre-login onboarding picker).
|
/// No auth required (i.e. to populate the pre-login onboarding picker).
|
||||||
fn refresh_public_models(&self, ctx: &mut ModelContext<Self>) {
|
///
|
||||||
let ai_api_client = ServerApiProvider::as_ref(ctx).get_ai_client();
|
/// NOTE: Disabled — Galaxy uses only locally configured providers (Bedrock/LiteLLM).
|
||||||
ctx.spawn(
|
/// No models are fetched from Warp's cloud API.
|
||||||
async move { ai_api_client.get_free_available_models(None).await },
|
fn refresh_public_models(&self, _ctx: &mut ModelContext<Self>) {
|
||||||
|me, result, ctx| match result {
|
log::debug!("[llm] Server model fetch disabled — using local providers only");
|
||||||
Ok(update) => {
|
|
||||||
if update != me.models_by_feature {
|
|
||||||
me.on_server_update(update, ctx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
report_error!(e.context("Failed to fetch free-tier LLMs from server"));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn refresh_available_models(&self, ctx: &mut ModelContext<Self>) {
|
/// NOTE: Disabled — Galaxy uses only locally configured providers (Bedrock/LiteLLM).
|
||||||
if AuthStateProvider::as_ref(ctx).get().is_logged_in() {
|
pub fn refresh_available_models(&self, _ctx: &mut ModelContext<Self>) {
|
||||||
self.refresh_authed_models(ctx);
|
log::debug!("[llm] Server model fetch disabled — using local providers only");
|
||||||
} else {
|
|
||||||
self.refresh_public_models(ctx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Disabled — Galaxy does not accept model updates from Warp's server.
|
||||||
pub fn update_feature_model_choices(
|
pub fn update_feature_model_choices(
|
||||||
&mut self,
|
&mut self,
|
||||||
choices_result: Result<ModelsByFeature, anyhow::Error>,
|
_choices_result: Result<ModelsByFeature, anyhow::Error>,
|
||||||
ctx: &mut ModelContext<Self>,
|
_ctx: &mut ModelContext<Self>,
|
||||||
) {
|
) {
|
||||||
if let Ok(choices) = choices_result {
|
log::debug!("[llm] Server model update ignored — using local providers only");
|
||||||
self.on_server_update(choices, ctx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_server_update(&mut self, update: ModelsByFeature, ctx: &mut ModelContext<Self>) {
|
/// Disabled — Galaxy does not accept model updates from Warp's server.
|
||||||
let has_existing_persisted_config = get_cached_models(ctx).is_some();
|
fn on_server_update(&mut self, _update: ModelsByFeature, _ctx: &mut ModelContext<Self>) {
|
||||||
|
log::debug!("[llm] Server model update ignored — using local providers only");
|
||||||
let old = std::mem::replace(&mut self.models_by_feature, update);
|
|
||||||
|
|
||||||
match serde_json::to_string(&self.models_by_feature) {
|
|
||||||
Ok(serialized_update) => {
|
|
||||||
if let Err(e) = ctx
|
|
||||||
.private_user_preferences()
|
|
||||||
.write_value(MODELS_BY_FEATURE_CACHE_KEY, serialized_update)
|
|
||||||
{
|
|
||||||
log::error!("Failed to cache LLMs: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to serialize LLMs for cache: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.reconcile_disabled_model_preferences(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);
|
|
||||||
if !new_choices.is_empty() {
|
|
||||||
self.last_update = Some(AvailableLLMsUpdate {
|
|
||||||
new_choices,
|
|
||||||
// We shouldn't show the update for the initial LLM config creation.
|
|
||||||
popup_visibility_state: Arc::new(FairMutex::new(
|
|
||||||
if has_existing_persisted_config {
|
|
||||||
UpdatePopupVisibilityState::WaitingToBeShown
|
|
||||||
} else {
|
|
||||||
UpdatePopupVisibilityState::Hidden
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.emit(LLMPreferencesEvent::UpdatedAvailableLLMs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear any model selections where the model is no longer supported
|
/// Clear any model selections where the model is no longer supported
|
||||||
@@ -2223,32 +2137,11 @@ fn custom_llm_info_from(endpoint: &CustomEndpoint, model: &CustomEndpointModel)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the last cached LLM metadata.
|
/// Gets the last cached LLM metadata.
|
||||||
fn get_cached_models(app: &mut AppContext) -> Option<ModelsByFeature> {
|
/// Disabled — Galaxy uses only locally configured providers. No server-fetched models
|
||||||
let value = app
|
/// are cached or restored. The model list is built exclusively from Bedrock/LiteLLM
|
||||||
.private_user_preferences()
|
/// settings at startup.
|
||||||
.read_value(MODELS_BY_FEATURE_CACHE_KEY)
|
fn get_cached_models(_app: &mut AppContext) -> Option<ModelsByFeature> {
|
||||||
.ok()
|
None
|
||||||
.flatten()?;
|
|
||||||
|
|
||||||
// Try to deserialize to the [`ModelsByFeature`] type.
|
|
||||||
match serde_json::from_str::<ModelsByFeature>(value.as_str()) {
|
|
||||||
Ok(config) => Some(config),
|
|
||||||
Err(e1) => {
|
|
||||||
// If that fails, try to deserialize directly to [`AvailableLLMs`].
|
|
||||||
// Before we had model choice by feature, all available LLMs were solely
|
|
||||||
// for Agent Mode.
|
|
||||||
match serde_json::from_str::<AvailableLLMs>(value.as_str()) {
|
|
||||||
Ok(config) => Some(ModelsByFeature {
|
|
||||||
agent_mode: config,
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
Err(e2) => {
|
|
||||||
log::warn!("Failed to deserialize cached LLMs: {e1}\n{e2}");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
Reference in New Issue
Block a user