use crate::{error::TranslationError, options::TranslateOptions, translator::Translator};
use futures::future::join_all;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::{Mutex, Semaphore};
use tokio::time::sleep;
use unic_langid::LanguageIdentifier;
#[derive(Debug, Clone)]
pub struct OpenAIConfig {
pub base_url: String,
pub model: String,
pub api_keys: Vec<String>,
pub rpm_limit: u32,
pub concurrent_limit: usize,
pub system_prompt: Option<String>,
}
impl Default for OpenAIConfig {
fn default() -> Self {
Self {
base_url: "https://api.openai.com/v1".to_string(),
model: "gpt-3.5-turbo".to_string(),
api_keys: vec![],
rpm_limit: 60,
concurrent_limit: 10,
system_prompt: None,
}
}
}
impl OpenAIConfig {
pub fn builder() -> OpenAIConfigBuilder {
OpenAIConfigBuilder::default()
}
}
#[derive(Debug, Default)]
pub struct OpenAIConfigBuilder {
base_url: Option<String>,
model: Option<String>,
api_keys: Option<Vec<String>>,
rpm_limit: Option<u32>,
concurrent_limit: Option<usize>,
system_prompt: Option<String>,
}
impl OpenAIConfigBuilder {
pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
self.base_url = Some(base_url.into());
self
}
pub fn model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
self
}
pub fn api_keys(mut self, api_keys: Vec<impl Into<String>>) -> Self {
self.api_keys = Some(api_keys.into_iter().map(|s| s.into()).collect());
self
}
pub fn rpm_limit(mut self, rpm_limit: u32) -> Self {
self.rpm_limit = Some(rpm_limit);
self
}
pub fn concurrent_limit(mut self, concurrent_limit: usize) -> Self {
self.concurrent_limit = Some(concurrent_limit);
self
}
pub fn system_prompt(mut self, system_prompt: impl Into<String>) -> Self {
self.system_prompt = Some(system_prompt.into());
self
}
pub fn build(self) -> OpenAIConfig {
OpenAIConfig {
base_url: self
.base_url
.unwrap_or_else(|| "https://api.openai.com/v1".to_string()),
model: self.model.unwrap_or_else(|| "gpt-3.5-turbo".to_string()),
api_keys: self.api_keys.unwrap_or_else(Vec::new),
rpm_limit: self.rpm_limit.unwrap_or(60),
concurrent_limit: self.concurrent_limit.unwrap_or(10),
system_prompt: self.system_prompt,
}
}
}
#[derive(Debug)]
struct KeyTracker {
semaphore: Arc<Semaphore>,
request_times: Option<Arc<Mutex<Vec<Instant>>>>,
}
#[derive(Serialize, Deserialize)]
struct Message {
role: String,
content: String,
}
#[derive(Serialize)]
struct Request {
model: String,
messages: Vec<Message>,
temperature: f32,
}
#[derive(Deserialize)]
struct Choice {
message: Message,
}
#[derive(Deserialize)]
struct Response {
choices: Vec<Choice>,
}
pub struct OpenAITranslator {
client: Client,
config: OpenAIConfig,
key_trackers: Arc<Vec<KeyTracker>>,
current_key_index: Arc<Mutex<usize>>,
}
impl OpenAITranslator {
pub fn new(config: OpenAIConfig) -> Self {
let mut key_trackers = Vec::new();
for _ in &config.api_keys {
let request_times = if config.rpm_limit > 0 {
Some(Arc::new(Mutex::new(Vec::new())))
} else {
None
};
key_trackers.push(KeyTracker {
semaphore: Arc::new(Semaphore::new(config.concurrent_limit)),
request_times,
});
}
Self {
client: Client::new(),
config,
key_trackers: Arc::new(key_trackers),
current_key_index: Arc::new(Mutex::new(0)),
}
}
async fn get_next_key_index(&self) -> usize {
let mut index = self.current_key_index.lock().await;
let current = *index;
*index = (*index + 1) % self.config.api_keys.len();
current
}
fn get_system_prompt(&self, target_lang: &str, source_lang: Option<&str>) -> String {
if let Some(prompt) = &self.config.system_prompt {
return prompt.clone();
}
let source_lang_str = source_lang.unwrap_or("auto");
format!(
"You are a raw translation engine. You are not an AI assistant. Your only function is to translate the user's text. Translate from {} to {}. Do not, under any circumstances, write anything other than the translated text. Do not apologize. Do not explain. Do not add any extra text. If you cannot translate the text, repeat the original text.\n\nExamples:\n\nUser: Hello\nAssistant: 你好\n\nUser: World\nAssistant: 世界\n\nUser: xyzabc\nAssistant: xyzabc",
source_lang_str, target_lang
)
}
async fn wait_for_rate_limit(&self, tracker: &KeyTracker) {
if let Some(request_times) = &tracker.request_times {
let mut times = request_times.lock().await;
let now = Instant::now();
times.retain(|&time| now.duration_since(time) < Duration::from_secs(60));
if self.config.rpm_limit > 0 && times.len() >= self.config.rpm_limit as usize {
if let Some(oldest) = times.first() {
let elapsed = now.duration_since(*oldest);
if elapsed < Duration::from_secs(60) {
sleep(Duration::from_secs(60) - elapsed).await;
}
}
}
times.push(now);
}
}
pub async fn translate_batch(
&self,
texts: &[&str],
target_lang: &LanguageIdentifier,
source_lang: Option<&LanguageIdentifier>,
options: &TranslateOptions,
) -> Result<Vec<String>, TranslationError> {
let mut futures = Vec::new();
for &text in texts {
let future = self.translate_text_with_retry(text, target_lang, source_lang, options);
futures.push(future);
}
let results: Vec<_> = join_all(futures).await;
results.into_iter().collect()
}
async fn translate_text_with_retry(
&self,
text: &str,
target_lang: &LanguageIdentifier,
source_lang: Option<&LanguageIdentifier>,
options: &TranslateOptions,
) -> Result<String, TranslationError> {
let mut errors = Vec::new();
for attempt in 0..=options.max_retries {
if attempt > 0 {
let delay = Duration::from_millis(100 * 2u64.pow(attempt - 1));
sleep(delay).await;
}
match self
.try_translate_single(text, target_lang, source_lang, options)
.await
{
Ok(result) => return Ok(result),
Err(e) => {
if e.is_retryable() {
errors.push(e);
} else {
return Err(e);
}
}
}
}
Err(TranslationError::MaxRetriesExceeded {
attempts: options.max_retries + 1,
errors,
})
}
async fn try_translate_single(
&self,
text: &str,
target_lang: &LanguageIdentifier,
source_lang: Option<&LanguageIdentifier>,
options: &TranslateOptions,
) -> Result<String, TranslationError> {
if self.config.api_keys.is_empty() {
return Err(TranslationError::ConfigurationError(
"No API keys configured".to_string(),
));
}
let key_index = self.get_next_key_index().await;
let selected_key = &self.config.api_keys[key_index];
let tracker = &self.key_trackers[key_index];
let _permit =
tracker.semaphore.acquire().await.map_err(|e| {
TranslationError::Other(format!("Failed to acquire semaphore: {}", e))
})?;
self.wait_for_rate_limit(tracker).await;
let client = if let Some(timeout) = options.timeout {
Client::builder()
.timeout(timeout)
.build()
.map_err(|e| TranslationError::NetworkError(e))?
} else {
self.client.clone()
};
let source_lang_str = source_lang.map(|s| s.to_string());
let system_prompt =
self.get_system_prompt(&target_lang.to_string(), source_lang_str.as_deref());
let request = Request {
model: self.config.model.clone(),
messages: vec![
Message {
role: "system".to_string(),
content: system_prompt,
},
Message {
role: "user".to_string(),
content: text.to_string(),
},
],
temperature: 0.0,
};
let response = client
.post(&format!("{}/chat/completions", self.config.base_url))
.header("Authorization", format!("Bearer {}", selected_key))
.header("Content-Type", "application/json")
.json(&request)
.send()
.await?;
if !response.status().is_success() {
let status = response.status();
let body = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
return Err(TranslationError::HttpError { status, body });
}
let response_body: Response = response.json().await?;
response_body
.choices
.into_iter()
.next()
.map(|c| c.message.content)
.ok_or_else(|| {
TranslationError::ServiceError("No translation results returned".to_string())
})
}
pub async fn translate_text(
&self,
text: &str,
target_lang: &LanguageIdentifier,
source_lang: Option<&LanguageIdentifier>,
options: &TranslateOptions,
) -> Result<String, TranslationError> {
self.translate_text_with_retry(text, target_lang, source_lang, options)
.await
}
}
#[async_trait::async_trait]
impl Translator for OpenAITranslator {
async fn translate_with_options(
&self,
text: &str,
target_lang: &LanguageIdentifier,
source_lang: Option<&LanguageIdentifier>,
options: &TranslateOptions,
) -> Result<String, TranslationError> {
self.translate_text(text, target_lang, source_lang, options)
.await
}
}
#[cfg(test)]
mod tests;