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:
Ryan Ward
2026-07-16 10:26:24 -05:00
parent 345057a147
commit f1c289fe85
2 changed files with 104 additions and 226 deletions
+10 -25
View File
@@ -198,31 +198,16 @@ pub async fn generate_multi_agent_output(
}
}
ProviderConfig::None => {
let response_stream = warp_multi_agent_client::generate_multi_agent_output(
server_api.base_client().as_ref(),
&request,
)
.await;
match response_stream {
Ok(stream) => {
let output_stream = stream
.then(|result| async {
match result {
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))
}
}
// No provider configured — do not fall back to Warp's cloud API.
let err = Arc::new(crate::server::server_api::AIApiError::Stream {
stream_type: "none",
source: anyhow::anyhow!(
"No AI provider configured. Enable Bedrock or OpenAI/LiteLLM in settings."
),
});
let (tx, rx) = async_channel::unbounded();
let _ = tx.send(Err(err)).await;
Ok(Box::pin(rx))
}
}
}
+94 -201
View File
@@ -423,8 +423,32 @@ impl AvailableLLMs {
}
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)
.expect("Default LLM ID must be present in choices")
.or_else(|| self.choices.first())
.unwrap_or(&NO_PROVIDER_FALLBACK)
}
#[cfg(feature = "integration_tests")]
@@ -499,74 +523,37 @@ fn default_computer_use_llms() -> AvailableLLMs {
}
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 {
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 {
agent_mode: AvailableLLMs {
default_id: "anthropic.claude-opus-4-6[1m]".to_owned().into(),
choices: vec![LLMInfo {
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,
}),
agent_mode: placeholder(),
coding: placeholder(),
cli_agent: Some(placeholder()),
computer_use: Some(default_computer_use_llms()),
}
}
@@ -765,16 +752,17 @@ impl LLMPreferences {
#[cfg(not(target_family = "wasm"))]
fn inject_bedrock_models(&mut self, ctx: &AppContext) {
// Strip both existing Bedrock models and placeholder Unknown models.
self.models_by_feature
.agent_mode
.choices
.retain(|m| m.provider != LLMProvider::Bedrock);
.retain(|m| m.provider != LLMProvider::Bedrock && m.provider != LLMProvider::Unknown);
self.models_by_feature
.coding
.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 {
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);
@@ -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,
// otherwise Claude Opus 4.6, falling back to the first available model.
// Note: external_config already loaded above for filtering
@@ -1781,6 +1756,17 @@ impl LLMPreferences {
}
/// 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 {
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.
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.
if !AuthStateProvider::as_ref(ctx).get().is_logged_in() {
return;
}
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"));
}
},
);
///
/// NOTE: Disabled — Galaxy uses only locally configured providers (Bedrock/LiteLLM).
/// No models are fetched from Warp's cloud API.
pub fn refresh_authed_models(&self, _ctx: &mut ModelContext<Self>) {
log::debug!("[llm] Server model fetch disabled — using local providers only");
}
/// 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();
ctx.spawn(
async move { ai_api_client.get_free_available_models(None).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 free-tier LLMs from server"));
}
},
);
///
/// NOTE: Disabled — Galaxy uses only locally configured providers (Bedrock/LiteLLM).
/// No models are fetched from Warp's cloud API.
fn refresh_public_models(&self, _ctx: &mut ModelContext<Self>) {
log::debug!("[llm] Server model fetch disabled — using local providers only");
}
pub fn refresh_available_models(&self, ctx: &mut ModelContext<Self>) {
if AuthStateProvider::as_ref(ctx).get().is_logged_in() {
self.refresh_authed_models(ctx);
} else {
self.refresh_public_models(ctx);
}
/// NOTE: Disabled — Galaxy uses only locally configured providers (Bedrock/LiteLLM).
pub fn refresh_available_models(&self, _ctx: &mut ModelContext<Self>) {
log::debug!("[llm] Server model fetch disabled — using local providers only");
}
/// Disabled — Galaxy does not accept model updates from Warp's server.
pub fn update_feature_model_choices(
&mut self,
choices_result: Result<ModelsByFeature, anyhow::Error>,
ctx: &mut ModelContext<Self>,
_choices_result: Result<ModelsByFeature, anyhow::Error>,
_ctx: &mut ModelContext<Self>,
) {
if let Ok(choices) = choices_result {
self.on_server_update(choices, ctx);
}
log::debug!("[llm] Server model update ignored — using local providers only");
}
fn on_server_update(&mut self, update: ModelsByFeature, ctx: &mut ModelContext<Self>) {
let has_existing_persisted_config = get_cached_models(ctx).is_some();
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);
/// Disabled — Galaxy does not accept model updates from Warp's server.
fn on_server_update(&mut self, _update: ModelsByFeature, _ctx: &mut ModelContext<Self>) {
log::debug!("[llm] Server model update ignored — using local providers only");
}
/// 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.
fn get_cached_models(app: &mut AppContext) -> Option<ModelsByFeature> {
let value = app
.private_user_preferences()
.read_value(MODELS_BY_FEATURE_CACHE_KEY)
.ok()
.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
}
}
}
}
/// Disabled — Galaxy uses only locally configured providers. No server-fetched models
/// are cached or restored. The model list is built exclusively from Bedrock/LiteLLM
/// settings at startup.
fn get_cached_models(_app: &mut AppContext) -> Option<ModelsByFeature> {
None
}
#[cfg(test)]