use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Role {
System,
Developer,
User,
Assistant,
Tool,
Function,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChatMessage {
pub role: Role,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(default, alias = "reasoning", skip_serializing_if = "Option::is_none")]
pub reasoning_content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub refusal: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCall>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
}
impl ChatMessage {
pub fn system(content: impl Into<String>) -> Self {
Self {
role: Role::System,
content: Some(content.into()),
reasoning_content: None,
refusal: None,
name: None,
tool_calls: None,
tool_call_id: None,
}
}
pub fn developer(content: impl Into<String>) -> Self {
Self {
role: Role::Developer,
content: Some(content.into()),
reasoning_content: None,
refusal: None,
name: None,
tool_calls: None,
tool_call_id: None,
}
}
pub fn user(content: impl Into<String>) -> Self {
Self {
role: Role::User,
content: Some(content.into()),
reasoning_content: None,
refusal: None,
name: None,
tool_calls: None,
tool_call_id: None,
}
}
pub fn assistant(content: impl Into<String>) -> Self {
Self {
role: Role::Assistant,
content: Some(content.into()),
reasoning_content: None,
refusal: None,
name: None,
tool_calls: None,
tool_call_id: None,
}
}
pub fn assistant_with_reasoning(
content: impl Into<String>,
reasoning: impl Into<String>,
) -> Self {
Self {
role: Role::Assistant,
content: Some(content.into()),
reasoning_content: Some(reasoning.into()),
refusal: None,
name: None,
tool_calls: None,
tool_call_id: None,
}
}
pub fn assistant_tool_calls(tool_calls: Vec<ToolCall>) -> Self {
Self {
role: Role::Assistant,
content: None,
reasoning_content: None,
refusal: None,
name: None,
tool_calls: Some(tool_calls),
tool_call_id: None,
}
}
pub fn tool(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
Self {
role: Role::Tool,
content: Some(content.into()),
reasoning_content: None,
refusal: None,
name: None,
tool_calls: None,
tool_call_id: Some(tool_call_id.into()),
}
}
pub fn function(name: impl Into<String>, content: impl Into<String>) -> Self {
Self {
role: Role::Function,
content: Some(content.into()),
reasoning_content: None,
refusal: None,
name: Some(name.into()),
tool_calls: None,
tool_call_id: None,
}
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn refusal(mut self, refusal: impl Into<String>) -> Self {
self.refusal = Some(refusal.into());
self
}
pub fn content(mut self, content: impl Into<String>) -> Self {
self.content = Some(content.into());
self
}
pub fn reasoning_content(mut self, reasoning: impl Into<String>) -> Self {
self.reasoning_content = Some(reasoning.into());
self
}
pub fn tool_calls(mut self, tool_calls: Vec<ToolCall>) -> Self {
self.tool_calls = if tool_calls.is_empty() {
None
} else {
Some(tool_calls)
};
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
#[serde(rename = "type")]
pub call_type: String,
pub function: FunctionCall,
}
impl ToolCall {
pub fn function(
id: impl Into<String>,
name: impl Into<String>,
arguments: impl Into<String>,
) -> Self {
Self {
id: id.into(),
call_type: "function".to_string(),
function: FunctionCall::new(name, arguments),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FunctionCall {
pub name: String,
pub arguments: String,
}
impl FunctionCall {
pub fn new(name: impl Into<String>, arguments: impl Into<String>) -> Self {
Self {
name: name.into(),
arguments: arguments.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolDefinition {
#[serde(rename = "type")]
pub tool_type: String,
pub function: FunctionDefinition,
}
impl ToolDefinition {
pub fn function(
name: impl Into<String>,
description: Option<String>,
parameters: serde_json::Value,
) -> Self {
Self {
tool_type: "function".to_string(),
function: FunctionDefinition {
name: name.into(),
description,
parameters,
strict: None,
},
}
}
pub fn strict_function(
name: impl Into<String>,
description: Option<String>,
parameters: serde_json::Value,
) -> Self {
Self {
tool_type: "function".to_string(),
function: FunctionDefinition {
name: name.into(),
description,
parameters,
strict: Some(true),
},
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FunctionDefinition {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub parameters: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub strict: Option<bool>,
}
impl FunctionDefinition {
pub fn new(name: impl Into<String>, parameters: serde_json::Value) -> Self {
Self {
name: name.into(),
description: None,
parameters,
strict: None,
}
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn strict(mut self, strict: bool) -> Self {
self.strict = Some(strict);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatCompletionRequest {
pub model: String,
pub messages: Vec<ChatMessage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_completion_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream_options: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<ToolDefinition>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parallel_tool_calls: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub response_format: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub presence_penalty: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub frequency_penalty: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub seed: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user: Option<String>,
}
impl ChatCompletionRequest {
pub fn new(model: impl Into<String>, messages: Vec<ChatMessage>) -> Self {
Self {
model: model.into(),
messages,
temperature: None,
top_p: None,
max_tokens: None,
max_completion_tokens: None,
stream_options: None,
stream: None,
tools: None,
parallel_tool_calls: None,
tool_choice: None,
response_format: None,
stop: None,
presence_penalty: None,
frequency_penalty: None,
seed: None,
user: None,
}
}
pub fn temperature(mut self, temperature: f32) -> Self {
self.temperature = Some(temperature);
self
}
pub fn top_p(mut self, top_p: f32) -> Self {
self.top_p = Some(top_p);
self
}
pub fn max_tokens(mut self, max_tokens: u32) -> Self {
self.max_tokens = Some(max_tokens);
self
}
pub fn max_completion_tokens(mut self, max_completion_tokens: u32) -> Self {
self.max_completion_tokens = Some(max_completion_tokens);
self
}
pub fn include_usage(mut self) -> Self {
self.stream_options = Some(serde_json::json!({ "include_usage": true }));
self
}
pub fn stream(mut self, stream: bool) -> Self {
self.stream = Some(stream);
self
}
pub fn tools(mut self, tools: Vec<ToolDefinition>) -> Self {
self.tools = if tools.is_empty() { None } else { Some(tools) };
self
}
pub fn parallel_tool_calls(mut self, parallel: bool) -> Self {
self.parallel_tool_calls = Some(parallel);
self
}
pub fn stop(mut self, stop: Vec<String>) -> Self {
self.stop = Some(stop);
self
}
pub fn stop_sequence(mut self, stop: impl Into<String>) -> Self {
let mut seqs = self.stop.unwrap_or_default();
seqs.push(stop.into());
self.stop = Some(seqs);
self
}
pub fn json_mode(mut self) -> Self {
self.response_format = Some(serde_json::json!({ "type": "json_object" }));
self
}
pub fn presence_penalty(mut self, presence_penalty: f32) -> Self {
self.presence_penalty = Some(presence_penalty);
self
}
pub fn frequency_penalty(mut self, frequency_penalty: f32) -> Self {
self.frequency_penalty = Some(frequency_penalty);
self
}
pub fn seed(mut self, seed: i64) -> Self {
self.seed = Some(seed);
self
}
pub fn user(mut self, user: impl Into<String>) -> Self {
self.user = Some(user.into());
self
}
pub fn tool_choice(mut self, tool_choice: serde_json::Value) -> Self {
self.tool_choice = Some(tool_choice);
self
}
pub fn response_format(mut self, response_format: serde_json::Value) -> Self {
self.response_format = Some(response_format);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Usage {
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub total_tokens: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChatChoice {
pub index: u32,
pub message: ChatMessage,
#[serde(default)]
pub finish_reason: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChatCompletionResponse {
pub id: String,
#[serde(default)]
pub object: Option<String>,
pub created: u64,
pub model: String,
pub choices: Vec<ChatChoice>,
#[serde(default)]
pub usage: Option<Usage>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChatChunkDelta {
#[serde(default)]
pub role: Option<Role>,
#[serde(default)]
pub content: Option<String>,
#[serde(default, alias = "reasoning")]
pub reasoning_content: Option<String>,
#[serde(default)]
pub refusal: Option<String>,
#[serde(default)]
pub tool_calls: Option<Vec<ChunkToolCall>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChunkToolCall {
pub index: u32,
#[serde(default)]
pub id: Option<String>,
#[serde(rename = "type", default)]
pub call_type: Option<String>,
#[serde(default)]
pub function: Option<ChunkFunctionCall>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChunkFunctionCall {
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub arguments: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChatChunkChoice {
pub index: u32,
#[serde(default)]
pub delta: ChatChunkDelta,
#[serde(default)]
pub finish_reason: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChatCompletionChunk {
pub id: String,
#[serde(default)]
pub object: Option<String>,
#[serde(default)]
pub created: u64,
#[serde(default)]
pub model: String,
#[serde(default)]
pub choices: Vec<ChatChunkChoice>,
#[serde(default)]
pub usage: Option<Usage>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EmbeddingInput {
Single(String),
Multiple(Vec<String>),
}
impl From<&str> for EmbeddingInput {
fn from(s: &str) -> Self {
Self::Single(s.to_string())
}
}
impl From<String> for EmbeddingInput {
fn from(s: String) -> Self {
Self::Single(s)
}
}
impl From<Vec<String>> for EmbeddingInput {
fn from(v: Vec<String>) -> Self {
Self::Multiple(v)
}
}
impl From<Vec<&str>> for EmbeddingInput {
fn from(v: Vec<&str>) -> Self {
Self::Multiple(v.into_iter().map(String::from).collect())
}
}
impl From<&[&str]> for EmbeddingInput {
fn from(s: &[&str]) -> Self {
Self::Multiple(s.iter().copied().map(String::from).collect())
}
}
impl Serialize for EmbeddingInput {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Self::Single(text) => serializer.serialize_str(text),
Self::Multiple(texts) => texts.serialize(serializer),
}
}
}
impl<'de> Deserialize<'de> for EmbeddingInput {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Helper {
Single(String),
Multiple(Vec<String>),
}
match Helper::deserialize(deserializer)? {
Helper::Single(s) => Ok(Self::Single(s)),
Helper::Multiple(v) => Ok(Self::Multiple(v)),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmbeddingRequest {
pub model: String,
pub input: EmbeddingInput,
#[serde(skip_serializing_if = "Option::is_none")]
pub dimensions: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user: Option<String>,
}
impl EmbeddingRequest {
pub fn new(model: impl Into<String>, input: impl Into<EmbeddingInput>) -> Self {
Self {
model: model.into(),
input: input.into(),
dimensions: None,
user: None,
}
}
pub fn dimensions(mut self, dims: u32) -> Self {
self.dimensions = Some(dims);
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EmbeddingData {
pub index: u32,
pub object: String,
pub embedding: Vec<f32>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EmbeddingResponse {
pub object: String,
pub data: Vec<EmbeddingData>,
pub model: String,
#[serde(default)]
pub usage: Option<Usage>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ModelInfo {
pub id: String,
#[serde(default)]
pub object: Option<String>,
#[serde(default)]
pub created: Option<u64>,
#[serde(default)]
pub owned_by: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ListModelsResponse {
#[serde(default)]
pub object: Option<String>,
pub data: Vec<ModelInfo>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chat_message_constructors_and_serialization() {
let msg = ChatMessage::system("System prompt");
assert_eq!(msg.role, Role::System);
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains("\"role\":\"system\""));
assert!(json.contains("\"content\":\"System prompt\""));
let user_msg = ChatMessage::user("Hello");
assert_eq!(user_msg.role, Role::User);
let tool_msg = ChatMessage::tool("call_123", "{\"result\": 42}");
assert_eq!(tool_msg.role, Role::Tool);
assert_eq!(tool_msg.tool_call_id.as_deref(), Some("call_123"));
let assistant_refusal = ChatMessage::assistant("Cannot comply")
.name("safety_agent")
.refusal("I cannot assist with that request.");
assert_eq!(assistant_refusal.name.as_deref(), Some("safety_agent"));
assert_eq!(
assistant_refusal.refusal.as_deref(),
Some("I cannot assist with that request.")
);
let refusal_json = serde_json::to_string(&assistant_refusal).unwrap();
assert!(refusal_json.contains("\"name\":\"safety_agent\""));
assert!(refusal_json.contains("\"refusal\":\"I cannot assist with that request.\""));
let chained_assistant = ChatMessage::assistant("Here is the tool invocation:")
.reasoning_content("Let me check the weather.")
.tool_calls(vec![ToolCall::function("call_1", "get_weather", "{}")]);
assert_eq!(
chained_assistant.reasoning_content.as_deref(),
Some("Let me check the weather.")
);
assert_eq!(chained_assistant.tool_calls.as_ref().unwrap().len(), 1);
}
#[test]
fn test_chat_completion_request_builder() {
let tool = ToolDefinition::function(
"get_weather",
Some("Fetch weather for location".to_string()),
serde_json::json!({
"type": "object",
"properties": {
"location": {"type": "string"}
},
"required": ["location"]
}),
);
let req = ChatCompletionRequest::new(
"gpt-4o-mini",
vec![ChatMessage::user("What is the weather?")],
)
.temperature(0.5)
.top_p(0.9)
.max_tokens(100)
.tools(vec![tool])
.json_mode()
.stop(vec!["\n".to_string()]);
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["model"], "gpt-4o-mini");
assert_eq!(json["temperature"], 0.5);
assert_eq!(json["max_tokens"], 100);
assert_eq!(json["tools"][0]["function"]["name"], "get_weather");
assert_eq!(json["response_format"]["type"], "json_object");
assert_eq!(json["stop"][0], "\n");
let empty_tools_req =
ChatCompletionRequest::new("gpt-4o-mini", vec![ChatMessage::user("Hi")])
.tools(Vec::new());
let empty_json = serde_json::to_value(&empty_tools_req).unwrap();
assert!(empty_json.get("tools").is_none());
}
#[test]
fn test_chat_completion_response_deserialization() {
let raw = r#"{
"id": "chatcmpl-123",
"object": "chat.completion",
"created": 1677652288,
"model": "gpt-4o-mini",
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": "Hello there!"
},
"finish_reason": "stop"
}],
"usage": {
"prompt_tokens": 9,
"completion_tokens": 12,
"total_tokens": 21
}
}"#;
let res: ChatCompletionResponse = serde_json::from_str(raw).unwrap();
assert_eq!(res.id, "chatcmpl-123");
assert_eq!(res.choices.len(), 1);
assert_eq!(res.choices[0].finish_reason.as_deref(), Some("stop"));
assert_eq!(
res.choices[0].message.content.as_deref(),
Some("Hello there!")
);
assert_eq!(res.usage.unwrap().total_tokens, 21);
}
#[test]
fn test_chat_completion_chunk_deserialization() {
let raw = r#"{
"id": "chatcmpl-chunk-1",
"object": "chat.completion.chunk",
"created": 1677652288,
"model": "gpt-4o",
"choices": [{
"index": 0,
"delta": {
"role": "assistant",
"content": "part"
},
"finish_reason": null
}]
}"#;
let chunk: ChatCompletionChunk = serde_json::from_str(raw).unwrap();
assert_eq!(chunk.id, "chatcmpl-chunk-1");
assert_eq!(chunk.choices[0].delta.content.as_deref(), Some("part"));
assert_eq!(chunk.choices[0].delta.role, Some(Role::Assistant));
}
#[test]
fn test_embedding_request_and_response() {
let req_single =
EmbeddingRequest::new("text-embedding-3-small", "test text").dimensions(512);
let val_single = serde_json::to_value(&req_single).unwrap();
assert_eq!(val_single["input"], "test text");
assert_eq!(val_single["dimensions"], 512);
let req_multi = EmbeddingRequest::new(
"text-embedding-3-small",
vec!["item1".to_string(), "item2".to_string()],
);
let val_multi = serde_json::to_value(&req_multi).unwrap();
assert_eq!(val_multi["input"][0], "item1");
assert_eq!(val_multi["input"][1], "item2");
let req_slice_vec =
EmbeddingRequest::new("text-embedding-3-small", vec!["slice1", "slice2"]);
let val_slice_vec = serde_json::to_value(&req_slice_vec).unwrap();
assert_eq!(val_slice_vec["input"][0], "slice1");
assert_eq!(val_slice_vec["input"][1], "slice2");
let items: &[&str] = &["item_a", "item_b"];
let req_slice = EmbeddingRequest::new("text-embedding-3-small", items);
let val_slice = serde_json::to_value(&req_slice).unwrap();
assert_eq!(val_slice["input"][0], "item_a");
assert_eq!(val_slice["input"][1], "item_b");
let raw_res = r#"{
"object": "list",
"data": [
{
"object": "embedding",
"index": 0,
"embedding": [0.1, -0.2, 0.3]
}
],
"model": "text-embedding-3-small",
"usage": {
"prompt_tokens": 5,
"total_tokens": 5,
"completion_tokens": 0
}
}"#;
let res: EmbeddingResponse = serde_json::from_str(raw_res).unwrap();
assert_eq!(res.data.len(), 1);
assert_eq!(res.data[0].embedding, vec![0.1, -0.2, 0.3]);
}
#[test]
fn test_models_list_deserialization() {
let raw = r#"{
"object": "list",
"data": [
{
"id": "gpt-4o",
"object": "model",
"created": 1700000000,
"owned_by": "openai"
}
]
}"#;
let res: ListModelsResponse = serde_json::from_str(raw).unwrap();
assert_eq!(res.data.len(), 1);
assert_eq!(res.data[0].id, "gpt-4o");
}
#[test]
fn test_function_definition_and_role_serialization() {
let msg = ChatMessage::function("calc", "42");
let val = serde_json::to_value(&msg).unwrap();
assert_eq!(val["role"], "function");
assert_eq!(val["name"], "calc");
assert_eq!(val["content"], "42");
let tool_def = ToolDefinition::strict_function(
"get_weather",
Some("Fetch current weather".to_string()),
serde_json::json!({
"type": "object",
"properties": { "location": { "type": "string" } },
"required": ["location"],
"additionalProperties": false
}),
);
let val_tool = serde_json::to_value(&tool_def).unwrap();
assert_eq!(val_tool["type"], "function");
assert_eq!(val_tool["function"]["name"], "get_weather");
assert_eq!(val_tool["function"]["strict"], true);
}
#[test]
fn test_reasoning_content_and_request_builder_methods() {
let msg = ChatMessage::assistant_with_reasoning("The answer is 42", "Let me compute 6 * 7");
let val = serde_json::to_value(&msg).unwrap();
assert_eq!(val["role"], "assistant");
assert_eq!(val["content"], "The answer is 42");
assert_eq!(val["reasoning_content"], "Let me compute 6 * 7");
let raw_chunk = r#"{
"id": "chunk-r1",
"created": 12345,
"model": "deepseek-r1",
"choices": [{
"index": 0,
"delta": {
"content": null,
"reasoning": "step 1"
}
}]
}"#;
let chunk: ChatCompletionChunk = serde_json::from_str(raw_chunk).unwrap();
assert_eq!(
chunk.choices[0].delta.reasoning_content.as_deref(),
Some("step 1")
);
let raw_non_streaming_reasoning = r#"{
"role": "assistant",
"content": "Result",
"reasoning": "thought process"
}"#;
let non_streaming_msg: ChatMessage =
serde_json::from_str(raw_non_streaming_reasoning).unwrap();
assert_eq!(
non_streaming_msg.reasoning_content.as_deref(),
Some("thought process")
);
let raw_chunk_omitted_delta = r#"{
"id": "chunk-term",
"choices": [{
"index": 0,
"finish_reason": "stop"
}]
}"#;
let chunk_term: ChatCompletionChunk =
serde_json::from_str(raw_chunk_omitted_delta).unwrap();
assert_eq!(chunk_term.choices[0].finish_reason.as_deref(), Some("stop"));
assert_eq!(chunk_term.choices[0].delta.content, None);
assert_eq!(chunk_term.model, "");
let raw_chunk_usage_only = r#"{
"id": "chunk-usage",
"usage": {
"prompt_tokens": 5,
"completion_tokens": 10,
"total_tokens": 15
}
}"#;
let chunk_usage: ChatCompletionChunk = serde_json::from_str(raw_chunk_usage_only).unwrap();
assert!(chunk_usage.choices.is_empty());
assert_eq!(chunk_usage.usage.unwrap().total_tokens, 15);
let tool_call = ToolCall::function("call_1", "get_stock", r#"{"symbol":"AAPL"}"#);
assert_eq!(tool_call.id, "call_1");
assert_eq!(tool_call.call_type, "function");
assert_eq!(tool_call.function.name, "get_stock");
let req = ChatCompletionRequest::new("gpt-4o", vec![ChatMessage::user("Hello")])
.presence_penalty(0.5)
.frequency_penalty(-0.2)
.seed(42)
.user("user_123")
.parallel_tool_calls(true)
.stop_sequence("END")
.tool_choice(serde_json::json!("auto"))
.response_format(serde_json::json!({ "type": "text" }));
let val_req = serde_json::to_value(&req).unwrap();
assert_eq!(val_req["presence_penalty"], 0.5);
assert!((val_req["frequency_penalty"].as_f64().unwrap() - -0.2).abs() < 1e-6);
assert_eq!(val_req["seed"], 42);
assert_eq!(val_req["user"], "user_123");
assert_eq!(val_req["parallel_tool_calls"], true);
assert_eq!(val_req["stop"][0], "END");
assert_eq!(val_req["tool_choice"], "auto");
assert_eq!(val_req["response_format"]["type"], "text");
}
}