mod cli;
mod codex_cli;
mod codex_usage;
mod config;
mod http;
mod pricing;
#[cfg(test)]
mod tests;
mod types;
mod usage;
use cli::call_cli;
use codex_cli::call_codex_cli;
use http::call_http;
use pricing::estimate_tokens;
use usage::record_usage;
pub(crate) use types::TokenUsage;
pub use types::UsageContext;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum AiExecutor {
Http,
ClaudeCli,
CodexCli,
}
pub async fn call_ai(
system: &str,
user_message: &str,
ctx: UsageContext<'_>,
) -> anyhow::Result<String> {
let result = match executor_for_operation(ctx.operation) {
Some(AiExecutor::Http) => call_http(system, user_message).await,
Some(AiExecutor::ClaudeCli) => call_cli(system, user_message).await,
Some(AiExecutor::CodexCli) => call_codex_cli(system, user_message).await,
None => call_auto(system, user_message).await,
}?;
let input_tokens = estimate_tokens(system) + estimate_tokens(user_message);
let output_tokens = estimate_tokens(&result.text);
record_usage(ctx, &result, input_tokens, output_tokens);
Ok(result.text)
}
async fn call_auto(system: &str, user_message: &str) -> anyhow::Result<types::AiCallResult> {
if std::env::var("ANTHROPIC_API_KEY").is_ok() || std::env::var("ANTHROPIC_AUTH_TOKEN").is_ok() {
match call_http(system, user_message).await {
Ok(result) => Ok(result),
Err(http_err) => {
crate::log::warn(
"ai",
&format!("HTTP failed, falling back to CLI: {}", http_err),
);
call_cli(system, user_message).await
}
}
} else {
call_cli(system, user_message).await
}
}
fn executor_for_operation(operation: &str) -> Option<AiExecutor> {
if matches!(operation, "flush" | "flush-task") {
return flush_executor();
}
for key in executor_env_keys(operation) {
if let Some(executor) = executor_from_env(key) {
return Some(executor);
}
}
None
}
fn executor_env_keys(operation: &str) -> &'static [&'static str] {
match operation {
"summarize"
| "session_rollup"
| "observation_extract"
| "memory_candidate"
| "rule_candidate"
| "index_update" => &["REMEM_SUMMARY_EXECUTOR", "REMEM_EXECUTOR"],
"flush" | "flush-task" => &["REMEM_FLUSH_EXECUTOR"],
"compress" => &["REMEM_COMPRESS_EXECUTOR"],
"dream" => &["REMEM_DREAM_EXECUTOR"],
_ => &["REMEM_EXECUTOR"],
}
}
fn flush_executor() -> Option<AiExecutor> {
executor_from_env("REMEM_FLUSH_EXECUTOR").or_else(|| {
executor_from_env("REMEM_SUMMARY_EXECUTOR")
.filter(|executor| *executor == AiExecutor::CodexCli)
})
}
fn executor_from_env(key: &str) -> Option<AiExecutor> {
match std::env::var(key).ok()?.as_str() {
"http" | "anthropic-http" | "anthropic" => Some(AiExecutor::Http),
"cli" | "claude-cli" | "claude" => Some(AiExecutor::ClaudeCli),
"codex-cli" | "codex" => Some(AiExecutor::CodexCli),
_ => None,
}
}
fn stable_working_dir() -> std::path::PathBuf {
let data_dir = crate::db::data_dir();
match std::fs::create_dir_all(&data_dir) {
Ok(()) => data_dir,
Err(err) => {
crate::log::warn(
"ai",
&format!(
"failed to create AI working dir {}: {}; falling back to temp dir",
data_dir.display(),
err
),
);
std::env::temp_dir()
}
}
}