use crate::memory::SymbolicContext;
use crate::ops::{OperatorMetadata, SomaOperator, UncertaintyModel};
use anyhow::{anyhow, Result};
use serde_json::{json, Value};
use std::collections::HashMap;
use std::env;
pub struct LLMOperator {
pub id: String,
pub model: String,
pub provider: String, pub api_key: String,
}
impl SomaOperator for LLMOperator {
fn execute(&self, inputs: &SymbolicContext) -> Result<SymbolicContext> {
let prompt = inputs.resolve_or_default("prompt", "");
if env::var("DEBUG_MODE").unwrap_or_default() == "true"
&& env::var("ENVIRONMENT").unwrap_or_default() == "dev"
{
println!(
"🔍 [DEBUG] Processing prompt for provider: {}",
self.provider
);
}
let use_mock = env::var("USE_MOCK_RESPONSES").unwrap_or_default() == "true";
let output = if use_mock {
format!(
"[MOCK_{}:{}] Mock response generated",
self.provider.to_uppercase(),
self.model
)
} else {
if !is_api_key_valid(&self.api_key, &self.provider) {
return Err(anyhow!(
"Invalid or missing API key for provider: {}",
self.provider
));
}
match self.provider.as_str() {
"gpt" => call_openai(&self.model, &self.api_key, &prompt)?,
"claude" => call_anthropic(&self.model, &self.api_key, &prompt)?,
"gemini" => call_google(&self.model, &self.api_key, &prompt)?,
"deepseek" => call_deepseek(&self.model, &self.api_key, &prompt)?,
"mistral" => call_mistral(&self.model, &self.api_key, &prompt)?,
"grok" => call_grok(&self.model, &self.api_key, &prompt)?,
_ => return Err(anyhow!("Unknown LLM provider: {}", self.provider)),
}
};
let mut ctx = SymbolicContext::new();
ctx.set("response", &output);
Ok(ctx)
}
fn metadata(&self) -> OperatorMetadata {
OperatorMetadata {
name: self.id.clone(),
description: format!("LLM operator for {}", self.provider),
category: "llm".to_string(),
}
}
fn cognitive_cost(&self) -> f64 {
3.0 }
fn uncertainty_propagation(&self) -> UncertaintyModel {
UncertaintyModel {
entropy: 0.15,
source: self.provider.clone(),
}
}
}
fn is_api_key_valid(api_key: &str, provider: &str) -> bool {
if api_key.is_empty() {
return false;
}
let placeholder_patterns = [
"your_",
"placeholder",
"example",
"test_key",
"fake_key",
"demo_key",
];
for pattern in &placeholder_patterns {
if api_key.to_lowercase().contains(pattern) {
return false;
}
}
match provider {
"gpt" => api_key.starts_with("sk-") && api_key.len() > 20,
"claude" => api_key.starts_with("sk-ant-") && api_key.len() > 30,
"gemini" => api_key.starts_with("AIza") && api_key.len() > 30,
"deepseek" => api_key.len() > 20, "mistral" => api_key.len() > 20, "grok" => api_key.len() > 20, _ => false,
}
}
#[allow(dead_code)]
fn sanitize_api_key_for_logging(api_key: &str) -> String {
if api_key.len() < 8 {
return "***".to_string();
}
format!("{}***{}", &api_key[..4], &api_key[api_key.len() - 4..])
}
async fn call_openai_async(model: &str, api_key: &str, prompt: &str) -> Result<String> {
let client = reqwest::Client::new();
let max_tokens: u32 = env::var("MAX_TOKENS")
.unwrap_or_default()
.parse()
.unwrap_or(2000);
let temperature: f32 = env::var("TEMPERATURE")
.unwrap_or_default()
.parse()
.unwrap_or(0.7);
let timeout_secs: u64 = env::var("API_TIMEOUT")
.unwrap_or_default()
.parse()
.unwrap_or(30);
let request_body = json!({
"model": model,
"messages": [
{
"role": "user",
"content": prompt
}
],
"max_tokens": max_tokens,
"temperature": temperature
});
let response = client
.post("https://api.openai.com/v1/chat/completions")
.header("Authorization", format!("Bearer {}", api_key))
.header("Content-Type", "application/json")
.timeout(std::time::Duration::from_secs(timeout_secs))
.json(&request_body)
.send()
.await
.map_err(|e| anyhow!("OpenAI API request failed: {}", e))?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
if env::var("LOG_API_REQUESTS").unwrap_or_default() == "true" {
println!("❌ OpenAI API Error [{}]: {}", status, error_text);
}
return Err(anyhow!(
"OpenAI API error [{}]: Authentication or quota issue",
status
));
}
let response_json: Value = response
.json()
.await
.map_err(|e| anyhow!("Failed to parse OpenAI response: {}", e))?;
let content = response_json["choices"][0]["message"]["content"]
.as_str()
.unwrap_or("No response")
.to_string();
Ok(content)
}
fn call_openai(model: &str, api_key: &str, prompt: &str) -> Result<String> {
tokio::runtime::Runtime::new()?.block_on(call_openai_async(model, api_key, prompt))
}
async fn call_anthropic_async(model: &str, api_key: &str, prompt: &str) -> Result<String> {
let client = reqwest::Client::new();
let max_tokens: u32 = env::var("MAX_TOKENS")
.unwrap_or_default()
.parse()
.unwrap_or(2000);
let timeout_secs: u64 = env::var("API_TIMEOUT")
.unwrap_or_default()
.parse()
.unwrap_or(30);
let request_body = json!({
"model": model,
"max_tokens": max_tokens,
"messages": [
{
"role": "user",
"content": prompt
}
]
});
let response = client
.post("https://api.anthropic.com/v1/messages")
.header("x-api-key", api_key)
.header("Content-Type", "application/json")
.header("anthropic-version", "2023-06-01")
.timeout(std::time::Duration::from_secs(timeout_secs))
.json(&request_body)
.send()
.await
.map_err(|e| anyhow!("Anthropic API request failed: {}", e))?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
if env::var("LOG_API_REQUESTS").unwrap_or_default() == "true" {
println!("❌ Anthropic API Error [{}]: {}", status, error_text);
}
return Err(anyhow!(
"Anthropic API error [{}]: Authentication or quota issue",
status
));
}
let response_json: Value = response
.json()
.await
.map_err(|e| anyhow!("Failed to parse Anthropic response: {}", e))?;
let content = response_json["content"][0]["text"]
.as_str()
.unwrap_or("No response")
.to_string();
Ok(content)
}
fn call_anthropic(model: &str, api_key: &str, prompt: &str) -> Result<String> {
tokio::runtime::Runtime::new()?.block_on(call_anthropic_async(model, api_key, prompt))
}
async fn call_google_async(model: &str, api_key: &str, prompt: &str) -> Result<String> {
let client = reqwest::Client::new();
let timeout_secs: u64 = env::var("API_TIMEOUT")
.unwrap_or_default()
.parse()
.unwrap_or(30);
let request_body = json!({
"contents": [
{
"parts": [
{
"text": prompt
}
]
}
]
});
let url = format!(
"https://generativelanguage.googleapis.com/v1/models/{}:generateContent?key={}",
model, api_key
);
let response = client
.post(&url)
.header("Content-Type", "application/json")
.timeout(std::time::Duration::from_secs(timeout_secs))
.json(&request_body)
.send()
.await
.map_err(|e| anyhow!("Google API request failed: {}", e))?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
if env::var("LOG_API_REQUESTS").unwrap_or_default() == "true" {
println!("❌ Google API Error [{}]: {}", status, error_text);
}
return Err(anyhow!(
"Google API error [{}]: Authentication or quota issue",
status
));
}
let response_json: Value = response
.json()
.await
.map_err(|e| anyhow!("Failed to parse Google response: {}", e))?;
let content = response_json["candidates"][0]["content"]["parts"][0]["text"]
.as_str()
.unwrap_or("No response")
.to_string();
Ok(content)
}
fn call_google(model: &str, api_key: &str, prompt: &str) -> Result<String> {
tokio::runtime::Runtime::new()?.block_on(call_google_async(model, api_key, prompt))
}
async fn call_deepseek_async(model: &str, api_key: &str, prompt: &str) -> Result<String> {
let client = reqwest::Client::new();
let max_tokens: u32 = env::var("MAX_TOKENS")
.unwrap_or_default()
.parse()
.unwrap_or(2000);
let temperature: f32 = env::var("TEMPERATURE")
.unwrap_or_default()
.parse()
.unwrap_or(0.7);
let timeout_secs: u64 = env::var("API_TIMEOUT")
.unwrap_or_default()
.parse()
.unwrap_or(30);
let request_body = json!({
"model": model,
"messages": [
{
"role": "user",
"content": prompt
}
],
"max_tokens": max_tokens,
"temperature": temperature,
"stream": false
});
let response = client
.post("https://api.deepseek.com/v1/chat/completions")
.header("Authorization", format!("Bearer {}", api_key))
.header("Content-Type", "application/json")
.timeout(std::time::Duration::from_secs(timeout_secs))
.json(&request_body)
.send()
.await
.map_err(|e| anyhow!("DeepSeek API request failed: {}", e))?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
if env::var("LOG_API_REQUESTS").unwrap_or_default() == "true" {
println!("❌ DeepSeek API Error [{}]: {}", status, error_text);
}
return Err(anyhow!(
"DeepSeek API error [{}]: Authentication or quota issue",
status
));
}
let response_json: Value = response
.json()
.await
.map_err(|e| anyhow!("Failed to parse DeepSeek response: {}", e))?;
let content = response_json["choices"][0]["message"]["content"]
.as_str()
.unwrap_or("No response")
.to_string();
Ok(content)
}
fn call_deepseek(model: &str, api_key: &str, prompt: &str) -> Result<String> {
if !is_api_key_valid(api_key, "deepseek") {
return Err(anyhow!("DeepSeek API key not configured or invalid"));
}
tokio::runtime::Runtime::new()?.block_on(call_deepseek_async(model, api_key, prompt))
}
async fn call_mistral_async(model: &str, api_key: &str, prompt: &str) -> Result<String> {
let client = reqwest::Client::new();
let max_tokens: u32 = env::var("MAX_TOKENS")
.unwrap_or_default()
.parse()
.unwrap_or(2000);
let temperature: f32 = env::var("TEMPERATURE")
.unwrap_or_default()
.parse()
.unwrap_or(0.7);
let timeout_secs: u64 = env::var("API_TIMEOUT")
.unwrap_or_default()
.parse()
.unwrap_or(30);
let request_body = json!({
"model": model,
"messages": [
{
"role": "user",
"content": prompt
}
],
"max_tokens": max_tokens,
"temperature": temperature,
"stream": false
});
let response = client
.post("https://api.mistral.ai/v1/chat/completions")
.header("Authorization", format!("Bearer {}", api_key))
.header("Content-Type", "application/json")
.timeout(std::time::Duration::from_secs(timeout_secs))
.json(&request_body)
.send()
.await
.map_err(|e| anyhow!("Mistral API request failed: {}", e))?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
if env::var("LOG_API_REQUESTS").unwrap_or_default() == "true" {
println!("❌ Mistral API Error [{}]: {}", status, error_text);
}
return Err(anyhow!(
"Mistral API error [{}]: Authentication or quota issue",
status
));
}
let response_json: Value = response
.json()
.await
.map_err(|e| anyhow!("Failed to parse Mistral response: {}", e))?;
let content = response_json["choices"][0]["message"]["content"]
.as_str()
.unwrap_or("No response")
.to_string();
Ok(content)
}
fn call_mistral(model: &str, api_key: &str, prompt: &str) -> Result<String> {
if !is_api_key_valid(api_key, "mistral") {
return Err(anyhow!("Mistral API key not configured or invalid"));
}
tokio::runtime::Runtime::new()?.block_on(call_mistral_async(model, api_key, prompt))
}
async fn call_grok_async(model: &str, api_key: &str, prompt: &str) -> Result<String> {
let client = reqwest::Client::new();
let max_tokens: u32 = env::var("MAX_TOKENS")
.unwrap_or_default()
.parse()
.unwrap_or(2000);
let temperature: f32 = env::var("TEMPERATURE")
.unwrap_or_default()
.parse()
.unwrap_or(0.7);
let timeout_secs: u64 = env::var("API_TIMEOUT")
.unwrap_or_default()
.parse()
.unwrap_or(30);
let request_body = json!({
"model": model,
"messages": [
{
"role": "user",
"content": prompt
}
],
"max_tokens": max_tokens,
"temperature": temperature,
"stream": false
});
let response = client
.post("https://api.x.ai/v1/chat/completions")
.header("Authorization", format!("Bearer {}", api_key))
.header("Content-Type", "application/json")
.timeout(std::time::Duration::from_secs(timeout_secs))
.json(&request_body)
.send()
.await
.map_err(|e| anyhow!("Grok API request failed: {}", e))?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
if env::var("LOG_API_REQUESTS").unwrap_or_default() == "true" {
println!("❌ Grok API Error [{}]: {}", status, error_text);
}
return Err(anyhow!(
"Grok API error [{}]: Authentication or quota issue",
status
));
}
let response_json: Value = response
.json()
.await
.map_err(|e| anyhow!("Failed to parse Grok response: {}", e))?;
let content = response_json["choices"][0]["message"]["content"]
.as_str()
.unwrap_or("No response")
.to_string();
Ok(content)
}
fn call_grok(model: &str, api_key: &str, prompt: &str) -> Result<String> {
if !is_api_key_valid(api_key, "grok") {
return Err(anyhow!("Grok API key not configured or invalid"));
}
tokio::runtime::Runtime::new()?.block_on(call_grok_async(model, api_key, prompt))
}
pub fn init_env() {
dotenv::dotenv().ok();
if env::var("VALIDATE_API_KEYS").unwrap_or_default() == "true" {
validate_environment_security();
}
}
fn validate_environment_security() {
let environment = env::var("ENVIRONMENT").unwrap_or_else(|_| "dev".to_string());
if environment == "prod" {
if env::var("USE_MOCK_RESPONSES").unwrap_or_default() == "true" {
println!("⚠️ WARNING: Mock responses enabled in production environment!");
}
if env::var("DEBUG_MODE").unwrap_or_default() == "true" {
println!("⚠️ WARNING: Debug mode enabled in production environment!");
}
if env::var("LOG_API_REQUESTS").unwrap_or_default() == "true" {
println!("⚠️ WARNING: API request logging enabled in production - may expose sensitive data!");
}
}
let providers = ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GOOGLE_API_KEY"];
let configured_providers: Vec<_> = providers
.iter()
.filter(|&&key| {
let value = env::var(key).unwrap_or_default();
!value.is_empty() && !value.contains("your_") && !value.contains("placeholder")
})
.collect();
if configured_providers.is_empty() {
println!(
"⚠️ WARNING: No valid API keys configured. Set USE_MOCK_RESPONSES=true for testing."
);
} else {
println!(
"✅ Environment validation complete. {} provider(s) configured.",
configured_providers.len()
);
}
}
pub fn llm_registry() -> HashMap<String, Box<dyn SomaOperator>> {
init_env();
let mut reg = HashMap::new();
let openai_key = env::var("OPENAI_API_KEY").unwrap_or_default();
let anthropic_key = env::var("ANTHROPIC_API_KEY").unwrap_or_default();
let google_key = env::var("GOOGLE_API_KEY").unwrap_or_default();
let deepseek_key = env::var("DEEPSEEK_API_KEY").unwrap_or_default();
let mistral_key = env::var("MISTRAL_API_KEY").unwrap_or_default();
let grok_key = env::var("GROK_API_KEY").unwrap_or_default();
let gpt_model = env::var("DEFAULT_GPT_MODEL").unwrap_or_else(|_| "gpt-4o-mini".to_string());
let claude_model = env::var("DEFAULT_CLAUDE_MODEL")
.unwrap_or_else(|_| "claude-3-5-sonnet-20241022".to_string());
let gemini_model =
env::var("DEFAULT_GEMINI_MODEL").unwrap_or_else(|_| "gemini-1.5-flash".to_string());
reg.insert(
"gpt_reflect".into(),
Box::new(LLMOperator {
id: "gpt_reflect".into(),
model: gpt_model.clone(),
provider: "gpt".into(),
api_key: openai_key.clone(),
}) as Box<dyn SomaOperator>,
);
reg.insert(
"claude_plan".into(),
Box::new(LLMOperator {
id: "claude_plan".into(),
model: claude_model.clone(),
provider: "claude".into(),
api_key: anthropic_key.clone(),
}) as Box<dyn SomaOperator>,
);
reg.insert(
"gemini_insight".into(),
Box::new(LLMOperator {
id: "gemini_insight".into(),
model: gemini_model.clone(),
provider: "gemini".into(),
api_key: google_key.clone(),
}) as Box<dyn SomaOperator>,
);
reg.insert(
"deepseek_reason".into(),
Box::new(LLMOperator {
id: "deepseek_reason".into(),
model: "deepseek-coder".into(),
provider: "deepseek".into(),
api_key: deepseek_key,
}) as Box<dyn SomaOperator>,
);
reg.insert(
"mistral_solve".into(),
Box::new(LLMOperator {
id: "mistral_solve".into(),
model: "mistral-large-latest".into(),
provider: "mistral".into(),
api_key: mistral_key,
}) as Box<dyn SomaOperator>,
);
reg.insert(
"grok_judge".into(),
Box::new(LLMOperator {
id: "grok_judge".into(),
model: "grok-1.5".into(),
provider: "grok".into(),
api_key: grok_key,
}) as Box<dyn SomaOperator>,
);
reg
}
#[cfg(test)]
mod tests {
use super::*;
use crate::memory::SymbolicContext;
#[test]
fn test_llm_operator_registry_and_execution() {
env::set_var("USE_MOCK_RESPONSES", "true");
env::set_var("ENVIRONMENT", "test");
let registry = llm_registry();
let mut ctx = SymbolicContext::new();
ctx.set("prompt", "Test prompt for unit testing");
let gpt = registry.get("gpt_reflect").unwrap();
let result_ctx = gpt.execute(&ctx).unwrap();
let response = result_ctx.get("response").unwrap();
assert!(response.contains("MOCK_GPT"));
let claude = registry.get("claude_plan").unwrap();
let result_ctx = claude.execute(&ctx).unwrap();
let response = result_ctx.get("response").unwrap();
assert!(response.contains("MOCK_CLAUDE"));
}
#[test]
fn test_api_key_validation() {
assert!(is_api_key_valid(
"sk-proj-1234567890abcdefghijklmnop",
"gpt"
));
assert!(is_api_key_valid(
"sk-ant-1234567890abcdefghijklmnopqrstuvwxyz",
"claude"
));
assert!(is_api_key_valid(
"AIza1234567890abcdefghijklmnopqrstuvwxyz",
"gemini"
));
assert!(!is_api_key_valid("", "gpt"));
assert!(!is_api_key_valid("your_openai_api_key_here", "gpt"));
assert!(!is_api_key_valid("placeholder_key", "claude"));
assert!(!is_api_key_valid("sk-", "gpt")); }
#[test]
fn test_env_initialization() {
env::set_var("ENVIRONMENT", "test");
init_env();
assert!(true);
}
}