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