add more changes

This commit is contained in:
Adil Hafeez 2025-06-03 15:00:57 -07:00
parent 2d4d0b01ee
commit f10e0fcece
No known key found for this signature in database
GPG key ID: 9B18EF7691369645
14 changed files with 486 additions and 139 deletions

View file

@ -5,12 +5,13 @@ pub mod providers;
#[cfg(test)]
mod tests {
use crate::providers::openai::types::OpenAIRequest;
use crate::providers::openai::types::ChatCompletionsRequest;
#[test]
fn openai_builder() {
let request = OpenAIRequest::builder()
.model("gpt-3.5-turbo")
let request = ChatCompletionsRequest::builder("gpt-3.5-turbo", vec![])
.temperature(0.7)
.top_p(0.9)
.n(1)
@ -22,14 +23,14 @@ mod tests {
.build()
.expect("Failed to build OpenAIRequest");
assert_eq!(request.base.model, "gpt-3.5-turbo");
assert_eq!(request.base.temperature, Some(0.7));
assert_eq!(request.base.top_p, Some(0.9));
assert_eq!(request.base.n, Some(1));
assert_eq!(request.base.max_tokens, Some(100));
assert_eq!(request.base.stream, Some(false));
assert_eq!(request.base.stop, Some(vec!["\n".to_string()]));
assert_eq!(request.base.presence_penalty, Some(0.0));
assert_eq!(request.base.frequency_penalty, Some(0.0));
assert_eq!(request.model, "gpt-3.5-turbo");
assert_eq!(request.temperature, Some(0.7));
assert_eq!(request.top_p, Some(0.9));
assert_eq!(request.n, Some(1));
assert_eq!(request.max_tokens, Some(100));
assert_eq!(request.stream, Some(false));
assert_eq!(request.stop, Some(vec!["\n".to_string()]));
assert_eq!(request.presence_penalty, Some(0.0));
assert_eq!(request.frequency_penalty, Some(0.0));
}
}

View file

@ -1,44 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub role: String,
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatRequestBase {
pub model: String,
pub messages: Option<Vec<Message>>,
pub temperature: Option<f32>,
pub top_p: Option<f32>,
pub n: Option<u32>,
pub max_tokens: Option<u32>,
pub stream: Option<bool>,
pub stop: Option<Vec<String>>,
pub presence_penalty: Option<f32>,
pub frequency_penalty: Option<f32>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ChatResponseBase {
pub id: String,
pub object: String,
pub created: u64,
pub choices: Vec<Choice>,
pub usage: Option<Usage>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Choice {
pub index: u32,
pub message: Message,
pub finish_reason: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Usage {
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub total_tokens: u32,
}

View file

@ -1,17 +1,19 @@
use serde::{Deserialize, Serialize};
use crate::providers::common_types::{ChatRequestBase, ChatResponseBase};
use crate::providers::openai::types::{ChatCompletionsRequest, ChatCompletionsResponse};
pub use crate::providers::openai::types::{Choice, Message, Usage};
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeepSeekRequest {
#[serde(flatten)]
pub base: ChatRequestBase,
pub base: ChatCompletionsRequest,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeepSeekResponse {
#[serde(flatten)]
pub base: ChatResponseBase,
pub base: ChatCompletionsResponse,
}
// Re-export for convenience
pub use crate::providers::common_types::{Message, Choice, Usage};

View file

@ -1,16 +1,19 @@
use serde::{Deserialize, Serialize};
use crate::providers::common_types::{ChatRequestBase, ChatResponseBase};
use crate::providers::openai::types::{ChatCompletionsRequest, ChatCompletionsResponse};
pub use crate::providers::openai::types::{Choice, Message, Usage};
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GroqRequest {
#[serde(flatten)]
pub base: ChatRequestBase,
pub base: ChatCompletionsRequest,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GroqResponse {
#[serde(flatten)]
pub base: ChatResponseBase,
pub base: ChatCompletionsResponse,
}
pub use crate::providers::common_types::{Message, Choice, Usage};

View file

@ -1,12 +1,3 @@
pub mod openai;
pub mod groq;
pub mod deepseek;
pub mod common_types;
/// Supported LLM providers.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Provider {
Groq,
OpenAI,
DeepSeek,
}
pub mod groq;
pub mod openai;

View file

@ -2,7 +2,7 @@ pub mod types;
use thiserror::Error;
use crate::providers::openai::types::{OpenAIRequest, OpenAIResponse};
use crate::providers::openai::types::{ChatCompletionsRequest, ChatCompletionsResponse};
#[derive(Debug, Error)]
pub enum OpenAIError {
@ -12,14 +12,14 @@ pub enum OpenAIError {
type Result<T> = std::result::Result<T, OpenAIError>;
impl TryFrom<&[u8]> for OpenAIRequest {
impl TryFrom<&[u8]> for ChatCompletionsRequest {
type Error = OpenAIError;
fn try_from(bytes: &[u8]) -> Result<Self> {
serde_json::from_slice(bytes).map_err(OpenAIError::from)
}
}
impl TryFrom<&[u8]> for OpenAIResponse {
impl TryFrom<&[u8]> for ChatCompletionsResponse {
type Error = OpenAIError;
fn try_from(bytes: &[u8]) -> Result<Self> {
serde_json::from_slice(bytes).map_err(OpenAIError::from)

View file

@ -1,15 +1,124 @@
use serde::{Deserialize, Serialize};
use crate::providers::common_types::{ChatRequestBase, ChatResponseBase};
use std::fmt::Display;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenAIRequest {
#[serde(flatten)]
pub base: ChatRequestBase,
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum MultiPartContentType {
#[serde(rename = "text")]
Text,
#[serde(rename = "image_url")]
ImageUrl,
}
#[derive(Debug, Default, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MultiPartContent {
pub text: Option<String>,
#[serde(rename = "type")]
pub content_type: MultiPartContentType,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum ContentType {
Text(String),
MultiPart(Vec<MultiPartContent>),
}
impl Display for ContentType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ContentType::Text(text) => write!(f, "{}", text),
ContentType::MultiPart(multi_part) => {
let text_parts: Vec<String> = multi_part
.iter()
.filter_map(|part| {
if part.content_type == MultiPartContentType::Text {
part.text.clone()
} else if part.content_type == MultiPartContentType::ImageUrl {
// skip image URLs or their data in text representation
None
} else {
panic!("Unsupported content type: {:?}", part.content_type);
}
})
.collect();
let combined_text = text_parts.join("\n");
write!(f, "{}", combined_text)
}
}
}
}
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub role: String,
pub content: Option<ContentType>,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatCompletionsRequest {
pub model: String,
pub messages: Vec<Message>,
pub temperature: Option<f32>,
pub top_p: Option<f32>,
pub n: Option<u32>,
pub max_tokens: Option<u32>,
pub stream: Option<bool>,
pub stop: Option<Vec<String>>,
pub presence_penalty: Option<f32>,
pub frequency_penalty: Option<f32>,
}
impl Default for ChatCompletionsRequest {
fn default() -> Self {
ChatCompletionsRequest {
model: String::new(),
messages: Vec::new(),
temperature: None,
top_p: None,
n: None,
max_tokens: None,
stream: None,
stop: None,
presence_penalty: None,
frequency_penalty: None,
}
}
}
#[skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ChatCompletionsResponse {
pub id: String,
pub object: String,
pub created: u64,
pub choices: Vec<Choice>,
pub usage: Option<Usage>,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Choice {
pub index: u32,
pub message: Message,
pub finish_reason: Option<String>,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Usage {
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub total_tokens: u32,
}
#[derive(Debug, Clone)]
pub struct OpenAIRequestBuilder {
model: Option<String>,
messages: Option<Vec<crate::providers::common_types::Message>>,
model: String,
messages: Vec<Message>,
temperature: Option<f32>,
top_p: Option<f32>,
n: Option<u32>,
@ -21,18 +130,19 @@ pub struct OpenAIRequestBuilder {
}
impl OpenAIRequestBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
self
}
pub fn messages(mut self, messages: Vec<crate::providers::common_types::Message>) -> Self {
self.messages = Some(messages);
self
pub fn new(model: impl Into<String>, messages: Vec<Message>) -> Self {
Self {
model: model.into(),
messages,
temperature: None,
top_p: None,
n: None,
max_tokens: None,
stream: None,
stop: None,
presence_penalty: None,
frequency_penalty: None,
}
}
pub fn temperature(mut self, temperature: f32) -> Self {
@ -75,10 +185,9 @@ impl OpenAIRequestBuilder {
self
}
pub fn build(self) -> Result<OpenAIRequest, &'static str> {
let model = self.model.ok_or("model is required")?;
let base = crate::providers::common_types::ChatRequestBase {
model,
pub fn build(self) -> Result<ChatCompletionsRequest, &'static str> {
let request = ChatCompletionsRequest {
model: self.model,
messages: self.messages,
temperature: self.temperature,
top_p: self.top_p,
@ -89,20 +198,83 @@ impl OpenAIRequestBuilder {
presence_penalty: self.presence_penalty,
frequency_penalty: self.frequency_penalty,
};
Ok(OpenAIRequest { base })
Ok(request)
}
}
impl OpenAIRequest {
pub fn builder() -> OpenAIRequestBuilder {
OpenAIRequestBuilder::new()
impl ChatCompletionsRequest {
pub fn builder(model: impl Into<String>, messages: Vec<Message>) -> OpenAIRequestBuilder {
OpenAIRequestBuilder::new(model, messages)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenAIResponse {
#[serde(flatten)]
pub base: ChatResponseBase,
}
#[cfg(test)]
mod tests {
use super::*;
pub use crate::providers::common_types::{Message, Choice, Usage};
#[test]
fn test_content_type_display() {
let text_content = ContentType::Text("Hello, world!".to_string());
assert_eq!(text_content.to_string(), "Hello, world!");
let multi_part_content = ContentType::MultiPart(vec![
MultiPartContent {
text: Some("This is a text part.".to_string()),
content_type: MultiPartContentType::Text,
},
MultiPartContent {
text: Some("https://example.com/image.png".to_string()),
content_type: MultiPartContentType::ImageUrl,
},
]);
assert_eq!(multi_part_content.to_string(), "This is a text part.");
}
#[test]
fn test_chat_completions_request_text_type_array() {
const CHAT_COMPLETIONS_REQUEST: &str = r#"
{
"model": "gpt-3.5-turbo",
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "What city do you want to know the weather for?"
},
{
"type": "text",
"text": "hello world"
}
]
}
]
}
"#;
let chat_completions_request: ChatCompletionsRequest =
serde_json::from_str(CHAT_COMPLETIONS_REQUEST).unwrap();
assert_eq!(chat_completions_request.model, "gpt-3.5-turbo");
if let Some(ContentType::MultiPart(multi_part_content)) =
chat_completions_request.messages[0].content.as_ref()
{
assert_eq!(multi_part_content.len(), 2);
assert_eq!(
multi_part_content[0].content_type,
MultiPartContentType::Text
);
assert_eq!(
multi_part_content[0].text,
Some("What city do you want to know the weather for?".to_string())
);
assert_eq!(
multi_part_content[1].content_type,
MultiPartContentType::Text
);
assert_eq!(multi_part_content[1].text, Some("hello world".to_string()));
} else {
panic!("Expected MultiPartContent");
}
}
}