pub mod request {
use crate::DeepSeekError;
use crate::chat::request::{
ReasoningEffort, ResponseFormat, Stop, StreamOptions, Thinking, ThinkingType, ToolChoice,
ToolType, is_none_or_empty_stop,
};
use crate::chat::response::ToolCall;
use crate::chat::{Chat, ChatStream, ChatStreamBlocking, ChatStreamItem, is_none_or_empty_vec};
use crate::{Credentials, DeepSeekRequest, api_post, api_request_stream};
use derive_builder::Builder;
use futures_util::StreamExt;
use reqwest::Method;
use reqwest_eventsource::Event;
use serde::{Deserialize, Serialize};
use std::sync::mpsc as std_mpsc;
use tokio::sync::mpsc;
fn is_false(value: &bool) -> bool {
!*value
}
#[derive(Clone, Debug, PartialEq, Serialize, Builder)]
#[builder(
pattern = "owned",
setter(into, strip_option),
build_fn(validate = "Self::validate"),
name = "BetaChatRequestBuilder"
)]
pub struct BetaChatRequest {
#[serde(skip_serializing)]
#[builder(default)]
pub credentials: Option<Credentials>,
#[builder(setter(each(name = "message", into)))]
pub messages: Vec<BetaChatMessage>,
pub model: String,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub thinking: Option<Thinking>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_effort: Option<ReasoningEffort>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub max_tokens: Option<u32>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub response_format: Option<ResponseFormat>,
#[builder(default)]
#[serde(skip_serializing_if = "is_none_or_empty_stop")]
pub stop: Option<Stop>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub stream_options: Option<StreamOptions>,
#[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, 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 logprobs: Option<bool>,
#[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_id: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(tag = "role", rename_all = "snake_case")]
pub enum BetaChatMessage {
System {
content: String,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
},
User {
content: String,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
},
Assistant {
#[serde(skip_serializing_if = "Option::is_none")]
content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
#[serde(default, skip_serializing_if = "is_false")]
prefix: bool,
#[serde(skip_serializing_if = "Option::is_none")]
reasoning_content: Option<String>,
#[serde(skip_serializing_if = "is_none_or_empty_vec")]
tool_calls: Option<Vec<ToolCall>>,
},
Tool {
content: String,
tool_call_id: String,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct Tool {
#[serde(rename = "type")]
pub typ: ToolType,
pub function: BetaToolFunctionDefinition,
}
impl Tool {
pub fn new(
name: impl Into<String>,
description: impl Into<String>,
parameters: Option<serde_json::Value>,
strict: Option<bool>,
) -> Self {
Tool {
typ: ToolType::Function,
function: BetaToolFunctionDefinition {
name: name.into(),
description: description.into(),
parameters,
strict,
},
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct BetaToolFunctionDefinition {
pub description: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub parameters: Option<serde_json::Value>,
pub strict: Option<bool>,
}
impl BetaChatRequestBuilder {
fn validate(&self) -> Result<(), String> {
if let Some(temperature) = self.temperature.flatten() {
if !(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() {
if !(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() {
if top_logprobs > 20 {
return Err("top_logprobs must be <= 20".to_string());
}
if self.logprobs.flatten() != Some(true) {
return Err("top_logprobs requires logprobs=true".to_string());
}
}
if let Some(thinking) = self
.thinking
.as_ref()
.and_then(|thinking| thinking.as_ref())
{
if let Some(reasoning_effort) = self
.reasoning_effort
.as_ref()
.and_then(|effort| effort.as_ref())
{
if matches!(thinking.typ, ThinkingType::Disabled)
&& matches!(
reasoning_effort,
ReasoningEffort::High | ReasoningEffort::Max
)
{
return Err(
"thinking options type cannot be disabled when reasoning_effort is set"
.to_string(),
);
}
}
}
if let Some(stream) = self.stream.flatten() {
if !stream && self.stream_options.is_some() {
return Err("stream_options cannot be set when stream is false".to_string());
}
}
if let Some(messages) = self.messages.as_ref() {
messages.iter().try_for_each(|message| {
if let BetaChatMessage::Assistant {
prefix: false,
reasoning_content: Some(_),
..
} = message
{
return Err(
"reasoning_content cannot be set when assistant message prefix is false".to_string(),
);
}
Ok(())
})?;
}
Ok(())
}
}
impl DeepSeekRequest for BetaChatRequest {
type Response = Chat;
type StreamItem = ChatStreamItem;
type BlockingStream = ChatStreamBlocking;
async fn send(self) -> Result<Chat, DeepSeekError> {
let credentials = self.credentials.clone();
api_post("/chat/completions", &self, credentials).await
}
async fn stream(self) -> Result<mpsc::Receiver<ChatStreamItem>, DeepSeekError> {
let mut request = self;
request.stream = Some(true);
let credentials = request.credentials.clone();
let mut event_source = api_request_stream(
Method::POST,
"/chat/completions",
|builder| builder.json(&request),
credentials,
)
.await?;
let (tx, rx) = mpsc::channel(32);
tokio::spawn(async move {
while let Some(event) = event_source.next().await {
match event {
Ok(Event::Open) => {}
Ok(Event::Message(message)) => {
if message.data == "[DONE]" {
break;
}
match serde_json::from_str::<ChatStream>(&message.data) {
Ok(chunk) => {
if tx.send(Ok(chunk)).await.is_err() {
break;
}
}
Err(err) => {
let _ = tx
.send(Err(DeepSeekError::decode(
err.to_string(),
message.data,
)))
.await;
break;
}
}
}
Err(err) => {
let _ = tx
.send(Err(DeepSeekError::decode(err.to_string(), String::new())))
.await;
break;
}
}
}
});
Ok(rx)
}
fn stream_blocking(self) -> Result<ChatStreamBlocking, DeepSeekError> {
let (tx, rx) = std_mpsc::channel();
std::thread::spawn(move || {
let runtime = match tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
{
Ok(runtime) => runtime,
Err(err) => {
let _ = tx.send(Err(DeepSeekError::decode(err.to_string(), String::new())));
return;
}
};
runtime.block_on(async move {
match self.stream().await {
Ok(mut stream_rx) => {
while let Some(item) = stream_rx.recv().await {
if tx.send(item).is_err() {
break;
}
}
}
Err(err) => {
let _ = tx.send(Err(err));
}
}
});
});
Ok(ChatStreamBlocking { rx })
}
}
}
#[cfg(test)]
mod tests {
use super::request::*;
use crate::{Credentials, DEFAULT_BETA_BASE_URL, DeepSeekRequest, chat::request::Thinking};
fn get_credentials() -> Credentials {
Credentials::new(
std::env::var("DEEPSEEK_API").unwrap(),
DEFAULT_BETA_BASE_URL.clone(),
)
}
fn get_builder() -> BetaChatRequestBuilder {
BetaChatRequestBuilder::default()
.credentials(get_credentials())
.model("deepseek-v4-flash")
.max_tokens(32 as u32)
.thinking(Thinking::disabled())
}
#[tokio::test]
async fn beta_chat() {
let req = get_builder()
.message(BetaChatMessage::User {
content: "Please write quick sort code".to_string(),
name: None,
})
.message(BetaChatMessage::Assistant {
content: Some("```python\n".to_string()),
name: None,
prefix: true,
reasoning_content: None,
tool_calls: None,
})
.stop("```")
.build()
.unwrap();
let response = req.send().await.unwrap();
println!("{:#?}", response);
}
}