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
+5 -50
View File
@@ -11,6 +11,11 @@ use serde_json::Value as JsonValue;
use super::external_config::ExternalBedrockConfig;
// Re-export shared provider types so existing imports from bedrock::convert continue to work.
pub use crate::ai::provider::types::{
ContentPart, ConversationMessage, MessageContent, MessageRole, ToolDefinition,
};
#[derive(Clone, Debug)]
pub struct CachingConfig {
pub enabled: bool,
@@ -42,56 +47,6 @@ pub struct ConvertedRequest {
pub tool_config: Option<ToolConfiguration>,
}
#[derive(Clone, Debug)]
pub struct ConversationMessage {
pub role: MessageRole,
pub content: MessageContent,
}
#[derive(Clone, Debug, PartialEq)]
pub enum MessageRole {
User,
Assistant,
}
#[derive(Clone, Debug)]
pub enum MessageContent {
Text(String),
ToolUse {
tool_use_id: String,
name: String,
input: JsonValue,
},
ToolResult {
tool_use_id: String,
content: String,
is_error: bool,
},
MultiPart(Vec<ContentPart>),
}
#[derive(Clone, Debug)]
pub enum ContentPart {
Text(String),
ToolUse {
tool_use_id: String,
name: String,
input: JsonValue,
},
ToolResult {
tool_use_id: String,
content: String,
is_error: bool,
},
}
#[derive(Clone, Debug)]
pub struct ToolDefinition {
pub name: String,
pub description: String,
pub input_schema: JsonValue,
}
pub fn build_converse_request(
messages: Vec<ConversationMessage>,
system_prompt: Option<String>,
+1
View File
@@ -92,6 +92,7 @@ fn test_get_effective_models_custom_overrides() {
model_id: "custom.model-v1:0".to_string(),
display_name: "Custom Model".to_string(),
vision_supported: false,
context_size: 200_000,
}];
let models = get_effective_models(&custom);
assert_eq!(models.len(), 1);
+6 -1
View File
@@ -992,7 +992,12 @@ pub fn extract_tools(request: &api::Request) -> Vec<ToolDefinition> {
// Filter out suggest_next_prompt — its action executor waits on a oneshot
// channel for UI interaction that never fires in the Bedrock path, causing
// the conversation to stay InProgress forever.
tools.retain(|t| t.name != "suggest_next_prompt");
// Filter out start_agent/send_message_to_agent — sub-agents are disabled.
tools.retain(|t| {
t.name != "suggest_next_prompt"
&& t.name != "start_agent"
&& t.name != "send_message_to_agent"
});
tools
}
+4 -4
View File
@@ -560,7 +560,7 @@ pub fn bedrock_stream_to_response_events(
Box::pin(stream)
}
pub(crate) fn build_create_task(task_id: &str) -> ResponseEvent {
pub fn build_create_task(task_id: &str) -> ResponseEvent {
let task = api::Task {
id: task_id.to_string(),
description: String::new(),
@@ -617,7 +617,7 @@ fn build_user_query_message(task_id: &str, query_text: &str) -> ResponseEvent {
}
}
pub(super) fn build_stream_init(request_id: &str, conversation_id: &str) -> ResponseEvent {
pub fn build_stream_init(request_id: &str, conversation_id: &str) -> ResponseEvent {
ResponseEvent {
r#type: Some(api::response_event::Type::Init(
api::response_event::StreamInit {
@@ -629,7 +629,7 @@ pub(super) fn build_stream_init(request_id: &str, conversation_id: &str) -> Resp
}
}
pub(super) fn build_stream_finished(
pub fn build_stream_finished(
reason: stream_finished::Reason,
input_tokens: i32,
output_tokens: i32,
@@ -820,7 +820,7 @@ fn build_append_text(task_id: &str, message_id: &str, text_delta: &str) -> Respo
}
}
fn build_tool_call_message(
pub fn build_tool_call_message(
task_id: &str,
tool_use_id: &str,
tool_name: &str,
@@ -19,7 +19,15 @@ fn test_build_stream_init_has_valid_ids() {
#[test]
fn test_build_stream_finished_done_reason() {
let reason = stream_finished::Reason::Done(stream_finished::Done {});
let event = build_stream_finished(reason, 100, 50, 20, 10, "anthropic.claude-sonnet-4-6", false);
let event = build_stream_finished(
reason,
100,
50,
20,
10,
"anthropic.claude-sonnet-4-6",
false,
);
match event.r#type {
Some(api::response_event::Type::Finished(finished)) => {