use serde::{Deserialize, Serialize};
use crate::model::role::Role;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
pub enum MessageContent {
Text {
text: String,
},
ImageUrl {
image_url: ImageUrl,
},
#[serde(rename = "video_url")]
VideoUrl {
video_url: VideoUrl,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageUrl {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>, }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VideoUrl {
pub url: String,
}
#[derive(Debug, Clone, Serialize)]
#[serde(untagged)]
pub enum DoubaoVisionMessage {
Text {
role: Role,
content: String,
},
Multimodal {
role: Role,
content: Vec<MessageContent>,
},
}
#[derive(Debug, Clone, Deserialize)]
pub struct DoubaoVisionMessageResponse {
pub role: Role,
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
#[serde(rename_all = "lowercase")]
pub enum ThinkingConfig {
Enabled,
Disabled,
}
#[derive(Debug, Clone, Serialize)]
pub struct DoubaoVisionRequest {
pub model: String, pub messages: Vec<DoubaoVisionMessage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thinking: Option<ThinkingConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop: Option<Vec<String>>,
}
#[derive(Debug, Deserialize)]
pub struct DoubaoChoice {
pub index: u32,
pub message: DoubaoResponseMessage,
pub finish_reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logprobs: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct DoubaoResponseMessage {
pub role: Role,
pub content: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_content: Option<String>, }
#[derive(Debug, Deserialize)]
pub struct DoubaoUsage {
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub total_tokens: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_tokens: Option<u32>, #[serde(skip_serializing_if = "Option::is_none")]
pub prompt_tokens_details: Option<PromptTokensDetails>,
#[serde(skip_serializing_if = "Option::is_none")]
pub completion_tokens_details: Option<CompletionTokensDetails>,
}
#[derive(Debug, Deserialize)]
pub struct PromptTokensDetails {
#[serde(skip_serializing_if = "Option::is_none")]
pub cached_tokens: Option<u32>,
}
#[derive(Debug, Deserialize)]
pub struct CompletionTokensDetails {
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_tokens: Option<u32>,
}
#[derive(Debug, Deserialize)]
pub struct DoubaoVisionResponse {
pub id: String,
pub object: String,
pub created: u64,
pub model: String,
pub choices: Vec<DoubaoChoice>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<DoubaoUsage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_tier: Option<String>,
}
impl DoubaoVisionRequest {
pub fn new(model: impl Into<String>, messages: Vec<DoubaoVisionMessage>) -> Self {
Self {
model: model.into(),
messages,
thinking: None,
stream: None,
temperature: None,
top_p: None,
max_tokens: None,
stop: None,
}
}
pub fn with_thinking(mut self, enabled: bool) -> Self {
self.thinking = Some(if enabled {
ThinkingConfig::Enabled
} else {
ThinkingConfig::Disabled
});
self
}
pub fn enable_thinking(mut self) -> Self {
self.thinking = Some(ThinkingConfig::Enabled);
self
}
pub fn disable_thinking(mut self) -> Self {
self.thinking = Some(ThinkingConfig::Disabled);
self
}
pub fn with_temperature(mut self, temperature: f32) -> Self {
self.temperature = Some(temperature);
self
}
pub fn with_max_tokens(mut self, max_tokens: u32) -> Self {
self.max_tokens = Some(max_tokens);
self
}
pub fn with_stream(mut self, stream: bool) -> Self {
self.stream = Some(stream);
self
}
pub fn with_top_p(mut self, top_p: f32) -> Self {
self.top_p = Some(top_p);
self
}
pub fn with_stop(mut self, stop: Vec<String>) -> Self {
self.stop = Some(stop);
self
}
}
impl DoubaoVisionMessage {
pub fn user(content: impl Into<String>) -> Self {
Self::Text {
role: Role::User,
content: content.into(),
}
}
pub fn assistant(content: impl Into<String>) -> Self {
Self::Text {
role: Role::Assistant,
content: content.into(),
}
}
pub fn system(content: impl Into<String>) -> Self {
Self::Text {
role: Role::System,
content: content.into(),
}
}
pub fn with_image(text: impl Into<String>, image_url: impl Into<String>) -> Self {
Self::Multimodal {
role: Role::User,
content: vec![
MessageContent::ImageUrl {
image_url: ImageUrl {
url: image_url.into(),
detail: None,
},
},
MessageContent::Text { text: text.into() },
],
}
}
pub fn with_image_detail(
text: impl Into<String>,
image_url: impl Into<String>,
detail: impl Into<String>,
) -> Self {
Self::Multimodal {
role: Role::User,
content: vec![
MessageContent::ImageUrl {
image_url: ImageUrl {
url: image_url.into(),
detail: Some(detail.into()),
},
},
MessageContent::Text { text: text.into() },
],
}
}
pub fn with_video(text: impl Into<String>, video_url: impl Into<String>) -> Self {
Self::Multimodal {
role: Role::User,
content: vec![
MessageContent::VideoUrl {
video_url: VideoUrl {
url: video_url.into(),
},
},
MessageContent::Text { text: text.into() },
],
}
}
pub fn with_images(text: impl Into<String>, image_urls: Vec<String>) -> Self {
let mut contents: Vec<MessageContent> = image_urls
.into_iter()
.map(|url| MessageContent::ImageUrl {
image_url: ImageUrl { url, detail: None },
})
.collect();
contents.push(MessageContent::Text { text: text.into() });
Self::Multimodal {
role: Role::User,
content: contents,
}
}
pub fn with_text_then_image(text: impl Into<String>, image_url: impl Into<String>) -> Self {
Self::Multimodal {
role: Role::User,
content: vec![
MessageContent::Text { text: text.into() },
MessageContent::ImageUrl {
image_url: ImageUrl {
url: image_url.into(),
detail: None,
},
},
],
}
}
}
impl DoubaoVisionResponse {
pub fn first_message(&self) -> Option<&DoubaoResponseMessage> {
self.choices.first().map(|choice| &choice.message)
}
pub fn first_content(&self) -> Option<&str> {
self.first_message().map(|msg| msg.content.as_str())
}
pub fn reasoning_content(&self) -> Option<&str> {
self.first_message()
.and_then(|msg| msg.reasoning_content.as_deref())
}
pub fn total_tokens(&self) -> Option<u32> {
self.usage.as_ref().map(|u| u.total_tokens)
}
pub fn reasoning_tokens(&self) -> Option<u32> {
self.usage.as_ref().and_then(|u| u.reasoning_tokens)
}
pub fn is_assistant_message(&self) -> bool {
self.first_message()
.map(|msg| matches!(msg.role, Role::Assistant))
.unwrap_or(false)
}
}
pub fn create_ocr_request(
image_url: impl Into<String>,
prompt: impl Into<String>,
) -> DoubaoVisionRequest {
let message = DoubaoVisionMessage::with_image(prompt, image_url);
DoubaoVisionRequest::new("doubao-1-5-thinking-vision-pro-250428", vec![message])
.disable_thinking() }
pub fn create_analysis_request(
image_url: impl Into<String>,
prompt: impl Into<String>,
) -> DoubaoVisionRequest {
let message = DoubaoVisionMessage::with_image(prompt, image_url);
DoubaoVisionRequest::new("doubao-1-5-thinking-vision-pro-250428", vec![message])
.enable_thinking() .with_temperature(0.1) }
pub fn create_conversation_request(
system_prompt: impl Into<String>,
user_prompt: impl Into<String>,
image_url: impl Into<String>,
) -> DoubaoVisionRequest {
let messages = vec![
DoubaoVisionMessage::system(system_prompt),
DoubaoVisionMessage::with_image(user_prompt, image_url),
];
DoubaoVisionRequest::new("doubao-1-5-thinking-vision-pro-250428", messages)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_user_message() {
let msg = DoubaoVisionMessage::user("Hello");
if let DoubaoVisionMessage::Text { role, content } = msg {
assert!(matches!(role, Role::User));
assert_eq!(content, "Hello");
} else {
panic!("Expected text message");
}
}
#[test]
fn test_create_image_message() {
let msg = DoubaoVisionMessage::with_image(
"What's in this image?",
"https://example.com/image.jpg",
);
if let DoubaoVisionMessage::Multimodal { role, content } = msg {
assert!(matches!(role, Role::User));
assert_eq!(content.len(), 2);
} else {
panic!("Expected multimodal message");
}
}
#[test]
fn test_create_multiple_images_message() {
let msg = DoubaoVisionMessage::with_images(
"Compare these images",
vec![
"https://example.com/image1.jpg".to_string(),
"https://example.com/image2.jpg".to_string(),
],
);
if let DoubaoVisionMessage::Multimodal { role, content } = msg {
assert!(matches!(role, Role::User));
assert_eq!(content.len(), 3); } else {
panic!("Expected multimodal message");
}
}
#[test]
fn test_request_builder() {
let messages = vec![
DoubaoVisionMessage::system("You are a helpful assistant"),
DoubaoVisionMessage::user("Hello"),
];
let request = DoubaoVisionRequest::new("doubao-1-5-thinking-vision-pro-250428", messages)
.enable_thinking()
.with_temperature(0.7)
.with_max_tokens(2000);
assert!(request.thinking.is_some());
assert_eq!(request.temperature, Some(0.7));
assert_eq!(request.max_tokens, Some(2000));
}
#[test]
fn test_role_serialization() {
let msg = DoubaoVisionMessage::assistant("I can help");
if let DoubaoVisionMessage::Text { role, content: _ } = msg {
assert!(matches!(role, Role::Assistant));
} else {
panic!("Expected text message");
}
}
}