Skip to main content

ort_openrouter_cli/common/
data.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::str::FromStr;
8
9extern crate alloc;
10use alloc::string::{String, ToString};
11use alloc::vec;
12use alloc::vec::Vec;
13
14use crate::{ErrorKind, OrtError, ort_error};
15
16const DEFAULT_SHOW_REASONING: bool = false;
17const DEFAULT_QUIET: bool = false;
18
19// Keep in sync with src/lib.rs
20pub const DEFAULT_MODEL: &str = "google/gemma-3n-e4b-it:free";
21
22// {
23//  "id":"gen-1756743299-7ytIBcjALWQQShwMQfw9",
24//  "provider":"Meta",
25//  "model":"meta-llama/llama-3.3-8b-instruct:free",
26//  "object":"chat.completion.chunk",
27//  "created":1756743300,
28//  "choices":[
29//      {
30//      "index":0,
31//      "delta":{"role":"assistant","content":""},
32//      "finish_reason":null,
33//      "native_finish_reason":null,
34//      "logprobs":null
35//      }
36//  ],
37//  "usage":{
38//      "prompt_tokens":42,
39//      "completion_tokens":2,
40//      "total_tokens":44,
41//      "cost":0,"
42//      is_byok":false,
43//      "prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},
44//      "cost_details":{"upstream_inference_cost":null,"upstream_inference_prompt_cost":0,"upstream_inference_completions_cost":0},
45//      "completion_tokens_details":{"reasoning_tokens":0,"image_tokens":0}}
46//  }
47
48pub struct ChatCompletionsResponse {
49    pub provider: Option<String>,
50    pub model: Option<String>,
51    pub choices: Vec<Choice>,
52    pub usage: Option<Usage>,
53}
54
55pub struct Choice {
56    pub delta: Message,
57}
58
59pub struct Usage {
60    pub cost: f32, // In dollars, usually a very small fraction
61}
62
63pub struct LastData {
64    pub opts: PromptOpts,
65    pub messages: Vec<Message>,
66}
67
68#[derive(Clone)]
69pub struct PromptOpts {
70    pub prompt: Option<String>,
71    /// Model IDs, e.g. 'moonshotai/kimi-k2'
72    pub models: Vec<String>,
73    /// Prefered provider slug
74    pub provider: Option<String>,
75    /// System prompt
76    pub system: Option<String>,
77    /// How to choose a provider
78    pub priority: Option<Priority>,
79    /// Reasoning config
80    pub reasoning: Option<ReasoningConfig>,
81    /// Show reasoning output
82    pub show_reasoning: Option<bool>,
83    /// Don't show stats after request
84    pub quiet: Option<bool>,
85    /// Whether to merge in the default settings from config file
86    pub merge_config: bool,
87}
88
89impl Default for PromptOpts {
90    fn default() -> Self {
91        Self {
92            prompt: None,
93            models: vec![DEFAULT_MODEL.to_string()],
94            provider: None,
95            system: None,
96            priority: None,
97            reasoning: Some(ReasoningConfig::default()),
98            show_reasoning: Some(false),
99            quiet: Some(false),
100            merge_config: true,
101        }
102    }
103}
104
105impl PromptOpts {
106    // Replace any blank or None fields on Self with values from other
107    // or with the defaults.
108    // After this call a PromptOpts is ready to use.
109    pub fn merge(&mut self, o: PromptOpts) {
110        self.prompt.get_or_insert(o.prompt.unwrap_or_default());
111        self.quiet.get_or_insert(o.quiet.unwrap_or(DEFAULT_QUIET));
112        if self.models.is_empty() {
113            // We don't merge the models, otherwise we'd try to query both the
114            // cmd line one, and the config file default.
115            self.models = o.models;
116        }
117        if let Some(provider) = o.provider {
118            self.provider.get_or_insert(provider);
119        }
120        if let Some(system) = o.system {
121            self.system.get_or_insert(system);
122        }
123        if let Some(priority) = o.priority {
124            self.priority.get_or_insert(priority);
125        }
126        self.reasoning
127            .get_or_insert(o.reasoning.unwrap_or_default());
128        self.show_reasoning
129            .get_or_insert(o.show_reasoning.unwrap_or(DEFAULT_SHOW_REASONING));
130    }
131}
132
133#[derive(Default, Debug, Clone, Copy)]
134pub enum Priority {
135    Price,
136    #[default]
137    Latency,
138    Throughput,
139}
140
141impl Priority {
142    pub fn as_str(&self) -> &'static str {
143        match self {
144            Priority::Price => "price",
145            Priority::Latency => "latency",
146            Priority::Throughput => "throughput",
147        }
148    }
149}
150
151impl FromStr for Priority {
152    type Err = OrtError;
153
154    fn from_str(s: &str) -> Result<Self, Self::Err> {
155        match s.to_lowercase().as_str() {
156            "price" => Ok(Priority::Price),
157            "latency" => Ok(Priority::Latency),
158            "throughput" => Ok(Priority::Throughput),
159            _ => Err(ort_error(
160                ErrorKind::FormatError,
161                "Priority: Invalid string value",
162            )), // Handle unknown strings
163        }
164    }
165}
166
167#[derive(Default, Debug, Clone)]
168pub struct ReasoningConfig {
169    pub enabled: bool,
170    pub effort: Option<ReasoningEffort>,
171    pub tokens: Option<u32>,
172}
173
174impl ReasoningConfig {
175    pub fn off() -> Self {
176        Self {
177            enabled: false,
178            ..Default::default()
179        }
180    }
181}
182
183#[derive(Default, Debug, Clone, Copy, PartialEq)]
184pub enum ReasoningEffort {
185    None, // GPT 5.x only
186    Low,
187    #[default]
188    Medium,
189    High,
190    XHigh, // GPT 5.x only
191}
192
193impl ReasoningEffort {
194    pub fn as_str(&self) -> &'static str {
195        match self {
196            ReasoningEffort::None => "none",
197            ReasoningEffort::Low => "low",
198            ReasoningEffort::Medium => "medium",
199            ReasoningEffort::High => "high",
200            ReasoningEffort::XHigh => "xhigh",
201        }
202    }
203}
204
205#[derive(Debug, Clone)]
206pub struct Message {
207    pub role: Role,
208    pub content: Option<String>,
209    pub reasoning: Option<String>,
210}
211
212impl Message {
213    pub fn new(role: Role, content: Option<String>, reasoning: Option<String>) -> Self {
214        Message {
215            role,
216            content,
217            reasoning,
218        }
219    }
220    pub fn system(content: String) -> Self {
221        Self::new(Role::System, Some(content), None)
222    }
223    pub fn user(content: String) -> Self {
224        Self::new(Role::User, Some(content), None)
225    }
226    pub fn assistant(content: String) -> Self {
227        Self::new(Role::Assistant, Some(content), None)
228    }
229
230    /// Estimate size in bytes
231    pub fn size(&self) -> u32 {
232        self.content
233            .as_ref()
234            .or(self.reasoning.as_ref())
235            .map(|c| c.len())
236            .unwrap_or(0) as u32
237            + 10
238    }
239}
240
241#[derive(Debug, Copy, Clone)]
242pub enum Role {
243    System,
244    User,
245    Assistant,
246}
247
248impl Role {
249    pub fn as_str(&self) -> &'static str {
250        match self {
251            Role::System => "system",
252            Role::User => "user",
253            Role::Assistant => "assistant",
254        }
255    }
256}
257
258impl FromStr for Role {
259    type Err = &'static str;
260    fn from_str(s: &str) -> Result<Self, Self::Err> {
261        match s.to_lowercase().as_str() {
262            "system" => Ok(Role::System),
263            "user" => Ok(Role::User),
264            "assistant" => Ok(Role::Assistant),
265            _ => Err("Invalid role"),
266        }
267    }
268}
269
270#[derive(Clone, Default)]
271pub enum Response {
272    /// The first time we get anything at all on the SSE stream
273    Start,
274    /// Reasoning events - start, some thoughts, stop
275    Think(ThinkEvent),
276    /// The good stuff
277    Content(String),
278    /// Summary stats at the end of the run
279    Stats(super::stats::Stats),
280    /// Less good things. Often you mistyped the model name.
281    Error(String),
282    /// For default
283    #[default]
284    None,
285}
286
287#[derive(Debug, Clone)]
288pub enum ThinkEvent {
289    Start,
290    Content(String),
291    Stop,
292}