mod count;
mod discovery;
mod drive;
mod events;
mod generate;
mod masq;
mod models;
mod providers;
mod raw;
mod request;
pub use count::{count_tokens, CountIo};
pub use discovery::VERSION;
pub(crate) use discovery::{emit, HELP, SKILL, VERSION_LINE};
pub use generate::generate;
pub use masq::{serve, Bind, Listener, ServeConn, ServeIo};
#[cfg(test)]
pub(crate) use models::models_req;
pub use models::{list_models, ListIo};
pub use providers::{list_providers, ProvidersIo};
#[cfg(test)]
pub(crate) use request::append_query;
use std::io::{self, Read, Write};
use crate::canonical::{CanonicalError, ExitClass};
use crate::cli::{parse_args, Args};
use crate::config::partial::OutMode;
use crate::config::{config_path, defaults, partial_from_env, read_config_file};
use crate::pipeline::{
open_input, pump, read_files, read_request, NdjsonSink, PrettySink, RawSink, Sink, Style,
TextSink,
};
use crate::store::{Clock, CredStore, ModelCache, ReplayStash};
use crate::transport::{Bytes, Transport};
pub struct Host<'a> {
pub transport: &'a dyn Transport,
pub store: &'a dyn CredStore,
pub cache: &'a dyn ModelCache,
pub clock: &'a dyn Clock,
pub stash: &'a ReplayStash,
}
pub fn run(
args: Args,
stdin: &mut dyn Read,
stdout: &mut dyn Write,
stderr: &mut dyn Write,
host: &Host,
) -> u8 {
let mut flags = match parse_args(&args.argv) {
Ok(f) => f,
Err(e) => return fail_early(stderr, e),
};
if flags.help {
return emit(stdout, HELP);
}
if flags.skill {
return emit(stdout, SKILL);
}
if flags.version {
return emit(stdout, VERSION_LINE);
}
let env = &args.env;
let cfg_path = config_path(flags.config_path.take(), env);
let file = match read_config_file(&cfg_path) {
Ok(p) => p,
Err(e) => return fail_early(stderr, e),
};
if flags.dump_config {
return discovery::dump(stdout, stderr, flags.config, env, file);
}
if args.tty && flags.prompt.is_none() && flags.input.is_none() && flags.files.is_empty() {
let _ = stderr.write_all(HELP.as_bytes());
return ExitClass::Usage.code();
}
let env_partial = match partial_from_env(env) {
Ok(p) => p,
Err(e) => return fail_early(stderr, e.into()),
};
let merged = flags.config.or(env_partial).or(file).or(defaults());
let output = merged.output.unwrap_or(OutMode::Text);
let thinking = merged.thinking.unwrap_or(false);
let raw_out = output == OutMode::Raw;
let raw_in = merged.raw_in.unwrap_or(raw_out);
if raw_in && !flags.files.is_empty() {
let _ = writeln!(stderr, "--file cannot be combined with --raw");
return ExitClass::Usage.code();
}
if flags.in_dialect.is_some() && raw_in {
let _ = writeln!(stderr, "--in cannot be combined with --raw=in");
return ExitClass::Usage.code();
}
if flags.in_dialect.is_some() && !flags.files.is_empty() {
let _ = writeln!(stderr, "--file cannot be combined with --in");
return ExitClass::Usage.code();
}
let mut input_file;
let reader: &mut dyn Read = match &flags.input {
Some(path) => match open_input(Some(path)) {
Ok(f) => {
input_file = f;
&mut *input_file
}
Err(_) => {
let _ = writeln!(stderr, "cannot open --input file `{}`", path.display());
return ExitClass::NoInput.code();
}
},
None => stdin,
};
if let Some(dialect) = flags.in_dialect {
return masq::filter(dialect, reader, merged, raw_out, stdout, host);
}
let file_parts = match read_files(&flags.files) {
Ok(parts) => parts,
Err((path, e)) => {
let _ = writeln!(stderr, "cannot read --file `{}`: {e}", path.display());
return ExitClass::NoInput.code();
}
};
let mut sink: Box<dyn Sink + '_> = match output {
OutMode::Text => match Style::resolve(args.stdout_tty, output, env) {
style if style.is_pretty() => {
Box::new(PrettySink::new(&mut *stdout, &mut *stderr, thinking, style))
}
_ => Box::new(TextSink::new(&mut *stdout, &mut *stderr, thinking)),
},
OutMode::Ndjson => Box::new(NdjsonSink::new(&mut *stdout)),
OutMode::Raw => Box::new(RawSink::new(&mut *stdout)),
};
if raw_in {
let sent = raw::send_raw(reader, merged, host);
return drive::drive(sent, raw_out, &mut *sink, host.clock.now());
}
let request = match read_request(flags.prompt.as_deref(), file_parts, reader) {
Ok(r) => r,
Err(e) => return events::fail_inband(&mut *sink, e),
};
let req_model = (!request.model.is_empty()).then(|| request.model.clone());
let cfg = match merged.into_resolved(req_model.as_deref(), Some(host.cache)) {
Ok(c) => c,
Err(e) => return events::fail_inband(&mut *sink, e.into()),
};
if raw_out {
let sent = generate::send_encoded(request, cfg, host);
return drive::drive(sent, true, &mut *sink, host.clock.now());
}
pump(generate(request, cfg, host), &mut *sink)
}
fn fail_early(stderr: &mut dyn Write, err: CanonicalError) -> u8 {
let _ = writeln!(stderr, "{}", err.message);
err.exit_code()
}
fn drain(body: Box<dyn Iterator<Item = io::Result<Bytes>>>) -> Result<Vec<u8>, io::Error> {
let mut buf = Vec::new();
for chunk in body {
buf.extend_from_slice(&chunk?);
}
Ok(buf)
}