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:
@@ -0,0 +1,92 @@
|
||||
use std::fmt;
|
||||
|
||||
use bytes::Bytes;
|
||||
use futures::Stream;
|
||||
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OpenAIClientConfig {
|
||||
pub base_url: String,
|
||||
pub api_key: Option<String>,
|
||||
pub model: Option<String>,
|
||||
}
|
||||
|
||||
pub struct OpenAIClient {
|
||||
http: reqwest::Client,
|
||||
base_url: String,
|
||||
api_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum OpenAIError {
|
||||
ConnectionFailed(String),
|
||||
AuthenticationFailed(String),
|
||||
RateLimited(String),
|
||||
BadRequest(String),
|
||||
ServerError(String),
|
||||
#[allow(dead_code)]
|
||||
StreamError(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for OpenAIError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::ConnectionFailed(msg) => write!(f, "Connection failed: {msg}"),
|
||||
Self::AuthenticationFailed(msg) => write!(f, "Authentication failed: {msg}"),
|
||||
Self::RateLimited(msg) => write!(f, "Rate limited: {msg}"),
|
||||
Self::BadRequest(msg) => write!(f, "Bad request: {msg}"),
|
||||
Self::ServerError(msg) => write!(f, "Server error: {msg}"),
|
||||
Self::StreamError(msg) => write!(f, "Stream error: {msg}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenAIClient {
|
||||
pub fn from_config(config: OpenAIClientConfig) -> Self {
|
||||
let http = reqwest::Client::new();
|
||||
Self {
|
||||
http,
|
||||
base_url: config.base_url,
|
||||
api_key: config.api_key,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn chat_completions_stream(
|
||||
&self,
|
||||
request_body: serde_json::Value,
|
||||
) -> Result<impl Stream<Item = Result<Bytes, reqwest::Error>>, OpenAIError> {
|
||||
let url = format!("{}/chat/completions", self.base_url.trim_end_matches('/'));
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
|
||||
if let Some(ref key) = self.api_key {
|
||||
headers.insert(
|
||||
AUTHORIZATION,
|
||||
HeaderValue::from_str(&format!("Bearer {key}"))
|
||||
.map_err(|e| OpenAIError::BadRequest(format!("Invalid API key header: {e}")))?,
|
||||
);
|
||||
}
|
||||
|
||||
let response = self
|
||||
.http
|
||||
.post(&url)
|
||||
.headers(headers)
|
||||
.json(&request_body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| OpenAIError::ConnectionFailed(e.to_string()))?;
|
||||
|
||||
let status = response.status();
|
||||
if !status.is_success() {
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
return Err(match status.as_u16() {
|
||||
401 => OpenAIError::AuthenticationFailed(body),
|
||||
429 => OpenAIError::RateLimited(body),
|
||||
400 => OpenAIError::BadRequest(body),
|
||||
_ => OpenAIError::ServerError(format!("HTTP {status}: {body}")),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(response.bytes_stream())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user