Add OpenAI/LiteLLM provider support with settings UI

- Add openai/ provider module with translator, client, convert, request/response translators
- Add shared provider/ types (ConversationMessage, MessageRole, ProviderConfig enum)
- Wire OpenAI-compatible provider dispatch alongside Bedrock in response_stream.rs
- Add ai.openai.* settings (enabled, base_url, api_key, model, models)
- Add OpenAI/LiteLLM settings page with model fetch, picker, and config UI
- Extend model menu items and llms.rs to surface LiteLLM models
- Update WARP.md with OpenAI provider architecture docs
This commit is contained in:
Ryan Ward
2026-06-17 14:14:40 -05:00
parent 59cfd0e2f5
commit 5ea378a38d
32 changed files with 2442 additions and 137 deletions
+90 -3
View File
@@ -16,7 +16,7 @@ use crate::{
network::{NetworkStatus, NetworkStatusEvent, NetworkStatusKind},
report_error,
server::server_api::ServerApiProvider,
settings::ai::{AISettings, AISettingsChangedEvent, BedrockModelConfig},
settings::ai::{AISettings, AISettingsChangedEvent, BedrockModelConfig, OpenAIModelConfig},
workspaces::user_workspaces::{UserWorkspaces, UserWorkspacesEvent},
};
@@ -43,6 +43,7 @@ pub fn is_using_api_key_for_provider(provider: &LLMProvider, app: &AppContext) -
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,
}
}
@@ -97,6 +98,8 @@ pub enum LLMProvider {
Google,
Xai,
Bedrock,
/// Models served through an OpenAI-compatible proxy (e.g. LiteLLM).
LiteLLM,
Unknown,
}
@@ -108,6 +111,7 @@ impl LLMProvider {
LLMProvider::Anthropic => Some(Icon::ClaudeLogo),
LLMProvider::Google => Some(Icon::GeminiLogo),
LLMProvider::Bedrock => Some(Icon::BedrockLogo),
LLMProvider::LiteLLM => Some(Icon::OpenAILogo),
LLMProvider::Xai => None,
LLMProvider::Unknown => None,
}
@@ -551,6 +555,15 @@ impl LLMPreferences {
me.inject_bedrock_models(ctx);
ctx.emit(LLMPreferencesEvent::UpdatedAvailableLLMs);
}
if matches!(
event,
AISettingsChangedEvent::OpenAIEnabled { .. }
| AISettingsChangedEvent::OpenAIModels { .. }
| AISettingsChangedEvent::OpenAIBaseUrl { .. }
) {
me.inject_openai_models(ctx);
ctx.emit(LLMPreferencesEvent::UpdatedAvailableLLMs);
}
});
let base_llm_for_terminal_view = HashMap::new();
@@ -572,6 +585,7 @@ impl LLMPreferences {
{
Self::ensure_default_models_in_settings(ctx);
me.inject_bedrock_models(ctx);
me.inject_openai_models(ctx);
}
me
@@ -582,8 +596,7 @@ impl LLMPreferences {
use crate::ai::bedrock::models::DEFAULT_BEDROCK_MODELS;
let settings = AISettings::as_ref(ctx);
let mut current_models: Vec<BedrockModelConfig> =
settings.bedrock_models.value().clone();
let mut current_models: Vec<BedrockModelConfig> = settings.bedrock_models.value().clone();
let existing_ids: std::collections::HashSet<String> =
current_models.iter().map(|m| m.model_id.clone()).collect();
@@ -786,6 +799,80 @@ impl LLMPreferences {
}
}
/// Injects models from the OpenAI-compatible (LiteLLM) provider into the available model lists.
#[cfg(not(target_family = "wasm"))]
fn inject_openai_models(&mut self, ctx: &AppContext) {
// Remove any previously injected LiteLLM models
self.models_by_feature
.agent_mode
.choices
.retain(|m| m.provider != LLMProvider::LiteLLM);
self.models_by_feature
.coding
.choices
.retain(|m| m.provider != LLMProvider::LiteLLM);
if let Some(ref mut cli) = self.models_by_feature.cli_agent {
cli.choices.retain(|m| m.provider != LLMProvider::LiteLLM);
}
let settings = AISettings::as_ref(ctx);
if !*settings.openai_enabled.value() {
return;
}
let user_models: Vec<OpenAIModelConfig> = settings.openai_models.value().clone();
if user_models.is_empty() {
return;
}
let base_url = settings.openai_base_url.value().clone();
let description_label = if base_url.contains("localhost") || base_url.contains("127.0.0.1")
{
"LiteLLM (local)".to_string()
} else {
"LiteLLM".to_string()
};
for model in &user_models {
let llm_info = LLMInfo {
id: LLMId::from(model.model_id.as_str()),
display_name: model.display_name.clone(),
base_model_name: model.display_name.clone(),
reasoning_level: None,
usage_metadata: LLMUsageMetadata {
request_multiplier: 1,
credit_multiplier: None,
},
description: Some(description_label.clone()),
disable_reason: None,
vision_supported: model.vision_supported,
spec: None,
provider: LLMProvider::LiteLLM,
host_configs: HashMap::from([(
LLMModelHost::DirectApi,
RoutingHostConfig {
enabled: true,
model_routing_host: LLMModelHost::DirectApi,
},
)]),
discount_percentage: None,
};
self.models_by_feature
.agent_mode
.choices
.push(llm_info.clone());
self.models_by_feature.coding.choices.push(llm_info.clone());
if let Some(ref mut cli) = self.models_by_feature.cli_agent {
cli.choices.push(llm_info);
}
}
log::info!(
"[openai/litellm] Injected {} model(s) into available choices",
user_models.len()
);
}
/// Returns the `LLMInfo` for the base LLM to be used for an Agent Mode request.
pub fn get_active_base_model<'a>(
&'a self,