use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum MessageRole {
User,
Assistant,
System,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChippMessage {
pub role: MessageRole,
pub content: String,
}
impl ChippMessage {
#[must_use]
pub fn user(content: impl Into<String>) -> Self {
Self {
role: MessageRole::User,
content: content.into(),
}
}
#[must_use]
pub fn assistant(content: impl Into<String>) -> Self {
Self {
role: MessageRole::Assistant,
content: content.into(),
}
}
#[must_use]
pub fn system(content: impl Into<String>) -> Self {
Self {
role: MessageRole::System,
content: content.into(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ChippSession {
pub chat_session_id: Option<String>,
}
impl ChippSession {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_id(chat_session_id: impl Into<String>) -> Self {
Self {
chat_session_id: Some(chat_session_id.into()),
}
}
pub fn reset(&mut self) {
self.chat_session_id = None;
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct Usage {
#[serde(default, deserialize_with = "deserialize_null_as_zero")]
pub prompt_tokens: u32,
#[serde(default, deserialize_with = "deserialize_null_as_zero")]
pub completion_tokens: u32,
#[serde(default, deserialize_with = "deserialize_null_as_zero")]
pub total_tokens: u32,
}
fn deserialize_null_as_zero<'de, D>(deserializer: D) -> std::result::Result<u32, D::Error>
where
D: serde::Deserializer<'de>,
{
let opt: Option<u32> = Option::deserialize(deserializer)?;
Ok(opt.unwrap_or(0))
}
#[derive(Debug, Clone)]
pub struct ChatResponse {
content: String,
session_id: String,
usage: Usage,
completion_id: String,
created_at: i64,
finish_reason: String,
model: String,
}
impl ChatResponse {
#[must_use]
pub fn content(&self) -> &str {
&self.content
}
#[must_use]
pub fn session_id(&self) -> &str {
&self.session_id
}
#[must_use]
pub fn usage(&self) -> &Usage {
&self.usage
}
#[must_use]
pub fn completion_id(&self) -> &str {
&self.completion_id
}
#[must_use]
pub fn created_at(&self) -> i64 {
self.created_at
}
#[must_use]
pub fn finish_reason(&self) -> &str {
&self.finish_reason
}
#[must_use]
pub fn model(&self) -> &str {
&self.model
}
}
#[derive(Debug, Serialize)]
pub(crate) struct ChatCompletionRequest {
pub model: String,
pub messages: Vec<ChippMessage>,
pub stream: bool,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "chatSessionId")]
pub chat_session_id: Option<String>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct ChatCompletionResponse {
#[serde(rename = "chatSessionId")]
pub chat_session_id: String,
pub id: String,
#[allow(dead_code)]
pub object: String,
pub created: i64,
pub model: String,
pub choices: Vec<Choice>,
pub usage: Usage,
}
#[derive(Debug, Deserialize)]
pub(crate) struct Choice {
#[allow(dead_code)]
pub index: u32,
pub message: ResponseMessage,
pub finish_reason: String,
}
#[derive(Debug, Deserialize)]
pub(crate) struct ResponseMessage {
#[allow(dead_code)]
pub role: String,
pub content: String,
}
impl From<ChatCompletionResponse> for ChatResponse {
fn from(response: ChatCompletionResponse) -> Self {
let choice = response
.choices
.into_iter()
.next()
.expect("API response must have at least one choice");
Self {
content: choice.message.content,
session_id: response.chat_session_id,
usage: response.usage,
completion_id: response.id,
created_at: response.created,
finish_reason: choice.finish_reason,
model: response.model,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_usage_deserialization() {
let json = r#"{
"prompt_tokens": 100,
"completion_tokens": 50,
"total_tokens": 150
}"#;
let usage: Usage = serde_json::from_str(json).expect("Usage should deserialize");
assert_eq!(usage.prompt_tokens, 100);
assert_eq!(usage.completion_tokens, 50);
assert_eq!(usage.total_tokens, 150);
}
#[test]
fn test_usage_deserialization_with_null_values() {
let json = r#"{
"prompt_tokens": 9240,
"completion_tokens": null,
"total_tokens": 9240
}"#;
let usage: Usage = serde_json::from_str(json).expect("Usage should deserialize with null");
assert_eq!(usage.prompt_tokens, 9240);
assert_eq!(usage.completion_tokens, 0); assert_eq!(usage.total_tokens, 9240);
}
#[test]
fn test_usage_equality() {
let usage1 = Usage {
prompt_tokens: 100,
completion_tokens: 50,
total_tokens: 150,
};
let usage2 = Usage {
prompt_tokens: 100,
completion_tokens: 50,
total_tokens: 150,
};
let usage3 = Usage {
prompt_tokens: 200,
completion_tokens: 50,
total_tokens: 250,
};
assert_eq!(usage1, usage2);
assert_ne!(usage1, usage3);
}
#[test]
fn test_usage_clone() {
let usage = Usage {
prompt_tokens: 100,
completion_tokens: 50,
total_tokens: 150,
};
let cloned = usage.clone();
assert_eq!(usage, cloned);
}
#[test]
fn test_usage_debug() {
let usage = Usage {
prompt_tokens: 100,
completion_tokens: 50,
total_tokens: 150,
};
let debug_str = format!("{:?}", usage);
assert!(debug_str.contains("prompt_tokens"));
assert!(debug_str.contains("100"));
}
#[test]
fn test_chat_response_accessors() {
let response = ChatResponse {
content: "Hello!".to_string(),
session_id: "session-123".to_string(),
usage: Usage {
prompt_tokens: 10,
completion_tokens: 5,
total_tokens: 15,
},
completion_id: "chatcmpl-456".to_string(),
created_at: 1234567890,
finish_reason: "stop".to_string(),
model: "myapp-123".to_string(),
};
assert_eq!(response.content(), "Hello!");
assert_eq!(response.session_id(), "session-123");
assert_eq!(response.usage().total_tokens, 15);
assert_eq!(response.completion_id(), "chatcmpl-456");
assert_eq!(response.created_at(), 1234567890);
assert_eq!(response.finish_reason(), "stop");
assert_eq!(response.model(), "myapp-123");
}
#[test]
fn test_chat_response_clone() {
let response = ChatResponse {
content: "Hello!".to_string(),
session_id: "session-123".to_string(),
usage: Usage {
prompt_tokens: 10,
completion_tokens: 5,
total_tokens: 15,
},
completion_id: "chatcmpl-456".to_string(),
created_at: 1234567890,
finish_reason: "stop".to_string(),
model: "myapp-123".to_string(),
};
let cloned = response.clone();
assert_eq!(response.content(), cloned.content());
assert_eq!(response.usage().total_tokens, cloned.usage().total_tokens);
}
#[test]
fn test_chat_response_from_internal() {
let internal = ChatCompletionResponse {
chat_session_id: "session-123".to_string(),
id: "chatcmpl-456".to_string(),
object: "chat.completion".to_string(),
created: 1234567890,
model: "myapp-123".to_string(),
choices: vec![Choice {
index: 0,
message: ResponseMessage {
role: "assistant".to_string(),
content: "Hello!".to_string(),
},
finish_reason: "stop".to_string(),
}],
usage: Usage {
prompt_tokens: 10,
completion_tokens: 5,
total_tokens: 15,
},
};
let response: ChatResponse = internal.into();
assert_eq!(response.content(), "Hello!");
assert_eq!(response.session_id(), "session-123");
assert_eq!(response.completion_id(), "chatcmpl-456");
assert_eq!(response.created_at(), 1234567890);
assert_eq!(response.model(), "myapp-123");
assert_eq!(response.finish_reason(), "stop");
assert_eq!(response.usage().prompt_tokens, 10);
assert_eq!(response.usage().completion_tokens, 5);
assert_eq!(response.usage().total_tokens, 15);
}
}