pub mod adapters;
pub mod clients;
pub mod cloud_detect;
pub mod config;
pub mod core;
pub mod dev_console;
pub mod error;
pub mod integrations;
pub mod middleware;
pub mod pricing;
pub mod scanner;
pub mod schema;
pub mod security;
pub mod transport;
use std::sync::{Arc, OnceLock};
use tokio::sync::Mutex;
pub use config::Config;
pub use core::context::{
clear_dexcost_context, create_auto_task, get_current_task, get_dexcost_context, set_context,
with_task, DexcostContext,
};
pub use core::heuristics::RetryHeuristicEngine;
pub use core::models::{CostConfidence, CostEvent, EventType, PricingSource, Task, TaskStatus};
pub use core::session::{get_session_manager, SessionManager};
pub use core::tracker::{
HeuristicConfig, RecordCostOptions, RecordLlmCallOptions, TaskOptions, TrackedTask,
};
pub use error::DexcostError;
pub use pricing::engine::{CostResult, PricingEngine};
pub use pricing::rates::{RateEntry, RateRegistry};
pub use pricing::service_catalog::ServiceCatalog;
pub use schema::validate::validate;
pub use security::redaction::{
enforce_metadata_limit, hash_value, redact_map, scrub_url, scrub_urls_in_text,
};
pub use transport::buffer::EventBuffer;
pub use transport::pusher::EventPusher;
pub use clients::tracked_anthropic::TrackedAnthropic;
pub use clients::tracked_gemini::TrackedGemini;
pub use clients::tracked_openai::TrackedOpenAI;
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const ALL_SUPPORTED_INSTRUMENTS: &[&str] = &["openai", "anthropic", "gemini"];
struct SdkState {
buffer: Arc<Mutex<EventBuffer>>,
pricing: Arc<Mutex<PricingEngine>>,
rate_registry: Arc<Mutex<RateRegistry>>,
service_catalog: Option<Arc<Mutex<ServiceCatalog>>>,
#[allow(dead_code)]
config: Config,
pusher: Option<EventPusher>,
}
static GLOBAL_STATE: OnceLock<SdkState> = OnceLock::new();
pub fn init(mut config: Config) -> Result<(), DexcostError> {
config.validate()?;
let buffer = {
let db_path = config
.buffer_path
.as_ref()
.map(|p| p.to_string_lossy().into_owned())
.or_else(|| std::env::var("DEXCOST_BUFFER_PATH").ok())
.or_else(|| {
dirs_next::home_dir().map(|h| {
h.join(".dexcost")
.join("buffer.db")
.to_string_lossy()
.into_owned()
})
});
match db_path {
Some(path) => match EventBuffer::open(&path) {
Ok(buf) => Arc::new(Mutex::new(buf)),
Err(e) => {
eprintln!("[dexcost] WARNING: failed to open buffer at {}: {}, falling back to in-memory", path, e);
Arc::new(Mutex::new(EventBuffer::new()?))
}
},
None => Arc::new(Mutex::new(EventBuffer::new()?)),
}
};
let pricing = Arc::new(Mutex::new(PricingEngine::new()));
let rate_registry = Arc::new(Mutex::new(RateRegistry::new()));
crate::cloud_detect::start_background_detection(config.track_http);
let service_catalog = if config.track_http {
Some(Arc::new(Mutex::new(ServiceCatalog::new())))
} else {
None
};
if let (Some(url), Some(catalog)) =
(config.service_catalog_url.clone(), service_catalog.clone())
{
tokio::spawn(async move {
let mut cat = catalog.lock().await;
if let Err(e) = cat.refresh_from_url(&url).await {
eprintln!(
"[dexcost] WARNING: service catalog refresh from {} failed: {}",
url, e
);
}
});
}
let pusher = if config.api_key.is_some() {
Some(EventPusher::new(buffer.clone(), config.clone()))
} else {
None
};
GLOBAL_STATE
.set(SdkState {
buffer,
pricing,
rate_registry,
service_catalog,
config,
pusher,
})
.map_err(|_| DexcostError::AlreadyInitialized)?;
if let Some(state) = GLOBAL_STATE.get() {
if let Some(ref p) = state.pusher {
drop(p.start());
}
}
Ok(())
}
pub fn service_catalog() -> Result<Option<Arc<Mutex<ServiceCatalog>>>, DexcostError> {
let state = get_state()?;
Ok(state.service_catalog.clone())
}
fn get_state() -> Result<&'static SdkState, DexcostError> {
GLOBAL_STATE.get().ok_or(DexcostError::NotInitialized)
}
pub async fn start_task(task_type: &str, opts: TaskOptions) -> Result<TrackedTask, DexcostError> {
let state = get_state()?;
let mut task = Task::new(task_type);
task.status = TaskStatus::Running;
task.customer_id = opts.customer_id;
task.project_id = opts.project_id;
task.experiment_id = opts.experiment_id;
task.variant = opts.variant;
if let Some(metadata) = opts.metadata {
task.metadata = metadata;
}
if opts.parent_task_id.is_some() {
task.parent_task_id = opts.parent_task_id;
} else if let Some(parent) = get_current_task() {
task.parent_task_id = Some(parent.task_id);
}
{
let mut buf = state.buffer.lock().await;
buf.upsert_task(task.clone());
}
if let Some(hcfg) = opts.heuristics {
let engine = Arc::new(std::sync::Mutex::new(RetryHeuristicEngine::new(
hcfg.window_seconds,
hcfg.threshold,
)?));
Ok(TrackedTask::with_heuristics(
task,
state.buffer.clone(),
Some(state.pricing.clone()),
Some(state.rate_registry.clone()),
engine,
))
} else {
Ok(TrackedTask::with_rate_registry(
task,
state.buffer.clone(),
Some(state.pricing.clone()),
Some(state.rate_registry.clone()),
))
}
}
pub async fn flush() -> Result<(), DexcostError> {
let state = get_state()?;
if let Some(ref pusher) = state.pusher {
pusher.flush().await?;
}
Ok(())
}
pub fn close() {
if let Some(state) = GLOBAL_STATE.get() {
if let Some(ref pusher) = state.pusher {
pusher.stop();
}
}
}
pub fn buffer() -> Result<Arc<Mutex<EventBuffer>>, DexcostError> {
let state = get_state()?;
Ok(state.buffer.clone())
}
pub fn pricing_engine() -> Result<Arc<Mutex<PricingEngine>>, DexcostError> {
let state = get_state()?;
Ok(state.pricing.clone())
}
pub fn rate_registry() -> Result<Arc<Mutex<RateRegistry>>, DexcostError> {
let state = get_state()?;
Ok(state.rate_registry.clone())
}