ort_openrouter_cli/input/
cli.rs

1//! ort: Open Router CLI
2//! https://github.com/grahamking/ort
3//!
4//! MIT License
5//! Copyright (c) 2025 Graham King
6
7use core::ffi::{c_int, c_void};
8
9extern crate alloc;
10use alloc::string::{String, ToString};
11use alloc::vec;
12use alloc::vec::Vec;
13
14use crate::OrtResult;
15use crate::PromptOpts;
16use crate::Write;
17use crate::common::buf_read;
18use crate::common::config;
19use crate::common::utils;
20use crate::input::args;
21use crate::libc;
22use crate::list;
23use crate::prompt;
24use crate::{ErrorKind, ort_error};
25
26const STDIN_FILENO: i32 = 0;
27const STDERR_FILENO: i32 = 0;
28
29pub fn print_usage() {
30    let usage = "Usage: ort [-m <model>] [-s \"<system prompt>\"] [-p <price|throughput|latency>] [-pr provider-slug] [-r] [-rr] [-q] [-nc] <prompt>\n\
31Defaults: -m ".to_string() + crate::DEFAULT_MODEL +" ; -s omitted ; -p omitted\n\
32Example:\n  ort -p price -m openai/gpt-oss-20b -r low -rr -s \"Respond like a pirate\" \"Write a limerick about AI\"
33
34See https://github.com/grahamking/ort for full docs.
35";
36    unsafe { libc::write(STDERR_FILENO, usage.as_ptr() as *const c_void, usage.len()) };
37}
38
39fn parse_args(args: Vec<String>) -> Result<args::Cmd, args::ArgParseError> {
40    // args[0] is program name
41    if args.len() == 1 {
42        return Err(args::ArgParseError::show_help());
43    }
44
45    if args[1].as_str() == "list" {
46        args::parse_list_args(&args)
47    } else {
48        let is_pipe_input = unsafe { libc::isatty(STDIN_FILENO) == 0 };
49        let stdin = if is_pipe_input {
50            let mut buffer = String::new();
51            buf_read::fd_read_to_string(STDIN_FILENO, &mut buffer);
52            Some(buffer)
53        } else {
54            None
55        };
56        args::parse_prompt_args(&args, stdin)
57    }
58}
59
60pub fn main(args: Vec<String>, is_terminal: bool, w: impl Write + Send) -> OrtResult<c_int> {
61    // Load ~/.config/ort.json
62    let cfg = config::load_config()?;
63
64    // Fail fast if key missing
65    let mut api_key = utils::get_env(c"OPENROUTER_API_KEY");
66    if api_key.is_empty() {
67        api_key = match cfg.get_openrouter_key() {
68            Some(k) => k,
69            None => {
70                return Err(ort_error(
71                    ErrorKind::MissingApiKey,
72                    "OPENROUTER_API_KEY is not set.",
73                ));
74            }
75        }
76    };
77
78    let cmd = match parse_args(args) {
79        Ok(cmd) => cmd,
80        Err(err) if err.is_help() => {
81            print_usage();
82            return Ok(0);
83        }
84        Err(err) => {
85            print_usage();
86            return Err(err.into());
87        }
88    };
89
90    let cancel_token = crate::CancelToken::init();
91
92    let cmd_result = match cmd {
93        args::Cmd::Prompt(mut cli_opts) => {
94            if cli_opts.merge_config {
95                cli_opts.merge(cfg.prompt_opts.unwrap_or_default());
96            } else {
97                cli_opts.merge(PromptOpts::default());
98            }
99            let mut messages = if let Some(sys) = cli_opts.system.take() {
100                vec![crate::Message::system(sys)]
101            } else {
102                vec![]
103            };
104            messages.push(crate::Message::user(cli_opts.prompt.take().unwrap()));
105            if cli_opts.models.len() == 1 {
106                prompt::run(
107                    &api_key,
108                    cancel_token,
109                    cfg.settings.unwrap_or_default(),
110                    cli_opts,
111                    messages,
112                    !is_terminal,
113                    w,
114                )
115            } else {
116                prompt::run_multi(
117                    &api_key,
118                    cancel_token,
119                    cfg.settings.unwrap_or_default(),
120                    cli_opts,
121                    messages,
122                    w,
123                )
124            }
125        }
126        args::Cmd::ContinueConversation(cli_opts) => prompt::run_continue(
127            &api_key,
128            cancel_token,
129            cfg.settings.unwrap_or_default(),
130            cli_opts,
131            !is_terminal,
132            w,
133        ),
134        args::Cmd::List(args) => list::run(
135            &api_key,
136            cancel_token,
137            cfg.settings.unwrap_or_default(),
138            args,
139            w,
140        ),
141    };
142    cmd_result.map(|_| 0)
143}