ort_openrouter_cli/input/
to_json.rs1extern crate alloc;
8use alloc::string::String;
9
10use crate::{ErrorKind, Message, OrtResult, PromptOpts, Write, common::data::Content, ort_error};
11
12pub fn build_body(idx: usize, opts: &PromptOpts, messages: &[Message]) -> OrtResult<String> {
15 let capacity: u32 = 1024 + messages.iter().map(|m| m.size()).sum::<u32>();
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, \"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('}')?;
75
76 Ok(string_buf)
77}
78
79impl PromptOpts {
80 pub fn to_json_writer<W: Write>(&self, writer: &mut W) -> OrtResult<()> {
81 let w = writer;
82
83 w.write_char('{')?;
84 let mut first = true;
85
86 if let Some(ref v) = self.prompt {
87 if !first {
88 w.write_char(',')?;
89 } else {
90 first = false;
91 }
92 w.write_str("\"prompt\":")?;
93 write_json_str(w, v)?;
94 }
95 if let Some(v) = self.models.first() {
97 if !first {
98 w.write_char(',')?;
99 } else {
100 first = false;
101 }
102 w.write_str("\"model\":")?;
103 write_json_str(w, v)?;
104 }
105 if let Some(ref v) = self.provider {
106 if !first {
107 w.write_char(',')?;
108 } else {
109 first = false;
110 }
111 w.write_str("\"provider\":")?;
112 write_json_str(w, v)?;
113 }
114 if let Some(ref v) = self.system {
115 if !first {
116 w.write_char(',')?;
117 } else {
118 first = false;
119 }
120 w.write_str("\"system\":")?;
121 write_json_str(w, v)?;
122 }
123 if let Some(ref p) = self.priority {
124 if !first {
125 w.write_char(',')?;
126 } else {
127 first = false;
128 }
129 w.write_str("\"priority\":")?;
130 write_json_str_simple(w, p.as_str())?;
131 }
132 if let Some(ref rc) = self.reasoning {
133 if !first {
134 w.write_char(',')?;
135 } else {
136 first = false;
137 }
138 w.write_str("\"reasoning\":{")?;
139 w.write_str("\"enabled\":")?;
141 write_bool(w, rc.enabled)?;
142 if let Some(ref eff) = rc.effort {
143 w.write_str(",\"effort\":")?;
144 write_json_str_simple(w, eff.as_str())?;
145 }
146 if let Some(tokens) = rc.tokens {
147 w.write_str(",\"tokens\":")?;
148 write_u32(w, tokens)?;
149 }
150 w.write_char('}')?;
151 }
152 if let Some(show) = self.show_reasoning {
153 if !first {
154 w.write_char(',')?;
155 } else {
156 first = false;
157 }
158 w.write_str("\"show_reasoning\":")?;
159 write_bool(w, show)?;
160 }
161 if let Some(quiet) = self.quiet {
162 if !first {
163 w.write_char(',')?;
164 } else {
165 }
167 w.write_str("\"quiet\":")?;
168 write_bool(w, quiet)?;
169 }
170
171 w.write_char(',')?;
173 w.write_str("\"merge_config\":")?;
174 write_bool(w, self.merge_config)?;
175
176 w.write_char('}')?;
177 Ok(())
178 }
179}
180
181const HEX: &[u8; 16] = b"0123456789ABCDEF";
182
183fn write_bool<W: Write>(w: &mut W, v: bool) -> OrtResult<usize> {
184 if v {
185 w.write_str("true")
186 } else {
187 w.write_str("false")
188 }
189}
190
191fn write_u32<W: Write>(w: &mut W, mut n: u32) -> OrtResult<usize> {
192 if n == 0 {
193 return w.write_str("0");
194 }
195 let mut buf = [0u8; 10];
196 let mut i = buf.len();
197 while n > 0 {
198 i -= 1;
199 buf[i] = b'0' + (n % 10) as u8;
200 n /= 10;
201 }
202 w.write(&buf[i..])
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 (content, Some(_)) if !content.is_empty() => {
224 return Err(ort_error(
225 ErrorKind::InvalidMessageSchema,
226 "Message must have exactly one of 'content' or 'reasoning'.",
227 ));
228 }
229 (content, None) if content.is_empty() => {
230 return Err(ort_error(
231 ErrorKind::InvalidMessageSchema,
232 "Message must have exactly one of 'content' or 'reasoning'.",
233 ));
234 }
235 (_, Some(reasoning)) => {
236 w.write_str(",\"reasoning\":")?;
237 write_json_str(w, reasoning)?;
238 }
239 (content, _) => {
240 w.write_str(",\"content\":")?;
241 match content.as_slice() {
242 [Content::Text(text)] => write_json_str(w, text)?,
243 _ => {
244 w.write_char('[')?;
245 for (i, item) in content.iter().enumerate() {
246 if i != 0 {
247 w.write_char(',')?;
248 }
249 item.to_json(w)?;
250 }
251 w.write_char(']')?;
252 }
253 }
254 }
255 }
256 w.write_char('}')?;
257 Ok(())
258}
259
260impl Content {
261 #[allow(dead_code)]
262 pub fn to_json<W: Write>(&self, w: &mut W) -> OrtResult<()> {
263 w.write_str("{\"type\":")?;
264 use Content::*;
265 match self {
266 Text(s) => {
267 write_json_str(w, "text")?;
268 w.write_str(", \"text\": ")?;
269 write_json_str(w, s.as_str())?;
270 }
271 Image { base64, mime_type } => {
272 write_json_str(w, "image_url")?;
273 w.write_str(", \"image_url\": { \"url\": \"data:")?;
274 w.write_str(mime_type)?;
275 w.write_str(";base64,")?; w.write_str(base64.as_str())?;
277 w.write_str("\"}")?;
278 }
279 ImageUrl(url) => {
280 write_json_str(w, "image_url")?;
281 w.write_str(", \"image_url\": { \"url\": \"")?;
282 w.write_str(url)?;
283 w.write_str("\"}")?;
284 }
285 File(f) => {
286 write_json_str(w, "file")?;
287 w.write_str(", \"file\": {\"filename\": ")?;
288 write_json_str(w, &f.filename)?;
289 w.write_str(", \"file_data\": \"data:application/pdf;base64,")?;
291 w.write_str(&f.base64)?;
292 w.write_str("\"}")?;
293 }
294 }
295 w.write_char('}')?;
296 Ok(())
297 }
298}
299
300pub(crate) fn write_json_str_simple<W: Write>(w: &mut W, s: &str) -> OrtResult<()> {
302 w.write_char('"')?;
303 w.write_str(s)?;
304 w.write_char('"')?;
305 Ok(())
306}
307
308fn write_json_str<W: Write>(w: &mut W, s: &str) -> OrtResult<()> {
310 w.write_char('"')?;
311 write_encoded_bytes(w, s.as_bytes())?;
312 w.write_char('"')?;
313
314 Ok(())
315}
316
317pub fn write_encoded_bytes<W: Write>(w: &mut W, bytes: &[u8]) -> OrtResult<()> {
318 let mut start = 0;
319
320 for i in 0..bytes.len() {
321 let b = bytes[i];
322 let esc = match b {
323 b'"' => Some(b"\\\""), b'\\' => Some(b"\\\\"),
325 b'\n' => Some(b"\\n"),
326 b'\r' => Some(b"\\r"),
327 b'\t' => Some(b"\\t"),
328 0x08 => Some(b"\\b"),
329 0x0C => Some(b"\\f"),
330 0x00..=0x1F => None, _ => continue,
332 };
333
334 if start < i {
335 w.write(&bytes[start..i])?;
336 }
337
338 if let Some(e) = esc {
339 w.write(e)?;
340 } else {
341 let mut buf = [0u8; 6];
343 buf[0] = b'\\';
344 buf[1] = b'u';
345 buf[2] = b'0';
346 buf[3] = b'0';
347 buf[4] = HEX[((b >> 4) & 0xF) as usize];
348 buf[5] = HEX[(b & 0xF) as usize];
349 w.write(&buf)?;
350 }
351
352 start = i + 1;
353 }
354
355 if start < bytes.len() {
356 w.write(&bytes[start..])?;
357 }
358 Ok(())
359}
360
361#[cfg(test)]
362mod tests {
363 extern crate alloc;
364 use alloc::string::ToString;
365 use alloc::vec;
366
367 use super::*;
368 use crate::ReasoningConfig;
369
370 #[test]
371 fn test_build_body() {
372 let opts = PromptOpts {
373 prompt: None,
374 models: vec!["google/gemma-3n-e4b-it:free".to_string()],
375 provider: Some("google-ai-studio".to_string()),
376 system: Some("System prompt here".to_string()),
377 priority: None,
378 reasoning: Some(ReasoningConfig::off()),
379 show_reasoning: Some(false),
380 quiet: None,
381 merge_config: false,
382 files: vec![], };
384 let messages = vec![
385 Message::user("Hello".to_string()),
386 Message::assistant("Hello there!".to_string()),
387 ];
388 let got = match build_body(0, &opts, &messages) {
389 Ok(got) => got,
390 Err(err) => {
391 panic!("{}", err.as_string());
392 }
393 };
394
395 let expected = r#"{"stream": 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!"}]}"#;
396
397 assert_eq!(got, expected);
398 }
399}