use clap::CommandFactory;
use clap::Parser;
use every_other_token::cli::Args;
use every_other_token::transforms::Transform;
use every_other_token::TokenInterceptor;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn")),
)
.with_writer(std::io::stderr)
.init();
let mut args = Args::parse();
if args.prompt.is_empty()
&& !args.web
&& !args.research
&& !args.dry_run
&& args.record.is_none()
&& args.replay.is_none()
&& !args.validate_config
&& args.list_models.is_none()
&& !args.json_schema
&& !args.diff_terminal
{
eprintln!("[eot] No prompt given — launching web UI at http://localhost:{}", args.port);
eprintln!("[eot] Tip: set OPENAI_API_KEY or ANTHROPIC_API_KEY in your environment.");
eprintln!("[eot] Run with --help for full CLI usage.");
args.web = true;
}
{
use every_other_token::config::EotConfig;
use every_other_token::providers::Provider;
let cfg = EotConfig::load();
if args.provider == Provider::Openai {
if let Some(ref p) = cfg.provider {
if let Ok(prov) = <Provider as std::str::FromStr>::from_str(p) {
args.provider = prov;
}
}
}
if args.model == "gpt-3.5-turbo" {
if let Some(m) = cfg.model {
args.model = m;
}
}
if args.transform == "reverse" {
if let Some(t) = cfg.transform {
args.transform = t;
}
}
if args.rate.is_none() {
if let Some(r) = cfg.rate {
args.rate = Some(r);
}
}
if args.port == 8888 {
if let Some(p) = cfg.port {
args.port = p;
}
}
if args.top_logprobs == 5 {
if let Some(t) = cfg.top_logprobs {
args.top_logprobs = t;
}
}
if args.system_a.is_none() {
args.system_a = cfg.system_a;
}
if args.anthropic_max_tokens == 4096 {
if let Some(t) = cfg.anthropic_max_tokens {
args.anthropic_max_tokens = t;
}
}
if args.api_key.is_none() {
args.api_key = cfg.api_key;
}
}
if args.prompt == "-" {
use std::io::Read;
let mut buf = String::new();
std::io::stdin().read_to_string(&mut buf)?;
args.prompt = buf.trim().to_string();
}
{
let model = every_other_token::cli::resolve_model(&args.provider, &args.model);
every_other_token::cli::validate_model(&args.provider, &model);
}
if args.validate_config {
use every_other_token::config::EotConfig;
let cfg = EotConfig::load();
println!("[eot config] provider: {}", args.provider);
println!("[eot config] model: {}", args.model);
println!("[eot config] transform: {}", args.transform);
println!("[eot config] rate: {}", args.rate.unwrap_or(0.5));
println!("[eot config] port: {}", args.port);
println!("[eot config] top_logprobs: {}", args.top_logprobs);
println!("[eot config] max_retries: {}", args.max_retries);
println!("[eot config] timeout: {}", args.timeout);
println!("[eot config] anthropic_max_tokens: {}", args.anthropic_max_tokens);
if let Some(ref sa) = args.system_a { println!("[eot config] system_a: {}", sa); }
drop(cfg); std::process::exit(0);
}
if let Some(ref provider_filter) = args.list_models.clone() {
let openai_models = ["gpt-3.5-turbo", "gpt-4", "gpt-4o", "gpt-4o-mini", "gpt-4-turbo"];
let anthropic_models = [
"claude-3-5-haiku-20241022",
"claude-3-5-sonnet-20241022",
"claude-opus-4-6",
"claude-sonnet-4-6",
];
let show_openai = provider_filter == "openai" || provider_filter == "all";
let show_anthropic = provider_filter == "anthropic" || provider_filter == "all";
if show_openai {
println!("[openai models]");
for m in &openai_models { println!(" {}", m); }
}
if show_anthropic {
println!("[anthropic models]");
for m in &anthropic_models { println!(" {}", m); }
}
if !show_openai && !show_anthropic {
for m in &openai_models { println!(" {}", m); }
for m in &anthropic_models { println!(" {}", m); }
}
std::process::exit(0);
}
if args.json_schema {
const RESEARCH_SCHEMA: &str = include_str!("../docs/research-schema.json");
println!("{}", RESEARCH_SCHEMA);
std::process::exit(0);
}
if let Some(ref record_path) = args.record {
if let Err(e) = std::fs::OpenOptions::new()
.create(true)
.write(true)
.open(record_path)
{
eprintln!("[eot] cannot open record file '{}': {}", record_path, e);
std::process::exit(1);
}
}
if let Some(shell) = args.completions {
clap_complete::generate(
shell,
&mut Args::command(),
"every-other-token",
&mut std::io::stdout(),
);
return Ok(());
}
if args.dry_run {
let transform = every_other_token::transforms::Transform::from_str_loose(&args.transform)
.map_err(|e| format!("Invalid transform: {}", e))?;
println!("[dry-run] Transform: {:?}", transform);
println!("[dry-run] Rate: {}", args.rate.unwrap_or(0.5));
println!("[dry-run] Sample token transformations:");
let sample_tokens = [
"The", " quick", " brown", " fox", " jumps", " over", " the", " lazy", " dog",
];
if let every_other_token::transforms::Transform::Chain(ref steps) = transform {
let step_names: Vec<String> = steps.iter().map(|s| format!("{:?}", s)).collect();
println!("Dry-run: Chain [{}]", step_names.join(", "));
for token in &sample_tokens {
println!(" Input: {:?}", token);
let mut current = token.to_string();
for step in steps {
let (next, _label) = step.apply_with_label(¤t);
println!(" After {:?}: {:?}", step, next);
current = next;
}
}
} else {
for (i, token) in sample_tokens.iter().enumerate() {
let (result, label) = transform.apply_with_label(token);
let marker = if (i % 2) == 1 {
"→ TRANSFORMED"
} else {
" (pass-through)"
};
println!(" [{}] {:15} {} {:?} ({})", i, token, marker, result, label);
}
}
return Ok(());
}
if let Some(ref tmpl) = args.template.clone() {
args.prompt = every_other_token::cli::apply_template(tmpl, &args.prompt);
}
if let Some(ref range_str) = args.rate_range.clone() {
if let Some((min, max)) = every_other_token::cli::parse_rate_range(range_str) {
use rand::Rng;
let rate = rand::thread_rng().gen_range(min..=max);
args.rate = Some(rate);
eprintln!(
"[rate-range] Selected rate: {:.4} from range [{}, {}]",
rate, min, max
);
} else {
eprintln!(
"[rate-range] Warning: could not parse rate range '{}', using --rate value",
range_str
);
}
}
if args.web {
tokio::select! {
result = every_other_token::web::serve(args.port, &args) => {
result?;
}
_ = tokio::signal::ctrl_c() => {
eprintln!("\n[eot] shutting down gracefully");
}
}
return Ok(());
}
if args.research {
if args.prompt_file.is_some() {
tokio::select! {
result = every_other_token::research::run_research_suite(&args) => {
result?;
}
_ = tokio::signal::ctrl_c() => {
eprintln!("\n[eot] interrupted");
}
}
} else {
tokio::select! {
result = every_other_token::research::run_research(&args) => {
result?;
}
_ = tokio::signal::ctrl_c() => {
eprintln!("\n[eot] interrupted — partial results may not have been written");
}
}
}
return Ok(());
}
if args.diff_terminal {
every_other_token::research::run_diff_terminal(&args).await?;
return Ok(());
}
if let Some(ref path) = args.synonym_file {
every_other_token::transforms::load_synonym_overrides(path)
.map_err(|e| format!("Failed to load synonym file '{}': {}", path, e))?;
}
let transform = Transform::from_str_loose(&args.transform)
.map_err(|e| format!("Invalid transform: {}", e))?;
let model = every_other_token::cli::resolve_model(&args.provider, &args.model);
let mut interceptor = {
let mut i = TokenInterceptor::new(
args.provider,
transform,
model,
args.visual,
args.heatmap,
args.orchestrator,
)?
.with_rate(args.rate.unwrap_or(0.5));
if let Some(seed) = args.seed {
i = i.with_seed(seed);
}
i
};
interceptor.top_logprobs = args.top_logprobs;
interceptor.json_stream = args.json_stream;
interceptor.orchestrator_url = args.orchestrator_url.clone();
interceptor.max_retries = args.max_retries;
interceptor.min_confidence = args.min_confidence;
interceptor.anthropic_max_tokens = args.anthropic_max_tokens;
if args.timeout > 0 {
interceptor = interceptor.with_timeout(args.timeout);
}
tokio::select! {
result = interceptor.intercept_stream(&args.prompt) => {
result?;
}
_ = tokio::signal::ctrl_c() => {
eprintln!("\n[eot] shutting down gracefully");
}
}
Ok(())
}