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:
@@ -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>,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)) => {
|
||||
|
||||
Reference in New Issue
Block a user