mod admin;
mod aggregate;
mod auth;
mod me;
mod pricing;
mod routes;
mod tokens;
use std::process::ExitCode;
use std::sync::Arc;
use agentix::server::{AnthropicServer, UsageLogger};
#[tokio::main]
async fn main() -> ExitCode {
init_tracing();
let listen: String = std::env::args()
.nth(1)
.unwrap_or_else(|| "127.0.0.1:7878".to_string());
let tokens_path = env_required("TOKENS_FILE");
let admin_password = env_required("ADMIN_PASSWORD");
let usage_log_path = env_required("USAGE_LOG");
let config_path = env_required("AAAGW_CONFIG");
if tokens_path.is_none()
|| admin_password.is_none()
|| usage_log_path.is_none()
|| config_path.is_none()
{
eprintln!(
"set env vars: TOKENS_FILE=tokens.toml ADMIN_PASSWORD=<pwd> \
USAGE_LOG=/path.jsonl AAAGW_CONFIG=aaagw.toml"
);
return ExitCode::from(2);
}
let tokens_path = tokens_path.unwrap();
let admin_password = admin_password.unwrap();
let usage_log_path = usage_log_path.unwrap();
let config_path = config_path.unwrap();
let routes = match routes::RoutesHandle::load(&config_path) {
Ok(r) => {
tracing::info!(path = %config_path, count = r.len(), "routes loaded");
r
}
Err(e) => {
eprintln!("routes: {e}");
return ExitCode::from(2);
}
};
let token_registry = match tokens::TokenRegistry::from_file(&tokens_path) {
Ok(r) => {
tracing::info!(path = %tokens_path, count = r.len(), "tokens loaded");
r
}
Err(e) => {
eprintln!("failed to read {tokens_path}: {e}");
return ExitCode::FAILURE;
}
};
let usage_logger = match UsageLogger::open(&usage_log_path, true) {
Ok(l) => Arc::new(l),
Err(e) => {
eprintln!("failed to open usage log {usage_log_path}: {e}");
return ExitCode::FAILURE;
}
};
let pricing_cache = env_required("PRICING_CACHE").unwrap_or_else(|| {
std::path::Path::new(&config_path)
.parent()
.map(|d| d.join("pricing.json"))
.unwrap_or_else(|| std::path::PathBuf::from("pricing.json"))
.to_string_lossy()
.into_owned()
});
let pricing = pricing::PricingHandle::load(&pricing_cache).await;
tracing::info!(path = %pricing_cache, models = pricing.len(), "price book ready");
pricing.spawn_periodic_refresh();
let resolver: Arc<dyn agentix::server::fallback::ChainResolver> = Arc::new(routes.clone());
let mut router = axum::Router::new();
let anthropic = AnthropicServer::with_resolver(resolver.clone())
.with_usage_logger(usage_logger.clone());
router = router.merge(anthropic.router());
#[cfg(feature = "server-openai-chat")]
{
use agentix::server::OpenAIChatServer;
let openai = OpenAIChatServer::with_resolver(resolver.clone())
.with_usage_logger(usage_logger.clone());
router = router.merge(openai.router());
}
#[cfg(feature = "server-openai-responses")]
{
use agentix::server::OpenAIResponsesServer;
let resp = OpenAIResponsesServer::with_resolver(resolver.clone())
.with_usage_logger(usage_logger.clone());
router = router.merge(resp.router());
}
let _ = resolver;
let token_layer = auth::token_auth_layer(token_registry.clone());
let quota = auth::quota_layer(
token_registry.clone(),
std::path::PathBuf::from(&usage_log_path),
);
router = router
.layer(axum::middleware::from_fn(quota))
.layer(axum::middleware::from_fn(token_layer));
let me_server = me::MeServer::new(
usage_log_path.clone(),
token_registry.clone(),
routes.clone(),
pricing.clone(),
);
router = router.merge(me_server.router());
let admin_server = admin::AdminServer::new(
usage_log_path.clone(),
admin_password,
token_registry,
routes.clone(),
pricing,
);
router = router.merge(admin_server.router());
let listener = match tokio::net::TcpListener::bind(listen.as_str()).await {
Ok(l) => l,
Err(e) => {
eprintln!("bind {listen}: {e}");
return ExitCode::FAILURE;
}
};
let local = listener.local_addr().unwrap();
tracing::info!(%local, "14_admin_relay listening");
let serve = axum::serve(listener, router).with_graceful_shutdown(async {
let _ = tokio::signal::ctrl_c().await;
tracing::info!("shutdown");
});
if let Err(e) = serve.await {
eprintln!("server: {e}");
return ExitCode::FAILURE;
}
let _ = usage_logger;
ExitCode::SUCCESS
}
fn env_required(k: &str) -> Option<String> {
std::env::var(k).ok().filter(|s| !s.is_empty())
}
fn init_tracing() {
use tracing_subscriber::{EnvFilter, fmt};
let filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info,agentix=info"));
let _ = fmt().with_env_filter(filter).try_init();
}