Skip to main content

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-2026 Graham King
6
7use core::ffi::{c_int, c_void};
8
9extern crate alloc;
10use alloc::string::String;
11use alloc::vec::Vec;
12
13use crate::OrtResult;
14use crate::PromptOpts;
15use crate::Write;
16use crate::common::config;
17use crate::common::utils;
18use crate::common::{buf_read, site};
19use crate::input::args;
20use crate::libc;
21use crate::list;
22use crate::prompt;
23use crate::{ErrorKind, ort_error};
24
25const STDIN_FILENO: i32 = 0;
26const STDERR_FILENO: i32 = 0;
27
28// Keep default mode in sync with lib.rs DEFAULT_MODEL
29const USAGE: &str = "Usage: ort [-m <model>] [-s \"<system prompt>\"] [-p <price|throughput|latency>] [-pr provider-slug] [-r] [-rr] [-q] [-nc] <prompt>\n\
30Defaults: -m google/gemma-3n-e4b-it:free; -s omitted ; -p omitted\n\
31Example:\n  ort -p price -m openai/gpt-oss-20b -r low -rr -s \"Respond like a pirate\" \"Write a limerick about AI\"
32
33See https://github.com/grahamking/ort for full docs.
34";
35
36pub fn print_usage() {
37    unsafe { libc::write(STDERR_FILENO, USAGE.as_ptr() as *const c_void, USAGE.len()) };
38}
39
40fn parse_args(args: &[String]) -> Result<args::Cmd, args::ArgParseError> {
41    // args[0] is program name
42    if args.len() == 1 {
43        return Err(args::ArgParseError::show_help());
44    }
45
46    if args[1].as_str() == "list" {
47        args::parse_list_args(args)
48    } else {
49        let is_pipe_input = unsafe { libc::isatty(STDIN_FILENO) == 0 };
50        let stdin = if is_pipe_input {
51            let mut buffer = String::with_capacity(8 * 1024);
52            buf_read::fd_read_to_string(STDIN_FILENO, &mut buffer);
53            Some(buffer)
54        } else {
55            None
56        };
57        args::parse_prompt_args(args, stdin)
58    }
59}
60
61pub fn main(args: &[String], is_terminal: bool, w: impl Write + Send) -> OrtResult<c_int> {
62    let site = match args[0].split('/').next_back().unwrap() {
63        "nrt" => site::NVIDIA,
64        _ => site::OPENROUTER,
65    };
66
67    // Load ~/.config/ort.json or nrt.json
68    let cfg = config::load_config(site.config_filename)?;
69
70    // Fail fast if key missing
71    let mut api_key = utils::get_env(site.api_key_env)
72        .to_string_lossy()
73        .into_owned();
74    if api_key.is_empty() {
75        api_key = match cfg.get_api_key() {
76            Some(k) => k,
77            None => {
78                return Err(ort_error(
79                    ErrorKind::MissingApiKey,
80                    "OPENROUTER_API_KEY or NVIDIA_API_KEY is not set.",
81                ));
82            }
83        }
84    };
85
86    let cmd = match parse_args(args) {
87        Ok(cmd) => cmd,
88        Err(err) if err.is_help() => {
89            print_usage();
90            return Ok(0);
91        }
92        Err(err) => {
93            print_usage();
94            return Err(err.into());
95        }
96    };
97
98    let cancel_token = crate::CancelToken::init();
99
100    let cmd_result = match cmd {
101        args::Cmd::Prompt(mut cli_opts) => {
102            if cli_opts.merge_config {
103                cli_opts.merge(cfg.prompt_opts.unwrap_or_default());
104            } else {
105                cli_opts.merge(PromptOpts::default());
106            }
107            // A Message is quite small, an enum and two Option<String>.
108            // Capacity 3 for:
109            // - System message (optiona)
110            // - User message (required)
111            // - and the assistant message that LastWriter appends, to save a realloc.
112            let mut messages = Vec::with_capacity(3);
113            if let Some(sys) = cli_opts.system.take() {
114                messages.push(crate::Message::system(sys));
115            };
116            let user_message = crate::Message::user(cli_opts.prompt.take().unwrap());
117            messages.push(user_message);
118            if cli_opts.models.len() == 1 {
119                prompt::run(
120                    &api_key,
121                    cancel_token,
122                    cfg.settings.unwrap_or_default(),
123                    cli_opts,
124                    site,
125                    messages,
126                    !is_terminal,
127                    w,
128                )
129            } else {
130                prompt::run_multi(
131                    &api_key,
132                    cancel_token,
133                    cfg.settings.unwrap_or_default(),
134                    cli_opts,
135                    site,
136                    messages,
137                    w,
138                )
139            }
140        }
141        args::Cmd::ContinueConversation(cli_opts) => prompt::run_continue(
142            &api_key,
143            cancel_token,
144            cfg.settings.unwrap_or_default(),
145            cli_opts,
146            site,
147            !is_terminal,
148            w,
149        ),
150        args::Cmd::List(args) => list::run(
151            &api_key,
152            cancel_token,
153            cfg.settings.unwrap_or_default(),
154            args,
155            site,
156            w,
157        ),
158    };
159    cmd_result.map(|_| 0)
160}