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