1use std::collections::HashMap;
4
5use serde::{Deserialize, Serialize};
6
7use crate::{
8 client::{Opencode, RequestOptions},
9 error::OpencodeError,
10};
11
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
18pub struct ModeConfig {
19 #[serde(skip_serializing_if = "Option::is_none")]
21 pub disable: Option<bool>,
22
23 #[serde(skip_serializing_if = "Option::is_none")]
25 pub model: Option<String>,
26
27 #[serde(skip_serializing_if = "Option::is_none")]
29 pub prompt: Option<String>,
30
31 #[serde(skip_serializing_if = "Option::is_none")]
33 pub temperature: Option<f64>,
34
35 #[serde(skip_serializing_if = "Option::is_none")]
37 pub tools: Option<HashMap<String, bool>>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
48pub struct AgentConfig {
49 pub description: String,
51
52 #[serde(flatten)]
54 pub mode: ModeConfig,
55}
56
57pub type Agent = HashMap<String, AgentConfig>;
61
62#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
68pub struct HookCommand {
69 pub command: Vec<String>,
71
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub environment: Option<HashMap<String, String>>,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
79pub struct Hook {
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub file_edited: Option<HashMap<String, Vec<HookCommand>>>,
83
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub session_completed: Option<Vec<HookCommand>>,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
91pub struct Experimental {
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub hook: Option<Hook>,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
103pub struct KeybindsConfig {
104 pub app_exit: String,
105 pub app_help: String,
106 pub editor_open: String,
107 pub file_close: String,
108 pub file_diff_toggle: String,
109 pub file_list: String,
110 pub file_search: String,
111 pub input_clear: String,
112 pub input_newline: String,
113 pub input_paste: String,
114 pub input_submit: String,
115 pub leader: String,
116 pub messages_copy: String,
117 pub messages_first: String,
118 pub messages_half_page_down: String,
119 pub messages_half_page_up: String,
120 pub messages_last: String,
121 pub messages_layout_toggle: String,
122 pub messages_next: String,
123 pub messages_page_down: String,
124 pub messages_page_up: String,
125 pub messages_previous: String,
126 pub messages_redo: String,
127 pub messages_revert: String,
128 pub messages_undo: String,
129 pub model_list: String,
130 pub project_init: String,
131 pub session_compact: String,
132 pub session_export: String,
133 pub session_interrupt: String,
134 pub session_list: String,
135 pub session_new: String,
136 pub session_share: String,
137 pub session_unshare: String,
138 pub switch_mode: String,
139 pub switch_mode_reverse: String,
140 pub theme_list: String,
141 pub tool_details: String,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
150pub struct McpLocalConfig {
151 pub command: Vec<String>,
153
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub enabled: Option<bool>,
157
158 #[serde(skip_serializing_if = "Option::is_none")]
160 pub environment: Option<HashMap<String, String>>,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
165pub struct McpRemoteConfig {
166 pub url: String,
168
169 #[serde(skip_serializing_if = "Option::is_none")]
171 pub enabled: Option<bool>,
172
173 #[serde(skip_serializing_if = "Option::is_none")]
175 pub headers: Option<HashMap<String, String>>,
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
180#[serde(tag = "type")]
181pub enum McpConfig {
182 #[serde(rename = "local")]
184 Local(McpLocalConfig),
185
186 #[serde(rename = "remote")]
188 Remote(McpRemoteConfig),
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
197pub struct ModelCost {
198 pub input: f64,
200
201 pub output: f64,
203
204 #[serde(skip_serializing_if = "Option::is_none")]
206 pub cache_read: Option<f64>,
207
208 #[serde(skip_serializing_if = "Option::is_none")]
210 pub cache_write: Option<f64>,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
215pub struct ModelLimit {
216 pub context: u64,
218
219 pub output: u64,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
225pub struct ProviderModelConfig {
226 #[serde(skip_serializing_if = "Option::is_none")]
228 pub id: Option<String>,
229
230 #[serde(skip_serializing_if = "Option::is_none")]
232 pub attachment: Option<bool>,
233
234 #[serde(skip_serializing_if = "Option::is_none")]
236 pub cost: Option<ModelCost>,
237
238 #[serde(skip_serializing_if = "Option::is_none")]
240 pub limit: Option<ModelLimit>,
241
242 #[serde(skip_serializing_if = "Option::is_none")]
244 pub name: Option<String>,
245
246 #[serde(skip_serializing_if = "Option::is_none")]
248 pub options: Option<HashMap<String, serde_json::Value>>,
249
250 #[serde(skip_serializing_if = "Option::is_none")]
252 pub reasoning: Option<bool>,
253
254 #[serde(skip_serializing_if = "Option::is_none")]
256 pub release_date: Option<String>,
257
258 #[serde(skip_serializing_if = "Option::is_none")]
260 pub temperature: Option<bool>,
261
262 #[serde(skip_serializing_if = "Option::is_none")]
264 pub tool_call: Option<bool>,
265}
266
267#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
269pub struct ProviderOptions {
270 #[serde(rename = "apiKey", skip_serializing_if = "Option::is_none")]
272 pub api_key: Option<String>,
273
274 #[serde(rename = "baseURL", skip_serializing_if = "Option::is_none")]
276 pub base_url: Option<String>,
277
278 #[serde(flatten)]
280 pub extra: HashMap<String, serde_json::Value>,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
285pub struct ProviderConfig {
286 pub models: HashMap<String, ProviderModelConfig>,
288
289 #[serde(skip_serializing_if = "Option::is_none")]
291 pub id: Option<String>,
292
293 #[serde(skip_serializing_if = "Option::is_none")]
295 pub api: Option<String>,
296
297 #[serde(skip_serializing_if = "Option::is_none")]
299 pub env: Option<Vec<String>>,
300
301 #[serde(skip_serializing_if = "Option::is_none")]
303 pub name: Option<String>,
304
305 #[serde(skip_serializing_if = "Option::is_none")]
307 pub npm: Option<String>,
308
309 #[serde(skip_serializing_if = "Option::is_none")]
311 pub options: Option<ProviderOptions>,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
320#[serde(rename_all = "lowercase")]
321pub enum ShareMode {
322 Manual,
324 Auto,
326 Disabled,
328}
329
330#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
332#[serde(rename_all = "lowercase")]
333pub enum Layout {
334 Auto,
336 Stretch,
338}
339
340pub type ModeMap = HashMap<String, ModeConfig>;
346
347#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
355pub struct Config {
356 #[serde(rename = "$schema", skip_serializing_if = "Option::is_none")]
358 pub schema: Option<String>,
359
360 #[serde(skip_serializing_if = "Option::is_none")]
362 pub agent: Option<Agent>,
363
364 #[serde(skip_serializing_if = "Option::is_none")]
366 pub autoshare: Option<bool>,
367
368 #[serde(skip_serializing_if = "Option::is_none")]
370 pub autoupdate: Option<bool>,
371
372 #[serde(skip_serializing_if = "Option::is_none")]
374 pub disabled_providers: Option<Vec<String>>,
375
376 #[serde(skip_serializing_if = "Option::is_none")]
378 pub experimental: Option<Experimental>,
379
380 #[serde(skip_serializing_if = "Option::is_none")]
382 pub instructions: Option<Vec<String>>,
383
384 #[serde(skip_serializing_if = "Option::is_none")]
386 pub keybinds: Option<KeybindsConfig>,
387
388 #[serde(skip_serializing_if = "Option::is_none")]
390 pub layout: Option<Layout>,
391
392 #[serde(skip_serializing_if = "Option::is_none")]
394 pub mcp: Option<HashMap<String, McpConfig>>,
395
396 #[serde(skip_serializing_if = "Option::is_none")]
398 pub mode: Option<ModeMap>,
399
400 #[serde(skip_serializing_if = "Option::is_none")]
402 pub model: Option<String>,
403
404 #[serde(skip_serializing_if = "Option::is_none")]
406 pub provider: Option<HashMap<String, ProviderConfig>>,
407
408 #[serde(skip_serializing_if = "Option::is_none")]
410 pub share: Option<ShareMode>,
411
412 #[serde(skip_serializing_if = "Option::is_none")]
414 pub small_model: Option<String>,
415
416 #[serde(skip_serializing_if = "Option::is_none")]
418 pub theme: Option<String>,
419
420 #[serde(skip_serializing_if = "Option::is_none")]
422 pub username: Option<String>,
423}
424
425#[derive(Debug, Clone)]
431pub struct ConfigResource<'a> {
432 client: &'a Opencode,
433}
434
435impl<'a> ConfigResource<'a> {
436 pub(crate) const fn new(client: &'a Opencode) -> Self {
438 Self { client }
439 }
440
441 pub async fn get(&self, options: Option<&RequestOptions>) -> Result<Config, OpencodeError> {
443 self.client.get("/config", options).await
444 }
445}
446
447#[cfg(test)]
452mod tests {
453 use serde_json::json;
454
455 use super::*;
456
457 #[test]
458 fn mode_config_round_trip() {
459 let mc = ModeConfig {
460 disable: Some(false),
461 model: Some("gpt-4o".into()),
462 prompt: Some("You are helpful.".into()),
463 temperature: Some(0.7),
464 tools: Some(HashMap::from([("bash".into(), true), ("file_write".into(), false)])),
465 };
466 let json_str = serde_json::to_string(&mc).unwrap();
467 let back: ModeConfig = serde_json::from_str(&json_str).unwrap();
468 assert_eq!(mc, back);
469 }
470
471 #[test]
472 fn mode_config_empty() {
473 let mc = ModeConfig::default();
474 let json_str = serde_json::to_string(&mc).unwrap();
475 assert_eq!(json_str, "{}");
476 let back: ModeConfig = serde_json::from_str(&json_str).unwrap();
477 assert_eq!(mc, back);
478 }
479
480 #[test]
481 fn mcp_local_round_trip() {
482 let cfg = McpConfig::Local(McpLocalConfig {
483 command: vec!["npx".into(), "mcp-server".into()],
484 enabled: Some(true),
485 environment: Some(HashMap::from([("NODE_ENV".into(), "production".into())])),
486 });
487 let v = serde_json::to_value(&cfg).unwrap();
488 assert_eq!(v["type"], "local");
489 assert_eq!(v["command"], json!(["npx", "mcp-server"]));
490 let back: McpConfig = serde_json::from_value(v).unwrap();
491 assert_eq!(cfg, back);
492 }
493
494 #[test]
495 fn mcp_remote_round_trip() {
496 let cfg = McpConfig::Remote(McpRemoteConfig {
497 url: "https://mcp.example.com".into(),
498 enabled: None,
499 headers: Some(HashMap::from([("Authorization".into(), "Bearer tok".into())])),
500 });
501 let v = serde_json::to_value(&cfg).unwrap();
502 assert_eq!(v["type"], "remote");
503 assert_eq!(v["url"], "https://mcp.example.com");
504 let back: McpConfig = serde_json::from_value(v).unwrap();
505 assert_eq!(cfg, back);
506 }
507
508 #[test]
509 fn keybinds_config_round_trip() {
510 let kb = KeybindsConfig {
511 app_exit: "ctrl+q".into(),
512 app_help: "ctrl+h".into(),
513 editor_open: "ctrl+e".into(),
514 file_close: "ctrl+w".into(),
515 file_diff_toggle: "ctrl+d".into(),
516 file_list: "ctrl+l".into(),
517 file_search: "ctrl+f".into(),
518 input_clear: "ctrl+u".into(),
519 input_newline: "shift+enter".into(),
520 input_paste: "ctrl+v".into(),
521 input_submit: "enter".into(),
522 leader: "ctrl+space".into(),
523 messages_copy: "ctrl+c".into(),
524 messages_first: "home".into(),
525 messages_half_page_down: "ctrl+d".into(),
526 messages_half_page_up: "ctrl+u".into(),
527 messages_last: "end".into(),
528 messages_layout_toggle: "ctrl+t".into(),
529 messages_next: "ctrl+n".into(),
530 messages_page_down: "pagedown".into(),
531 messages_page_up: "pageup".into(),
532 messages_previous: "ctrl+p".into(),
533 messages_redo: "ctrl+y".into(),
534 messages_revert: "ctrl+r".into(),
535 messages_undo: "ctrl+z".into(),
536 model_list: "ctrl+m".into(),
537 project_init: "ctrl+i".into(),
538 session_compact: "ctrl+k".into(),
539 session_export: "ctrl+shift+e".into(),
540 session_interrupt: "escape".into(),
541 session_list: "ctrl+s".into(),
542 session_new: "ctrl+shift+n".into(),
543 session_share: "ctrl+shift+s".into(),
544 session_unshare: "ctrl+shift+u".into(),
545 switch_mode: "tab".into(),
546 switch_mode_reverse: "shift+tab".into(),
547 theme_list: "ctrl+shift+t".into(),
548 tool_details: "ctrl+shift+d".into(),
549 };
550 let json_str = serde_json::to_string(&kb).unwrap();
551 let back: KeybindsConfig = serde_json::from_str(&json_str).unwrap();
552 assert_eq!(kb, back);
553 }
554
555 #[test]
556 fn config_with_schema_field() {
557 let cfg = Config {
558 schema: Some("https://opencode.ai/config.schema.json".into()),
559 ..Default::default()
560 };
561 let v = serde_json::to_value(&cfg).unwrap();
562 assert_eq!(v["$schema"], "https://opencode.ai/config.schema.json");
563 assert!(v.get("schema").is_none(), "$schema must not appear as 'schema'");
564 let back: Config = serde_json::from_value(v).unwrap();
565 assert_eq!(cfg, back);
566 }
567
568 #[test]
569 fn config_full_round_trip() {
570 let cfg = Config {
571 schema: Some("https://opencode.ai/schema.json".into()),
572 agent: Some(HashMap::from([(
573 "general".into(),
574 AgentConfig {
575 description: "Default agent".into(),
576 mode: ModeConfig {
577 model: Some("claude-3-opus".into()),
578 temperature: Some(0.5),
579 ..Default::default()
580 },
581 },
582 )])),
583 autoshare: Some(false),
584 autoupdate: Some(true),
585 disabled_providers: Some(vec!["azure".into()]),
586 experimental: Some(Experimental {
587 hook: Some(Hook {
588 file_edited: Some(HashMap::from([(
589 "*.rs".into(),
590 vec![HookCommand {
591 command: vec!["cargo".into(), "fmt".into()],
592 environment: None,
593 }],
594 )])),
595 session_completed: Some(vec![HookCommand {
596 command: vec!["notify-send".into(), "done".into()],
597 environment: Some(HashMap::from([("DISPLAY".into(), ":0".into())])),
598 }]),
599 }),
600 }),
601 instructions: Some(vec!["Be concise.".into()]),
602 keybinds: None,
603 layout: Some(Layout::Auto),
604 mcp: Some(HashMap::from([(
605 "local-server".into(),
606 McpConfig::Local(McpLocalConfig {
607 command: vec!["node".into(), "server.js".into()],
608 enabled: Some(true),
609 environment: None,
610 }),
611 )])),
612 mode: Some(HashMap::from([(
613 "build".into(),
614 ModeConfig { model: Some("gpt-4o".into()), ..Default::default() },
615 )])),
616 model: Some("claude-3-opus".into()),
617 provider: Some(HashMap::from([(
618 "openai".into(),
619 ProviderConfig {
620 models: HashMap::from([(
621 "gpt-4o".into(),
622 ProviderModelConfig {
623 id: Some("gpt-4o".into()),
624 attachment: Some(true),
625 cost: Some(ModelCost {
626 input: 5.0,
627 output: 15.0,
628 cache_read: None,
629 cache_write: None,
630 }),
631 limit: Some(ModelLimit { context: 128_000, output: 4_096 }),
632 name: Some("GPT-4o".into()),
633 options: None,
634 reasoning: Some(false),
635 release_date: Some("2024-05-13".into()),
636 temperature: Some(true),
637 tool_call: Some(true),
638 },
639 )]),
640 id: Some("openai".into()),
641 api: Some("https://api.openai.com/v1".into()),
642 env: Some(vec!["OPENAI_API_KEY".into()]),
643 name: Some("OpenAI".into()),
644 npm: None,
645 options: Some(ProviderOptions {
646 api_key: None,
647 base_url: Some("https://api.openai.com/v1".into()),
648 extra: HashMap::new(),
649 }),
650 },
651 )])),
652 share: Some(ShareMode::Manual),
653 small_model: Some("gpt-4o-mini".into()),
654 theme: Some("dark".into()),
655 username: Some("developer".into()),
656 };
657 let json_str = serde_json::to_string(&cfg).unwrap();
658 let back: Config = serde_json::from_str(&json_str).unwrap();
659 assert_eq!(cfg, back);
660 }
661
662 #[test]
663 fn share_mode_serde() {
664 for (variant, expected) in [
665 (ShareMode::Manual, "manual"),
666 (ShareMode::Auto, "auto"),
667 (ShareMode::Disabled, "disabled"),
668 ] {
669 let json_str = serde_json::to_string(&variant).unwrap();
670 assert_eq!(json_str, format!("\"{expected}\""));
671 let back: ShareMode = serde_json::from_str(&json_str).unwrap();
672 assert_eq!(variant, back);
673 }
674 }
675
676 #[test]
677 fn layout_serde() {
678 for (variant, expected) in [(Layout::Auto, "auto"), (Layout::Stretch, "stretch")] {
679 let json_str = serde_json::to_string(&variant).unwrap();
680 assert_eq!(json_str, format!("\"{expected}\""));
681 let back: Layout = serde_json::from_str(&json_str).unwrap();
682 assert_eq!(variant, back);
683 }
684 }
685
686 #[test]
687 fn agent_config_flatten() {
688 let ac = AgentConfig {
689 description: "Build agent".into(),
690 mode: ModeConfig {
691 model: Some("gpt-4o".into()),
692 tools: Some(HashMap::from([("bash".into(), true)])),
693 ..Default::default()
694 },
695 };
696 let v = serde_json::to_value(&ac).unwrap();
697 assert_eq!(v["description"], "Build agent");
699 assert_eq!(v["model"], "gpt-4o");
700 assert_eq!(v["tools"]["bash"], true);
701 let back: AgentConfig = serde_json::from_value(v).unwrap();
702 assert_eq!(ac, back);
703 }
704
705 #[test]
706 fn provider_options_with_extras() {
707 let opts = ProviderOptions {
708 api_key: Some("sk-test".into()),
709 base_url: None,
710 extra: HashMap::from([("organization".into(), json!("org-123"))]),
711 };
712 let v = serde_json::to_value(&opts).unwrap();
713 assert_eq!(v["apiKey"], "sk-test");
714 assert_eq!(v["organization"], "org-123");
715 let back: ProviderOptions = serde_json::from_value(v).unwrap();
716 assert_eq!(opts, back);
717 }
718
719 #[test]
720 fn config_empty_round_trip() {
721 let cfg = Config::default();
722 let json_str = serde_json::to_string(&cfg).unwrap();
723 assert_eq!(json_str, "{}");
724 let back: Config = serde_json::from_str(&json_str).unwrap();
725 assert_eq!(cfg, back);
726 }
727
728 #[test]
731 fn config_minimal_partial_fields() {
732 let cfg =
733 Config { theme: Some("dark".into()), autoupdate: Some(false), ..Default::default() };
734 let json_str = serde_json::to_string(&cfg).unwrap();
735 assert!(json_str.contains("theme"));
737 assert!(json_str.contains("autoupdate"));
738 assert!(!json_str.contains("$schema"));
739 assert!(!json_str.contains("agent"));
740 assert!(!json_str.contains("mcp"));
741 let back: Config = serde_json::from_str(&json_str).unwrap();
742 assert_eq!(cfg, back);
743 }
744
745 #[test]
746 fn mcp_local_minimal() {
747 let cfg = McpConfig::Local(McpLocalConfig {
748 command: vec!["my-server".into()],
749 enabled: None,
750 environment: None,
751 });
752 let v = serde_json::to_value(&cfg).unwrap();
753 assert_eq!(v["type"], "local");
754 assert!(v.get("enabled").is_none());
755 assert!(v.get("environment").is_none());
756 let back: McpConfig = serde_json::from_value(v).unwrap();
757 assert_eq!(cfg, back);
758 }
759
760 #[test]
761 fn mcp_remote_minimal() {
762 let cfg = McpConfig::Remote(McpRemoteConfig {
763 url: "https://remote.example.com".into(),
764 enabled: None,
765 headers: None,
766 });
767 let v = serde_json::to_value(&cfg).unwrap();
768 assert_eq!(v["type"], "remote");
769 assert!(v.get("enabled").is_none());
770 assert!(v.get("headers").is_none());
771 let back: McpConfig = serde_json::from_value(v).unwrap();
772 assert_eq!(cfg, back);
773 }
774
775 #[test]
776 fn mode_config_single_field() {
777 let mc = ModeConfig { temperature: Some(0.3), ..Default::default() };
778 let v = serde_json::to_value(&mc).unwrap();
779 assert_eq!(v["temperature"], 0.3);
780 assert!(v.get("disable").is_none());
781 assert!(v.get("model").is_none());
782 assert!(v.get("prompt").is_none());
783 assert!(v.get("tools").is_none());
784 let back: ModeConfig = serde_json::from_value(v).unwrap();
785 assert_eq!(mc, back);
786 }
787
788 #[test]
789 fn config_with_empty_collections() {
790 let cfg = Config {
791 disabled_providers: Some(vec![]),
792 instructions: Some(vec![]),
793 mcp: Some(HashMap::new()),
794 mode: Some(HashMap::new()),
795 provider: Some(HashMap::new()),
796 ..Default::default()
797 };
798 let json_str = serde_json::to_string(&cfg).unwrap();
799 let back: Config = serde_json::from_str(&json_str).unwrap();
800 assert_eq!(cfg, back);
801 }
802
803 #[test]
804 fn hook_command_minimal() {
805 let hc = HookCommand { command: vec!["echo".into(), "done".into()], environment: None };
806 let v = serde_json::to_value(&hc).unwrap();
807 assert!(v.get("environment").is_none());
808 let back: HookCommand = serde_json::from_value(v).unwrap();
809 assert_eq!(hc, back);
810 }
811
812 #[test]
813 fn experimental_no_hooks() {
814 let exp = Experimental { hook: None };
815 let v = serde_json::to_value(&exp).unwrap();
816 assert!(v.get("hook").is_none());
817 let back: Experimental = serde_json::from_value(v).unwrap();
818 assert_eq!(exp, back);
819 }
820
821 #[test]
822 fn provider_config_minimal() {
823 let pc = ProviderConfig {
824 models: HashMap::new(),
825 id: None,
826 api: None,
827 env: None,
828 name: None,
829 npm: None,
830 options: None,
831 };
832 let v = serde_json::to_value(&pc).unwrap();
833 assert!(v.get("id").is_none());
834 assert!(v.get("api").is_none());
835 assert!(v.get("name").is_none());
836 let back: ProviderConfig = serde_json::from_value(v).unwrap();
837 assert_eq!(pc, back);
838 }
839
840 #[test]
841 fn provider_model_config_all_none() {
842 let pmc = ProviderModelConfig::default();
843 let json_str = serde_json::to_string(&pmc).unwrap();
844 assert_eq!(json_str, "{}");
845 let back: ProviderModelConfig = serde_json::from_str(&json_str).unwrap();
846 assert_eq!(pmc, back);
847 }
848}