use serde::{Deserialize, Serialize};
use super::{Message, Plugin, Provider, ReasoningConfig, ResponseFormat, Tool, ToolChoice};
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct ChatCompletionRequest {
pub model: String,
pub messages: Vec<Message>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub temperature: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub top_p: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub top_k: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub max_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub stream: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub stop: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub seed: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub frequency_penalty: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub presence_penalty: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub repetition_penalty: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub logit_bias: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub logprobs: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub top_logprobs: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub min_p: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub top_a: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub tools: Option<Vec<Tool>>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub tool_choice: Option<ToolChoice>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub response_format: Option<ResponseFormat>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub provider: Option<Provider>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub reasoning: Option<ReasoningConfig>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub transforms: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub plugins: Option<Vec<Plugin>>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub usage: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub user: Option<String>,
}
impl ChatCompletionRequest {
pub fn new(model: impl Into<String>, messages: Vec<Message>) -> Self {
Self {
model: model.into(),
messages,
..Default::default()
}
}
pub fn with_tools(mut self, tools: Vec<Tool>) -> Self {
self.tools = Some(tools);
self
}
pub fn with_tool_choice(mut self, choice: ToolChoice) -> Self {
self.tool_choice = Some(choice);
self
}
pub fn with_response_format(mut self, format: ResponseFormat) -> Self {
self.response_format = Some(format);
self
}
pub fn with_json_schema(
self,
name: impl Into<String>,
strict: bool,
schema: serde_json::Value,
) -> Self {
self.with_response_format(ResponseFormat::json_schema(name, strict, schema))
}
pub fn with_json_mode(self) -> Self {
self.with_response_format(ResponseFormat::json_object())
}
pub fn with_transforms<S, I>(mut self, transforms: I) -> Self
where
S: Into<String>,
I: IntoIterator<Item = S>,
{
self.transforms = Some(transforms.into_iter().map(Into::into).collect());
self
}
pub fn with_provider(mut self, provider: Provider) -> Self {
self.provider = Some(provider);
self
}
fn provider_mut(&mut self) -> &mut Provider {
self.provider.get_or_insert_with(Provider::default)
}
pub fn with_provider_order<S, I>(mut self, order: I) -> Self
where
S: Into<String>,
I: IntoIterator<Item = S>,
{
self.provider_mut().order = Some(order.into_iter().map(Into::into).collect());
self
}
pub fn with_provider_sort(mut self, sort: impl Into<String>) -> Self {
self.provider_mut().sort = Some(sort.into());
self
}
pub fn with_only_providers<S, I>(mut self, only: I) -> Self
where
S: Into<String>,
I: IntoIterator<Item = S>,
{
self.provider_mut().only = Some(only.into_iter().map(Into::into).collect());
self
}
pub fn with_ignore_providers<S, I>(mut self, ignore: I) -> Self
where
S: Into<String>,
I: IntoIterator<Item = S>,
{
self.provider_mut().ignore = Some(ignore.into_iter().map(Into::into).collect());
self
}
pub fn with_quantizations<S, I>(mut self, q: I) -> Self
where
S: Into<String>,
I: IntoIterator<Item = S>,
{
self.provider_mut().quantizations = Some(q.into_iter().map(Into::into).collect());
self
}
pub fn with_max_price(mut self, price: serde_json::Value) -> Self {
self.provider_mut().max_price = Some(price);
self
}
pub fn with_data_collection(mut self, policy: impl Into<String>) -> Self {
self.provider_mut().data_collection = Some(policy.into());
self
}
pub fn with_require_parameters(mut self, required: bool) -> Self {
self.provider_mut().require_parameters = Some(required);
self
}
pub fn with_allow_fallbacks(mut self, allow: bool) -> Self {
self.provider_mut().allow_fallbacks = Some(allow);
self
}
pub fn with_zdr(mut self, zdr: bool) -> Self {
self.provider_mut().zdr = Some(zdr);
self
}
pub fn with_nitro(self) -> Self {
self.with_provider_sort("throughput")
}
pub fn with_floor(self) -> Self {
self.with_provider_sort("price")
}
pub fn with_reasoning(mut self, reasoning: ReasoningConfig) -> Self {
self.reasoning = Some(reasoning);
self
}
fn reasoning_mut(&mut self) -> &mut ReasoningConfig {
self.reasoning.get_or_insert_with(ReasoningConfig::default)
}
pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
self.reasoning_mut().effort = Some(effort.into());
self
}
pub fn with_reasoning_max_tokens(mut self, max_tokens: u32) -> Self {
self.reasoning_mut().max_tokens = Some(max_tokens);
self
}
pub fn with_reasoning_exclude(mut self, exclude: bool) -> Self {
self.reasoning_mut().exclude = Some(exclude);
self
}
pub fn with_plugins(mut self, plugins: Vec<Plugin>) -> Self {
self.plugins = Some(plugins);
self
}
pub fn with_web_search(mut self) -> Self {
self.plugins
.get_or_insert_with(Vec::new)
.push(Plugin::web());
self
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct CompletionRequest {
pub model: String,
pub prompt: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub temperature: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub top_p: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub max_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub stream: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub stop: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub seed: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub frequency_penalty: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub presence_penalty: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub provider: Option<Provider>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub transforms: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub plugins: Option<Vec<Plugin>>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub user: Option<String>,
}
impl CompletionRequest {
pub fn new(model: impl Into<String>, prompt: impl Into<String>) -> Self {
Self {
model: model.into(),
prompt: prompt.into(),
..Default::default()
}
}
pub fn with_transforms<S, I>(mut self, transforms: I) -> Self
where
S: Into<String>,
I: IntoIterator<Item = S>,
{
self.transforms = Some(transforms.into_iter().map(Into::into).collect());
self
}
pub fn with_provider(mut self, provider: Provider) -> Self {
self.provider = Some(provider);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{FunctionDef, Tool, ToolChoice};
use pretty_assertions::assert_eq;
use serde_json::json;
#[test]
fn with_tools_serializes_only_set_fields() {
let req = ChatCompletionRequest::new("x/y", vec![Message::user("hi")])
.with_tools(vec![Tool::function(FunctionDef::new(
"f",
json!({"type":"object"}),
))])
.with_tool_choice(ToolChoice::required());
let v = serde_json::to_value(&req).unwrap();
assert_eq!(
v,
json!({
"model": "x/y",
"messages": [{"role":"user","content":"hi"}],
"tools": [{
"type": "function",
"function": {"name":"f","parameters":{"type":"object"}}
}],
"tool_choice": "required"
})
);
}
#[test]
fn tool_choice_function_serializes() {
let v = serde_json::to_value(ToolChoice::function("get_weather")).unwrap();
assert_eq!(
v,
json!({"type":"function","function":{"name":"get_weather"}})
);
}
#[test]
fn with_json_mode_serializes() {
let req = ChatCompletionRequest::new("x/y", vec![Message::user("hi")]).with_json_mode();
let v = serde_json::to_value(&req).unwrap();
assert_eq!(v["response_format"], json!({"type":"json_object"}));
}
#[test]
fn with_transforms_serializes_array() {
let req = ChatCompletionRequest::new("x/y", vec![Message::user("hi")])
.with_transforms(["middle-out"]);
let v = serde_json::to_value(&req).unwrap();
assert_eq!(v["transforms"], json!(["middle-out"]));
}
#[test]
fn with_transforms_empty_disables_defaults() {
let req = ChatCompletionRequest::new("x/y", vec![Message::user("hi")])
.with_transforms(Vec::<String>::new());
let v = serde_json::to_value(&req).unwrap();
assert_eq!(v["transforms"], json!([]));
}
#[test]
fn with_provider_helpers_compose_into_one_object() {
let req = ChatCompletionRequest::new("x/y", vec![Message::user("hi")])
.with_provider_order(["openai", "anthropic"])
.with_only_providers(["openai"])
.with_zdr(true)
.with_nitro();
let v = serde_json::to_value(&req).unwrap();
assert_eq!(
v["provider"],
json!({
"order": ["openai", "anthropic"],
"only": ["openai"],
"zdr": true,
"sort": "throughput"
})
);
}
#[test]
fn with_web_search_serializes_default_plugin() {
let req = ChatCompletionRequest::new("x/y", vec![Message::user("hi")]).with_web_search();
let v = serde_json::to_value(&req).unwrap();
assert_eq!(v["plugins"], json!([{"id":"web"}]));
}
#[test]
fn with_plugins_custom_config_serializes() {
use crate::types::{Plugin, WebPluginConfig};
let plugin = Plugin::web_with(
WebPluginConfig::new()
.with_max_results(3)
.with_search_prompt("Cite sources.")
.with_engine("native"),
);
let req =
ChatCompletionRequest::new("x/y", vec![Message::user("hi")]).with_plugins(vec![plugin]);
let v = serde_json::to_value(&req).unwrap();
assert_eq!(
v["plugins"],
json!([{
"id":"web",
"max_results":3,
"search_prompt":"Cite sources.",
"engine":"native"
}])
);
}
#[test]
fn with_reasoning_helpers_compose() {
let req = ChatCompletionRequest::new("x/y", vec![Message::user("hi")])
.with_reasoning_effort("high")
.with_reasoning_max_tokens(512)
.with_reasoning_exclude(false);
let v = serde_json::to_value(&req).unwrap();
assert_eq!(
v["reasoning"],
json!({"effort":"high","max_tokens":512,"exclude":false})
);
}
#[test]
fn completion_with_transforms_serializes_array() {
let req = CompletionRequest::new("x/y", "hello").with_transforms(["middle-out"]);
let v = serde_json::to_value(&req).unwrap();
assert_eq!(v["transforms"], json!(["middle-out"]));
}
#[test]
fn with_json_schema_serializes_strict_named_schema() {
let req = ChatCompletionRequest::new("x/y", vec![Message::user("hi")]).with_json_schema(
"answer",
true,
json!({"type":"object","properties":{"x":{"type":"number"}}}),
);
let v = serde_json::to_value(&req).unwrap();
assert_eq!(
v["response_format"],
json!({
"type": "json_schema",
"json_schema": {
"name": "answer",
"schema": {"type":"object","properties":{"x":{"type":"number"}}},
"strict": true
}
})
);
}
}