///|
/// A parsed command line.
pub(all) struct Cli {
settings : Settings
/// Prompt collected from positional arguments; `None` means "read stdin".
prompt : String?
/// Stream the reply token by token.
stream : Bool
/// Stream the chain of thought too, on stderr, so it cannot pollute a pipe.
show_cot : Bool
/// Suppress the status lines. Errors and the empty-reply warning still go to
/// stderr: they are diagnostics, not progress.
quiet : Bool
/// Print usage and exit.
show_help : Bool
}
///|
pub let usage : String =
#|faceoff — ask an OpenAI-compatible chat completion endpoint
#|
#|usage:
#| faceoff [options] [prompt ...]
#| echo "a question" | faceoff [options]
#|
#|options:
#| -s, --stream stream the reply as it is generated
#| --show-cot stream the chain of thought too, on stderr
#| -q, --quiet no status lines on stderr (errors still go there)
#| --model model id (env MOONLLM_MODEL / OPENAI_MODEL)
#| --base-url API base URL (env MOONLLM_BASE_URL / OPENAI_BASE_URL)
#| --api-key API key (env MOONLLM_API_KEY / OPENAI_API_KEY / LLM_API_KEY)
#| --system system prompt (env MOONLLM_SYSTEM)
#| --temperature sampling temperature
#| --max-tokens maximum generated tokens
#| --timeout-ms per-request timeout, defaults to 60000
#| --no-key allow an empty API key (local endpoints)
#| -h, --help show this help
#|
#| -- treat every following argument as prompt text
///|
/// Parse `argv` (without the program name) into a `Cli`.
pub fn Cli::parse(
env : Map[String, String],
argv : Array[String],
) -> Cli raise ConfigError {
let settings = Settings::from_env(env)
let prompt_parts : Array[String] = []
let mut stream = false
let mut show_cot = false
let mut quiet = false
let mut show_help = false
let mut no_key = false
let mut literal = false
let mut i = 0
while i < argv.length() {
let arg = argv[i]
i = i + 1
if literal {
prompt_parts.push(arg)
continue
}
match arg {
"-s" | "--stream" => stream = true
"--show-cot" => show_cot = true
"-q" | "--quiet" => quiet = true
"-h" | "--help" => show_help = true
"--no-key" => no_key = true
"--" => literal = true
"--model" => {
settings.model = next_value(argv, i, "--model")
i = i + 1
}
"--base-url" => {
settings.base_url = next_value(argv, i, "--base-url")
i = i + 1
}
"--api-key" => {
settings.api_key = next_value(argv, i, "--api-key")
i = i + 1
}
"--system" => {
settings.system = next_value(argv, i, "--system")
i = i + 1
}
"--temperature" => {
let raw = next_value(argv, i, "--temperature")
i = i + 1
settings.temperature = Some(parse_double_flag("--temperature", raw))
}
"--max-tokens" => {
let raw = next_value(argv, i, "--max-tokens")
i = i + 1
settings.max_tokens = Some(parse_int_flag("--max-tokens", raw))
}
"--timeout-ms" => {
let raw = next_value(argv, i, "--timeout-ms")
i = i + 1
settings.timeout_ms = parse_int_flag("--timeout-ms", raw)
}
_ =>
if arg.has_prefix("-") && arg.length() > 1 {
raise UnknownFlag(flag=arg)
} else {
prompt_parts.push(arg)
}
}
}
if settings.base_url == "" {
settings.base_url = default_base_url
}
if !show_help && settings.api_key == "" && !no_key {
raise MissingApiKey
}
{
settings,
prompt: if prompt_parts.is_empty() {
None
} else {
Some(prompt_parts.join(" "))
},
stream,
show_cot,
quiet,
show_help,
}
}
///|
/// The value that must follow a flag, or a `MissingValue` error.
fn next_value(
argv : Array[String],
i : Int,
flag : String,
) -> String raise ConfigError {
if i >= argv.length() {
raise MissingValue(flag~)
}
argv[i]
}
///|
fn parse_double_flag(flag : String, raw : String) -> Double raise ConfigError {
@string.parse_double(raw) catch {
_ => raise BadNumber(flag~, value=raw)
}
}
///|
fn parse_int_flag(flag : String, raw : String) -> Int raise ConfigError {
@string.parse_int(raw) catch {
_ => raise BadNumber(flag~, value=raw)
}
}