use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Role {
System,
User,
Assistant,
Tool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Tool {
#[serde(rename = "type")]
pub tool_type: String,
pub function: FunctionDef,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FunctionDef {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub parameters: serde_json::Value,
}
impl Tool {
pub fn function(
name: impl Into<String>,
description: Option<String>,
parameters: serde_json::Value,
) -> Self {
Self {
tool_type: "function".to_string(),
function: FunctionDef {
name: name.into(),
description,
parameters,
},
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum ToolChoice {
Mode(String),
Function {
#[serde(rename = "type")]
choice_type: String,
function: ToolChoiceFunction,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ToolChoiceFunction {
pub name: String,
}
impl ToolChoice {
pub fn auto() -> Self {
Self::Mode("auto".to_string())
}
pub fn none() -> Self {
Self::Mode("none".to_string())
}
pub fn required() -> Self {
Self::Mode("required".to_string())
}
pub fn function(name: impl Into<String>) -> Self {
Self::Function {
choice_type: "function".to_string(),
function: ToolChoiceFunction { name: name.into() },
}
}
}
#[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)]
pub struct FunctionCall {
pub name: String,
pub arguments: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentPart {
Text { text: String },
ImageUrl { image_url: ImageUrl },
}
impl ContentPart {
pub fn text(text: impl Into<String>) -> Self {
ContentPart::Text { text: text.into() }
}
pub fn image_url(url: impl Into<String>) -> Self {
ContentPart::ImageUrl {
image_url: ImageUrl {
url: url.into(),
detail: None,
},
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ImageUrl {
pub url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum Content {
Text(String),
Parts(Vec<ContentPart>),
}
impl Default for Content {
fn default() -> Self {
Content::Text(String::new())
}
}
impl Content {
pub fn is_empty(&self) -> bool {
match self {
Content::Text(s) => s.is_empty(),
Content::Parts(parts) => parts.is_empty(),
}
}
pub fn as_text(&self) -> String {
match self {
Content::Text(s) => s.clone(),
Content::Parts(parts) => {
let mut out = String::new();
for part in parts {
if let ContentPart::Text { text } = part {
out.push_str(text);
}
}
out
}
}
}
pub fn images(&self) -> Vec<&ImageUrl> {
let mut out = Vec::new();
if let Content::Parts(parts) = self {
for part in parts {
if let ContentPart::ImageUrl { image_url } = part {
out.push(image_url);
}
}
}
out
}
}
impl From<String> for Content {
fn from(s: String) -> Self {
Content::Text(s)
}
}
impl From<&str> for Content {
fn from(s: &str) -> Self {
Content::Text(s.to_string())
}
}
impl From<Vec<ContentPart>> for Content {
fn from(parts: Vec<ContentPart>) -> Self {
Content::Parts(parts)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Message {
pub role: Role,
pub content: Content,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCall>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
impl Message {
pub fn system(content: impl Into<String>) -> Self {
Self {
role: Role::System,
content: Content::Text(content.into()),
tool_calls: None,
tool_call_id: None,
name: None,
}
}
pub fn user(content: impl Into<String>) -> Self {
Self {
role: Role::User,
content: Content::Text(content.into()),
tool_calls: None,
tool_call_id: None,
name: None,
}
}
pub fn assistant(content: impl Into<String>) -> Self {
Self {
role: Role::Assistant,
content: Content::Text(content.into()),
tool_calls: None,
tool_call_id: None,
name: None,
}
}
pub fn user_with_parts(parts: Vec<ContentPart>) -> Self {
Self {
role: Role::User,
content: Content::Parts(parts),
tool_calls: None,
tool_call_id: None,
name: None,
}
}
pub fn user_with_image(text: impl Into<String>, image_url: impl Into<String>) -> Self {
Self::user_with_parts(vec![
ContentPart::text(text),
ContentPart::image_url(image_url),
])
}
pub fn tool(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
Self {
role: Role::Tool,
content: Content::Text(content.into()),
tool_calls: None,
tool_call_id: Some(tool_call_id.into()),
name: None,
}
}
pub fn assistant_tool_calls(tool_calls: Vec<ToolCall>) -> Self {
Self {
role: Role::Assistant,
content: Content::Text(String::new()),
tool_calls: Some(tool_calls),
tool_call_id: None,
name: None,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Usage {
pub prompt_tokens: u64,
pub completion_tokens: u64,
pub total_tokens: u64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct LogProbs {
#[serde(default)]
pub content: Vec<TokenLogProb>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct TokenLogProb {
pub token: String,
pub logprob: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bytes: Option<Vec<u8>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub top_logprobs: Vec<TopLogProb>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct TopLogProb {
pub token: String,
pub logprob: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bytes: Option<Vec<u8>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FinishReason {
Stop,
Length,
ToolCalls,
ContentFilter,
EndTurn,
MaxTokens,
StopSequence,
ToolUse,
Other(String),
}
impl FinishReason {
pub fn as_str(&self) -> &str {
match self {
FinishReason::Stop => "stop",
FinishReason::Length => "length",
FinishReason::ToolCalls => "tool_calls",
FinishReason::ContentFilter => "content_filter",
FinishReason::EndTurn => "end_turn",
FinishReason::MaxTokens => "max_tokens",
FinishReason::StopSequence => "stop_sequence",
FinishReason::ToolUse => "tool_use",
FinishReason::Other(s) => s.as_str(),
}
}
}
impl From<String> for FinishReason {
fn from(s: String) -> Self {
match s.as_str() {
"stop" => FinishReason::Stop,
"length" => FinishReason::Length,
"tool_calls" => FinishReason::ToolCalls,
"content_filter" => FinishReason::ContentFilter,
"end_turn" => FinishReason::EndTurn,
"max_tokens" => FinishReason::MaxTokens,
"stop_sequence" => FinishReason::StopSequence,
"tool_use" => FinishReason::ToolUse,
_ => FinishReason::Other(s),
}
}
}
impl Serialize for FinishReason {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for FinishReason {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let s = String::deserialize(d)?;
Ok(FinishReason::from(s))
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ChatResponse {
pub content: String,
pub model: String,
pub usage: Option<Usage>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCall>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub finish_reason: Option<FinishReason>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub logprobs: Option<LogProbs>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct StreamChunk {
pub delta: String,
pub done: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub finish_reason: Option<FinishReason>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub usage: Option<Usage>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCall>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ResponseFormat {
Text,
JsonObject,
JsonSchema {
json_schema: serde_json::Value,
},
}
impl ResponseFormat {
pub fn json_object() -> Self {
ResponseFormat::JsonObject
}
pub fn json_schema(json_schema: serde_json::Value) -> Self {
ResponseFormat::JsonSchema { json_schema }
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct ChatRequest {
pub model: String,
pub messages: Vec<Message>,
pub temperature: Option<f64>,
pub max_tokens: Option<u64>,
pub stream: bool,
pub top_p: Option<f64>,
pub tools: Option<Vec<Tool>>,
pub tool_choice: Option<ToolChoice>,
pub response_format: Option<ResponseFormat>,
pub stop: Option<Vec<String>>,
pub n: Option<u32>,
pub seed: Option<i64>,
pub presence_penalty: Option<f64>,
pub frequency_penalty: Option<f64>,
pub logprobs: Option<bool>,
pub top_logprobs: Option<u32>,
pub parallel_tool_calls: Option<bool>,
pub service_tier: Option<String>,
pub store: Option<bool>,
pub metadata: Option<serde_json::Value>,
pub user: Option<String>,
pub extra: HashMap<String, serde_json::Value>,
}
impl ChatRequest {
pub fn new(model: impl Into<String>, prompt: impl Into<String>) -> Self {
Self {
model: model.into(),
messages: vec![Message::user(prompt)],
..Default::default()
}
}
pub fn from_messages(model: impl Into<String>, messages: Vec<Message>) -> Self {
Self {
model: model.into(),
messages,
..Default::default()
}
}
pub fn with_messages(mut self, messages: Vec<Message>) -> Self {
self.messages = messages;
self
}
pub fn with_system(mut self, system: impl Into<String>) -> Self {
self.messages.insert(0, Message::system(system));
self
}
pub fn with_temperature(mut self, temp: f64) -> Self {
self.temperature = Some(temp);
self
}
pub fn with_max_tokens(mut self, max: u64) -> Self {
self.max_tokens = Some(max);
self
}
pub fn with_stream(mut self) -> Self {
self.stream = true;
self
}
pub fn with_top_p(mut self, top_p: f64) -> Self {
self.top_p = Some(top_p);
self
}
pub fn with_tools(mut self, tools: Vec<Tool>) -> Self {
self.tools = Some(tools);
self
}
pub fn with_tool_choice(mut self, tool_choice: ToolChoice) -> Self {
self.tool_choice = Some(tool_choice);
self
}
pub fn with_response_format(mut self, format: ResponseFormat) -> Self {
self.response_format = Some(format);
self
}
pub fn with_json_mode(mut self) -> Self {
self.response_format = Some(ResponseFormat::JsonObject);
self
}
pub fn with_stop(mut self, stop: Vec<String>) -> Self {
self.stop = Some(stop);
self
}
pub fn with_n(mut self, n: u32) -> Self {
self.n = Some(n);
self
}
pub fn with_seed(mut self, seed: i64) -> Self {
self.seed = Some(seed);
self
}
pub fn with_presence_penalty(mut self, penalty: f64) -> Self {
self.presence_penalty = Some(penalty);
self
}
pub fn with_frequency_penalty(mut self, penalty: f64) -> Self {
self.frequency_penalty = Some(penalty);
self
}
pub fn with_logprobs(mut self, logprobs: bool) -> Self {
self.logprobs = Some(logprobs);
self
}
pub fn with_top_logprobs(mut self, top_logprobs: u32) -> Self {
self.top_logprobs = Some(top_logprobs);
self
}
pub fn with_parallel_tool_calls(mut self, enabled: bool) -> Self {
self.parallel_tool_calls = Some(enabled);
self
}
pub fn with_service_tier(mut self, tier: impl Into<String>) -> Self {
self.service_tier = Some(tier.into());
self
}
pub fn with_store(mut self, store: bool) -> Self {
self.store = Some(store);
self
}
pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
self.metadata = Some(metadata);
self
}
pub fn with_user(mut self, user: impl Into<String>) -> Self {
self.user = Some(user.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[non_exhaustive]
pub struct EmbeddingRequest {
pub model: String,
pub input: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dimensions: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub user: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty", flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
impl EmbeddingRequest {
pub fn new(model: impl Into<String>, input: impl Into<String>) -> Self {
Self {
model: model.into(),
input: vec![input.into()],
dimensions: None,
user: None,
extra: HashMap::new(),
}
}
pub fn batch<I, S>(model: impl Into<String>, inputs: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self {
model: model.into(),
input: inputs.into_iter().map(Into::into).collect(),
dimensions: None,
user: None,
extra: HashMap::new(),
}
}
pub fn with_dimensions(mut self, dimensions: u32) -> Self {
self.dimensions = Some(dimensions);
self
}
pub fn with_user(mut self, user: impl Into<String>) -> Self {
self.user = Some(user.into());
self
}
pub fn with_extra(
mut self,
key: impl Into<String>,
value: impl Into<serde_json::Value>,
) -> Self {
self.extra.insert(key.into(), value.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Embedding {
pub index: usize,
pub embedding: Vec<f32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct EmbeddingUsage {
pub prompt_tokens: u64,
pub total_tokens: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct EmbeddingResponse {
pub model: String,
pub data: Vec<Embedding>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub usage: Option<EmbeddingUsage>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tool_choice_mode_serializes_as_string() {
assert_eq!(
serde_json::to_value(ToolChoice::auto()).unwrap(),
serde_json::json!("auto")
);
assert_eq!(
serde_json::to_value(ToolChoice::required()).unwrap(),
serde_json::json!("required")
);
}
#[test]
fn tool_choice_function_serializes_as_object() {
let v = serde_json::to_value(ToolChoice::function("get_weather")).unwrap();
assert_eq!(v["type"], "function");
assert_eq!(v["function"]["name"], "get_weather");
}
#[test]
fn tool_serializes_with_function_schema() {
let tool = Tool::function(
"get_weather",
Some("Get the weather".to_string()),
serde_json::json!({"type": "object"}),
);
let v = serde_json::to_value(&tool).unwrap();
assert_eq!(v["type"], "function");
assert_eq!(v["function"]["name"], "get_weather");
assert_eq!(v["function"]["description"], "Get the weather");
assert_eq!(v["function"]["parameters"]["type"], "object");
}
#[test]
fn tool_message_has_role_and_id() {
let msg = Message::tool("call_1", "result");
assert_eq!(msg.role, Role::Tool);
assert_eq!(msg.tool_call_id.as_deref(), Some("call_1"));
}
#[test]
fn content_text_serializes_as_string() {
let c = Content::Text("hi".to_string());
assert_eq!(serde_json::to_value(&c).unwrap(), serde_json::json!("hi"));
}
#[test]
fn content_parts_serialize_as_openai_array() {
let c = Content::Parts(vec![
ContentPart::text("look"),
ContentPart::image_url("https://example.com/a.png"),
]);
let v = serde_json::to_value(&c).unwrap();
assert!(v.is_array());
assert_eq!(v[0]["type"], "text");
assert_eq!(v[0]["text"], "look");
assert_eq!(v[1]["type"], "image_url");
assert_eq!(v[1]["image_url"]["url"], "https://example.com/a.png");
assert!(v[1]["image_url"].get("detail").is_none());
}
#[test]
fn content_deserializes_from_string_and_array() {
let from_str: Content = serde_json::from_value(serde_json::json!("hi")).unwrap();
assert_eq!(from_str, Content::Text("hi".to_string()));
let from_arr: Content = serde_json::from_value(serde_json::json!([
{"type": "text", "text": "a"},
{"type": "image_url", "image_url": {"url": "u"}}
]))
.unwrap();
assert_eq!(from_arr.images().len(), 1);
assert_eq!(from_arr.as_text(), "a");
}
#[test]
fn message_user_with_image_holds_parts() {
let msg = Message::user_with_image("desc", "https://example.com/a.png");
assert_eq!(msg.role, Role::User);
assert_eq!(msg.content.images().len(), 1);
assert_eq!(msg.content.as_text(), "desc");
}
#[test]
fn response_format_serializes_as_openai_wire_format() {
assert_eq!(
serde_json::to_value(ResponseFormat::Text).unwrap(),
serde_json::json!({"type": "text"})
);
assert_eq!(
serde_json::to_value(ResponseFormat::json_object()).unwrap(),
serde_json::json!({"type": "json_object"})
);
let schema = serde_json::json!({"name": "person", "schema": {"type": "object"}});
let v = serde_json::to_value(ResponseFormat::json_schema(schema.clone())).unwrap();
assert_eq!(v["type"], "json_schema");
assert_eq!(v["json_schema"], schema);
}
#[test]
fn chat_request_sampling_builders_set_fields() {
let req = ChatRequest::new("gpt-4o", "hi")
.with_json_mode()
.with_stop(vec!["\n".to_string()])
.with_seed(42)
.with_top_p(0.9)
.with_n(2)
.with_presence_penalty(0.5)
.with_frequency_penalty(-0.3)
.with_logprobs(true)
.with_top_logprobs(5)
.with_parallel_tool_calls(false)
.with_service_tier("flex")
.with_store(true)
.with_metadata(serde_json::json!({"trace_id": "abc"}))
.with_user("user-123");
assert_eq!(req.response_format, Some(ResponseFormat::JsonObject));
assert_eq!(req.stop, Some(vec!["\n".to_string()]));
assert_eq!(req.seed, Some(42));
assert_eq!(req.top_p, Some(0.9));
assert_eq!(req.n, Some(2));
assert_eq!(req.presence_penalty, Some(0.5));
assert_eq!(req.frequency_penalty, Some(-0.3));
assert_eq!(req.logprobs, Some(true));
assert_eq!(req.top_logprobs, Some(5));
assert_eq!(req.parallel_tool_calls, Some(false));
assert_eq!(req.service_tier.as_deref(), Some("flex"));
assert_eq!(req.store, Some(true));
assert_eq!(req.metadata, Some(serde_json::json!({"trace_id": "abc"})));
assert_eq!(req.user.as_deref(), Some("user-123"));
}
#[test]
fn chat_request_from_messages_sets_messages() {
let req = ChatRequest::from_messages("gpt-4o", vec![Message::user("hi")]);
assert_eq!(req.model, "gpt-4o");
assert_eq!(req.messages.len(), 1);
let req = req.with_messages(vec![Message::user("a"), Message::user("b")]);
assert_eq!(req.messages.len(), 2);
}
#[test]
fn stream_chunk_serializes_tool_calls_only_when_present() {
let empty = StreamChunk::default();
let v = serde_json::to_value(&empty).unwrap();
assert!(v.get("tool_calls").is_none());
let chunk = StreamChunk {
done: true,
finish_reason: Some(FinishReason::ToolCalls),
tool_calls: Some(vec![ToolCall {
id: "call_1".to_string(),
call_type: "function".to_string(),
function: FunctionCall {
name: "get_weather".to_string(),
arguments: "{\"city\":\"SF\"}".to_string(),
},
}]),
..Default::default()
};
let v = serde_json::to_value(&chunk).unwrap();
assert_eq!(v["tool_calls"][0]["id"], "call_1");
assert_eq!(v["tool_calls"][0]["function"]["name"], "get_weather");
assert_eq!(v["finish_reason"], "tool_calls");
}
#[test]
fn chat_response_serializes_logprobs_only_when_present() {
let resp = ChatResponse {
content: "Hi".to_string(),
model: "gpt-4o".to_string(),
..Default::default()
};
let v = serde_json::to_value(&resp).unwrap();
assert!(v.get("logprobs").is_none());
let resp = ChatResponse {
content: "Hi".to_string(),
model: "gpt-4o".to_string(),
logprobs: Some(LogProbs {
content: vec![TokenLogProb {
token: "Hi".to_string(),
logprob: -0.25,
bytes: Some(vec![72, 105]),
top_logprobs: vec![TopLogProb {
token: "Hi".to_string(),
logprob: -0.25,
bytes: Some(vec![72, 105]),
}],
}],
}),
..Default::default()
};
let v = serde_json::to_value(&resp).unwrap();
assert_eq!(v["logprobs"]["content"][0]["token"], "Hi");
assert_eq!(v["logprobs"]["content"][0]["logprob"], -0.25);
assert_eq!(
v["logprobs"]["content"][0]["top_logprobs"][0]["token"],
"Hi"
);
}
}