use std::env;
use serde::{Deserialize, Serialize};
use crate::builder::llm::LlmProvider;
use crate::errors::{AigentError, Result};
const DEFAULT_MODEL: &str = "gpt-4o";
const DEFAULT_BASE_URL: &str = "https://api.openai.com/v1";
pub struct OpenAiProvider {
api_key: String,
base_url: String,
model: String,
}
impl OpenAiProvider {
pub fn from_env() -> Option<Self> {
let api_key = env::var("OPENAI_API_KEY").ok()?;
if api_key.is_empty() {
return None;
}
let base_url = env::var("OPENAI_API_BASE")
.or_else(|_| env::var("OPENAI_BASE_URL"))
.ok()
.filter(|s| !s.is_empty())
.unwrap_or_else(|| DEFAULT_BASE_URL.to_string());
let model = env::var("OPENAI_MODEL")
.ok()
.filter(|s| !s.is_empty())
.unwrap_or_else(|| DEFAULT_MODEL.to_string());
Some(Self {
api_key,
base_url,
model,
})
}
}
#[derive(Serialize)]
struct Message {
role: String,
content: String,
}
#[derive(Serialize)]
struct RequestBody {
model: String,
messages: Vec<Message>,
}
#[derive(Deserialize)]
struct Choice {
message: ChoiceMessage,
}
#[derive(Deserialize)]
struct ChoiceMessage {
content: String,
}
#[derive(Deserialize)]
struct ResponseBody {
choices: Vec<Choice>,
}
impl LlmProvider for OpenAiProvider {
fn generate(&self, system: &str, user: &str) -> Result<String> {
let url = format!("{}/chat/completions", self.base_url.trim_end_matches('/'));
let body = RequestBody {
model: self.model.clone(),
messages: vec![
Message {
role: "system".to_string(),
content: system.to_string(),
},
Message {
role: "user".to_string(),
content: user.to_string(),
},
],
};
let mut response = ureq::post(&url)
.header("Authorization", &format!("Bearer {}", self.api_key))
.header("Content-Type", "application/json")
.send_json(&body)
.map_err(|e| AigentError::Build {
message: format!("OpenAI API request failed: {e}"),
})?;
let resp: ResponseBody =
response
.body_mut()
.read_json()
.map_err(|e| AigentError::Build {
message: format!("OpenAI API response parse failed: {e}"),
})?;
resp.choices
.into_iter()
.next()
.map(|c| c.message.content)
.ok_or_else(|| AigentError::Build {
message: "OpenAI API returned empty choices".to_string(),
})
}
}