use super::*;
use derive_builder::Builder;
#[derive(Clone, Debug, PartialEq, Serialize, Builder)]
#[builder(
pattern = "owned",
setter(into, strip_option),
build_fn(validate = "Self::validate"),
name = "ResponsesRequestBuilder"
)]
pub struct ResponsesRequest {
#[serde(skip_serializing)]
pub client: DeepSeekClient,
pub model: String,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub input: Option<Input>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning: Option<Reasoning>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub max_output_tokens: Option<u32>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f64>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f64>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<Text>,
#[builder(default, setter(each(name = "tool", into)))]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub tools: Vec<Tool>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<ToolChoice>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub top_logprobs: Option<u32>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub user: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(untagged)]
pub enum Input {
TextInput(String),
InputItemList(Vec<InputItem>),
}
impl From<String> for Input {
fn from(value: String) -> Self {
Input::TextInput(value)
}
}
impl From<&str> for Input {
fn from(value: &str) -> Self {
Input::TextInput(value.to_string())
}
}
impl From<Vec<InputItem>> for Input {
fn from(value: Vec<InputItem>) -> Self {
Input::InputItemList(value)
}
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct InputItem {
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub typ: Option<InputItemType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub role: Option<InputRole>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<InputContent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub call_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output: Option<String>,
}
impl InputItem {
pub fn user(content: impl Into<String>) -> Self {
InputItem {
typ: None,
role: Some(InputRole::User),
content: Some(InputContent::Text(content.into())),
call_id: None,
name: None,
arguments: None,
output: None,
}
}
pub fn assistant(content: impl Into<String>) -> Self {
InputItem {
typ: None,
role: Some(InputRole::Assistant),
content: Some(InputContent::Text(content.into())),
call_id: None,
name: None,
arguments: None,
output: None,
}
}
pub fn function_call(
call_id: impl Into<String>,
name: impl Into<String>,
arguments: impl Into<String>,
) -> Self {
InputItem {
typ: Some(InputItemType::FunctionCall),
role: None,
content: None,
call_id: Some(call_id.into()),
name: Some(name.into()),
arguments: Some(arguments.into()),
output: None,
}
}
pub fn function_call_output(call_id: impl Into<String>, output: impl Into<String>) -> Self {
InputItem {
typ: Some(InputItemType::FunctionCallOutput),
role: None,
content: None,
call_id: Some(call_id.into()),
name: None,
arguments: None,
output: Some(output.into()),
}
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum InputItemType {
Message,
FunctionCall,
FunctionCallOutput,
Reasoning,
WebSearchCall,
#[serde(other)]
Unknown,
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum InputRole {
User,
Assistant,
System,
Developer,
#[serde(other)]
Unknown,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(untagged)]
pub enum InputContent {
Text(String),
Parts(Vec<InputContentPart>),
}
impl From<String> for InputContent {
fn from(value: String) -> Self {
InputContent::Text(value)
}
}
impl From<&str> for InputContent {
fn from(value: &str) -> Self {
InputContent::Text(value.to_string())
}
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum InputContentPart {
InputText { text: String },
OutputText { text: String },
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct Reasoning {
pub effort: ReasoningEffort,
}
impl Reasoning {
pub fn new(effort: ReasoningEffort) -> Self {
Reasoning { effort }
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ReasoningEffort {
None,
Minimal,
Low,
Medium,
High,
#[serde(rename = "xhigh")]
XHigh,
#[serde(rename = "max")]
Max,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Text {
pub format: TextFormat,
}
impl Text {
pub fn new(format: TextFormat) -> Self {
Text { format }
}
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum TextFormat {
Text,
JsonObject,
JsonSchema {
name: String,
schema: serde_json::Value,
},
}
impl TextFormat {
pub fn text() -> Self {
TextFormat::Text
}
pub fn json_object() -> Self {
TextFormat::JsonObject
}
pub fn json_schema(name: impl Into<String>, schema: serde_json::Value) -> Self {
TextFormat::JsonSchema {
name: name.into(),
schema,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Tool {
#[serde(rename = "type")]
pub typ: ToolType,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parameters: Option<serde_json::Value>,
}
impl Tool {
pub fn function(
name: impl Into<String>,
description: impl Into<String>,
parameters: Option<serde_json::Value>,
) -> Self {
Tool {
typ: ToolType::Function,
name: Some(name.into()),
description: Some(description.into()),
parameters,
}
}
pub fn web_search() -> Self {
Tool {
typ: ToolType::WebSearch,
name: None,
description: None,
parameters: None,
}
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolType {
Function,
WebSearch,
#[serde(rename = "web_search_2025_08_26")]
WebSearch2025_08_26,
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(untagged)]
pub enum ToolChoice {
Mode(ToolChoiceMode),
Named(NamedToolChoice),
}
impl ToolChoice {
pub fn none() -> Self {
ToolChoice::Mode(ToolChoiceMode::None)
}
pub fn auto() -> Self {
ToolChoice::Mode(ToolChoiceMode::Auto)
}
pub fn required() -> Self {
ToolChoice::Mode(ToolChoiceMode::Required)
}
pub fn named(name: impl Into<String>) -> Self {
ToolChoice::Named(NamedToolChoice {
typ: ToolType::Function,
name: Some(name.into()),
})
}
pub fn web_search() -> Self {
ToolChoice::Named(NamedToolChoice {
typ: ToolType::WebSearch,
name: None,
})
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolChoiceMode {
None,
Auto,
Required,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct NamedToolChoice {
#[serde(rename = "type")]
pub typ: ToolType,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
impl ResponsesRequestBuilder {
fn validate(&self) -> Result<(), String> {
if self.input.as_ref().and_then(|o| o.as_ref()).is_none()
&& self
.instructions
.as_ref()
.and_then(|o| o.as_ref())
.is_none()
{
return Err("at least one of `input` and `instructions` is required".to_string());
}
if let Some(temperature) = self.temperature.flatten()
&& !(0.0..=2.0).contains(&temperature)
{
return Err("temperature must be between 0 and 2".to_string());
}
if let Some(top_p) = self.top_p.flatten()
&& !(0.0..=1.0).contains(&top_p)
{
return Err("top_p must be between 0 and 1".to_string());
}
if let Some(top_logprobs) = self.top_logprobs.flatten()
&& top_logprobs > 20
{
return Err("top_logprobs must be <= 20".to_string());
}
if let Some(user) = self.user.as_ref().and_then(|u| u.as_ref()) {
if user.len() > 512 {
return Err("user must be at most 512 characters".to_string());
}
if !user
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
{
return Err("user must only contain [a-zA-Z0-9\\-_]".to_string());
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn client() -> DeepSeekClient {
DeepSeekClient::new(
std::env::var("DEEPSEEK_API_KEY").expect("DEEPSEEK_API_KEY is not set"),
crate::DEFAULT_BASE_URL.clone(),
)
}
#[test]
fn input_serializes_as_string_or_list() {
let text = Input::TextInput("Hi".to_string());
assert_eq!(serde_json::to_value(text).unwrap(), json!("Hi"));
let items = Input::InputItemList(vec![InputItem::user("Hi")]);
assert_eq!(
serde_json::to_value(items).unwrap(),
json!([{"role": "user", "content": "Hi"}])
);
}
#[test]
fn reasoning_effort_serializes_effort_values() {
assert_eq!(
serde_json::to_value(ReasoningEffort::None).unwrap(),
json!("none")
);
assert_eq!(
serde_json::to_value(ReasoningEffort::XHigh).unwrap(),
json!("xhigh")
);
assert_eq!(
serde_json::to_value(ReasoningEffort::Max).unwrap(),
json!("max")
);
}
#[test]
fn tool_type_serializes_web_search_names() {
assert_eq!(
serde_json::to_value(ToolType::WebSearch).unwrap(),
json!("web_search")
);
assert_eq!(
serde_json::to_value(ToolType::WebSearch2025_08_26).unwrap(),
json!("web_search_2025_08_26")
);
}
#[test]
fn text_format_serializes_json_schema() {
let format = TextFormat::json_schema(
"math_response",
json!({"type": "object", "properties": {"answer": {"type": "number"}}}),
);
assert_eq!(
serde_json::to_value(format).unwrap(),
json!({
"type": "json_schema",
"name": "math_response",
"schema": {"type": "object", "properties": {"answer": {"type": "number"}}}
})
);
}
#[test]
fn tool_choice_serializes_mode_and_named() {
assert_eq!(
serde_json::to_value(ToolChoice::auto()).unwrap(),
json!("auto")
);
assert_eq!(
serde_json::to_value(ToolChoice::named("get_weather")).unwrap(),
json!({"type": "function", "name": "get_weather"})
);
}
#[test]
fn request_serializes_full_payload() {
let req = ResponsesRequestBuilder::default()
.client(client())
.model("deepseek-v4-flash")
.input("Hi")
.instructions("You are a helpful assistant.")
.reasoning(Reasoning::new(ReasoningEffort::Low))
.max_output_tokens(256_u32)
.temperature(0.7_f64)
.tool(Tool::function("get_weather", "Get the weather", None))
.build()
.unwrap();
let value = serde_json::to_value(&req).unwrap();
assert_eq!(value.get("model"), Some(&json!("deepseek-v4-flash")));
assert_eq!(value.get("input"), Some(&json!("Hi")));
assert_eq!(value.get("reasoning"), Some(&json!({"effort": "low"})));
assert_eq!(value.get("client"), None);
}
#[test]
fn builder_validation_rejects_invalid_values() {
let base = || {
ResponsesRequestBuilder::default()
.client(client())
.model("deepseek-v4-flash")
};
assert!(base().build().is_err(), "no input nor instructions");
assert!(
base().input("Hi").temperature(2.5_f64).build().is_err(),
"temperature out of range"
);
assert!(
base().input("Hi").top_p(1.5_f64).build().is_err(),
"top_p out of range"
);
assert!(
base().input("Hi").top_logprobs(21_u32).build().is_err(),
"top_logprobs out of range"
);
assert!(
base().input("Hi").user("not allowed!").build().is_err(),
"user charset"
);
assert!(
base().instructions("sys").build().is_ok(),
"instructions alone is valid"
);
}
#[test]
fn deserialize_unknown_enum_variants() {
let typ: InputItemType = serde_json::from_value(json!("file_search")).unwrap();
assert_eq!(typ, InputItemType::Unknown);
let role: InputRole = serde_json::from_value(json!("bot")).unwrap();
assert_eq!(role, InputRole::Unknown);
}
}