ort_openrouter_cli/input/
to_json.rs1extern crate alloc;
8use alloc::string::String;
9
10use crate::{ErrorKind, LastData, Message, OrtResult, PromptOpts, Write, ort_error};
11
12pub fn build_body(idx: usize, opts: &PromptOpts, messages: &[Message]) -> OrtResult<String> {
15 let capacity: u32 = messages.iter().map(|m| m.size()).sum::<u32>() + 100;
16 let mut string_buf = String::with_capacity(capacity as usize);
17 let mut w = unsafe { string_buf.as_mut_vec() };
18
19 w.write_str("{\"stream\": true, \"usage\": {\"include\": true}, \"model\": ")?;
20 write_json_str(&mut w, opts.models.get(idx).expect("Missing model"))?;
21
22 if opts.priority.is_some() || opts.provider.is_some() {
23 w.write_str(", \"provider\": {")?;
24 let mut is_first = true;
25 if let Some(p) = opts.priority {
26 w.write_str("\"sort\":")?;
27 write_json_str_simple(&mut w, p.as_str())?;
28 is_first = false;
29 }
30 if let Some(pr) = &opts.provider {
31 if !is_first {
32 w.write_str(", ")?;
33 }
34 w.write_str("\"order\": [")?;
35 write_json_str(&mut w, pr)?;
36 w.write_char(']')?;
37 }
38 w.write_char('}')?;
39 }
40
41 w.write_str(", \"reasoning\": ")?;
42 match &opts.reasoning {
43 None => {
45 w.write_str("{\"enabled\": false}")?;
46 }
47 Some(r_cfg) if !r_cfg.enabled => {
49 w.write_str("{\"enabled\": false}")?;
50 }
51 Some(r_cfg) => match (r_cfg.effort, r_cfg.tokens) {
53 (Some(effort), _) => {
54 w.write_str("{\"exclude\": false, \"enabled\": true, \"effort\":")?;
55 write_json_str_simple(&mut w, effort.as_str())?;
56 w.write_char('}')?;
57 }
58 (_, Some(tokens)) => {
59 w.write_str("{\"exclude\": false, \"enabled\": true, \"max_tokens\":")?;
60 write_u32(&mut w, tokens)?;
61 w.write_char('}')?;
62 }
63 _ => unreachable!("Reasoning effort and tokens cannot both be null"),
64 },
65 };
66
67 w.write_str(", \"messages\":")?;
68 Message::write_json_array(messages, &mut w)?;
69
70 w.write_char('}')?;
71
72 Ok(string_buf)
73}
74
75impl LastData {
76 pub fn to_json_writer<W: Write>(&self, writer: W) -> OrtResult<()> {
77 let mut w = writer;
78
79 w.write_str("{\"opts\":{")?;
80 let mut first = true;
81
82 if let Some(ref v) = self.opts.prompt {
83 if !first {
84 w.write_char(',')?;
85 } else {
86 first = false;
87 }
88 w.write_str("\"prompt\":")?;
89 write_json_str(&mut w, v)?;
90 }
91 if let Some(v) = self.opts.models.first() {
93 if !first {
94 w.write_char(',')?;
95 } else {
96 first = false;
97 }
98 w.write_str("\"model\":")?;
99 write_json_str(&mut w, v)?;
100 }
101 if let Some(ref v) = self.opts.provider {
102 if !first {
103 w.write_char(',')?;
104 } else {
105 first = false;
106 }
107 w.write_str("\"provider\":")?;
108 write_json_str(&mut w, v)?;
109 }
110 if let Some(ref v) = self.opts.system {
111 if !first {
112 w.write_char(',')?;
113 } else {
114 first = false;
115 }
116 w.write_str("\"system\":")?;
117 write_json_str(&mut w, v)?;
118 }
119 if let Some(ref p) = self.opts.priority {
120 if !first {
121 w.write_char(',')?;
122 } else {
123 first = false;
124 }
125 w.write_str("\"priority\":")?;
126 write_json_str_simple(&mut w, p.as_str())?;
127 }
128 if let Some(ref rc) = self.opts.reasoning {
129 if !first {
130 w.write_char(',')?;
131 } else {
132 first = false;
133 }
134 w.write_str("\"reasoning\":{")?;
135 w.write_str("\"enabled\":")?;
137 write_bool(&mut w, rc.enabled)?;
138 if let Some(ref eff) = rc.effort {
139 w.write_str(",\"effort\":")?;
140 write_json_str_simple(&mut w, eff.as_str())?;
141 }
142 if let Some(tokens) = rc.tokens {
143 w.write_str(",\"tokens\":")?;
144 write_u32(&mut w, tokens)?;
145 }
146 w.write_char('}')?;
147 }
148 if let Some(show) = self.opts.show_reasoning {
149 if !first {
150 w.write_char(',')?;
151 } else {
152 first = false;
153 }
154 w.write_str("\"show_reasoning\":")?;
155 write_bool(&mut w, show)?;
156 }
157 if let Some(quiet) = self.opts.quiet {
158 if !first {
159 w.write_char(',')?;
160 } else {
161 }
163 w.write_str("\"quiet\":")?;
164 write_bool(&mut w, quiet)?;
165 }
166
167 w.write_char(',')?;
169 w.write_str("\"merge_config\":")?;
170 write_bool(&mut w, self.opts.merge_config)?;
171
172 w.write_str("},\"messages\":")?;
173 Message::write_json_array(&self.messages, &mut w)?;
174
175 w.write_char('}')?;
176 Ok(())
177 }
178}
179
180const HEX: &[u8; 16] = b"0123456789ABCDEF";
181
182fn write_bool<W: Write>(w: &mut W, v: bool) -> OrtResult<usize> {
183 if v {
184 w.write_str("true")
185 } else {
186 w.write_str("false")
187 }
188}
189
190fn write_u32<W: Write>(w: &mut W, mut n: u32) -> OrtResult<usize> {
191 if n == 0 {
192 return w.write_str("0");
193 }
194 let mut buf = [0u8; 10];
195 let mut i = buf.len();
196 while n > 0 {
197 i -= 1;
198 buf[i] = b'0' + (n % 10) as u8;
199 n /= 10;
200 }
201 let s = core::str::from_utf8(&buf[i..]).unwrap();
202 w.write_str(s)
203}
204
205impl Message {
206 pub fn write_json_array<W: Write>(msgs: &[Message], w: &mut W) -> OrtResult<()> {
207 w.write_char('[')?;
208 for (i, msg) in msgs.iter().enumerate() {
209 if i != 0 {
210 w.write_char(',')?;
211 }
212 write_json(msg, w)?;
213 }
214 w.write_char(']')?;
215 Ok(())
216 }
217}
218
219pub fn write_json<W: Write>(data: &Message, w: &mut W) -> OrtResult<()> {
220 w.write_str("{\"role\":")?;
221 write_json_str_simple(w, data.role.as_str())?;
222 match (&data.content, &data.reasoning) {
223 (Some(_), Some(_)) | (None, None) => {
224 return Err(ort_error(
225 ErrorKind::InvalidMessageSchema,
226 "Message must have exactly one of 'content' or 'reasoning'.",
227 ));
228 }
229 (Some(content), _) => {
230 w.write_str(",\"content\":")?;
231 write_json_str(w, content)?;
232 }
233 (_, Some(reasoning)) => {
234 w.write_str(",\"reasoning\":")?;
235 write_json_str(w, reasoning)?;
236 }
237 }
238 w.write_char('}')?;
239 Ok(())
240}
241
242fn write_json_str_simple<W: Write>(w: &mut W, s: &str) -> OrtResult<()> {
244 w.write_char('"')?;
245 w.write_str(s)?;
246 w.write_char('"')?;
247 Ok(())
248}
249
250fn write_json_str<W: Write>(w: &mut W, s: &str) -> OrtResult<()> {
252 w.write_char('"')?;
253 let bytes = s.as_bytes();
254 let mut start = 0;
255
256 for i in 0..bytes.len() {
257 let b = bytes[i];
258 let esc = match b {
259 b'"' => Some(b"\\\""), b'\\' => Some(b"\\\\"),
261 b'\n' => Some(b"\\n"),
262 b'\r' => Some(b"\\r"),
263 b'\t' => Some(b"\\t"),
264 0x08 => Some(b"\\b"),
265 0x0C => Some(b"\\f"),
266 0x00..=0x1F => None, _ => continue,
268 };
269
270 if start < i {
271 w.write_str(core::str::from_utf8(&bytes[start..i]).unwrap())?;
272 }
273
274 if let Some(e) = esc {
275 w.write_str(core::str::from_utf8(e).unwrap())?;
276 } else {
277 let mut buf = [0u8; 6];
279 buf[0] = b'\\';
280 buf[1] = b'u';
281 buf[2] = b'0';
282 buf[3] = b'0';
283 buf[4] = HEX[((b >> 4) & 0xF) as usize];
284 buf[5] = HEX[(b & 0xF) as usize];
285 w.write_str(core::str::from_utf8(&buf).unwrap())?;
286 }
287
288 start = i + 1;
289 }
290
291 if start < bytes.len() {
292 w.write_str(core::str::from_utf8(&bytes[start..]).unwrap())?;
293 }
294 w.write_char('"')?;
295 Ok(())
296}
297
298#[cfg(test)]
299mod tests {
300 extern crate alloc;
301 use alloc::string::ToString;
302 use alloc::vec;
303
304 use super::*;
305 use crate::ReasoningConfig;
306
307 #[test]
308 fn test_last_data() {
309 let opts = PromptOpts {
310 prompt: None,
311 models: vec!["google/gemma-3n-e4b-it:free".to_string()],
312 provider: Some("google-ai-studio".to_string()),
313 system: Some("System prompt here".to_string()),
314 priority: None,
315 reasoning: Some(ReasoningConfig::off()),
316 show_reasoning: Some(false),
317 quiet: None,
318 merge_config: true,
319 };
320 let messages = vec![
321 Message::user("Hello".to_string()),
322 Message::assistant("Hello there!".to_string()),
323 ];
324 let l = LastData { opts, messages };
325
326 let mut got = String::with_capacity(64);
327 l.to_json_writer(unsafe { got.as_mut_vec() }).unwrap();
328
329 let expected = r#"{"opts":{"model":"google/gemma-3n-e4b-it:free","provider":"google-ai-studio","system":"System prompt here","reasoning":{"enabled":false},"show_reasoning":false,"merge_config":true},"messages":[{"role":"user","content":"Hello"},{"role":"assistant","content":"Hello there!"}]}"#;
330
331 assert_eq!(got, expected);
332 }
333
334 #[test]
335 fn test_build_body() {
336 let opts = PromptOpts {
337 prompt: None,
338 models: vec!["google/gemma-3n-e4b-it:free".to_string()],
339 provider: Some("google-ai-studio".to_string()),
340 system: Some("System prompt here".to_string()),
341 priority: None,
342 reasoning: Some(ReasoningConfig::off()),
343 show_reasoning: Some(false),
344 quiet: None,
345 merge_config: false,
346 };
347 let messages = vec![
348 Message::user("Hello".to_string()),
349 Message::assistant("Hello there!".to_string()),
350 ];
351 let got = build_body(0, &opts, &messages).unwrap();
352
353 let expected = r#"{"stream": true, "usage": {"include": true}, "model": "google/gemma-3n-e4b-it:free", "provider": {"order": ["google-ai-studio"]}, "reasoning": {"enabled": false}, "messages":[{"role":"user","content":"Hello"},{"role":"assistant","content":"Hello there!"}]}"#;
354
355 assert_eq!(got, expected);
356 }
357}