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