Skip to main content

ort_openrouter_cli/output/
from_json.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::str::FromStr;
8
9extern crate alloc;
10use alloc::borrow::{Cow, ToOwned};
11use alloc::string::{String, ToString};
12use alloc::vec;
13use alloc::vec::Vec;
14
15use crate::common::config;
16use crate::common::data::{Content, PromptFile, PromptFileKind};
17use crate::{
18    ChatCompletionsResponse, Choice, LastData, Message, Priority, PromptOpts, ReasoningConfig,
19    ReasoningEffort, Role, Usage,
20};
21
22impl ChatCompletionsResponse {
23    pub fn from_json(json: &str) -> Result<Self, Cow<'static, str>> {
24        let mut p = Parser::new(json);
25        p.skip_ws();
26        p.expect(b'{')?;
27
28        let mut provider = None;
29        let mut model = None;
30        let mut choices = vec![];
31        let mut usage = None;
32
33        loop {
34            p.skip_ws();
35            if p.try_consume(b'}') {
36                break;
37            }
38
39            let key = p
40                .parse_simple_str()
41                .map_err(|err| "ChatCompletionsResponse parsing key: ".to_string() + err)?;
42            p.skip_ws();
43            p.expect(b':')?;
44            p.skip_ws();
45
46            match key {
47                "provider" => {
48                    if provider.is_some() {
49                        return Err("duplicate field: provider".into());
50                    }
51                    provider = Some(p.parse_string()?);
52                }
53                "model" => {
54                    if model.is_some() {
55                        return Err("duplicate field: model".into());
56                    }
57                    model = Some(p.parse_string()?);
58                }
59                "choices" => {
60                    if !choices.is_empty() {
61                        return Err("duplicate field: choices".into());
62                    }
63                    if !p.try_consume(b'[') {
64                        return Err("choices: Expected array".into());
65                    }
66                    p.skip_ws();
67                    // If the array isn't empty..
68                    if !p.try_consume(b']') {
69                        loop {
70                            let j = p.value_slice()?;
71                            let choice = Choice::from_json(j)?;
72                            choices.push(choice);
73                            p.skip_ws();
74                            if p.try_consume(b',') {
75                                continue;
76                            }
77                            p.skip_ws();
78                            if p.try_consume(b']') {
79                                break;
80                            }
81                        }
82                    }
83                }
84                "usage" => {
85                    if p.peek_is_null() {
86                        p.parse_null()?;
87                        usage = None;
88                    } else {
89                        let j = p.value_slice()?;
90                        usage = Some(Usage::from_json(j)?);
91                    }
92                }
93                _ => {
94                    p.skip_value()?;
95                }
96            }
97
98            p.skip_ws();
99            if p.try_consume(b',') {
100                continue;
101            }
102            p.skip_ws();
103            if p.try_consume(b'}') {
104                break;
105            }
106        }
107
108        Ok(ChatCompletionsResponse {
109            provider,
110            model,
111            choices,
112            usage,
113        })
114    }
115}
116
117impl Choice {
118    pub fn from_json(json: &str) -> Result<Self, String> {
119        let mut p = Parser::new(json);
120        p.skip_ws();
121        p.expect(b'{')?;
122
123        let mut delta = None;
124
125        'top: loop {
126            p.skip_ws();
127            if p.try_consume(b'}') {
128                break;
129            }
130
131            let key = p
132                .parse_simple_str()
133                .map_err(|err| "Choice::from_json parsing key: ".to_string() + err)?;
134            p.skip_ws();
135            p.expect(b':')?;
136            p.skip_ws();
137
138            match key {
139                "delta" => {
140                    let j = p.value_slice()?;
141                    delta = Some(Message::from_json(j)?);
142                    break 'top;
143                }
144                _ => {
145                    p.skip_value()?;
146                }
147            }
148
149            p.skip_ws();
150            if p.try_consume(b',') {
151                continue;
152            }
153            p.skip_ws();
154            if p.try_consume(b'}') {
155                break;
156            }
157        }
158
159        Ok(Choice {
160            delta: delta.expect("Missing delta in message"),
161        })
162    }
163}
164
165impl Usage {
166    pub fn from_json(json: &str) -> Result<Self, String> {
167        let mut p = Parser::new(json);
168        p.skip_ws();
169        p.expect(b'{')?;
170
171        // Currently we only extract cost
172        let mut cost = 0.0;
173
174        'top: loop {
175            p.skip_ws();
176            if p.try_consume(b'}') {
177                break;
178            }
179
180            let key = p
181                .parse_simple_str()
182                .map_err(|err| "Usage parsing key: ".to_string() + err)?;
183            p.skip_ws();
184            p.expect(b':')?;
185            p.skip_ws();
186
187            match key {
188                "cost" => {
189                    cost = p.parse_f32()?;
190                    // As we only care about cost, we are done as soon as we have it
191                    break 'top;
192                }
193                _ => {
194                    p.skip_value()?;
195                }
196            }
197
198            p.skip_ws();
199            if p.try_consume(b',') {
200                continue;
201            }
202            p.skip_ws();
203            if p.try_consume(b'}') {
204                break;
205            }
206        }
207
208        Ok(Usage { cost })
209    }
210}
211
212impl LastData {
213    pub fn from_json(json: &str) -> Result<Self, Cow<'static, str>> {
214        if json.is_empty() {
215            return Err(
216                "Cannot continue, last-<$TMUX_PANE>.json file is empty. Usually that mains previous run failed.".into(),
217            );
218        }
219        let mut p = Parser::new(json);
220        p.skip_ws();
221        p.expect(b'{')?;
222
223        let mut opts = None;
224        let mut messages = vec![];
225
226        loop {
227            p.skip_ws();
228            if p.try_consume(b'}') {
229                break;
230            }
231
232            let key = p
233                .parse_simple_str()
234                .map_err(|err| "LastData parsing key: ".to_string() + err)?;
235            p.skip_ws();
236            p.expect(b':')?;
237            p.skip_ws();
238
239            match key {
240                "opts" => {
241                    if opts.is_some() {
242                        return Err("duplicate field: opts".into());
243                    }
244                    let j = p.value_slice()?;
245                    opts = Some(PromptOpts::from_json(j)?);
246                }
247                "messages" => {
248                    if !messages.is_empty() {
249                        return Err("duplicate field: messages".into());
250                    }
251                    if !p.try_consume(b'[') {
252                        return Err("messages: Expected array".into());
253                    }
254                    loop {
255                        let j = p.value_slice()?;
256                        let msg = Message::from_json(j)?;
257                        messages.push(msg);
258                        p.skip_ws();
259                        if p.try_consume(b',') {
260                            continue;
261                        }
262                        p.skip_ws();
263                        if p.try_consume(b']') {
264                            break;
265                        }
266                    }
267                }
268                _ => return Err("unknown field".into()),
269            }
270
271            p.skip_ws();
272            if p.try_consume(b',') {
273                continue;
274            }
275            p.skip_ws();
276            if p.try_consume(b'}') {
277                break;
278            }
279        }
280
281        Ok(LastData {
282            opts: opts.expect("Missing prompt opts"),
283            messages,
284        })
285    }
286}
287
288impl Message {
289    pub fn from_json(json: &str) -> Result<Self, Cow<'static, str>> {
290        let mut p = Parser::new(json);
291        p.skip_ws();
292        p.expect(b'{')?;
293
294        let mut role = None;
295        let mut content = vec![];
296        let mut content_seen = false;
297        let mut reasoning = None;
298
299        loop {
300            p.skip_ws();
301            if p.try_consume(b'}') {
302                break;
303            }
304
305            let key = p
306                .parse_simple_str()
307                .map_err(|err| "Message parsing key: ".to_string() + err)?;
308            p.skip_ws();
309            p.expect(b':')?;
310            p.skip_ws();
311
312            match key {
313                "role" => {
314                    if role.is_some() {
315                        return Err("duplicate field: role".into());
316                    }
317                    if p.peek_is_null() {
318                        p.parse_null()?;
319                        role = None;
320                    } else {
321                        let r = p.parse_simple_str()?;
322                        role = Some(Role::from_str(r)?);
323                    }
324                }
325                "content" => {
326                    if content_seen {
327                        return Err("duplicate field: content".into());
328                    }
329                    content_seen = true;
330                    if p.peek_is_null() {
331                        p.parse_null()?;
332                    } else if p.peek() == Some(b'[') {
333                        p.expect(b'[')?;
334                        p.skip_ws();
335                        if !p.try_consume(b']') {
336                            loop {
337                                let j = p.value_slice()?;
338                                content.push(Content::from_json(j)?);
339                                p.skip_ws();
340                                if p.try_consume(b',') {
341                                    continue;
342                                }
343                                p.skip_ws();
344                                if p.try_consume(b']') {
345                                    break;
346                                }
347                            }
348                        }
349                    } else {
350                        content.push(Content::Text(p.parse_string()?));
351                    }
352                }
353                "reasoning" => {
354                    if reasoning.is_some() {
355                        return Err("duplicate field: reasoning".into());
356                    }
357                    if p.peek_is_null() {
358                        p.parse_null()?;
359                        reasoning = None
360                    } else {
361                        reasoning = Some(p.parse_string()?);
362                    }
363                }
364                _ => {
365                    p.skip_value()?;
366                }
367            }
368
369            p.skip_ws();
370            if p.try_consume(b',') {
371                continue;
372            }
373            p.skip_ws();
374            if p.try_consume(b'}') {
375                break;
376            }
377        }
378
379        Ok(Message::with_content(
380            // NVIDIA doesn't always send it. sus.
381            role.unwrap_or(Role::Assistant),
382            content,
383            reasoning,
384        ))
385    }
386}
387
388impl Content {
389    pub fn from_json(json: &str) -> Result<Self, String> {
390        let mut p = Parser::new(json);
391        p.skip_ws();
392        p.expect(b'{')?;
393
394        let mut kind = None;
395        let mut text = None;
396        let mut base64_data = None;
397        let mut mime_type = None;
398        let mut image_url = None;
399        let mut file = None;
400
401        loop {
402            p.skip_ws();
403            if p.try_consume(b'}') {
404                break;
405            }
406
407            let key = p
408                .parse_simple_str()
409                .map_err(|err| "Content parsing key: ".to_string() + err)?;
410            p.skip_ws();
411            p.expect(b':')?;
412            p.skip_ws();
413
414            match key {
415                "type" => {
416                    kind = Some(p.parse_simple_str()?.to_string());
417                }
418                "text" => {
419                    text = Some(p.parse_string()?);
420                }
421                "image_url" => {
422                    let j = p.value_slice()?;
423                    if j.starts_with("http") {
424                        image_url = Some(j);
425                    } else {
426                        let (base64, mt) = parse_image_url(j)?;
427                        base64_data = Some(base64);
428                        mime_type = Some(mt);
429                    }
430                }
431                "file" => {
432                    let j = p.value_slice()?;
433                    file = Some(PromptFile::from_json(j)?);
434                }
435                _ => {
436                    p.skip_value()?;
437                }
438            }
439
440            p.skip_ws();
441            if p.try_consume(b',') {
442                continue;
443            }
444            p.skip_ws();
445            if p.try_consume(b'}') {
446                break;
447            }
448        }
449
450        match kind.as_deref() {
451            Some("text") => Ok(Content::Text(text.ok_or("missing text")?)),
452            Some("image_url") => {
453                if let Some(image_url) = image_url {
454                    Ok(Content::ImageUrl(image_url.to_string()))
455                } else {
456                    Ok(Content::Image {
457                        base64: base64_data.ok_or("missing image_url")?,
458                        mime_type: mime_type.unwrap(),
459                    })
460                }
461            }
462            Some("file") => Ok(Content::File(file.ok_or("missing file")?)),
463            Some(other) => Err("unsupported content type: ".to_string() + other),
464            None => Err("missing content type".to_string()),
465        }
466    }
467}
468
469impl PromptFile {
470    fn from_json(json: &str) -> Result<Self, String> {
471        let mut p = Parser::new(json);
472        p.skip_ws();
473        p.expect(b'{')?;
474
475        let mut filename = None;
476        let mut base64 = None;
477
478        loop {
479            p.skip_ws();
480            if p.try_consume(b'}') {
481                break;
482            }
483
484            let key = p
485                .parse_simple_str()
486                .map_err(|err| "PromptFile parsing key: ".to_string() + err)?;
487            p.skip_ws();
488            p.expect(b':')?;
489            p.skip_ws();
490
491            match key {
492                "filename" => filename = Some(p.parse_string()?),
493                "file_data" => {
494                    let data = p.parse_string()?;
495                    base64 = Some(
496                        data.strip_prefix("data:application/pdf;base64,")
497                            .unwrap_or(data.as_str())
498                            .to_string(),
499                    );
500                }
501                _ => {
502                    p.skip_value()?;
503                }
504            }
505
506            p.skip_ws();
507            if p.try_consume(b',') {
508                continue;
509            }
510            p.skip_ws();
511            if p.try_consume(b'}') {
512                break;
513            }
514        }
515
516        Ok(PromptFile::from_parts(
517            PromptFileKind::File,
518            filename.ok_or("missing filename")?,
519            base64.ok_or("missing file_data")?,
520        ))
521    }
522}
523
524/// Returns (base64_data, mime_type)
525fn parse_image_url(json: &str) -> Result<(String, &'static str), String> {
526    let mut p = Parser::new(json);
527    p.skip_ws();
528    p.expect(b'{')?;
529
530    let mut base64 = None;
531    let mut mime_type = None;
532
533    loop {
534        p.skip_ws();
535        if p.try_consume(b'}') {
536            break;
537        }
538
539        let key = p
540            .parse_simple_str()
541            .map_err(|err| "Image parsing key: ".to_string() + err)?;
542        p.skip_ws();
543        p.expect(b':')?;
544        p.skip_ws();
545
546        match key {
547            "url" => {
548                let data = p.parse_string()?;
549                if data.starts_with("data:image/jpeg") {
550                    mime_type = Some("image/jpeg");
551                    base64 = Some(
552                        data.strip_prefix("data:image/jpeg;base64,")
553                            .unwrap()
554                            .to_string(),
555                    );
556                } else if data.starts_with("data:image/png") {
557                    mime_type = Some("image/png");
558                    base64 = Some(
559                        data.strip_prefix("data:image/png;base64,")
560                            .unwrap()
561                            .to_string(),
562                    );
563                } else {
564                    return Err("Invalid mime type in saved image_url".to_string());
565                };
566            }
567            _ => {
568                p.skip_value()?;
569            }
570        }
571
572        p.skip_ws();
573        if p.try_consume(b',') {
574            continue;
575        }
576        p.skip_ws();
577        if p.try_consume(b'}') {
578            break;
579        }
580    }
581
582    Ok((base64.expect("Missing image URL"), mime_type.unwrap()))
583}
584
585impl ReasoningConfig {
586    pub fn from_json(json: &str) -> Result<ReasoningConfig, Cow<'static, str>> {
587        let mut p = Parser::new(json);
588        p.skip_ws();
589        p.expect(b'{')?;
590
591        let mut enabled: Option<bool> = None;
592        let mut effort: Option<ReasoningEffort> = None;
593        let mut tokens: Option<u32> = None;
594
595        loop {
596            p.skip_ws();
597            if p.try_consume(b'}') {
598                break;
599            }
600
601            // Key
602            let key = p
603                .parse_simple_str()
604                .map_err(|err| "ReasoningConfig parsing key: ".to_string() + err)?;
605            p.skip_ws();
606            p.expect(b':')?;
607            p.skip_ws();
608
609            // Value by key
610            match key {
611                "enabled" => {
612                    if enabled.is_some() {
613                        return Err("duplicate field: enabled".into());
614                    }
615                    if p.peek_is_null() {
616                        p.parse_null()?;
617                        enabled = None;
618                    } else {
619                        enabled = Some(p.parse_bool()?);
620                    }
621                }
622                "effort" => {
623                    if effort.is_some() {
624                        return Err("duplicate field: effort".into());
625                    }
626                    if p.peek_is_null() {
627                        p.parse_null()?;
628                        effort = None;
629                    } else {
630                        let v = p
631                            .parse_simple_str()
632                            .map_err(|err| "Parsing effort: ".to_string() + err)?;
633                        let e = if v.eq_ignore_ascii_case("none") {
634                            ReasoningEffort::None
635                        } else if v.eq_ignore_ascii_case("low") {
636                            ReasoningEffort::Low
637                        } else if v.eq_ignore_ascii_case("medium") {
638                            ReasoningEffort::Medium
639                        } else if v.eq_ignore_ascii_case("high") {
640                            ReasoningEffort::High
641                        } else if v.eq_ignore_ascii_case("xhigh") {
642                            ReasoningEffort::XHigh
643                        } else {
644                            return Err("invalid effort".into());
645                        };
646                        effort = Some(e);
647                    }
648                }
649                "tokens" => {
650                    if tokens.is_some() {
651                        return Err("duplicate field: tokens".into());
652                    }
653                    if p.peek_is_null() {
654                        p.parse_null()?;
655                        tokens = None;
656                    } else {
657                        tokens = Some(p.parse_u32()?);
658                    }
659                }
660                _ => return Err("unknown field".into()),
661            }
662
663            p.skip_ws();
664            if p.try_consume(b',') {
665                continue;
666            }
667
668            p.skip_ws();
669            if p.try_consume(b'}') {
670                break;
671            }
672
673            // If neither comma nor closing brace, it's malformed.
674            if !p.eof() {
675                return Err("expected ',' or '}'".into());
676            } else {
677                return Err("unexpected end of input".into());
678            }
679        }
680
681        p.skip_ws();
682        if !p.eof() {
683            return Err("trailing characters after JSON object".into());
684        }
685
686        let enabled = enabled.ok_or("missing required field: enabled")?;
687
688        Ok(ReasoningConfig {
689            enabled,
690            effort,
691            tokens,
692        })
693    }
694}
695
696impl PromptOpts {
697    pub fn from_json(input: &str) -> Result<Self, Cow<'static, str>> {
698        let mut p = Parser::new(input);
699
700        p.skip_ws();
701        p.expect(b'{')?;
702
703        let mut prompt: Option<String> = None;
704        let mut model: Option<String> = None;
705        let mut provider: Option<String> = None;
706        let mut system: Option<String> = None;
707        let mut priority: Option<Priority> = None;
708        let mut reasoning: Option<ReasoningConfig> = None;
709        let mut show_reasoning: Option<bool> = None;
710        let mut quiet: Option<bool> = None;
711        let mut merge_config = true;
712
713        p.skip_ws();
714        if p.try_consume(b'}') {
715            return Ok(PromptOpts {
716                prompt,
717                models: vec![],
718                provider,
719                system,
720                priority,
721                reasoning,
722                show_reasoning,
723                quiet,
724                merge_config,
725                // TODO: store files in last json, so resume works with files
726                files: vec![],
727            });
728        }
729
730        loop {
731            p.skip_ws();
732            let key = p.parse_simple_str()?;
733            p.skip_ws();
734            p.expect(b':')?;
735            p.skip_ws();
736
737            match key {
738                "prompt" => {
739                    prompt = p.parse_opt_string()?;
740                }
741                "model" => {
742                    model = p.parse_opt_string()?;
743                }
744                "provider" => {
745                    provider = p.parse_opt_string()?;
746                }
747                "system" => {
748                    system = p.parse_opt_string()?;
749                }
750                "priority" => {
751                    if p.peek_is_null() {
752                        p.parse_null()?;
753                        priority = None;
754                    } else {
755                        let s = p.parse_simple_str()?;
756                        priority = Some(Priority::from_str(s).map_err(|_| "invalid priority")?);
757                    }
758                }
759                "reasoning" => {
760                    if p.peek_is_null() {
761                        p.parse_null()?;
762                        reasoning = None;
763                    } else {
764                        // Grab the exact object slice and delegate to ReasoningConfig::from_json
765                        let slice = p.value_slice()?; // must be an object
766                        let cfg = ReasoningConfig::from_json(slice).map_err(|e| {
767                            "parser::PromptOpts::from_json invalid reasoning: ".to_string() + &e
768                        })?;
769                        reasoning = Some(cfg);
770                    }
771                }
772                "show_reasoning" => {
773                    if p.peek_is_null() {
774                        p.parse_null()?;
775                        show_reasoning = None;
776                    } else {
777                        show_reasoning = Some(p.parse_bool()?);
778                    }
779                }
780                "quiet" => {
781                    if p.peek_is_null() {
782                        p.parse_null()?;
783                        quiet = None;
784                    } else {
785                        quiet = Some(p.parse_bool()?);
786                    }
787                }
788                "merge_config" => {
789                    if p.peek_is_null() {
790                        p.parse_null()?;
791                        merge_config = true;
792                    } else {
793                        merge_config = p.parse_bool()?;
794                    }
795                }
796                _ => {
797                    // Unknown field: skip its value
798                    p.skip_value()?;
799                }
800            }
801
802            p.skip_ws();
803            if p.try_consume(b',') {
804                continue;
805            } else {
806                p.expect(b'}')?;
807                break;
808            }
809        }
810
811        Ok(PromptOpts {
812            prompt,
813            models: model.map(|m| vec![m]).unwrap_or_default(),
814            provider,
815            system,
816            priority,
817            reasoning,
818            show_reasoning,
819            quiet,
820            merge_config,
821            files: vec![],
822        })
823    }
824}
825
826impl config::ConfigFile {
827    pub fn from_json(json: &str) -> Result<Self, Cow<'static, str>> {
828        let mut p = Parser::new(json);
829        p.skip_ws();
830        p.expect(b'{')?;
831
832        let mut settings: Option<config::Settings> = None;
833        let mut keys: Vec<config::ApiKey> = vec![];
834        let mut prompt_opts: Option<PromptOpts> = None;
835
836        loop {
837            p.skip_ws();
838            if p.try_consume(b'}') {
839                break;
840            }
841
842            let key = p
843                .parse_simple_str()
844                .map_err(|err| "ConfigFile parsing key: ".to_string() + err)?;
845            p.skip_ws();
846            p.expect(b':')?;
847            p.skip_ws();
848
849            match key {
850                "settings" => {
851                    if settings.is_some() {
852                        return Err("duplicate field: settings".into());
853                    }
854                    let settings_json = p.value_slice()?;
855                    settings = Some(config::Settings::from_json(settings_json)?);
856                }
857                "keys" => {
858                    if !keys.is_empty() {
859                        return Err("duplicate field: keys".into());
860                    }
861                    if !p.try_consume(b'[') {
862                        return Err("keys: Expected array".into());
863                    }
864                    loop {
865                        let j = p.value_slice()?;
866                        let api_key = config::ApiKey::from_json(j)?;
867                        keys.push(api_key);
868                        p.skip_ws();
869                        if p.try_consume(b',') {
870                            continue;
871                        }
872                        p.skip_ws();
873                        if p.try_consume(b']') {
874                            break;
875                        }
876                    }
877                }
878                "prompt_opts" => {
879                    if prompt_opts.is_some() {
880                        return Err("duplicate field: prompt_opts".into());
881                    }
882                    let opts_json = p.value_slice()?;
883                    prompt_opts = Some(PromptOpts::from_json(opts_json)?);
884                }
885                _ => return Err("unknown field".into()),
886            }
887            p.skip_ws();
888            if p.try_consume(b',') {
889                continue;
890            }
891            p.skip_ws();
892            if p.try_consume(b'}') {
893                break;
894            }
895        }
896
897        Ok(config::ConfigFile {
898            settings,
899            keys,
900            prompt_opts,
901        })
902    }
903}
904
905impl config::Settings {
906    pub fn from_json(json: &str) -> Result<Self, Cow<'static, str>> {
907        let mut p = Parser::new(json);
908        p.skip_ws();
909        p.expect(b'{')?;
910
911        let mut save_to_file = None;
912        let mut dns = vec![];
913
914        loop {
915            p.skip_ws();
916            if p.try_consume(b'}') {
917                break;
918            }
919
920            let key = p
921                .parse_simple_str()
922                .map_err(|err| "Settings parsing key: ".to_string() + err)?;
923            p.skip_ws();
924            p.expect(b':')?;
925            p.skip_ws();
926
927            match key {
928                "save_to_file" => {
929                    if save_to_file.is_some() {
930                        return Err("duplicate field: save_to_file".into());
931                    }
932                    if p.peek_is_null() {
933                        p.parse_null()?;
934                        save_to_file = None;
935                    } else {
936                        save_to_file = Some(p.parse_bool()?);
937                    }
938                }
939                "dns" => {
940                    if !dns.is_empty() {
941                        return Err("duplicate field: dns".into());
942                    }
943                    if !p.try_consume(b'[') {
944                        return Err("dns: Expected array".into());
945                    }
946                    loop {
947                        let addr = p.parse_string()?;
948                        dns.push(addr);
949                        p.skip_ws();
950                        if p.try_consume(b',') {
951                            continue;
952                        }
953                        p.skip_ws();
954                        if p.try_consume(b']') {
955                            break;
956                        }
957                    }
958                }
959                _ => return Err("unknown field".into()),
960            }
961
962            p.skip_ws();
963            if p.try_consume(b',') {
964                continue;
965            }
966            p.skip_ws();
967            if p.try_consume(b'}') {
968                break;
969            }
970
971            // If neither comma nor closing brace, it's malformed.
972            if !p.eof() {
973                return Err("expected ',' or '}'".into());
974            } else {
975                return Err("unexpected end of input".into());
976            }
977        }
978
979        let default = config::Settings::default();
980        Ok(config::Settings {
981            save_to_file: save_to_file.unwrap_or(default.save_to_file),
982            dns,
983        })
984    }
985}
986
987impl config::ApiKey {
988    pub fn from_json(json: &str) -> Result<Self, Cow<'static, str>> {
989        let mut p = Parser::new(json);
990        p.skip_ws();
991        p.expect(b'{')?;
992
993        let mut name = None;
994        let mut value = None;
995
996        loop {
997            p.skip_ws();
998            if p.try_consume(b'}') {
999                break;
1000            }
1001
1002            let key = p
1003                .parse_simple_str()
1004                .map_err(|err| "ApiKey parsing key: ".to_string() + err)?;
1005            p.skip_ws();
1006            p.expect(b':')?;
1007            p.skip_ws();
1008
1009            match key {
1010                "name" => {
1011                    if name.is_some() {
1012                        return Err("duplicate field: name".into());
1013                    }
1014                    name = Some(
1015                        p.parse_string()
1016                            .map_err(|err| "Parsing name: ".to_string() + &err)?,
1017                    );
1018                }
1019                "value" => {
1020                    if value.is_some() {
1021                        return Err("duplicate field: value".into());
1022                    }
1023                    value = Some(
1024                        p.parse_string()
1025                            .map_err(|err| "Parsing name: ".to_string() + &err)?,
1026                    );
1027                }
1028                _ => return Err("unknown field".into()),
1029            }
1030            p.skip_ws();
1031            if p.try_consume(b',') {
1032                continue;
1033            } else {
1034                p.expect(b'}')?;
1035                break;
1036            }
1037        }
1038
1039        Ok(config::ApiKey::new(
1040            name.expect("Missing ApiKey name"),
1041            value.expect("Missing ApiKey value"),
1042        ))
1043    }
1044}
1045
1046// --------------------------------------------
1047
1048// Minimal, fast JSON scanner tailored for our needs.
1049struct Parser<'a> {
1050    s: &'a str,
1051    b: &'a [u8],
1052    i: usize,
1053}
1054
1055impl<'a> Parser<'a> {
1056    fn new(s: &'a str) -> Self {
1057        Self {
1058            s,
1059            b: s.as_bytes(),
1060            i: 0,
1061        }
1062    }
1063
1064    fn eof(&self) -> bool {
1065        self.i >= self.b.len()
1066    }
1067
1068    fn peek(&self) -> Option<u8> {
1069        if self.eof() {
1070            None
1071        } else {
1072            Some(self.b[self.i])
1073        }
1074    }
1075
1076    fn try_consume(&mut self, ch: u8) -> bool {
1077        if self.peek() == Some(ch) {
1078            self.i += 1;
1079            true
1080        } else {
1081            false
1082        }
1083    }
1084
1085    fn expect(&mut self, ch: u8) -> Result<(), &'static str> {
1086        if self.try_consume(ch) {
1087            Ok(())
1088        } else {
1089            Err("expected character")
1090        }
1091    }
1092
1093    fn skip_ws(&mut self) {
1094        while let Some(c) = self.peek() {
1095            match c {
1096                b' ' | b'\n' | b'\r' | b'\t' => self.i += 1,
1097                _ => break,
1098            }
1099        }
1100    }
1101
1102    fn starts_with_bytes(&self, pat: &[u8]) -> bool {
1103        let end = self.i + pat.len();
1104        end <= self.b.len() && &self.b[self.i..end] == pat
1105    }
1106
1107    fn parse_null(&mut self) -> Result<(), &'static str> {
1108        if self.starts_with_bytes(b"null") {
1109            self.i += 4;
1110            Ok(())
1111        } else {
1112            Err("expected null")
1113        }
1114    }
1115
1116    fn peek_is_null(&self) -> bool {
1117        self.starts_with_bytes(b"null")
1118    }
1119
1120    fn parse_bool(&mut self) -> Result<bool, String> {
1121        self.skip_ws();
1122        if self.starts_with_bytes(b"true") {
1123            self.i += 4;
1124            Ok(true)
1125        } else if self.starts_with_bytes(b"false") {
1126            self.i += 5;
1127            Ok(false)
1128        } else {
1129            Err("Expected boolean, got: ".to_string() + &String::from_utf8_lossy(&self.b[self.i..]))
1130        }
1131    }
1132
1133    fn parse_u32(&mut self) -> Result<u32, &'static str> {
1134        self.skip_ws();
1135        if self.eof() {
1136            return Err("expected number");
1137        }
1138        if self.peek() == Some(b'-') {
1139            return Err("negative not allowed");
1140        }
1141        let mut val: u32 = 0;
1142        let mut read_any = false;
1143        let len = self.b.len();
1144        while self.i < len {
1145            let c = self.b[self.i];
1146            if c.is_ascii_digit() {
1147                read_any = true;
1148                let digit = (c - b'0') as u32;
1149                // Overflow-safe accumulation
1150                if val > (u32::MAX - digit) / 10 {
1151                    return Err("u32 overflow");
1152                }
1153                val = val * 10 + digit;
1154                self.i += 1;
1155            } else {
1156                break;
1157            }
1158        }
1159        if !read_any {
1160            return Err("expected integer");
1161        }
1162        Ok(val)
1163    }
1164
1165    fn parse_f32(&mut self) -> Result<f32, &'static str> {
1166        self.skip_ws();
1167        if self.eof() {
1168            return Err("expected number");
1169        }
1170
1171        let len = self.b.len();
1172
1173        // Sign
1174        let mut neg = false;
1175        if let Some(c) = self.peek() {
1176            if c == b'-' {
1177                neg = true;
1178                self.i += 1;
1179            } else if c == b'+' {
1180                self.i += 1;
1181            }
1182        }
1183
1184        // Mantissa accumulation (up to 9 significant digits)
1185        let mut mant: u32 = 0;
1186        let mut mant_digits: i32 = 0;
1187        let mut ints: i32 = 0;
1188
1189        // Integer part
1190        while self.i < len {
1191            let c = self.b[self.i];
1192            if c.is_ascii_digit() {
1193                if mant_digits < 9 {
1194                    mant = mant.saturating_mul(10).wrapping_add((c - b'0') as u32);
1195                    mant_digits += 1;
1196                }
1197                self.i += 1;
1198                ints += 1;
1199            } else {
1200                break;
1201            }
1202        }
1203
1204        // Fractional part
1205        let mut frac_any = false;
1206        if self.peek() == Some(b'.') {
1207            self.i += 1;
1208            let start_frac = self.i;
1209            while self.i < len {
1210                let c = self.b[self.i];
1211                if c.is_ascii_digit() {
1212                    if mant_digits < 9 {
1213                        mant = mant.saturating_mul(10).wrapping_add((c - b'0') as u32);
1214                        mant_digits += 1;
1215                    }
1216                    self.i += 1;
1217                } else {
1218                    break;
1219                }
1220            }
1221            frac_any = self.i > start_frac;
1222        }
1223
1224        if ints == 0 && !frac_any {
1225            return Err("expected number");
1226        }
1227
1228        // Exponent part
1229        let mut exp_part: i32 = 0;
1230        if let Some(ech) = self.peek()
1231            && (ech == b'e' || ech == b'E')
1232        {
1233            self.i += 1;
1234            let mut eneg = false;
1235            if let Some(signch) = self.peek() {
1236                if signch == b'-' {
1237                    eneg = true;
1238                    self.i += 1;
1239                } else if signch == b'+' {
1240                    self.i += 1;
1241                }
1242            }
1243            if self.eof() || !self.b[self.i].is_ascii_digit() {
1244                return Err("expected exponent");
1245            }
1246            let mut eacc: i32 = 0;
1247            while self.i < len {
1248                let c = self.b[self.i];
1249                if c.is_ascii_digit() {
1250                    let d = (c - b'0') as i32;
1251                    if eacc < 1_000_000_000 / 10 {
1252                        eacc = eacc * 10 + d;
1253                    } else {
1254                        eacc = 1_000_000_000; // clamp large exponents
1255                    }
1256                    self.i += 1;
1257                } else {
1258                    break;
1259                }
1260            }
1261            exp_part = if eneg { -eacc } else { eacc };
1262        }
1263
1264        // Effective base-10 exponent relative to the mantissa we built
1265        let exp10 = ints - mant_digits + exp_part;
1266
1267        // Scale using f64 to avoid premature underflow; cast to f32 at the end
1268        let mut val = mant as f64;
1269
1270        const POW10_POS: [f64; 39] = [
1271            1.0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14, 1e15,
1272            1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22, 1e23, 1e24, 1e25, 1e26, 1e27, 1e28, 1e29,
1273            1e30, 1e31, 1e32, 1e33, 1e34, 1e35, 1e36, 1e37, 1e38,
1274        ];
1275        const POW10_NEG: [f64; 46] = [
1276            1.0, 1e-1, 1e-2, 1e-3, 1e-4, 1e-5, 1e-6, 1e-7, 1e-8, 1e-9, 1e-10, 1e-11, 1e-12, 1e-13,
1277            1e-14, 1e-15, 1e-16, 1e-17, 1e-18, 1e-19, 1e-20, 1e-21, 1e-22, 1e-23, 1e-24, 1e-25,
1278            1e-26, 1e-27, 1e-28, 1e-29, 1e-30, 1e-31, 1e-32, 1e-33, 1e-34, 1e-35, 1e-36, 1e-37,
1279            1e-38, 1e-39, 1e-40, 1e-41, 1e-42, 1e-43, 1e-44, 1e-45,
1280        ];
1281
1282        if exp10 > 0 {
1283            let mut e = exp10;
1284            while e > 0 {
1285                let chunk = if e > 38 { 38 } else { e } as usize;
1286                val *= POW10_POS[chunk];
1287                if !val.is_finite() {
1288                    return Err("f32 overflow");
1289                }
1290                e -= chunk as i32;
1291            }
1292        } else if exp10 < 0 {
1293            let mut e = -exp10;
1294            while e > 0 {
1295                let chunk = if e > 45 { 45 } else { e } as usize;
1296                val *= POW10_NEG[chunk];
1297                if val == 0.0 {
1298                    break;
1299                }
1300                e -= chunk as i32;
1301            }
1302        }
1303
1304        let mut out = val as f32;
1305        if !out.is_finite() {
1306            return Err("f32 overflow");
1307        }
1308        if neg {
1309            out = -out;
1310        }
1311        Ok(out)
1312    }
1313
1314    fn parse_simple_str(&mut self) -> Result<&'a str, &'static str> {
1315        self.skip_ws();
1316        if self.peek() != Some(b'"') {
1317            return Err("expected string");
1318        }
1319        self.i += 1;
1320        let start = self.i;
1321        let len = self.b.len();
1322        while self.i < len {
1323            let c = self.b[self.i];
1324            if c == b'\\' {
1325                // For maximum speed and simplicity, we reject escapes.
1326                return Err("string escapes are not supported");
1327            }
1328            if c == b'"' {
1329                let end = self.i;
1330                self.i += 1; // consume closing quote
1331                // Safety: start and end are at UTF-8 code point boundaries (quotes),
1332                // so slicing is valid even if contents contain non-ASCII.
1333                return Ok(&self.s[start..end]);
1334            }
1335            self.i += 1;
1336        }
1337        Err("unterminated string")
1338    }
1339
1340    fn parse_string(&mut self) -> Result<String, Cow<'static, str>> {
1341        self.skip_ws();
1342        if self.peek() != Some(b'"') {
1343            return Err(("expected string got: ".to_string()
1344                + &String::from_utf8_lossy(&self.b[self.i..]))
1345                .into());
1346        }
1347        let start = self.i + 1;
1348        let mut i = start;
1349        let len = self.b.len();
1350
1351        // First pass: detect if we need to unescape
1352        let mut needs_unescape = false;
1353        while i < len {
1354            let c = self.b[i];
1355            if c == b'\\' {
1356                needs_unescape = true;
1357                break;
1358            }
1359            if c == b'"' {
1360                // no escapes
1361                let s = core::str::from_utf8(&self.b[start..i]).map_err(|_| "utf8 error")?;
1362                self.i = i + 1;
1363                return Ok(s.to_owned());
1364            }
1365            i += 1;
1366        }
1367        if !needs_unescape {
1368            return Err("unterminated string".into());
1369        }
1370
1371        // Second pass: build with unescape
1372        // The "256" should include the biggest single-chunk reasoning item, which
1373        // depends on the inference server's caching.
1374        let mut out = String::with_capacity((i - start) + 256);
1375        let mut seg_start = start;
1376        while i < len {
1377            let c = self.b[i];
1378            if c == b'\\' {
1379                // push preceding segment
1380                if i > seg_start {
1381                    let prev =
1382                        core::str::from_utf8(&self.b[seg_start..i]).map_err(|_| "utf8 error")?;
1383                    out.push_str(prev);
1384                }
1385                i += 1;
1386                if i >= len {
1387                    return Err("bad escape".into());
1388                }
1389                let e = self.b[i];
1390                match e {
1391                    b'"' => out.push('"'),
1392                    b'\\' => out.push('\\'),
1393                    b'/' => out.push('/'),
1394                    b'b' => out.push('\u{0008}'),
1395                    b'f' => out.push('\u{000C}'),
1396                    b'n' => out.push('\n'),
1397                    b'r' => out.push('\r'),
1398                    b't' => out.push('\t'),
1399                    b'u' => {
1400                        let (cp, new_i) = self.parse_u_escape(i + 1)?;
1401                        i = new_i - 1; // -1 because loop will i += 1 at end
1402                        if let Some(ch) = core::char::from_u32(cp) {
1403                            out.push(ch);
1404                        } else {
1405                            return Err("invalid unicode".into());
1406                        }
1407                    }
1408                    _ => return Err("bad escape".into()),
1409                }
1410                i += 1;
1411                seg_start = i;
1412                continue;
1413            } else if c == b'"' {
1414                // end
1415                if i > seg_start {
1416                    out.push_str(
1417                        core::str::from_utf8(&self.b[seg_start..i]).map_err(|_| "utf8 error")?,
1418                    );
1419                }
1420                self.i = i + 1;
1421                return Ok(out);
1422            } else {
1423                i += 1;
1424            }
1425        }
1426        Err("unterminated string".into())
1427    }
1428
1429    // Parses \uXXXX (with surrogate-pair handling). Input index points at first hex digit after 'u'.
1430    fn parse_u_escape(&self, i: usize) -> Result<(u32, usize), &'static str> {
1431        fn hex4(bytes: &[u8], i: usize) -> Result<(u16, usize), &'static str> {
1432            let end = i + 4;
1433            if end > bytes.len() {
1434                return Err("short \\u");
1435            }
1436            let mut v: u16 = 0;
1437            for b in bytes.iter().take(end).skip(i) {
1438                v = (v << 4) | hex_val(*b)?;
1439            }
1440            Ok((v, end))
1441        }
1442        fn hex_val(b: u8) -> Result<u16, &'static str> {
1443            match b {
1444                b'0'..=b'9' => Ok((b - b'0') as u16),
1445                b'a'..=b'f' => Ok((b - b'a' + 10) as u16),
1446                b'A'..=b'F' => Ok((b - b'A' + 10) as u16),
1447                _ => Err("bad hex"),
1448            }
1449        }
1450
1451        let (first, i2) = hex4(self.b, i)?;
1452        let cp = first as u32;
1453
1454        // Surrogate pair handling
1455        if (0xD800..=0xDBFF).contains(&first) {
1456            // Expect \uXXXX next
1457            if i2 + 2 > self.b.len() || self.b[i2] != b'\\' || self.b[i2 + 1] != b'u' {
1458                return Err("missing low surrogate");
1459            }
1460            let (second, i3) = hex4(self.b, i2 + 2)?;
1461            if !(0xDC00..=0xDFFF).contains(&second) {
1462                return Err("invalid low surrogate");
1463            }
1464            let high = (first as u32) - 0xD800;
1465            let low = (second as u32) - 0xDC00;
1466            let code = 0x10000 + ((high << 10) | low);
1467            Ok((code, i3))
1468        } else if (0xDC00..=0xDFFF).contains(&first) {
1469            Err("unpaired low surrogate")
1470        } else {
1471            Ok((cp, i2))
1472        }
1473    }
1474
1475    fn parse_opt_string(&mut self) -> Result<Option<String>, Cow<'static, str>> {
1476        if self.peek_is_null() {
1477            self.parse_null()?;
1478            Ok(None)
1479        } else {
1480            let s = self.parse_string()?;
1481            Ok(Some(s))
1482        }
1483    }
1484
1485    // Returns a slice of the next JSON value and advances past it.
1486    fn value_slice(&mut self) -> Result<&'a str, &'static str> {
1487        self.skip_ws();
1488        let start = self.i;
1489        let end = self.find_value_end()?;
1490        let out = &self.s[start..end];
1491        self.i = end;
1492        Ok(out)
1493    }
1494
1495    // Skips the next JSON value (string/number/boolean/null/object/array).
1496    fn skip_value(&mut self) -> Result<(), &'static str> {
1497        let _ = self.value_slice()?;
1498        Ok(())
1499    }
1500
1501    fn find_value_end(&mut self) -> Result<usize, &'static str> {
1502        if self.eof() {
1503            return Err("unexpected end");
1504        }
1505        match self.b[self.i] {
1506            b'"' => self.scan_string_end(),
1507            b'{' => self.scan_brace_block(b'{', b'}'),
1508            b'[' => self.scan_brace_block(b'[', b']'),
1509            b't' => {
1510                if self.starts_with_bytes(b"true") {
1511                    Ok(self.i + 4)
1512                } else {
1513                    Err("bad literal")
1514                }
1515            }
1516            b'f' => {
1517                if self.starts_with_bytes(b"false") {
1518                    Ok(self.i + 5)
1519                } else {
1520                    Err("bad literal")
1521                }
1522            }
1523            b'n' => {
1524                if self.starts_with_bytes(b"null") {
1525                    Ok(self.i + 4)
1526                } else {
1527                    Err("bad literal")
1528                }
1529            }
1530            b'-' | b'0'..=b'9' => self.scan_number_end(),
1531            _t => {
1532                //let t_str = crate::utils::num_to_string(t as usize);
1533                //crate::utils::print_string(c"unexpected token: ", &t_str);
1534                Err("unexpected token")
1535            }
1536        }
1537    }
1538
1539    fn scan_string_end(&self) -> Result<usize, &'static str> {
1540        let mut i = self.i + 1;
1541        let len = self.b.len();
1542        let mut escaped = false;
1543        while i < len {
1544            let c = self.b[i];
1545            if escaped {
1546                escaped = false;
1547                i += 1;
1548                continue;
1549            }
1550            if c == b'\\' {
1551                escaped = true;
1552                i += 1;
1553                continue;
1554            }
1555            if c == b'"' {
1556                return Ok(i + 1);
1557            }
1558            i += 1;
1559        }
1560        Err("unterminated string")
1561    }
1562
1563    fn scan_brace_block(&self, open: u8, close: u8) -> Result<usize, &'static str> {
1564        let mut i = self.i;
1565        let len = self.b.len();
1566        let mut depth = 0usize;
1567        while i < len {
1568            let c = self.b[i];
1569            if c == b'"' {
1570                // Skip string
1571                let p = Parser {
1572                    s: self.s,
1573                    b: self.b,
1574                    i,
1575                };
1576                i = p.scan_string_end()?; // returns position after closing "
1577                continue;
1578            }
1579            if c == open {
1580                depth += 1;
1581            } else if c == close {
1582                depth -= 1;
1583                if depth == 0 {
1584                    return Ok(i + 1);
1585                }
1586            }
1587            i += 1;
1588        }
1589        Err("unterminated structure")
1590    }
1591
1592    fn scan_number_end(&self) -> Result<usize, &'static str> {
1593        let len = self.b.len();
1594        let mut i = self.i;
1595
1596        if self.b[i] == b'-' {
1597            i += 1;
1598            if i >= len {
1599                return Err("bad number");
1600            }
1601        }
1602
1603        // int part
1604        match self.b[i] {
1605            b'0' => {
1606                i += 1;
1607            }
1608            b'1'..=b'9' => {
1609                i += 1;
1610                while i < len {
1611                    match self.b[i] {
1612                        b'0'..=b'9' => i += 1,
1613                        _ => break,
1614                    }
1615                }
1616            }
1617            _ => return Err("bad number"),
1618        }
1619
1620        // frac
1621        if i < len && self.b[i] == b'.' {
1622            i += 1;
1623            if i >= len || !self.b[i].is_ascii_digit() {
1624                return Err("bad number");
1625            }
1626            while i < len && self.b[i].is_ascii_digit() {
1627                i += 1;
1628            }
1629        }
1630
1631        // exp
1632        if i < len && (self.b[i] == b'e' || self.b[i] == b'E') {
1633            i += 1;
1634            if i < len && (self.b[i] == b'+' || self.b[i] == b'-') {
1635                i += 1;
1636            }
1637            if i >= len || !self.b[i].is_ascii_digit() {
1638                return Err("bad number");
1639            }
1640            while i < len && self.b[i].is_ascii_digit() {
1641                i += 1;
1642            }
1643        }
1644
1645        Ok(i)
1646    }
1647}
1648
1649#[cfg(test)]
1650mod tests {
1651    extern crate alloc;
1652    use alloc::string::ToString;
1653
1654    use super::*;
1655    use crate::LastData;
1656
1657    #[test]
1658    fn rp1() {
1659        let cfg = ReasoningConfig::from_json(r#"{"enabled": false}"#).unwrap();
1660        assert!(!cfg.enabled);
1661        assert!(cfg.effort.is_none());
1662        assert!(cfg.tokens.is_none());
1663    }
1664
1665    #[test]
1666    fn rp2() {
1667        let cfg = ReasoningConfig::from_json(r#"{"enabled": true, "effort": "medium"}"#).unwrap();
1668        assert!(cfg.enabled);
1669        assert_eq!(cfg.effort, Some(ReasoningEffort::Medium));
1670        assert!(cfg.tokens.is_none());
1671    }
1672
1673    #[test]
1674    fn rp3() {
1675        let cfg = ReasoningConfig::from_json(r#"{"enabled": true, "tokens": 2048}"#).unwrap();
1676        assert!(cfg.enabled);
1677        assert_eq!(cfg.tokens, Some(2048));
1678        assert!(cfg.effort.is_none());
1679    }
1680
1681    #[test]
1682    fn rp4() {
1683        let cfg = ReasoningConfig::from_json(r#"{"enabled":true,"effort":"high","tokens":null}"#)
1684            .unwrap();
1685        assert!(cfg.enabled);
1686        assert_eq!(cfg.effort, Some(ReasoningEffort::High));
1687        assert!(cfg.tokens.is_none());
1688    }
1689
1690    #[test]
1691    fn cpo1() {
1692        let s = r#"
1693 {
1694     "prompt": "\n\nExample JSON 1: {\"enabled\": false}\n",
1695     "model": "google/gemma-3n-e4b-it:free",
1696     "system": "Make your answer concise but complete. No yapping. Direct professional tone. No emoji.",
1697     "show_reasoning": false,
1698     "reasoning": { "enabled": false },
1699     "merge_config": true
1700 }
1701 "#;
1702        let opts = PromptOpts::from_json(s).unwrap();
1703        assert!(!opts.show_reasoning.unwrap());
1704        assert_eq!(opts.models, vec!["google/gemma-3n-e4b-it:free"]);
1705        assert!(!opts.reasoning.unwrap().enabled);
1706        assert!(opts.merge_config);
1707    }
1708
1709    #[test]
1710    fn cpo2() {
1711        let s = r#"
1712    {"model":"openai/gpt-5","provider":"openai","system":"Make your answer concise but complete. No yapping. Direct professional tone. No emoji.","priority":null,"reasoning":{"enabled":true,"effort":"high","tokens":null},"show_reasoning":false,"quiet":true}
1713    "#;
1714        let opts = PromptOpts::from_json(s).unwrap();
1715        assert!(!opts.show_reasoning.unwrap());
1716        assert_eq!(opts.models, vec!["openai/gpt-5"]);
1717        assert!(opts.reasoning.as_ref().unwrap().enabled);
1718        assert_eq!(
1719            opts.reasoning.as_ref().unwrap().effort,
1720            Some(ReasoningEffort::High)
1721        );
1722    }
1723
1724    #[test]
1725    fn last_data() {
1726        let s = r#"
1727{"opts":{"model":"google/gemma-3n-e4b-it:free","provider":"google-ai-studio","system":"Make your answer concise but complete. No yapping. Direct professional tone. No emoji.","priority":null,"reasoning":{"enabled":false,"effort":null,"tokens":null},"show_reasoning":false},"messages":[{"role":"user","content":"Hello"},{"role":"assistant","content":"Hello there! 😊How can I help you today? I'm ready for anything – questions, stories, ideas, or just a friendly chat!Let me know what's on your mind. ✨"}]}
1728"#;
1729        let l = LastData::from_json(s).unwrap();
1730        assert_eq!(l.opts.provider.as_deref(), Some("google-ai-studio"));
1731        assert_eq!(l.messages.len(), 2);
1732    }
1733
1734    #[test]
1735    fn test_usage() {
1736        let s = r#"{"prompt_tokens":42,"completion_tokens":2,"total_tokens":44,"cost":0.0534,"is_byok":false,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"cost_details":{"upstream_inference_cost":null,"upstream_inference_prompt_cost":0,"upstream_inference_completions_cost":0},"completion_tokens_details":{"reasoning_tokens":0,"image_tokens":0}}"#;
1737        let usage = Usage::from_json(s).unwrap();
1738        assert_eq!(usage.cost, 0.0534);
1739    }
1740
1741    #[test]
1742    fn test_choice() {
1743        let s = r#"{"index":0,"delta":{"role":"assistant","content":"Hello"},"finish_reason":"stop","native_finish_reason":"stop","logprobs":null}"#;
1744        let choice = Choice::from_json(s).unwrap();
1745        assert_eq!(choice.delta.text(), Some("Hello"));
1746    }
1747
1748    #[test]
1749    fn test_chat_completions_response_simple() {
1750        let arr = [
1751            r#"{"id":"gen-1756743299-7ytIBcjALWQQShwMQfw9","provider":"Meta","model":"meta-llama/llama-3.3-8b-instruct:free","object":"chat.completion.chunk","created":1756743300,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}"#,
1752            r#"{"id":"gen-1756743299-7ytIBcjALWQQShwMQfw9","provider":"Meta","model":"meta-llama/llama-3.3-8b-instruct:free","object":"chat.completion.chunk","created":1756743300,"choices":[{"index":0,"delta":{"role":"assistant","content":"Hello"},"finish_reason":"stop","native_finish_reason":"stop","logprobs":null}]}"#,
1753            r#"{"id":"gen-1756743299-7ytIBcjALWQQShwMQfw9","provider":"Meta","model":"meta-llama/llama-3.3-8b-instruct:free","object":"chat.completion.chunk","created":1756743300,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"usage":{"prompt_tokens":42,"completion_tokens":2,"total_tokens":44,"cost":0,"is_byok":false,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"cost_details":{"upstream_inference_cost":null,"upstream_inference_prompt_cost":0,"upstream_inference_completions_cost":0},"completion_tokens_details":{"reasoning_tokens":0,"image_tokens":0}}}"#,
1754        ];
1755        for a in arr {
1756            let ccr = ChatCompletionsResponse::from_json(a).unwrap();
1757            assert_eq!(ccr.provider.as_deref(), Some("Meta"));
1758            assert_eq!(
1759                ccr.model.as_deref(),
1760                Some("meta-llama/llama-3.3-8b-instruct:free")
1761            );
1762            assert_eq!(ccr.choices.len(), 1);
1763        }
1764    }
1765
1766    #[test]
1767    fn test_chat_completions_response_more() {
1768        let arr = [
1769            r#"{"id":"gen-1756749262-liysSWPMM37eb25U5gXO","provider":"WandB","model":"deepseek/deepseek-chat-v3.1","object":"chat.completion.chunk","created":1756749262,"choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning":null,"reasoning_details":[]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}"#,
1770            r#"{"id":"gen-1756749262-liysSWPMM37eb25U5gXO","provider":"WandB","model":"deepseek/deepseek-chat-v3.1","object":"chat.completion.chunk","created":1756749262,"choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning":null,"reasoning_details":[]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}"#,
1771            r#"{"id":"gen-1756749262-liysSWPMM37eb25U5gXO","provider":"WandB","model":"deepseek/deepseek-chat-v3.1","object":"chat.completion.chunk","created":1756749262,"choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning":null,"reasoning_details":[]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}"#,
1772            r#"{"id":"gen-1756749262-liysSWPMM37eb25U5gXO","provider":"WandB","model":"deepseek/deepseek-chat-v3.1","object":"chat.completion.chunk","created":1756749262,"choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning":null,"reasoning_details":[]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}"#,
1773            r#"{"id":"gen-1756749262-liysSWPMM37eb25U5gXO","provider":"WandB","model":"deepseek/deepseek-chat-v3.1","object":"chat.completion.chunk","created":1756749262,"choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning":null,"reasoning_details":[]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}"#,
1774            r#"{"id":"gen-1756749262-liysSWPMM37eb25U5gXO","provider":"WandB","model":"deepseek/deepseek-chat-v3.1","object":"chat.completion.chunk","created":1756749262,"choices":[{"index":0,"delta":{"role":"assistant","content":"Rea","reasoning":null,"reasoning_details":[]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}"#,
1775            r#"{"id":"gen-1756749262-liysSWPMM37eb25U5gXO","provider":"WandB","model":"deepseek/deepseek-chat-v3.1","object":"chat.completion.chunk","created":1756749262,"choices":[{"index":0,"delta":{"role":"assistant","content":"l","reasoning":null,"reasoning_details":[]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}"#,
1776            r#"{"id":"gen-1756749262-liysSWPMM37eb25U5gXO","provider":"WandB","model":"deepseek/deepseek-chat-v3.1","object":"chat.completion.chunk","created":1756749262,"choices":[{"index":0,"delta":{"role":"assistant","content":" Madrid, 14 times.","reasoning":null,"reasoning_details":[]},"finish_reason":"stop","native_finish_reason":"stop","logprobs":null}]}"#,
1777            r#"{"id":"gen-1756749262-liysSWPMM37eb25U5gXO","provider":"WandB","model":"deepseek/deepseek-chat-v3.1","object":"chat.completion.chunk","created":1756749262,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"usage":{"prompt_tokens":33,"completion_tokens":8,"total_tokens":41,"cost":0.0000310365,"is_byok":false,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"cost_details":{"upstream_inference_cost":null,"upstream_inference_prompt_cost":0.00001815,"upstream_inference_completions_cost":0.0000132},"completion_tokens_details":{"reasoning_tokens":0,"image_tokens":0}}}"#,
1778        ];
1779        for a in arr {
1780            let ccr = ChatCompletionsResponse::from_json(a).unwrap();
1781            assert_eq!(ccr.provider.as_deref(), Some("WandB"));
1782            assert_eq!(ccr.model.as_deref(), Some("deepseek/deepseek-chat-v3.1"));
1783            assert_eq!(ccr.choices.len(), 1);
1784        }
1785    }
1786
1787    // Various null fields, including inside the message, and usage.
1788    #[test]
1789    fn test_nvidia_misc() {
1790        let s = r#"{"id":"8f20d6699e194a0abed38c671384d32d","object":"chat.completion.chunk","created":1770582573,"model":"qwen/qwen3-next-80b-a3b-instruct","choices":[{"index":0,"delta":{"role":null,"content":"Ta","reasoning_content":null,"tool_calls":null},"logprobs":null,"finish_reason":null,"matched_stop":null}],"usage":null}"#;
1791        let ccr = ChatCompletionsResponse::from_json(s).unwrap();
1792        assert_eq!(ccr.choices[0].delta.text(), Some("Ta"));
1793    }
1794
1795    #[test]
1796    fn message_content_array() {
1797        let s = r#"{"role":"user","content":[{"type":"text","text":"Hello"},{"type":"text","text":" there"}]}"#;
1798        let msg = Message::from_json(s).unwrap();
1799        assert_eq!(msg.content.len(), 2);
1800        assert_eq!(msg.content[0].text(), Some("Hello"));
1801        assert_eq!(msg.content[1].text(), Some(" there"));
1802    }
1803
1804    #[test]
1805    fn api_key() {
1806        let s = r#"{"name":"openrouter","value":"sk-or-v1-a123b456c789d012a345b8032470394876576573242374098174093274abcdef"}"#;
1807        let got = config::ApiKey::from_json(s).unwrap();
1808        let expect = config::ApiKey::new(
1809            "openrouter".to_string(),
1810            "sk-or-v1-a123b456c789d012a345b8032470394876576573242374098174093274abcdef".to_string(),
1811        );
1812        assert_eq!(got, expect);
1813    }
1814
1815    #[test]
1816    fn settings() {
1817        let s = r#"{
1818    "save_to_file": true,
1819    "dns": ["104.18.2.115", "104.18.3.115"]
1820}"#;
1821        let settings = config::Settings::from_json(s).unwrap();
1822        assert!(settings.save_to_file);
1823        assert_eq!(settings.dns, ["104.18.2.115", "104.18.3.115"]);
1824    }
1825
1826    #[test]
1827    fn config_file() {
1828        let s = r#"
1829{
1830    "keys": [{"name": "openrouter", "value": "sk-or-v1-abcd1234"}],
1831    "settings": {
1832        "save_to_file": true,
1833        "dns": ["104.18.2.115", "104.18.3.115"]
1834    },
1835    "prompt_opts": {
1836        "model": "google/gemma-3n-e4b-it:free",
1837        "system": "Make your answer concise but complete. No yapping. Direct professional tone. No emoji.",
1838        "quiet": false,
1839        "show_reasoning": false,
1840        "reasoning": {
1841            "enabled": false
1842        }
1843    }
1844}
1845"#;
1846        let cfg = config::ConfigFile::from_json(s).unwrap();
1847        assert_eq!(cfg.keys.len(), 1);
1848        assert!(cfg.settings.is_some());
1849        assert!(cfg.prompt_opts.is_some());
1850    }
1851}