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::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 !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 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 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 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
524fn 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 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 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 !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 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 let slice = p.value_slice()?; 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 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 !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
1046struct 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 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 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 let mut mant: u32 = 0;
1186 let mut mant_digits: i32 = 0;
1187 let mut ints: i32 = 0;
1188
1189 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 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 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; }
1256 self.i += 1;
1257 } else {
1258 break;
1259 }
1260 }
1261 exp_part = if eneg { -eacc } else { eacc };
1262 }
1263
1264 let exp10 = ints - mant_digits + exp_part;
1266
1267 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 return Err("string escapes are not supported");
1327 }
1328 if c == b'"' {
1329 let end = self.i;
1330 self.i += 1; 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 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 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 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 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; 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 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 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 if (0xD800..=0xDBFF).contains(&first) {
1456 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 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 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 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 let p = Parser {
1572 s: self.s,
1573 b: self.b,
1574 i,
1575 };
1576 i = p.scan_string_end()?; 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 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 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 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 #[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}