feat: add full OpenAI chat completion serde types

This commit is contained in:
2026-06-05 23:37:09 +07:00
parent 20a41f404d
commit 776cdbab69
+488
View File
@@ -0,0 +1,488 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentPart {
Text {
text: String,
},
ImageUrl {
image_url: ImageUrl,
},
InputAudio {
input_audio: InputAudio,
},
File {
file: FileContent,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ImageUrl {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct InputAudio {
pub data: String,
pub format: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FileContent {
#[serde(skip_serializing_if = "Option::is_none")]
pub file_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub filename: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_data: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum MessageContent {
Text(String),
Parts(Vec<ContentPart>),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Function {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub parameters: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub strict: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Tool {
#[serde(rename = "type")]
pub tool_type: String,
pub function: Function,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FunctionCall {
pub name: String,
pub arguments: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ToolCall {
pub id: String,
#[serde(rename = "type")]
pub call_type: String,
pub function: FunctionCall,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "role", rename_all = "lowercase")]
pub enum Message {
System {
content: MessageContent,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
},
User {
content: MessageContent,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
},
Assistant {
#[serde(skip_serializing_if = "Option::is_none")]
content: Option<MessageContent>,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
tool_calls: Option<Vec<ToolCall>>,
#[serde(skip_serializing_if = "Option::is_none")]
function_call: Option<FunctionCall>,
#[serde(skip_serializing_if = "Option::is_none")]
refusal: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
audio: Option<AssistantAudio>,
},
Tool {
content: MessageContent,
tool_call_id: String,
},
Function {
content: MessageContent,
name: String,
},
Developer {
content: MessageContent,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AssistantAudio {
pub id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum ToolChoice {
String(String),
Object(ToolChoiceObject),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ToolChoiceObject {
#[serde(rename = "type")]
pub choice_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub function: Option<ToolChoiceFunction>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ToolChoiceFunction {
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ResponseFormat {
#[serde(rename = "type")]
pub format_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub json_schema: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct StreamOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub include_usage: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AudioOutput {
pub voice: String,
pub format: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Prediction {
#[serde(rename = "type")]
pub prediction_type: String,
pub content: MessageContent,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatCompletionRequest {
pub model: String,
pub messages: Vec<Message>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream_options: Option<StreamOptions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub n: Option<u32>,
#[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 stop: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub presence_penalty: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub frequency_penalty: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logit_bias: Option<HashMap<String, f64>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub seed: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logprobs: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_logprobs: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub response_format: Option<ResponseFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<Tool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<ToolChoice>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parallel_tool_calls: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub functions: Option<Vec<Function>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub function_call: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_tier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub modalities: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub audio: Option<AudioOutput>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_effort: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<HashMap<String, Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub store: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prediction: Option<Prediction>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Usage {
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub total_tokens: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub completion_tokens_details: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt_tokens_details: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogprobContent {
pub token: String,
pub logprob: f64,
pub bytes: Option<Vec<u8>>,
pub top_logprobs: Vec<TopLogprob>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TopLogprob {
pub token: String,
pub logprob: f64,
pub bytes: Option<Vec<u8>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Logprobs {
pub content: Option<Vec<LogprobContent>>,
pub refusal: Option<Vec<LogprobContent>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponseMessage {
pub role: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCall>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub function_call: Option<FunctionCall>,
#[serde(skip_serializing_if = "Option::is_none")]
pub refusal: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub audio: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Choice {
pub index: u32,
pub message: ResponseMessage,
pub finish_reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logprobs: Option<Logprobs>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatCompletionResponse {
pub id: String,
pub object: String,
pub created: i64,
pub model: String,
pub choices: Vec<Choice>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<Usage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub system_fingerprint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_tier: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChunkDelta {
#[serde(skip_serializing_if = "Option::is_none")]
pub role: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub function_call: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub refusal: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreamChoice {
pub index: u32,
pub delta: ChunkDelta,
pub finish_reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logprobs: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatCompletionChunk {
pub id: String,
pub object: String,
pub created: i64,
pub model: String,
pub choices: Vec<StreamChoice>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<Usage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub system_fingerprint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_tier: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_deserialize_simple_user_message() {
let json = r#"{"role":"user","content":"Hello!"}"#;
let msg: Message = serde_json::from_str(json).unwrap();
assert!(matches!(msg, Message::User { .. }));
}
#[test]
fn test_deserialize_system_message() {
let json = r#"{"role":"system","content":"You are helpful."}"#;
let msg: Message = serde_json::from_str(json).unwrap();
assert!(matches!(msg, Message::System { .. }));
}
#[test]
fn test_deserialize_assistant_with_tool_calls() {
let json = r#"{
"role": "assistant",
"content": null,
"tool_calls": [{
"id": "call_abc",
"type": "function",
"function": {"name": "get_weather", "arguments": "{\"city\":\"NYC\"}"}
}]
}"#;
let msg: Message = serde_json::from_str(json).unwrap();
match msg {
Message::Assistant { tool_calls: Some(tc), .. } => {
assert_eq!(tc[0].id, "call_abc");
assert_eq!(tc[0].function.name, "get_weather");
}
_ => panic!("expected assistant with tool_calls"),
}
}
#[test]
fn test_deserialize_tool_message() {
let json = r#"{"role":"tool","content":"72 degrees","tool_call_id":"call_abc"}"#;
let msg: Message = serde_json::from_str(json).unwrap();
assert!(matches!(msg, Message::Tool { .. }));
}
#[test]
fn test_deserialize_content_parts() {
let json = r#"{"role":"user","content":[{"type":"text","text":"hello"},{"type":"image_url","image_url":{"url":"https://example.com/img.png"}}]}"#;
let msg: Message = serde_json::from_str(json).unwrap();
match msg {
Message::User { content: MessageContent::Parts(parts), .. } => {
assert_eq!(parts.len(), 2);
assert!(matches!(&parts[0], ContentPart::Text { text } if text == "hello"));
}
_ => panic!("expected user with parts"),
}
}
#[test]
fn test_deserialize_full_request() {
let json = r#"{
"model": "gpt-4o",
"messages": [
{"role": "system", "content": "You are helpful."},
{"role": "user", "content": "What is 2+2?"}
],
"temperature": 0.7,
"max_tokens": 100,
"stream": false
}"#;
let req: ChatCompletionRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.model, "gpt-4o");
assert_eq!(req.messages.len(), 2);
assert_eq!(req.temperature, Some(0.7));
assert_eq!(req.max_tokens, Some(100));
assert_eq!(req.stream, Some(false));
}
#[test]
fn test_deserialize_streaming_chunk() {
let json = r#"{
"id": "chatcmpl-xyz",
"object": "chat.completion.chunk",
"created": 1699000000,
"model": "gpt-4o",
"choices": [{
"index": 0,
"delta": {"role": "assistant", "content": "Hello"},
"finish_reason": null
}]
}"#;
let chunk: ChatCompletionChunk = serde_json::from_str(json).unwrap();
assert_eq!(chunk.id, "chatcmpl-xyz");
assert_eq!(chunk.choices[0].delta.content.as_deref(), Some("Hello"));
}
#[test]
fn test_roundtrip_request_serialization() {
let req = ChatCompletionRequest {
model: "gpt-4o".to_string(),
messages: vec![Message::User {
content: MessageContent::Text("hi".to_string()),
name: None,
}],
stream: Some(true),
temperature: Some(1.0),
tools: None,
tool_choice: None,
functions: None,
function_call: None,
top_p: None,
n: None,
max_tokens: None,
max_completion_tokens: None,
stop: None,
presence_penalty: None,
frequency_penalty: None,
logit_bias: None,
user: None,
seed: None,
logprobs: None,
top_logprobs: None,
response_format: None,
stream_options: None,
parallel_tool_calls: None,
service_tier: None,
modalities: None,
audio: None,
reasoning_effort: None,
metadata: None,
store: None,
prediction: None,
};
let json = serde_json::to_string(&req).unwrap();
let back: ChatCompletionRequest = serde_json::from_str(&json).unwrap();
assert_eq!(back.model, "gpt-4o");
assert_eq!(back.stream, Some(true));
}
}