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)]
18pub struct App {
19 pub git: bool,
21 pub hostname: String,
23 pub path: AppPath,
25 pub time: AppTime,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
31pub struct AppPath {
32 pub config: String,
34 pub cwd: String,
36 pub data: String,
38 pub root: String,
40 pub state: String,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
46pub struct AppTime {
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub initialized: Option<f64>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
58pub struct Mode {
59 pub name: String,
61 pub tools: HashMap<String, bool>,
63 #[serde(skip_serializing_if = "Option::is_none")]
65 pub model: Option<ModeModel>,
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub prompt: Option<String>,
69 #[serde(skip_serializing_if = "Option::is_none")]
71 pub temperature: Option<f64>,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
76pub struct ModeModel {
77 #[serde(rename = "modelID")]
79 pub model_id: String,
80 #[serde(rename = "providerID")]
82 pub provider_id: String,
83}
84
85#[allow(clippy::struct_excessive_bools)]
91#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
92pub struct ModelMediaCapabilities {
93 pub text: bool,
95 pub audio: bool,
97 pub image: bool,
99 pub video: bool,
101 pub pdf: bool,
103}
104
105#[allow(clippy::struct_excessive_bools)]
107#[allow(clippy::derive_partial_eq_without_eq)]
108#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
109pub struct ModelCapabilities {
110 pub temperature: bool,
112 pub reasoning: bool,
114 pub attachment: bool,
116 pub toolcall: bool,
118 #[serde(default)]
120 pub input: ModelMediaCapabilities,
121 #[serde(default)]
123 pub output: ModelMediaCapabilities,
124 #[serde(default)]
126 pub interleaved: serde_json::Value,
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
131pub struct ModelApi {
132 pub id: String,
134 pub url: String,
136 pub npm: String,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
142#[serde(rename_all = "lowercase")]
143pub enum ModelStatus {
144 Alpha,
146 Beta,
148 Deprecated,
150 Active,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
156pub struct CostCache {
157 pub read: f64,
159 pub write: f64,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
165#[serde(rename_all = "camelCase")]
166pub struct CostExperimentalOver200K {
167 pub input: f64,
169 pub output: f64,
171 pub cache: CostCache,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
177pub struct Model {
178 pub id: String,
180 #[serde(rename = "providerID", default)]
182 pub provider_id: String,
183 #[serde(default)]
185 pub api: ModelApi,
186 pub name: String,
188 #[serde(skip_serializing_if = "Option::is_none")]
190 pub family: Option<String>,
191 #[serde(default)]
193 pub capabilities: ModelCapabilities,
194 pub cost: ModelCost,
196 pub limit: ModelLimit,
198 #[serde(default = "default_model_status")]
200 pub status: ModelStatus,
201 pub options: HashMap<String, serde_json::Value>,
203 #[serde(default)]
205 pub headers: HashMap<String, String>,
206 pub release_date: String,
208 #[serde(skip_serializing_if = "Option::is_none")]
210 pub variants: Option<HashMap<String, HashMap<String, serde_json::Value>>>,
211}
212
213const fn default_model_status() -> ModelStatus {
214 ModelStatus::Active
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
219pub struct ModelCost {
220 pub input: f64,
222 pub output: f64,
224 #[serde(default)]
226 pub cache: CostCache,
227 #[serde(rename = "experimentalOver200K", skip_serializing_if = "Option::is_none")]
229 pub experimental_over_200k: Option<CostExperimentalOver200K>,
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
234pub struct ModelLimit {
235 pub context: f64,
237 #[serde(skip_serializing_if = "Option::is_none")]
239 pub input: Option<f64>,
240 pub output: f64,
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
250#[serde(rename_all = "lowercase")]
251pub enum ProviderSource {
252 Env,
254 Config,
256 Custom,
258 Api,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
264pub struct Provider {
265 pub id: String,
267 pub name: String,
269 #[serde(default = "default_provider_source")]
271 pub source: ProviderSource,
272 pub env: Vec<String>,
274 #[serde(skip_serializing_if = "Option::is_none")]
276 pub key: Option<String>,
277 #[serde(default)]
279 pub options: HashMap<String, serde_json::Value>,
280 pub models: HashMap<String, Model>,
282}
283
284const fn default_provider_source() -> ProviderSource {
285 ProviderSource::Env
286}
287
288pub type AppInitResponse = bool;
294
295pub type AppLogResponse = bool;
297
298pub type AppModesResponse = Vec<Mode>;
300
301#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
303pub struct AppProvidersResponse {
304 pub default: HashMap<String, String>,
306 pub providers: Vec<Provider>,
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
312#[serde(rename_all = "lowercase")]
313pub enum LogLevel {
314 Debug,
316 Info,
318 Error,
320 Warn,
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
326pub struct AppLogParams {
327 pub level: LogLevel,
329 pub message: String,
331 pub service: String,
333 #[serde(skip_serializing_if = "Option::is_none")]
335 pub extra: Option<HashMap<String, serde_json::Value>>,
336}
337
338pub struct AppResource<'a> {
344 client: &'a Opencode,
345}
346
347impl<'a> AppResource<'a> {
348 pub(crate) const fn new(client: &'a Opencode) -> Self {
350 Self { client }
351 }
352
353 pub async fn get(&self, options: Option<&RequestOptions>) -> Result<App, OpencodeError> {
355 self.client.get("/app", options).await
356 }
357
358 pub async fn init(
360 &self,
361 options: Option<&RequestOptions>,
362 ) -> Result<AppInitResponse, OpencodeError> {
363 self.client.post::<bool, ()>("/app/init", None, options).await
364 }
365
366 pub async fn log(
368 &self,
369 params: &AppLogParams,
370 options: Option<&RequestOptions>,
371 ) -> Result<AppLogResponse, OpencodeError> {
372 self.client.post("/log", Some(params), options).await
373 }
374
375 pub async fn modes(
377 &self,
378 options: Option<&RequestOptions>,
379 ) -> Result<AppModesResponse, OpencodeError> {
380 self.client.get("/mode", options).await
381 }
382
383 pub async fn providers(
385 &self,
386 options: Option<&RequestOptions>,
387 ) -> Result<AppProvidersResponse, OpencodeError> {
388 self.client.get("/config/providers", options).await
389 }
390}
391
392#[cfg(test)]
397mod tests {
398 use serde_json::json;
399
400 use super::*;
401
402 #[test]
403 fn app_round_trip() {
404 let app = App {
405 git: true,
406 hostname: "dev-machine".into(),
407 path: AppPath {
408 config: "/home/user/.config/opencode".into(),
409 cwd: "/home/user/project".into(),
410 data: "/home/user/.local/share/opencode".into(),
411 root: "/home/user/project".into(),
412 state: "/home/user/.local/state/opencode".into(),
413 },
414 time: AppTime { initialized: Some(1_700_000_000.0) },
415 };
416 let json_str = serde_json::to_string(&app).unwrap();
417 let back: App = serde_json::from_str(&json_str).unwrap();
418 assert_eq!(app, back);
419 }
420
421 #[test]
422 fn app_time_optional_initialized() {
423 let app = App {
424 git: false,
425 hostname: "ci".into(),
426 path: AppPath {
427 config: "/tmp/cfg".into(),
428 cwd: "/tmp".into(),
429 data: "/tmp/data".into(),
430 root: "/tmp".into(),
431 state: "/tmp/state".into(),
432 },
433 time: AppTime { initialized: None },
434 };
435 let json_str = serde_json::to_string(&app).unwrap();
436 assert!(!json_str.contains("initialized"));
438 let back: App = serde_json::from_str(&json_str).unwrap();
439 assert_eq!(app, back);
440 }
441
442 #[test]
443 fn mode_full_round_trip() {
444 let mode = Mode {
445 name: "code".into(),
446 tools: HashMap::from([("bash".into(), true), ("edit".into(), false)]),
447 model: Some(ModeModel { model_id: "gpt-4o".into(), provider_id: "openai".into() }),
448 prompt: Some("You are a coding assistant.".into()),
449 temperature: Some(0.7),
450 };
451 let json_str = serde_json::to_string(&mode).unwrap();
452 assert!(json_str.contains("modelID"));
454 assert!(json_str.contains("providerID"));
455 let back: Mode = serde_json::from_str(&json_str).unwrap();
456 assert_eq!(mode, back);
457 }
458
459 #[test]
460 fn mode_minimal() {
461 let mode = Mode {
462 name: "default".into(),
463 tools: HashMap::new(),
464 model: None,
465 prompt: None,
466 temperature: None,
467 };
468 let json_str = serde_json::to_string(&mode).unwrap();
469 assert!(!json_str.contains("model"));
470 assert!(!json_str.contains("prompt"));
471 assert!(!json_str.contains("temperature"));
472 let back: Mode = serde_json::from_str(&json_str).unwrap();
473 assert_eq!(mode, back);
474 }
475
476 fn test_model() -> Model {
478 Model {
479 id: "gpt-4o".into(),
480 provider_id: "openai".into(),
481 api: ModelApi {
482 id: "openai".into(),
483 url: "https://api.openai.com/v1".into(),
484 npm: "openai".into(),
485 },
486 name: "GPT-4o".into(),
487 family: None,
488 capabilities: ModelCapabilities {
489 temperature: true,
490 reasoning: false,
491 attachment: true,
492 toolcall: true,
493 input: ModelMediaCapabilities {
494 text: true,
495 audio: false,
496 image: true,
497 video: false,
498 pdf: false,
499 },
500 output: ModelMediaCapabilities { text: true, ..Default::default() },
501 interleaved: json!(false),
502 },
503 cost: ModelCost {
504 input: 5.0,
505 output: 15.0,
506 cache: CostCache { read: 2.5, write: 0.0 },
507 experimental_over_200k: None,
508 },
509 limit: ModelLimit { context: 128_000.0, input: None, output: 4_096.0 },
510 status: ModelStatus::Active,
511 options: HashMap::from([("streaming".into(), json!(true))]),
512 headers: HashMap::new(),
513 release_date: "2024-05-13".into(),
514 variants: None,
515 }
516 }
517
518 #[test]
519 fn model_round_trip() {
520 let model = test_model();
521 let json_str = serde_json::to_string(&model).unwrap();
522 assert!(json_str.contains("providerID"));
523 assert!(json_str.contains("capabilities"));
524 let back: Model = serde_json::from_str(&json_str).unwrap();
525 assert_eq!(model, back);
526 }
527
528 #[test]
529 fn model_cost_default_cache() {
530 let cost = ModelCost {
531 input: 1.0,
532 output: 2.0,
533 cache: CostCache::default(),
534 experimental_over_200k: None,
535 };
536 let json_str = serde_json::to_string(&cost).unwrap();
537 assert!(!json_str.contains("experimentalOver200K"));
538 let back: ModelCost = serde_json::from_str(&json_str).unwrap();
539 assert_eq!(cost, back);
540 }
541
542 #[test]
543 fn provider_round_trip() {
544 let provider = Provider {
545 id: "openai".into(),
546 name: "OpenAI".into(),
547 source: ProviderSource::Env,
548 env: vec!["OPENAI_API_KEY".into()],
549 key: None,
550 options: HashMap::new(),
551 models: HashMap::from([("gpt-4o".into(), test_model())]),
552 };
553 let json_str = serde_json::to_string(&provider).unwrap();
554 assert!(json_str.contains("\"source\":\"env\""));
555 let back: Provider = serde_json::from_str(&json_str).unwrap();
556 assert_eq!(provider, back);
557 }
558
559 #[test]
560 fn app_log_params_with_extra() {
561 let params = AppLogParams {
562 level: LogLevel::Info,
563 message: "server started".into(),
564 service: "api-gateway".into(),
565 extra: Some(HashMap::from([
566 ("port".into(), json!(8080)),
567 ("env".into(), json!("production")),
568 ])),
569 };
570 let json_str = serde_json::to_string(¶ms).unwrap();
571 assert!(json_str.contains(r#""level":"info"#));
572 let back: AppLogParams = serde_json::from_str(&json_str).unwrap();
573 assert_eq!(params, back);
574 }
575
576 #[test]
577 fn app_log_params_without_extra() {
578 let params = AppLogParams {
579 level: LogLevel::Error,
580 message: "something broke".into(),
581 service: "worker".into(),
582 extra: None,
583 };
584 let json_str = serde_json::to_string(¶ms).unwrap();
585 assert!(!json_str.contains("extra"));
586 assert!(json_str.contains(r#""level":"error"#));
587 let back: AppLogParams = serde_json::from_str(&json_str).unwrap();
588 assert_eq!(params, back);
589 }
590
591 #[test]
592 fn log_level_variants() {
593 for (variant, expected) in [
594 (LogLevel::Debug, "debug"),
595 (LogLevel::Info, "info"),
596 (LogLevel::Error, "error"),
597 (LogLevel::Warn, "warn"),
598 ] {
599 let json_str = serde_json::to_string(&variant).unwrap();
600 assert_eq!(json_str, format!("\"{expected}\""));
601 let back: LogLevel = serde_json::from_str(&json_str).unwrap();
602 assert_eq!(variant, back);
603 }
604 }
605
606 #[test]
607 fn app_providers_response_round_trip() {
608 let resp = AppProvidersResponse {
609 default: HashMap::from([
610 ("openai".into(), "gpt-4o".into()),
611 ("anthropic".into(), "claude-3-opus".into()),
612 ]),
613 providers: vec![Provider {
614 id: "openai".into(),
615 name: "OpenAI".into(),
616 source: ProviderSource::Env,
617 env: vec!["OPENAI_API_KEY".into()],
618 key: None,
619 options: HashMap::new(),
620 models: HashMap::new(),
621 }],
622 };
623 let json_str = serde_json::to_string(&resp).unwrap();
624 let back: AppProvidersResponse = serde_json::from_str(&json_str).unwrap();
625 assert_eq!(resp, back);
626 }
627
628 #[test]
629 fn mode_model_serde_rename() {
630 let m = ModeModel { model_id: "claude-3-opus".into(), provider_id: "anthropic".into() };
631 let v: serde_json::Value = serde_json::to_value(&m).unwrap();
632 assert_eq!(v["modelID"], "claude-3-opus");
633 assert_eq!(v["providerID"], "anthropic");
634 let back: ModeModel = serde_json::from_value(v).unwrap();
635 assert_eq!(m, back);
636 }
637
638 #[test]
641 fn provider_no_key() {
642 let provider = Provider {
643 id: "custom".into(),
644 name: "Custom".into(),
645 source: ProviderSource::Custom,
646 env: vec![],
647 key: None,
648 options: HashMap::new(),
649 models: HashMap::new(),
650 };
651 let json_str = serde_json::to_string(&provider).unwrap();
652 assert!(!json_str.contains("key"));
653 assert!(json_str.contains("\"source\":\"custom\""));
654 let back: Provider = serde_json::from_str(&json_str).unwrap();
655 assert_eq!(provider, back);
656 }
657
658 #[test]
659 fn cost_cache_round_trip() {
660 let cache = CostCache { read: 1.5, write: 3.0 };
661 let json_str = serde_json::to_string(&cache).unwrap();
662 let back: CostCache = serde_json::from_str(&json_str).unwrap();
663 assert_eq!(cache, back);
664 }
665
666 #[test]
667 fn model_cost_with_experimental() {
668 let cost = ModelCost {
669 input: 3.0,
670 output: 6.0,
671 cache: CostCache { read: 1.5, write: 0.0 },
672 experimental_over_200k: Some(CostExperimentalOver200K {
673 input: 6.0,
674 output: 12.0,
675 cache: CostCache { read: 3.0, write: 0.0 },
676 }),
677 };
678 let json_str = serde_json::to_string(&cost).unwrap();
679 assert!(json_str.contains("experimentalOver200K"));
680 let back: ModelCost = serde_json::from_str(&json_str).unwrap();
681 assert_eq!(cost, back);
682 }
683
684 #[test]
685 fn app_time_initialized_absent_from_json() {
686 let raw = r#"{"git":true,"hostname":"h","path":{"config":"c","cwd":"w","data":"d","root":"r","state":"s"},"time":{}}"#;
688 let app: App = serde_json::from_str(raw).unwrap();
689 assert_eq!(app.time.initialized, None);
690 }
691
692 #[test]
693 fn app_log_params_extra_empty_map() {
694 let params = AppLogParams {
695 level: LogLevel::Debug,
696 message: "trace".into(),
697 service: "svc".into(),
698 extra: Some(HashMap::new()),
699 };
700 let json_str = serde_json::to_string(¶ms).unwrap();
701 assert!(json_str.contains("extra"));
702 let back: AppLogParams = serde_json::from_str(&json_str).unwrap();
703 assert_eq!(params, back);
704 }
705
706 #[test]
707 fn mode_with_empty_tools_and_some_model() {
708 let mode = Mode {
709 name: "review".into(),
710 tools: HashMap::new(),
711 model: Some(ModeModel { model_id: "o1".into(), provider_id: "openai".into() }),
712 prompt: None,
713 temperature: None,
714 };
715 let json_str = serde_json::to_string(&mode).unwrap();
716 assert!(!json_str.contains("prompt"));
717 assert!(!json_str.contains("temperature"));
718 assert!(json_str.contains("modelID"));
719 let back: Mode = serde_json::from_str(&json_str).unwrap();
720 assert_eq!(mode, back);
721 }
722
723 #[test]
724 fn model_with_empty_options() {
725 let model = Model {
726 id: "small".into(),
727 provider_id: "local".into(),
728 api: ModelApi::default(),
729 name: "Small Model".into(),
730 family: None,
731 capabilities: ModelCapabilities::default(),
732 cost: ModelCost::default(),
733 limit: ModelLimit { context: 4096.0, input: None, output: 512.0 },
734 status: ModelStatus::Active,
735 options: HashMap::new(),
736 headers: HashMap::new(),
737 release_date: "2025-01-01".into(),
738 variants: None,
739 };
740 let json_str = serde_json::to_string(&model).unwrap();
741 let back: Model = serde_json::from_str(&json_str).unwrap();
742 assert_eq!(model, back);
743 }
744
745 #[test]
746 fn model_from_spec_json() {
747 let raw = json!({
748 "id": "claude-sonnet-4-20250514",
749 "providerID": "anthropic",
750 "api": { "id": "anthropic", "url": "https://api.anthropic.com", "npm": "@anthropic-ai/sdk" },
751 "name": "Claude Sonnet 4",
752 "family": "claude",
753 "capabilities": {
754 "temperature": true,
755 "reasoning": true,
756 "attachment": true,
757 "toolcall": true,
758 "input": { "text": true, "audio": false, "image": true, "video": false, "pdf": true },
759 "output": { "text": true, "audio": false, "image": false, "video": false, "pdf": false },
760 "interleaved": { "field": "reasoning_content" }
761 },
762 "cost": {
763 "input": 3.0,
764 "output": 15.0,
765 "cache": { "read": 0.3, "write": 3.75 }
766 },
767 "limit": { "context": 200000, "input": 190000, "output": 16384 },
768 "status": "active",
769 "options": {},
770 "headers": { "anthropic-beta": "interleaved-thinking-2025-05-14" },
771 "release_date": "2025-05-14"
772 });
773 let model: Model = serde_json::from_value(raw).unwrap();
774 assert_eq!(model.id, "claude-sonnet-4-20250514");
775 assert_eq!(model.provider_id, "anthropic");
776 assert_eq!(model.family.as_deref(), Some("claude"));
777 assert!(model.capabilities.reasoning);
778 assert!(model.capabilities.input.pdf);
779 assert_eq!(model.cost.cache.read, 0.3);
780 assert_eq!(model.limit.input, Some(190_000.0));
781 assert_eq!(model.status, ModelStatus::Active);
782 assert_eq!(
783 model.headers.get("anthropic-beta").map(String::as_str),
784 Some("interleaved-thinking-2025-05-14")
785 );
786 }
787
788 #[test]
789 fn model_status_round_trip() {
790 for (variant, expected) in [
791 (ModelStatus::Alpha, "alpha"),
792 (ModelStatus::Beta, "beta"),
793 (ModelStatus::Deprecated, "deprecated"),
794 (ModelStatus::Active, "active"),
795 ] {
796 let json_str = serde_json::to_string(&variant).unwrap();
797 assert_eq!(json_str, format!("\"{}\"", expected));
798 let back: ModelStatus = serde_json::from_str(&json_str).unwrap();
799 assert_eq!(variant, back);
800 }
801 }
802
803 #[test]
804 fn model_capabilities_round_trip() {
805 let caps = ModelCapabilities {
806 temperature: true,
807 reasoning: true,
808 attachment: false,
809 toolcall: true,
810 input: ModelMediaCapabilities {
811 text: true,
812 audio: false,
813 image: true,
814 video: false,
815 pdf: true,
816 },
817 output: ModelMediaCapabilities { text: true, ..Default::default() },
818 interleaved: json!(true),
819 };
820 let json_str = serde_json::to_string(&caps).unwrap();
821 let back: ModelCapabilities = serde_json::from_str(&json_str).unwrap();
822 assert_eq!(caps, back);
823 }
824
825 #[test]
826 fn model_api_round_trip() {
827 let api = ModelApi {
828 id: "openai".into(),
829 url: "https://api.openai.com/v1".into(),
830 npm: "openai".into(),
831 };
832 let json_str = serde_json::to_string(&api).unwrap();
833 let back: ModelApi = serde_json::from_str(&json_str).unwrap();
834 assert_eq!(api, back);
835 }
836
837 #[test]
838 fn provider_from_spec_json() {
839 let raw = json!({
840 "id": "anthropic",
841 "name": "Anthropic",
842 "source": "env",
843 "env": ["ANTHROPIC_API_KEY"],
844 "key": "sk-ant-xxx",
845 "options": {},
846 "models": {}
847 });
848 let provider: Provider = serde_json::from_value(raw).unwrap();
849 assert_eq!(provider.id, "anthropic");
850 assert_eq!(provider.source, ProviderSource::Env);
851 assert_eq!(provider.key.as_deref(), Some("sk-ant-xxx"));
852 }
853
854 #[test]
855 fn provider_source_variants() {
856 for (variant, expected) in [
857 (ProviderSource::Env, "env"),
858 (ProviderSource::Config, "config"),
859 (ProviderSource::Custom, "custom"),
860 (ProviderSource::Api, "api"),
861 ] {
862 let json_str = serde_json::to_string(&variant).unwrap();
863 assert_eq!(json_str, format!("\"{}\"", expected));
864 let back: ProviderSource = serde_json::from_str(&json_str).unwrap();
865 assert_eq!(variant, back);
866 }
867 }
868
869 #[test]
870 fn model_limit_with_input() {
871 let limit = ModelLimit { context: 200_000.0, input: Some(190_000.0), output: 16_384.0 };
872 let json_str = serde_json::to_string(&limit).unwrap();
873 assert!(json_str.contains("input"));
874 let back: ModelLimit = serde_json::from_str(&json_str).unwrap();
875 assert_eq!(limit, back);
876 }
877
878 #[test]
879 fn model_limit_without_input() {
880 let limit = ModelLimit { context: 128_000.0, input: None, output: 4_096.0 };
881 let json_str = serde_json::to_string(&limit).unwrap();
882 assert!(!json_str.contains("input"));
883 let back: ModelLimit = serde_json::from_str(&json_str).unwrap();
884 assert_eq!(limit, back);
885 }
886}