use crate::nat::{NatConfig, NatResult};
use crate::provider::Provider;
use crate::server::{build_router, ServeConfig};
#[derive(Debug, Clone)]
pub struct ServiceInfo {
pub local_url: String,
pub public_url: Option<String>,
pub models: Vec<String>,
pub nat: Option<NatResult>,
}
pub struct ProviderServiceBuilder {
provider: Provider,
server_config: ServeConfig,
nat_config: Option<NatConfig>,
}
impl ProviderServiceBuilder {
pub fn new(provider: Provider) -> Self {
Self {
provider,
server_config: ServeConfig::default(),
nat_config: None,
}
}
pub fn port(mut self, port: u16) -> Self {
self.server_config.port = port;
self
}
pub fn host(mut self, host: &str) -> Self {
self.server_config.host = host.to_string();
self
}
pub fn token(mut self, token: &str) -> Self {
self.server_config.auth_token = Some(token.to_string());
self
}
pub fn server_config(mut self, config: ServeConfig) -> Self {
self.server_config = config;
self
}
pub fn nat(mut self) -> Self {
self.nat_config = Some(NatConfig::default());
self
}
pub fn nat_config(mut self, config: NatConfig) -> Self {
self.nat_config = Some(config);
self
}
pub async fn start(self) -> Result<ServiceInfo, crate::Error> {
let local_url = format!("http://{}:{}", self.server_config.host, self.server_config.port);
let models: Vec<String> = match self.provider.models().await {
Ok(m) => m.into_iter().map(|m| m.name).collect(),
Err(e) => {
eprintln!("Warning: could not fetch models from backend: {}", e);
Vec::new()
}
};
let nat = if let Some(ref nat_config) = self.nat_config {
let result = crate::nat::discover_and_map(nat_config, self.server_config.port).await;
Some(result)
} else {
None
};
let public_url = nat.as_ref().and_then(|n| n.public_url.clone());
let info = ServiceInfo {
local_url: local_url.clone(),
public_url,
models,
nat,
};
println!("ai_assistant_core provider service");
println!("──────────────────────────────────");
println!(" Local: {}", info.local_url);
if let Some(ref pub_url) = info.public_url {
println!(" Public: {}", pub_url);
}
if !info.models.is_empty() {
println!(" Models: {}", info.models.join(", "));
}
if let Some(ref nat) = info.nat {
println!(" NAT type: {}", nat.nat_type);
if nat.upnp_success {
println!(" UPnP: port mapped");
}
if nat.nat_pmp_success {
println!(" NAT-PMP: port mapped");
}
}
if info.public_url.is_none() && self.nat_config.is_some() {
println!(" Note: NAT traversal attempted but no public URL available");
}
println!("──────────────────────────────────");
println!(" Endpoints:");
println!(" GET /health");
println!(" GET /v1/models");
println!(" POST /v1/chat/completions");
if self.server_config.auth_token.is_some() {
println!(" Auth: Bearer token required");
}
println!();
let router = build_router(self.provider, &self.server_config);
let addr = format!("{}:{}", self.server_config.host, self.server_config.port);
let listener = tokio::net::TcpListener::bind(&addr)
.await
.map_err(|e| crate::Error::Provider(format!("Failed to bind {}: {}", addr, e)))?;
println!("Listening on {} — press Ctrl+C to stop", addr);
axum::serve(listener, router)
.with_graceful_shutdown(shutdown_signal())
.await
.map_err(|e| crate::Error::Provider(format!("Server error: {}", e)))?;
println!("\nServer stopped.");
Ok(info)
}
}
async fn shutdown_signal() {
tokio::signal::ctrl_c()
.await
.expect("Failed to install Ctrl+C handler");
}
pub async fn quick(provider: Provider) -> Result<ServiceInfo, crate::Error> {
ProviderServiceBuilder::new(provider).start().await
}