use serde::{Deserialize, Serialize};
use crate::core::types::message::{MessageContent, MessageRole};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct DeepLTranslateRequest {
text: Vec<String>,
target_lang: String,
#[serde(skip_serializing_if = "Option::is_none")]
source_lang: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
formality: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct DeepLTranslation {
detected_source_language: String,
text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct DeepLTranslateResponse {
translations: Vec<DeepLTranslation>,
}
crate::define_http_provider_with_hooks!(
provider: super::PROVIDER_NAME,
struct_name: DeepLProvider,
config: super::config::DeepLConfig,
error_mapper: super::error_mapper::DeepLErrorMapper,
model_info: super::model_info::get_supported_models,
capabilities: &[
crate::core::types::model::ProviderCapability::AudioTranslation,
],
url_builder: |provider: &DeepLProvider| -> String {
let base_url = provider
.config
.base
.api_base
.as_deref()
.unwrap_or(super::DEFAULT_BASE_URL);
format!("{}/translate", base_url)
},
request_builder: |provider: &DeepLProvider, url: &str| -> reqwest::RequestBuilder {
provider.http_client.post(url)
},
supported_params: ["temperature"],
build_headers: |provider: &DeepLProvider, headers: &mut std::collections::HashMap<String, String>| {
if let Some(api_key) = &provider.config.base.api_key {
headers.insert(
"Authorization".to_string(),
format!("DeepL-Auth-Key {}", api_key),
);
}
headers.insert("Content-Type".to_string(), "application/json".to_string());
},
with_api_key: |api_key: String| -> Result<DeepLProvider, crate::core::providers::unified_provider::ProviderError> {
let config = super::config::DeepLConfig::new("deepl")
.with_api_key(api_key);
DeepLProvider::new(config)
},
request_transform: |provider: &DeepLProvider,
request: crate::core::types::chat::ChatRequest|
-> Result<serde_json::Value, crate::core::providers::unified_provider::ProviderError> {
let (target_lang, source_lang, text) = provider.extract_translation_params(&request)?;
let translate_request = DeepLTranslateRequest {
text: vec![text],
target_lang,
source_lang,
formality: None, };
serde_json::to_value(translate_request).map_err(|e| {
crate::core::providers::unified_provider::ProviderError::serialization(
"deepl",
e.to_string(),
)
})
},
response_transform: |_provider: &DeepLProvider,
raw_response: &[u8],
_model: &str,
_request_id: &str|
-> Result<crate::core::types::responses::ChatResponse, crate::core::providers::unified_provider::ProviderError> {
let response_text = String::from_utf8_lossy(raw_response);
let deepl_response: DeepLTranslateResponse =
serde_json::from_str(&response_text).map_err(|e| {
crate::core::providers::unified_provider::ProviderError::serialization(
"deepl",
e.to_string(),
)
})?;
let translation = deepl_response
.translations
.first()
.ok_or_else(|| {
crate::core::providers::unified_provider::ProviderError::api_error(
"deepl",
500,
"No translation returned",
)
})?;
let response = serde_json::json!({
"id": format!("deepl-{}", uuid::Uuid::new_v4()),
"object": "chat.completion",
"created": chrono::Utc::now().timestamp(),
"model": "deepl-translate",
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": translation.text.clone(),
},
"finish_reason": "stop"
}],
"usage": {
"prompt_tokens": 0,
"completion_tokens": 0,
"total_tokens": 0
}
});
serde_json::from_value(response).map_err(|e| {
crate::core::providers::unified_provider::ProviderError::serialization(
"deepl",
e.to_string(),
)
})
},
error_map: |_provider: &DeepLProvider,
status: u16,
error_text: String,
_request: &crate::core::types::chat::ChatRequest|
-> crate::core::providers::unified_provider::ProviderError {
match status {
401 | 403 => crate::core::providers::unified_provider::ProviderError::authentication(
"deepl",
error_text,
),
429 => crate::core::providers::unified_provider::ProviderError::rate_limit(
"deepl",
None,
),
456 => crate::core::providers::unified_provider::ProviderError::quota_exceeded(
"deepl",
"Quota exceeded",
),
400 => crate::core::providers::unified_provider::ProviderError::invalid_request(
"deepl",
error_text,
),
_ => crate::core::providers::unified_provider::ProviderError::api_error(
"deepl",
status,
error_text,
),
}
},
health_check: |provider: &DeepLProvider| {
let base_url = provider
.config
.base
.api_base
.as_deref()
.unwrap_or(super::DEFAULT_BASE_URL)
.to_string();
let headers = provider.build_headers();
let http_client = provider.http_client.clone();
async move {
let url = format!("{}/usage", base_url);
let mut req_builder = http_client.get(&url);
for (key, value) in headers {
req_builder = req_builder.header(key, value);
}
match req_builder.send().await {
Ok(response) if response.status().is_success() => {
crate::core::types::health::HealthStatus::Healthy
}
Ok(_) => crate::core::types::health::HealthStatus::Unhealthy,
Err(_) => crate::core::types::health::HealthStatus::Unhealthy,
}
}
},
streaming_error: "Streaming is not supported for translation",
);
impl DeepLProvider {
fn extract_translation_params(
&self,
request: &crate::core::types::chat::ChatRequest,
) -> Result<
(String, Option<String>, String),
crate::core::providers::unified_provider::ProviderError,
> {
let user_message = request
.messages
.iter()
.rev()
.find(|m| m.role == MessageRole::User)
.ok_or_else(|| {
crate::core::providers::unified_provider::ProviderError::invalid_request(
"deepl",
"No user message found",
)
})?;
let content = user_message.content.as_ref().ok_or_else(|| {
crate::core::providers::unified_provider::ProviderError::invalid_request(
"deepl",
"Message content is empty",
)
})?;
let text = match content {
MessageContent::Text(text_content) => text_content.as_str(),
MessageContent::Parts(parts) => {
use crate::core::types::content::ContentPart;
parts
.iter()
.find_map(|p| match p {
ContentPart::Text { text } => Some(text.as_str()),
_ => None,
})
.ok_or_else(|| {
crate::core::providers::unified_provider::ProviderError::invalid_request(
"deepl",
"No text content found in message",
)
})?
}
};
let parts: Vec<&str> = text.split(':').collect();
if parts.len() < 2 {
return Err(
crate::core::providers::unified_provider::ProviderError::invalid_request(
"deepl",
"Invalid translation format. Expected: 'Translate to {lang}: {text}'",
),
);
}
let instruction = parts[0].trim();
let text_to_translate = parts[1..].join(":").trim().to_string();
let (source_lang, target_lang) =
if instruction.contains("from") && instruction.contains("to") {
let lang_parts: Vec<&str> = instruction.split_whitespace().collect();
let from_idx = lang_parts
.iter()
.position(|&s| s == "from")
.ok_or_else(|| {
crate::core::providers::unified_provider::ProviderError::invalid_request(
"deepl",
"Invalid translation format",
)
})?;
let to_idx = lang_parts.iter().position(|&s| s == "to").ok_or_else(|| {
crate::core::providers::unified_provider::ProviderError::invalid_request(
"deepl",
"Invalid translation format",
)
})?;
let source = if from_idx + 1 < lang_parts.len() {
Some(lang_parts[from_idx + 1].to_uppercase())
} else {
None
};
let target = if to_idx + 1 < lang_parts.len() {
lang_parts[to_idx + 1].to_uppercase()
} else {
return Err(
crate::core::providers::unified_provider::ProviderError::invalid_request(
"deepl",
"Target language not specified",
),
);
};
(source, target)
} else if instruction.contains("to") {
let lang_parts: Vec<&str> = instruction.split_whitespace().collect();
let to_idx = lang_parts.iter().position(|&s| s == "to").ok_or_else(|| {
crate::core::providers::unified_provider::ProviderError::invalid_request(
"deepl",
"Invalid translation format",
)
})?;
let target = if to_idx + 1 < lang_parts.len() {
lang_parts[to_idx + 1].to_uppercase()
} else {
return Err(
crate::core::providers::unified_provider::ProviderError::invalid_request(
"deepl",
"Target language not specified",
),
);
};
(None, target)
} else {
return Err(
crate::core::providers::unified_provider::ProviderError::invalid_request(
"deepl",
"Invalid translation format. Expected: 'Translate to {lang}: {text}'",
),
);
};
Ok((target_lang, source_lang, text_to_translate))
}
}
#[cfg(test)]
mod tests {
use super::super::config::DeepLConfig;
use super::*;
use crate::core::traits::provider::llm_provider::trait_definition::LLMProvider;
use crate::core::types::model::ProviderCapability;
#[tokio::test]
async fn test_provider_creation() {
let config = DeepLConfig::new("deepl").with_api_key("test-key");
let provider = DeepLProvider::new(config);
assert!(provider.is_ok());
}
#[test]
fn test_provider_capabilities() {
let config = DeepLConfig::new("deepl").with_api_key("test-key");
let provider = DeepLProvider::new(config).unwrap();
let caps = provider.capabilities();
assert!(caps.contains(&ProviderCapability::AudioTranslation));
assert!(!caps.contains(&ProviderCapability::ChatCompletion));
}
#[test]
fn test_provider_models() {
let config = DeepLConfig::new("deepl").with_api_key("test-key");
let provider = DeepLProvider::new(config).unwrap();
let models = provider.models();
assert!(!models.is_empty());
assert!(models.iter().any(|m| m.id == "deepl-translate"));
}
#[test]
fn test_provider_name() {
let config = DeepLConfig::new("deepl").with_api_key("test-key");
let provider = DeepLProvider::new(config).unwrap();
assert_eq!(provider.name(), "deepl");
}
}