1use 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::{
17 ChatCompletionsResponse, Choice, LastData, Message, Priority, PromptOpts, ReasoningConfig,
18 ReasoningEffort, Role, Usage,
19};
20
21impl ChatCompletionsResponse {
22 pub fn from_json(json: &str) -> Result<Self, Cow<'static, str>> {
23 let mut p = Parser::new(json);
24 p.skip_ws();
25 p.expect(b'{')?;
26
27 let mut provider = None;
28 let mut model = None;
29 let mut choices = vec![];
30 let mut usage = None;
31
32 loop {
33 p.skip_ws();
34 if p.try_consume(b'}') {
35 break;
36 }
37
38 let key = p
39 .parse_simple_str()
40 .map_err(|err| "ChatCompletionsResponse parsing key: ".to_string() + err)?;
41 p.skip_ws();
42 p.expect(b':')?;
43 p.skip_ws();
44
45 match key {
46 "provider" => {
47 if provider.is_some() {
48 return Err("duplicate field: provider".into());
49 }
50 provider = Some(p.parse_string()?);
51 }
52 "model" => {
53 if model.is_some() {
54 return Err("duplicate field: model".into());
55 }
56 model = Some(p.parse_string()?);
57 }
58 "choices" => {
59 if !choices.is_empty() {
60 return Err("duplicate field: choices".into());
61 }
62 if !p.try_consume(b'[') {
63 return Err("choices: Expected array".into());
64 }
65 p.skip_ws();
66 if !p.try_consume(b']') {
68 loop {
69 let j = p.value_slice()?;
70 let choice = Choice::from_json(j)?;
71 choices.push(choice);
72 p.skip_ws();
73 if p.try_consume(b',') {
74 continue;
75 }
76 p.skip_ws();
77 if p.try_consume(b']') {
78 break;
79 }
80 }
81 }
82 }
83 "usage" => {
84 if p.peek_is_null() {
85 p.parse_null()?;
86 usage = None;
87 } else {
88 let j = p.value_slice()?;
89 usage = Some(Usage::from_json(j)?);
90 }
91 }
92 _ => {
93 p.skip_value()?;
94 }
95 }
96
97 p.skip_ws();
98 if p.try_consume(b',') {
99 continue;
100 }
101 p.skip_ws();
102 if p.try_consume(b'}') {
103 break;
104 }
105 }
106
107 Ok(ChatCompletionsResponse {
108 provider,
109 model,
110 choices,
111 usage,
112 })
113 }
114}
115
116impl Choice {
117 pub fn from_json(json: &str) -> Result<Self, String> {
118 let mut p = Parser::new(json);
119 p.skip_ws();
120 p.expect(b'{')?;
121
122 let mut delta = None;
123
124 'top: loop {
125 p.skip_ws();
126 if p.try_consume(b'}') {
127 break;
128 }
129
130 let key = p
131 .parse_simple_str()
132 .map_err(|err| "Choice::from_json parsing key: ".to_string() + err)?;
133 p.skip_ws();
134 p.expect(b':')?;
135 p.skip_ws();
136
137 match key {
138 "delta" => {
139 let j = p.value_slice()?;
140 delta = Some(Message::from_json(j)?);
141 break 'top;
142 }
143 _ => {
144 p.skip_value()?;
145 }
146 }
147
148 p.skip_ws();
149 if p.try_consume(b',') {
150 continue;
151 }
152 p.skip_ws();
153 if p.try_consume(b'}') {
154 break;
155 }
156 }
157
158 Ok(Choice {
159 delta: delta.expect("Missing delta in message"),
160 })
161 }
162}
163
164impl Usage {
165 pub fn from_json(json: &str) -> Result<Self, String> {
166 let mut p = Parser::new(json);
167 p.skip_ws();
168 p.expect(b'{')?;
169
170 let mut cost = 0.0;
172
173 'top: loop {
174 p.skip_ws();
175 if p.try_consume(b'}') {
176 break;
177 }
178
179 let key = p
180 .parse_simple_str()
181 .map_err(|err| "Usage parsing key: ".to_string() + err)?;
182 p.skip_ws();
183 p.expect(b':')?;
184 p.skip_ws();
185
186 match key {
187 "cost" => {
188 cost = p.parse_f32()?;
189 break 'top;
191 }
192 _ => {
193 p.skip_value()?;
194 }
195 }
196
197 p.skip_ws();
198 if p.try_consume(b',') {
199 continue;
200 }
201 p.skip_ws();
202 if p.try_consume(b'}') {
203 break;
204 }
205 }
206
207 Ok(Usage { cost })
208 }
209}
210
211impl LastData {
212 pub fn from_json(json: &str) -> Result<Self, Cow<'static, str>> {
213 if json.is_empty() {
214 return Err(
215 "Cannot continue, last-<$TMUX_PANE>.json file is empty. Usually that mains previous run failed.".into(),
216 );
217 }
218 let mut p = Parser::new(json);
219 p.skip_ws();
220 p.expect(b'{')?;
221
222 let mut opts = None;
223 let mut messages = vec![];
224
225 loop {
226 p.skip_ws();
227 if p.try_consume(b'}') {
228 break;
229 }
230
231 let key = p
232 .parse_simple_str()
233 .map_err(|err| "LastData parsing key: ".to_string() + err)?;
234 p.skip_ws();
235 p.expect(b':')?;
236 p.skip_ws();
237
238 match key {
239 "opts" => {
240 if opts.is_some() {
241 return Err("duplicate field: opts".into());
242 }
243 let j = p.value_slice()?;
244 opts = Some(PromptOpts::from_json(j)?);
245 }
246 "messages" => {
247 if !messages.is_empty() {
248 return Err("duplicate field: messages".into());
249 }
250 if !p.try_consume(b'[') {
251 return Err("messages: Expected array".into());
252 }
253 loop {
254 let j = p.value_slice()?;
255 let msg = Message::from_json(j)?;
256 messages.push(msg);
257 p.skip_ws();
258 if p.try_consume(b',') {
259 continue;
260 }
261 p.skip_ws();
262 if p.try_consume(b']') {
263 break;
264 }
265 }
266 }
267 _ => return Err("unknown field".into()),
268 }
269
270 p.skip_ws();
271 if p.try_consume(b',') {
272 continue;
273 }
274 p.skip_ws();
275 if p.try_consume(b'}') {
276 break;
277 }
278 }
279
280 Ok(LastData {
281 opts: opts.expect("Missing prompt opts"),
282 messages,
283 })
284 }
285}
286
287impl Message {
288 pub fn from_json(json: &str) -> Result<Self, Cow<'static, str>> {
289 let mut p = Parser::new(json);
290 p.skip_ws();
291 p.expect(b'{')?;
292
293 let mut role = None;
294 let mut content = None;
295 let mut reasoning = None;
296
297 loop {
298 p.skip_ws();
299 if p.try_consume(b'}') {
300 break;
301 }
302
303 let key = p
304 .parse_simple_str()
305 .map_err(|err| "Message parsing key: ".to_string() + err)?;
306 p.skip_ws();
307 p.expect(b':')?;
308 p.skip_ws();
309
310 match key {
311 "role" => {
312 if role.is_some() {
313 return Err("duplicate field: role".into());
314 }
315 if p.peek_is_null() {
316 p.parse_null()?;
317 role = None;
318 } else {
319 let r = p.parse_simple_str()?;
320 role = Some(Role::from_str(r)?);
321 }
322 }
323 "content" => {
324 if content.is_some() {
325 return Err("duplicate field: content".into());
326 }
327 if p.peek_is_null() {
328 p.parse_null()?;
329 content = None;
330 } else {
331 content = Some(p.parse_string()?);
332 }
333 }
334 "reasoning" => {
335 if reasoning.is_some() {
336 return Err("duplicate field: reasoning".into());
337 }
338 if p.peek_is_null() {
339 p.parse_null()?;
340 reasoning = None
341 } else {
342 reasoning = Some(p.parse_string()?);
343 }
344 }
345 _ => {
346 p.skip_value()?;
347 }
348 }
349
350 p.skip_ws();
351 if p.try_consume(b',') {
352 continue;
353 }
354 p.skip_ws();
355 if p.try_consume(b'}') {
356 break;
357 }
358 }
359
360 Ok(Message::new(
361 role.unwrap_or(Role::Assistant),
363 content,
364 reasoning,
365 ))
366 }
367}
368
369impl ReasoningConfig {
370 pub fn from_json(json: &str) -> Result<ReasoningConfig, Cow<'static, str>> {
371 let mut p = Parser::new(json);
372 p.skip_ws();
373 p.expect(b'{')?;
374
375 let mut enabled: Option<bool> = None;
376 let mut effort: Option<ReasoningEffort> = None;
377 let mut tokens: Option<u32> = None;
378
379 loop {
380 p.skip_ws();
381 if p.try_consume(b'}') {
382 break;
383 }
384
385 let key = p
387 .parse_simple_str()
388 .map_err(|err| "ReasoningConfig parsing key: ".to_string() + err)?;
389 p.skip_ws();
390 p.expect(b':')?;
391 p.skip_ws();
392
393 match key {
395 "enabled" => {
396 if enabled.is_some() {
397 return Err("duplicate field: enabled".into());
398 }
399 if p.peek_is_null() {
400 p.parse_null()?;
401 enabled = None;
402 } else {
403 enabled = Some(p.parse_bool()?);
404 }
405 }
406 "effort" => {
407 if effort.is_some() {
408 return Err("duplicate field: effort".into());
409 }
410 if p.peek_is_null() {
411 p.parse_null()?;
412 effort = None;
413 } else {
414 let v = p
415 .parse_simple_str()
416 .map_err(|err| "Parsing effort: ".to_string() + err)?;
417 let e = if v.eq_ignore_ascii_case("none") {
418 ReasoningEffort::None
419 } else if v.eq_ignore_ascii_case("low") {
420 ReasoningEffort::Low
421 } else if v.eq_ignore_ascii_case("medium") {
422 ReasoningEffort::Medium
423 } else if v.eq_ignore_ascii_case("high") {
424 ReasoningEffort::High
425 } else if v.eq_ignore_ascii_case("xhigh") {
426 ReasoningEffort::XHigh
427 } else {
428 return Err("invalid effort".into());
429 };
430 effort = Some(e);
431 }
432 }
433 "tokens" => {
434 if tokens.is_some() {
435 return Err("duplicate field: tokens".into());
436 }
437 if p.peek_is_null() {
438 p.parse_null()?;
439 tokens = None;
440 } else {
441 tokens = Some(p.parse_u32()?);
442 }
443 }
444 _ => return Err("unknown field".into()),
445 }
446
447 p.skip_ws();
448 if p.try_consume(b',') {
449 continue;
450 }
451
452 p.skip_ws();
453 if p.try_consume(b'}') {
454 break;
455 }
456
457 if !p.eof() {
459 return Err("expected ',' or '}'".into());
460 } else {
461 return Err("unexpected end of input".into());
462 }
463 }
464
465 p.skip_ws();
466 if !p.eof() {
467 return Err("trailing characters after JSON object".into());
468 }
469
470 let enabled = enabled.ok_or("missing required field: enabled")?;
471
472 Ok(ReasoningConfig {
473 enabled,
474 effort,
475 tokens,
476 })
477 }
478}
479
480impl PromptOpts {
481 pub fn from_json(input: &str) -> Result<Self, Cow<'static, str>> {
482 let mut p = Parser::new(input);
483
484 p.skip_ws();
485 p.expect(b'{')?;
486
487 let mut prompt: Option<String> = None;
488 let mut model: Option<String> = None;
489 let mut provider: Option<String> = None;
490 let mut system: Option<String> = None;
491 let mut priority: Option<Priority> = None;
492 let mut reasoning: Option<ReasoningConfig> = None;
493 let mut show_reasoning: Option<bool> = None;
494 let mut quiet: Option<bool> = None;
495 let mut merge_config = true;
496
497 p.skip_ws();
498 if p.try_consume(b'}') {
499 return Ok(PromptOpts {
500 prompt,
501 models: vec![],
502 provider,
503 system,
504 priority,
505 reasoning,
506 show_reasoning,
507 quiet,
508 merge_config,
509 });
510 }
511
512 loop {
513 p.skip_ws();
514 let key = p.parse_simple_str()?;
515 p.skip_ws();
516 p.expect(b':')?;
517 p.skip_ws();
518
519 match key {
520 "prompt" => {
521 prompt = p.parse_opt_string()?;
522 }
523 "model" => {
524 model = p.parse_opt_string()?;
525 }
526 "provider" => {
527 provider = p.parse_opt_string()?;
528 }
529 "system" => {
530 system = p.parse_opt_string()?;
531 }
532 "priority" => {
533 if p.peek_is_null() {
534 p.parse_null()?;
535 priority = None;
536 } else {
537 let s = p.parse_simple_str()?;
538 priority = Some(Priority::from_str(s).map_err(|_| "invalid priority")?);
539 }
540 }
541 "reasoning" => {
542 if p.peek_is_null() {
543 p.parse_null()?;
544 reasoning = None;
545 } else {
546 let slice = p.value_slice()?; let cfg = ReasoningConfig::from_json(slice).map_err(|e| {
549 "parser::PromptOpts::from_json invalid reasoning: ".to_string() + &e
550 })?;
551 reasoning = Some(cfg);
552 }
553 }
554 "show_reasoning" => {
555 if p.peek_is_null() {
556 p.parse_null()?;
557 show_reasoning = None;
558 } else {
559 show_reasoning = Some(p.parse_bool()?);
560 }
561 }
562 "quiet" => {
563 if p.peek_is_null() {
564 p.parse_null()?;
565 quiet = None;
566 } else {
567 quiet = Some(p.parse_bool()?);
568 }
569 }
570 "merge_config" => {
571 if p.peek_is_null() {
572 p.parse_null()?;
573 merge_config = true;
574 } else {
575 merge_config = p.parse_bool()?;
576 }
577 }
578 _ => {
579 p.skip_value()?;
581 }
582 }
583
584 p.skip_ws();
585 if p.try_consume(b',') {
586 continue;
587 } else {
588 p.expect(b'}')?;
589 break;
590 }
591 }
592
593 Ok(PromptOpts {
594 prompt,
595 models: model.map(|m| vec![m]).unwrap_or_default(),
596 provider,
597 system,
598 priority,
599 reasoning,
600 show_reasoning,
601 quiet,
602 merge_config,
603 })
604 }
605}
606
607impl config::ConfigFile {
608 pub fn from_json(json: &str) -> Result<Self, Cow<'static, str>> {
609 let mut p = Parser::new(json);
610 p.skip_ws();
611 p.expect(b'{')?;
612
613 let mut settings: Option<config::Settings> = None;
614 let mut keys: Vec<config::ApiKey> = vec![];
615 let mut prompt_opts: Option<PromptOpts> = None;
616
617 loop {
618 p.skip_ws();
619 if p.try_consume(b'}') {
620 break;
621 }
622
623 let key = p
624 .parse_simple_str()
625 .map_err(|err| "ConfigFile parsing key: ".to_string() + err)?;
626 p.skip_ws();
627 p.expect(b':')?;
628 p.skip_ws();
629
630 match key {
631 "settings" => {
632 if settings.is_some() {
633 return Err("duplicate field: settings".into());
634 }
635 let settings_json = p.value_slice()?;
636 settings = Some(config::Settings::from_json(settings_json)?);
637 }
638 "keys" => {
639 if !keys.is_empty() {
640 return Err("duplicate field: keys".into());
641 }
642 if !p.try_consume(b'[') {
643 return Err("keys: Expected array".into());
644 }
645 loop {
646 let j = p.value_slice()?;
647 let api_key = config::ApiKey::from_json(j)?;
648 keys.push(api_key);
649 p.skip_ws();
650 if p.try_consume(b',') {
651 continue;
652 }
653 p.skip_ws();
654 if p.try_consume(b']') {
655 break;
656 }
657 }
658 }
659 "prompt_opts" => {
660 if prompt_opts.is_some() {
661 return Err("duplicate field: prompt_opts".into());
662 }
663 let opts_json = p.value_slice()?;
664 prompt_opts = Some(PromptOpts::from_json(opts_json)?);
665 }
666 _ => return Err("unknown field".into()),
667 }
668 p.skip_ws();
669 if p.try_consume(b',') {
670 continue;
671 }
672 p.skip_ws();
673 if p.try_consume(b'}') {
674 break;
675 }
676 }
677
678 Ok(config::ConfigFile {
679 settings,
680 keys,
681 prompt_opts,
682 })
683 }
684}
685
686impl config::Settings {
687 pub fn from_json(json: &str) -> Result<Self, Cow<'static, str>> {
688 let mut p = Parser::new(json);
689 p.skip_ws();
690 p.expect(b'{')?;
691
692 let mut save_to_file = None;
693 let mut dns = vec![];
694
695 loop {
696 p.skip_ws();
697 if p.try_consume(b'}') {
698 break;
699 }
700
701 let key = p
702 .parse_simple_str()
703 .map_err(|err| "Settings parsing key: ".to_string() + err)?;
704 p.skip_ws();
705 p.expect(b':')?;
706 p.skip_ws();
707
708 match key {
709 "save_to_file" => {
710 if save_to_file.is_some() {
711 return Err("duplicate field: save_to_file".into());
712 }
713 if p.peek_is_null() {
714 p.parse_null()?;
715 save_to_file = None;
716 } else {
717 save_to_file = Some(p.parse_bool()?);
718 }
719 }
720 "dns" => {
721 if !dns.is_empty() {
722 return Err("duplicate field: dns".into());
723 }
724 if !p.try_consume(b'[') {
725 return Err("dns: Expected array".into());
726 }
727 loop {
728 let addr = p.parse_string()?;
729 dns.push(addr);
730 p.skip_ws();
731 if p.try_consume(b',') {
732 continue;
733 }
734 p.skip_ws();
735 if p.try_consume(b']') {
736 break;
737 }
738 }
739 }
740 _ => return Err("unknown field".into()),
741 }
742
743 p.skip_ws();
744 if p.try_consume(b',') {
745 continue;
746 }
747 p.skip_ws();
748 if p.try_consume(b'}') {
749 break;
750 }
751
752 if !p.eof() {
754 return Err("expected ',' or '}'".into());
755 } else {
756 return Err("unexpected end of input".into());
757 }
758 }
759
760 let default = config::Settings::default();
761 Ok(config::Settings {
762 save_to_file: save_to_file.unwrap_or(default.save_to_file),
763 dns,
764 })
765 }
766}
767
768impl config::ApiKey {
769 pub fn from_json(json: &str) -> Result<Self, Cow<'static, str>> {
770 let mut p = Parser::new(json);
771 p.skip_ws();
772 p.expect(b'{')?;
773
774 let mut name = None;
775 let mut value = None;
776
777 loop {
778 p.skip_ws();
779 if p.try_consume(b'}') {
780 break;
781 }
782
783 let key = p
784 .parse_simple_str()
785 .map_err(|err| "ApiKey parsing key: ".to_string() + err)?;
786 p.skip_ws();
787 p.expect(b':')?;
788 p.skip_ws();
789
790 match key {
791 "name" => {
792 if name.is_some() {
793 return Err("duplicate field: name".into());
794 }
795 name = Some(
796 p.parse_string()
797 .map_err(|err| "Parsing name: ".to_string() + &err)?,
798 );
799 }
800 "value" => {
801 if value.is_some() {
802 return Err("duplicate field: value".into());
803 }
804 value = Some(
805 p.parse_string()
806 .map_err(|err| "Parsing name: ".to_string() + &err)?,
807 );
808 }
809 _ => return Err("unknown field".into()),
810 }
811 p.skip_ws();
812 if p.try_consume(b',') {
813 continue;
814 } else {
815 p.expect(b'}')?;
816 break;
817 }
818 }
819
820 Ok(config::ApiKey::new(
821 name.expect("Missing ApiKey name"),
822 value.expect("Missing ApiKey value"),
823 ))
824 }
825}
826
827struct Parser<'a> {
831 s: &'a str,
832 b: &'a [u8],
833 i: usize,
834}
835
836impl<'a> Parser<'a> {
837 fn new(s: &'a str) -> Self {
838 Self {
839 s,
840 b: s.as_bytes(),
841 i: 0,
842 }
843 }
844
845 fn eof(&self) -> bool {
846 self.i >= self.b.len()
847 }
848
849 fn peek(&self) -> Option<u8> {
850 if self.eof() {
851 None
852 } else {
853 Some(self.b[self.i])
854 }
855 }
856
857 fn try_consume(&mut self, ch: u8) -> bool {
858 if self.peek() == Some(ch) {
859 self.i += 1;
860 true
861 } else {
862 false
863 }
864 }
865
866 fn expect(&mut self, ch: u8) -> Result<(), &'static str> {
867 if self.try_consume(ch) {
868 Ok(())
869 } else {
870 Err("expected character")
871 }
872 }
873
874 fn skip_ws(&mut self) {
875 while let Some(c) = self.peek() {
876 match c {
877 b' ' | b'\n' | b'\r' | b'\t' => self.i += 1,
878 _ => break,
879 }
880 }
881 }
882
883 fn starts_with_bytes(&self, pat: &[u8]) -> bool {
884 let end = self.i + pat.len();
885 end <= self.b.len() && &self.b[self.i..end] == pat
886 }
887
888 fn parse_null(&mut self) -> Result<(), &'static str> {
889 if self.starts_with_bytes(b"null") {
890 self.i += 4;
891 Ok(())
892 } else {
893 Err("expected null")
894 }
895 }
896
897 fn peek_is_null(&self) -> bool {
898 self.starts_with_bytes(b"null")
899 }
900
901 fn parse_bool(&mut self) -> Result<bool, String> {
902 self.skip_ws();
903 if self.starts_with_bytes(b"true") {
904 self.i += 4;
905 Ok(true)
906 } else if self.starts_with_bytes(b"false") {
907 self.i += 5;
908 Ok(false)
909 } else {
910 Err("Expected boolean, got: ".to_string() + &String::from_utf8_lossy(&self.b[self.i..]))
911 }
912 }
913
914 fn parse_u32(&mut self) -> Result<u32, &'static str> {
915 self.skip_ws();
916 if self.eof() {
917 return Err("expected number");
918 }
919 if self.peek() == Some(b'-') {
920 return Err("negative not allowed");
921 }
922 let mut val: u32 = 0;
923 let mut read_any = false;
924 let len = self.b.len();
925 while self.i < len {
926 let c = self.b[self.i];
927 if c.is_ascii_digit() {
928 read_any = true;
929 let digit = (c - b'0') as u32;
930 if val > (u32::MAX - digit) / 10 {
932 return Err("u32 overflow");
933 }
934 val = val * 10 + digit;
935 self.i += 1;
936 } else {
937 break;
938 }
939 }
940 if !read_any {
941 return Err("expected integer");
942 }
943 Ok(val)
944 }
945
946 fn parse_f32(&mut self) -> Result<f32, &'static str> {
947 self.skip_ws();
948 if self.eof() {
949 return Err("expected number");
950 }
951
952 let len = self.b.len();
953
954 let mut neg = false;
956 if let Some(c) = self.peek() {
957 if c == b'-' {
958 neg = true;
959 self.i += 1;
960 } else if c == b'+' {
961 self.i += 1;
962 }
963 }
964
965 let mut mant: u32 = 0;
967 let mut mant_digits: i32 = 0;
968 let mut ints: i32 = 0;
969
970 while self.i < len {
972 let c = self.b[self.i];
973 if c.is_ascii_digit() {
974 if mant_digits < 9 {
975 mant = mant.saturating_mul(10).wrapping_add((c - b'0') as u32);
976 mant_digits += 1;
977 }
978 self.i += 1;
979 ints += 1;
980 } else {
981 break;
982 }
983 }
984
985 let mut frac_any = false;
987 if self.peek() == Some(b'.') {
988 self.i += 1;
989 let start_frac = self.i;
990 while self.i < len {
991 let c = self.b[self.i];
992 if c.is_ascii_digit() {
993 if mant_digits < 9 {
994 mant = mant.saturating_mul(10).wrapping_add((c - b'0') as u32);
995 mant_digits += 1;
996 }
997 self.i += 1;
998 } else {
999 break;
1000 }
1001 }
1002 frac_any = self.i > start_frac;
1003 }
1004
1005 if ints == 0 && !frac_any {
1006 return Err("expected number");
1007 }
1008
1009 let mut exp_part: i32 = 0;
1011 if let Some(ech) = self.peek()
1012 && (ech == b'e' || ech == b'E')
1013 {
1014 self.i += 1;
1015 let mut eneg = false;
1016 if let Some(signch) = self.peek() {
1017 if signch == b'-' {
1018 eneg = true;
1019 self.i += 1;
1020 } else if signch == b'+' {
1021 self.i += 1;
1022 }
1023 }
1024 if self.eof() || !self.b[self.i].is_ascii_digit() {
1025 return Err("expected exponent");
1026 }
1027 let mut eacc: i32 = 0;
1028 while self.i < len {
1029 let c = self.b[self.i];
1030 if c.is_ascii_digit() {
1031 let d = (c - b'0') as i32;
1032 if eacc < 1_000_000_000 / 10 {
1033 eacc = eacc * 10 + d;
1034 } else {
1035 eacc = 1_000_000_000; }
1037 self.i += 1;
1038 } else {
1039 break;
1040 }
1041 }
1042 exp_part = if eneg { -eacc } else { eacc };
1043 }
1044
1045 let exp10 = ints - mant_digits + exp_part;
1047
1048 let mut val = mant as f64;
1050
1051 const POW10_POS: [f64; 39] = [
1052 1.0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14, 1e15,
1053 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22, 1e23, 1e24, 1e25, 1e26, 1e27, 1e28, 1e29,
1054 1e30, 1e31, 1e32, 1e33, 1e34, 1e35, 1e36, 1e37, 1e38,
1055 ];
1056 const POW10_NEG: [f64; 46] = [
1057 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,
1058 1e-14, 1e-15, 1e-16, 1e-17, 1e-18, 1e-19, 1e-20, 1e-21, 1e-22, 1e-23, 1e-24, 1e-25,
1059 1e-26, 1e-27, 1e-28, 1e-29, 1e-30, 1e-31, 1e-32, 1e-33, 1e-34, 1e-35, 1e-36, 1e-37,
1060 1e-38, 1e-39, 1e-40, 1e-41, 1e-42, 1e-43, 1e-44, 1e-45,
1061 ];
1062
1063 if exp10 > 0 {
1064 let mut e = exp10;
1065 while e > 0 {
1066 let chunk = if e > 38 { 38 } else { e } as usize;
1067 val *= POW10_POS[chunk];
1068 if !val.is_finite() {
1069 return Err("f32 overflow");
1070 }
1071 e -= chunk as i32;
1072 }
1073 } else if exp10 < 0 {
1074 let mut e = -exp10;
1075 while e > 0 {
1076 let chunk = if e > 45 { 45 } else { e } as usize;
1077 val *= POW10_NEG[chunk];
1078 if val == 0.0 {
1079 break;
1080 }
1081 e -= chunk as i32;
1082 }
1083 }
1084
1085 let mut out = val as f32;
1086 if !out.is_finite() {
1087 return Err("f32 overflow");
1088 }
1089 if neg {
1090 out = -out;
1091 }
1092 Ok(out)
1093 }
1094
1095 fn parse_simple_str(&mut self) -> Result<&'a str, &'static str> {
1096 self.skip_ws();
1097 if self.peek() != Some(b'"') {
1098 return Err("expected string");
1099 }
1100 self.i += 1;
1101 let start = self.i;
1102 let len = self.b.len();
1103 while self.i < len {
1104 let c = self.b[self.i];
1105 if c == b'\\' {
1106 return Err("string escapes are not supported");
1108 }
1109 if c == b'"' {
1110 let end = self.i;
1111 self.i += 1; return Ok(&self.s[start..end]);
1115 }
1116 self.i += 1;
1117 }
1118 Err("unterminated string")
1119 }
1120
1121 fn parse_string(&mut self) -> Result<String, Cow<'static, str>> {
1122 self.skip_ws();
1123 if self.peek() != Some(b'"') {
1124 return Err(("expected string got: ".to_string()
1125 + &String::from_utf8_lossy(&self.b[self.i..]))
1126 .into());
1127 }
1128 let start = self.i + 1;
1129 let mut i = start;
1130 let len = self.b.len();
1131
1132 let mut needs_unescape = false;
1134 while i < len {
1135 let c = self.b[i];
1136 if c == b'\\' {
1137 needs_unescape = true;
1138 break;
1139 }
1140 if c == b'"' {
1141 let s = core::str::from_utf8(&self.b[start..i]).map_err(|_| "utf8 error")?;
1143 self.i = i + 1;
1144 return Ok(s.to_owned());
1145 }
1146 i += 1;
1147 }
1148 if !needs_unescape {
1149 return Err("unterminated string".into());
1150 }
1151
1152 let mut out = String::with_capacity((i - start) + 256);
1156 let mut seg_start = start;
1157 while i < len {
1158 let c = self.b[i];
1159 if c == b'\\' {
1160 if i > seg_start {
1162 let prev =
1163 core::str::from_utf8(&self.b[seg_start..i]).map_err(|_| "utf8 error")?;
1164 out.push_str(prev);
1165 }
1166 i += 1;
1167 if i >= len {
1168 return Err("bad escape".into());
1169 }
1170 let e = self.b[i];
1171 match e {
1172 b'"' => out.push('"'),
1173 b'\\' => out.push('\\'),
1174 b'/' => out.push('/'),
1175 b'b' => out.push('\u{0008}'),
1176 b'f' => out.push('\u{000C}'),
1177 b'n' => out.push('\n'),
1178 b'r' => out.push('\r'),
1179 b't' => out.push('\t'),
1180 b'u' => {
1181 let (cp, new_i) = self.parse_u_escape(i + 1)?;
1182 i = new_i - 1; if let Some(ch) = core::char::from_u32(cp) {
1184 out.push(ch);
1185 } else {
1186 return Err("invalid unicode".into());
1187 }
1188 }
1189 _ => return Err("bad escape".into()),
1190 }
1191 i += 1;
1192 seg_start = i;
1193 continue;
1194 } else if c == b'"' {
1195 if i > seg_start {
1197 out.push_str(
1198 core::str::from_utf8(&self.b[seg_start..i]).map_err(|_| "utf8 error")?,
1199 );
1200 }
1201 self.i = i + 1;
1202 return Ok(out);
1203 } else {
1204 i += 1;
1205 }
1206 }
1207 Err("unterminated string".into())
1208 }
1209
1210 fn parse_u_escape(&self, i: usize) -> Result<(u32, usize), &'static str> {
1212 fn hex4(bytes: &[u8], i: usize) -> Result<(u16, usize), &'static str> {
1213 let end = i + 4;
1214 if end > bytes.len() {
1215 return Err("short \\u");
1216 }
1217 let mut v: u16 = 0;
1218 for b in bytes.iter().take(end).skip(i) {
1219 v = (v << 4) | hex_val(*b)?;
1220 }
1221 Ok((v, end))
1222 }
1223 fn hex_val(b: u8) -> Result<u16, &'static str> {
1224 match b {
1225 b'0'..=b'9' => Ok((b - b'0') as u16),
1226 b'a'..=b'f' => Ok((b - b'a' + 10) as u16),
1227 b'A'..=b'F' => Ok((b - b'A' + 10) as u16),
1228 _ => Err("bad hex"),
1229 }
1230 }
1231
1232 let (first, i2) = hex4(self.b, i)?;
1233 let cp = first as u32;
1234
1235 if (0xD800..=0xDBFF).contains(&first) {
1237 if i2 + 2 > self.b.len() || self.b[i2] != b'\\' || self.b[i2 + 1] != b'u' {
1239 return Err("missing low surrogate");
1240 }
1241 let (second, i3) = hex4(self.b, i2 + 2)?;
1242 if !(0xDC00..=0xDFFF).contains(&second) {
1243 return Err("invalid low surrogate");
1244 }
1245 let high = (first as u32) - 0xD800;
1246 let low = (second as u32) - 0xDC00;
1247 let code = 0x10000 + ((high << 10) | low);
1248 Ok((code, i3))
1249 } else if (0xDC00..=0xDFFF).contains(&first) {
1250 Err("unpaired low surrogate")
1251 } else {
1252 Ok((cp, i2))
1253 }
1254 }
1255
1256 fn parse_opt_string(&mut self) -> Result<Option<String>, Cow<'static, str>> {
1257 if self.peek_is_null() {
1258 self.parse_null()?;
1259 Ok(None)
1260 } else {
1261 let s = self.parse_string()?;
1262 Ok(Some(s))
1263 }
1264 }
1265
1266 fn value_slice(&mut self) -> Result<&'a str, &'static str> {
1268 self.skip_ws();
1269 let start = self.i;
1270 let end = self.find_value_end()?;
1271 let out = &self.s[start..end];
1272 self.i = end;
1273 Ok(out)
1274 }
1275
1276 fn skip_value(&mut self) -> Result<(), &'static str> {
1278 let _ = self.value_slice()?;
1279 Ok(())
1280 }
1281
1282 fn find_value_end(&mut self) -> Result<usize, &'static str> {
1283 if self.eof() {
1284 return Err("unexpected end");
1285 }
1286 match self.b[self.i] {
1287 b'"' => self.scan_string_end(),
1288 b'{' => self.scan_brace_block(b'{', b'}'),
1289 b'[' => self.scan_brace_block(b'[', b']'),
1290 b't' => {
1291 if self.starts_with_bytes(b"true") {
1292 Ok(self.i + 4)
1293 } else {
1294 Err("bad literal")
1295 }
1296 }
1297 b'f' => {
1298 if self.starts_with_bytes(b"false") {
1299 Ok(self.i + 5)
1300 } else {
1301 Err("bad literal")
1302 }
1303 }
1304 b'n' => {
1305 if self.starts_with_bytes(b"null") {
1306 Ok(self.i + 4)
1307 } else {
1308 Err("bad literal")
1309 }
1310 }
1311 b'-' | b'0'..=b'9' => self.scan_number_end(),
1312 _t => {
1313 Err("unexpected token")
1316 }
1317 }
1318 }
1319
1320 fn scan_string_end(&self) -> Result<usize, &'static str> {
1321 let mut i = self.i + 1;
1322 let len = self.b.len();
1323 let mut escaped = false;
1324 while i < len {
1325 let c = self.b[i];
1326 if escaped {
1327 escaped = false;
1328 i += 1;
1329 continue;
1330 }
1331 if c == b'\\' {
1332 escaped = true;
1333 i += 1;
1334 continue;
1335 }
1336 if c == b'"' {
1337 return Ok(i + 1);
1338 }
1339 i += 1;
1340 }
1341 Err("unterminated string")
1342 }
1343
1344 fn scan_brace_block(&self, open: u8, close: u8) -> Result<usize, &'static str> {
1345 let mut i = self.i;
1346 let len = self.b.len();
1347 let mut depth = 0usize;
1348 while i < len {
1349 let c = self.b[i];
1350 if c == b'"' {
1351 let p = Parser {
1353 s: self.s,
1354 b: self.b,
1355 i,
1356 };
1357 i = p.scan_string_end()?; continue;
1359 }
1360 if c == open {
1361 depth += 1;
1362 } else if c == close {
1363 depth -= 1;
1364 if depth == 0 {
1365 return Ok(i + 1);
1366 }
1367 }
1368 i += 1;
1369 }
1370 Err("unterminated structure")
1371 }
1372
1373 fn scan_number_end(&self) -> Result<usize, &'static str> {
1374 let len = self.b.len();
1375 let mut i = self.i;
1376
1377 if self.b[i] == b'-' {
1378 i += 1;
1379 if i >= len {
1380 return Err("bad number");
1381 }
1382 }
1383
1384 match self.b[i] {
1386 b'0' => {
1387 i += 1;
1388 }
1389 b'1'..=b'9' => {
1390 i += 1;
1391 while i < len {
1392 match self.b[i] {
1393 b'0'..=b'9' => i += 1,
1394 _ => break,
1395 }
1396 }
1397 }
1398 _ => return Err("bad number"),
1399 }
1400
1401 if i < len && self.b[i] == b'.' {
1403 i += 1;
1404 if i >= len || !self.b[i].is_ascii_digit() {
1405 return Err("bad number");
1406 }
1407 while i < len && self.b[i].is_ascii_digit() {
1408 i += 1;
1409 }
1410 }
1411
1412 if i < len && (self.b[i] == b'e' || self.b[i] == b'E') {
1414 i += 1;
1415 if i < len && (self.b[i] == b'+' || self.b[i] == b'-') {
1416 i += 1;
1417 }
1418 if i >= len || !self.b[i].is_ascii_digit() {
1419 return Err("bad number");
1420 }
1421 while i < len && self.b[i].is_ascii_digit() {
1422 i += 1;
1423 }
1424 }
1425
1426 Ok(i)
1427 }
1428}
1429
1430#[cfg(test)]
1431mod tests {
1432 extern crate alloc;
1433 use alloc::string::ToString;
1434
1435 use super::*;
1436 use crate::LastData;
1437
1438 #[test]
1439 fn rp1() {
1440 let cfg = ReasoningConfig::from_json(r#"{"enabled": false}"#).unwrap();
1441 assert!(!cfg.enabled);
1442 assert!(cfg.effort.is_none());
1443 assert!(cfg.tokens.is_none());
1444 }
1445
1446 #[test]
1447 fn rp2() {
1448 let cfg = ReasoningConfig::from_json(r#"{"enabled": true, "effort": "medium"}"#).unwrap();
1449 assert!(cfg.enabled);
1450 assert_eq!(cfg.effort, Some(ReasoningEffort::Medium));
1451 assert!(cfg.tokens.is_none());
1452 }
1453
1454 #[test]
1455 fn rp3() {
1456 let cfg = ReasoningConfig::from_json(r#"{"enabled": true, "tokens": 2048}"#).unwrap();
1457 assert!(cfg.enabled);
1458 assert_eq!(cfg.tokens, Some(2048));
1459 assert!(cfg.effort.is_none());
1460 }
1461
1462 #[test]
1463 fn rp4() {
1464 let cfg = ReasoningConfig::from_json(r#"{"enabled":true,"effort":"high","tokens":null}"#)
1465 .unwrap();
1466 assert!(cfg.enabled);
1467 assert_eq!(cfg.effort, Some(ReasoningEffort::High));
1468 assert!(cfg.tokens.is_none());
1469 }
1470
1471 #[test]
1472 fn cpo1() {
1473 let s = r#"
1474 {
1475 "prompt": "\n\nExample JSON 1: {\"enabled\": false}\n",
1476 "model": "google/gemma-3n-e4b-it:free",
1477 "system": "Make your answer concise but complete. No yapping. Direct professional tone. No emoji.",
1478 "show_reasoning": false,
1479 "reasoning": { "enabled": false },
1480 "merge_config": true
1481 }
1482 "#;
1483 let opts = PromptOpts::from_json(s).unwrap();
1484 assert!(!opts.show_reasoning.unwrap());
1485 assert_eq!(opts.models, vec!["google/gemma-3n-e4b-it:free"]);
1486 assert!(!opts.reasoning.unwrap().enabled);
1487 assert!(opts.merge_config);
1488 }
1489
1490 #[test]
1491 fn cpo2() {
1492 let s = r#"
1493 {"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}
1494 "#;
1495 let opts = PromptOpts::from_json(s).unwrap();
1496 assert!(!opts.show_reasoning.unwrap());
1497 assert_eq!(opts.models, vec!["openai/gpt-5"]);
1498 assert!(opts.reasoning.as_ref().unwrap().enabled);
1499 assert_eq!(
1500 opts.reasoning.as_ref().unwrap().effort,
1501 Some(ReasoningEffort::High)
1502 );
1503 }
1504
1505 #[test]
1506 fn last_data() {
1507 let s = r#"
1508{"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. ✨"}]}
1509"#;
1510 let l = LastData::from_json(s).unwrap();
1511 assert_eq!(l.opts.provider.as_deref(), Some("google-ai-studio"));
1512 assert_eq!(l.messages.len(), 2);
1513 }
1514
1515 #[test]
1516 fn test_usage() {
1517 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}}"#;
1518 let usage = Usage::from_json(s).unwrap();
1519 assert_eq!(usage.cost, 0.0534);
1520 }
1521
1522 #[test]
1523 fn test_choice() {
1524 let s = r#"{"index":0,"delta":{"role":"assistant","content":"Hello"},"finish_reason":"stop","native_finish_reason":"stop","logprobs":null}"#;
1525 let choice = Choice::from_json(s).unwrap();
1526 assert_eq!(choice.delta.content.as_deref(), Some("Hello"));
1527 }
1528
1529 #[test]
1530 fn test_chat_completions_response_simple() {
1531 let arr = [
1532 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}]}"#,
1533 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}]}"#,
1534 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}}}"#,
1535 ];
1536 for a in arr {
1537 let ccr = ChatCompletionsResponse::from_json(a).unwrap();
1538 assert_eq!(ccr.provider.as_deref(), Some("Meta"));
1539 assert_eq!(
1540 ccr.model.as_deref(),
1541 Some("meta-llama/llama-3.3-8b-instruct:free")
1542 );
1543 assert_eq!(ccr.choices.len(), 1);
1544 }
1545 }
1546
1547 #[test]
1548 fn test_chat_completions_response_more() {
1549 let arr = [
1550 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}]}"#,
1551 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}]}"#,
1552 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}]}"#,
1553 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}]}"#,
1554 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}]}"#,
1555 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}]}"#,
1556 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}]}"#,
1557 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}]}"#,
1558 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}}}"#,
1559 ];
1560 for a in arr {
1561 let ccr = ChatCompletionsResponse::from_json(a).unwrap();
1562 assert_eq!(ccr.provider.as_deref(), Some("WandB"));
1563 assert_eq!(ccr.model.as_deref(), Some("deepseek/deepseek-chat-v3.1"));
1564 assert_eq!(ccr.choices.len(), 1);
1565 }
1566 }
1567
1568 #[test]
1570 fn test_nvidia_misc() {
1571 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}"#;
1572 let ccr = ChatCompletionsResponse::from_json(s).unwrap();
1573 assert_eq!(ccr.choices[0].delta.content.as_deref(), Some("Ta"));
1574 }
1575
1576 #[test]
1577 fn api_key() {
1578 let s = r#"{"name":"openrouter","value":"sk-or-v1-a123b456c789d012a345b8032470394876576573242374098174093274abcdef"}"#;
1579 let got = config::ApiKey::from_json(s).unwrap();
1580 let expect = config::ApiKey::new(
1581 "openrouter".to_string(),
1582 "sk-or-v1-a123b456c789d012a345b8032470394876576573242374098174093274abcdef".to_string(),
1583 );
1584 assert_eq!(got, expect);
1585 }
1586
1587 #[test]
1588 fn settings() {
1589 let s = r#"{
1590 "save_to_file": true,
1591 "dns": ["104.18.2.115", "104.18.3.115"]
1592}"#;
1593 let settings = config::Settings::from_json(s).unwrap();
1594 assert!(settings.save_to_file);
1595 assert_eq!(settings.dns, ["104.18.2.115", "104.18.3.115"]);
1596 }
1597
1598 #[test]
1599 fn config_file() {
1600 let s = r#"
1601{
1602 "keys": [{"name": "openrouter", "value": "sk-or-v1-abcd1234"}],
1603 "settings": {
1604 "save_to_file": true,
1605 "dns": ["104.18.2.115", "104.18.3.115"]
1606 },
1607 "prompt_opts": {
1608 "model": "google/gemma-3n-e4b-it:free",
1609 "system": "Make your answer concise but complete. No yapping. Direct professional tone. No emoji.",
1610 "quiet": false,
1611 "show_reasoning": false,
1612 "reasoning": {
1613 "enabled": false
1614 }
1615 }
1616}
1617"#;
1618 let cfg = config::ConfigFile::from_json(s).unwrap();
1619 assert_eq!(cfg.keys.len(), 1);
1620 assert!(cfg.settings.is_some());
1621 assert!(cfg.prompt_opts.is_some());
1622 }
1623}