1use std::collections::BTreeMap;
4use std::sync::Arc;
5
6use serde::{Deserialize, Serialize};
7use serde_json::{Map, Number, Value};
8
9use crate::text::SharedText;
10
11pub type Api = String;
13
14pub type ProviderId = String;
16
17#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
19#[serde(rename_all = "lowercase")]
20pub enum ThinkingLevel {
21 Minimal,
23 Low,
25 Medium,
27 High,
29 Xhigh,
31 Max,
33}
34
35#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
37#[serde(rename_all = "lowercase")]
38pub enum ModelThinkingLevel {
39 Off,
41 Minimal,
43 Low,
45 Medium,
47 High,
49 Xhigh,
51 Max,
53}
54
55pub type ThinkingLevelMap = BTreeMap<ModelThinkingLevel, Option<String>>;
60
61#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
63#[serde(rename_all = "lowercase")]
64pub enum CacheRetention {
65 None,
67 Short,
69 Long,
71}
72
73#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
75#[serde(rename_all = "kebab-case")]
76pub enum Transport {
77 Sse,
79 Websocket,
81 WebsocketCached,
83 Auto,
85}
86
87#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
89#[serde(rename_all = "lowercase")]
90pub enum ModelInput {
91 Text,
93 Image,
95}
96
97#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
99pub enum StopReason {
100 #[serde(rename = "stop")]
102 Stop,
103 #[serde(rename = "length")]
105 Length,
106 #[serde(rename = "toolUse")]
108 ToolUse,
109 #[serde(rename = "error")]
111 Error,
112 #[serde(rename = "aborted")]
114 Aborted,
115}
116
117#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
119pub enum DoneReason {
120 #[serde(rename = "stop")]
122 Stop,
123 #[serde(rename = "length")]
125 Length,
126 #[serde(rename = "toolUse")]
128 ToolUse,
129}
130
131#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
133#[serde(rename_all = "lowercase")]
134pub enum ErrorReason {
135 Aborted,
137 Error,
139}
140
141#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
142enum TextContentType {
143 #[serde(rename = "text")]
144 Text,
145}
146
147#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
149#[serde(rename_all = "camelCase")]
150pub struct TextContent {
151 #[serde(rename = "type")]
152 kind: TextContentType,
153 pub text: SharedText,
155 #[serde(skip_serializing_if = "Option::is_none")]
157 pub text_signature: Option<Arc<str>>,
158}
159
160impl TextContent {
161 #[must_use]
163 pub fn new(text: impl Into<SharedText>) -> Self {
164 Self {
165 kind: TextContentType::Text,
166 text: text.into(),
167 text_signature: None,
168 }
169 }
170}
171
172#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
173enum ThinkingContentType {
174 #[serde(rename = "thinking")]
175 Thinking,
176}
177
178#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
180#[serde(rename_all = "camelCase")]
181pub struct ThinkingContent {
182 #[serde(rename = "type")]
183 kind: ThinkingContentType,
184 pub thinking: SharedText,
186 #[serde(skip_serializing_if = "Option::is_none")]
188 pub thinking_signature: Option<Arc<str>>,
189 #[serde(skip_serializing_if = "Option::is_none")]
191 pub redacted: Option<bool>,
192}
193
194impl ThinkingContent {
195 #[must_use]
197 pub fn new(thinking: impl Into<SharedText>) -> Self {
198 Self {
199 kind: ThinkingContentType::Thinking,
200 thinking: thinking.into(),
201 thinking_signature: None,
202 redacted: None,
203 }
204 }
205}
206
207#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
208enum ImageContentType {
209 #[serde(rename = "image")]
210 Image,
211}
212
213#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
215#[serde(rename_all = "camelCase")]
216pub struct ImageContent {
217 #[serde(rename = "type")]
218 kind: ImageContentType,
219 pub data: String,
221 pub mime_type: String,
223}
224
225impl ImageContent {
226 #[must_use]
228 pub fn new(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
229 Self {
230 kind: ImageContentType::Image,
231 data: data.into(),
232 mime_type: mime_type.into(),
233 }
234 }
235}
236
237#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
238enum ToolCallType {
239 #[serde(rename = "toolCall")]
240 ToolCall,
241}
242
243#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
245#[serde(rename_all = "camelCase")]
246pub struct ToolCall {
247 #[serde(rename = "type")]
248 kind: ToolCallType,
249 pub id: String,
251 pub name: String,
253 pub arguments: Arc<Map<String, Value>>,
255 #[serde(skip_serializing_if = "Option::is_none")]
257 pub thought_signature: Option<Arc<str>>,
258}
259
260impl ToolCall {
261 #[must_use]
263 pub fn new(
264 id: impl Into<String>,
265 name: impl Into<String>,
266 arguments: impl Into<Arc<Map<String, Value>>>,
267 ) -> Self {
268 Self {
269 kind: ToolCallType::ToolCall,
270 id: id.into(),
271 name: name.into(),
272 arguments: arguments.into(),
273 thought_signature: None,
274 }
275 }
276}
277
278#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
280#[serde(rename_all = "camelCase")]
281pub struct UsageCost {
282 #[serde(default)]
284 pub input: f64,
285 #[serde(default)]
287 pub output: f64,
288 #[serde(default)]
290 pub cache_read: f64,
291 #[serde(default)]
293 pub cache_write: f64,
294 #[serde(default)]
296 pub total: f64,
297}
298
299#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
301#[serde(rename_all = "camelCase")]
302pub struct Usage {
303 #[serde(default)]
305 pub input: u64,
306 #[serde(default)]
308 pub output: u64,
309 #[serde(default)]
311 pub cache_read: u64,
312 #[serde(default)]
314 pub cache_write: u64,
315 #[serde(default, skip_serializing_if = "Option::is_none")]
317 pub cache_write1h: Option<u64>,
318 #[serde(default, skip_serializing_if = "Option::is_none")]
320 pub reasoning: Option<u64>,
321 #[serde(default)]
323 pub total_tokens: u64,
324 #[serde(default)]
326 pub cost: UsageCost,
327}
328
329#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
331#[serde(untagged)]
332pub enum DiagnosticCode {
333 String(String),
335 Number(Number),
337}
338
339#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
341pub struct DiagnosticErrorInfo {
342 #[serde(skip_serializing_if = "Option::is_none")]
344 pub name: Option<String>,
345 pub message: String,
347 #[serde(skip_serializing_if = "Option::is_none")]
349 pub stack: Option<String>,
350 #[serde(skip_serializing_if = "Option::is_none")]
352 pub code: Option<DiagnosticCode>,
353}
354
355#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
357pub struct AssistantMessageDiagnostic {
358 #[serde(rename = "type")]
360 pub kind: String,
361 pub timestamp: i64,
363 #[serde(skip_serializing_if = "Option::is_none")]
365 pub error: Option<DiagnosticErrorInfo>,
366 #[serde(skip_serializing_if = "Option::is_none")]
368 pub details: Option<Map<String, Value>>,
369}
370
371#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
373#[serde(untagged)]
374pub enum UserContent {
375 Text(TextContent),
377 Image(ImageContent),
379}
380
381#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
383#[serde(untagged)]
384pub enum UserMessageContent {
385 Text(String),
387 Blocks(Vec<UserContent>),
389}
390
391#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
393#[serde(untagged)]
394pub enum AssistantContent {
395 Text(TextContent),
397 Thinking(ThinkingContent),
399 ToolCall(ToolCall),
401}
402
403#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
405#[serde(untagged)]
406pub enum ToolResultContent {
407 Text(TextContent),
409 Image(ImageContent),
411}
412
413#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
414enum UserRole {
415 #[serde(rename = "user")]
416 User,
417}
418
419#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
421pub struct UserMessage {
422 role: UserRole,
423 pub content: UserMessageContent,
425 pub timestamp: i64,
427}
428
429impl UserMessage {
430 #[must_use]
432 pub fn new(content: UserMessageContent, timestamp: i64) -> Self {
433 Self {
434 role: UserRole::User,
435 content,
436 timestamp,
437 }
438 }
439}
440
441#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
442enum AssistantRole {
443 #[serde(rename = "assistant")]
444 Assistant,
445}
446
447#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
449#[serde(rename_all = "camelCase")]
450pub struct AssistantMessage {
451 role: AssistantRole,
452 pub content: Vec<AssistantContent>,
454 pub api: Api,
456 pub provider: ProviderId,
458 pub model: String,
460 #[serde(skip_serializing_if = "Option::is_none")]
462 pub response_model: Option<String>,
463 #[serde(skip_serializing_if = "Option::is_none")]
465 pub response_id: Option<String>,
466 #[serde(skip_serializing_if = "Option::is_none")]
468 pub diagnostics: Option<Vec<AssistantMessageDiagnostic>>,
469 pub usage: Usage,
471 pub stop_reason: StopReason,
473 #[serde(skip_serializing_if = "Option::is_none")]
475 pub error_message: Option<String>,
476 pub timestamp: i64,
478}
479
480impl AssistantMessage {
481 #[must_use]
483 pub fn new(
484 api: impl Into<Api>,
485 provider: impl Into<ProviderId>,
486 model: impl Into<String>,
487 timestamp: i64,
488 ) -> Self {
489 Self {
490 role: AssistantRole::Assistant,
491 content: Vec::new(),
492 api: api.into(),
493 provider: provider.into(),
494 model: model.into(),
495 response_model: None,
496 response_id: None,
497 diagnostics: None,
498 usage: Usage::default(),
499 stop_reason: StopReason::Stop,
500 error_message: None,
501 timestamp,
502 }
503 }
504}
505
506#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
507enum ToolResultRole {
508 #[serde(rename = "toolResult")]
509 ToolResult,
510}
511
512#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
514#[serde(rename_all = "camelCase")]
515pub struct ToolResultMessage {
516 role: ToolResultRole,
517 pub tool_call_id: String,
519 pub tool_name: String,
521 pub content: Vec<ToolResultContent>,
523 #[serde(skip_serializing_if = "Option::is_none")]
525 pub details: Option<Value>,
526 #[serde(skip_serializing_if = "Option::is_none")]
528 pub added_tool_names: Option<Vec<String>>,
529 pub is_error: bool,
531 pub timestamp: i64,
533}
534
535impl ToolResultMessage {
536 #[must_use]
538 pub fn new(
539 tool_call_id: impl Into<String>,
540 tool_name: impl Into<String>,
541 content: Vec<ToolResultContent>,
542 is_error: bool,
543 timestamp: i64,
544 ) -> Self {
545 Self {
546 role: ToolResultRole::ToolResult,
547 tool_call_id: tool_call_id.into(),
548 tool_name: tool_name.into(),
549 content,
550 details: None,
551 added_tool_names: None,
552 is_error,
553 timestamp,
554 }
555 }
556}
557
558#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
560#[serde(untagged)]
561pub enum Message {
562 User(UserMessage),
564 Assistant(AssistantMessage),
566 ToolResult(ToolResultMessage),
568}
569
570#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
572pub struct Tool {
573 pub name: String,
575 pub description: String,
577 pub parameters: Value,
579}
580
581#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
583#[serde(rename_all = "camelCase")]
584pub struct Context {
585 #[serde(skip_serializing_if = "Option::is_none")]
587 pub system_prompt: Option<String>,
588 pub messages: Vec<Message>,
590 #[serde(skip_serializing_if = "Option::is_none")]
592 pub tools: Option<Vec<Tool>>,
593}
594
595#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
597#[serde(rename_all = "camelCase")]
598pub struct ModelCostRates {
599 pub input: f64,
601 pub output: f64,
603 pub cache_read: f64,
605 pub cache_write: f64,
607}
608
609#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
611#[serde(rename_all = "camelCase")]
612pub struct ModelCostTier {
613 pub input: f64,
615 pub output: f64,
617 pub cache_read: f64,
619 pub cache_write: f64,
621 pub input_tokens_above: u64,
623}
624
625#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
627#[serde(rename_all = "camelCase")]
628pub struct ModelCost {
629 pub input: f64,
631 pub output: f64,
633 pub cache_read: f64,
635 pub cache_write: f64,
637 #[serde(skip_serializing_if = "Option::is_none")]
639 pub tiers: Option<Vec<ModelCostTier>>,
640}
641
642#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
644#[serde(rename_all = "camelCase")]
645pub struct Model {
646 pub id: String,
648 pub name: String,
650 pub api: Api,
652 pub provider: ProviderId,
654 pub base_url: String,
656 pub reasoning: bool,
658 #[serde(skip_serializing_if = "Option::is_none")]
660 pub thinking_level_map: Option<ThinkingLevelMap>,
661 pub input: Vec<ModelInput>,
663 pub cost: ModelCost,
665 pub context_window: u64,
667 pub max_tokens: u64,
669 #[serde(skip_serializing_if = "Option::is_none")]
671 pub headers: Option<BTreeMap<String, String>>,
672 #[serde(skip_serializing_if = "Option::is_none")]
674 pub compat: Option<Value>,
675 #[serde(flatten)]
677 pub extra: BTreeMap<String, Value>,
678}
679
680#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
682#[serde(tag = "type")]
683pub enum AssistantMessageEvent {
684 #[serde(rename = "start")]
686 Start {
687 partial: AssistantMessage,
689 },
690 #[serde(rename = "text_start")]
692 TextStart {
693 #[serde(rename = "contentIndex")]
695 content_index: u64,
696 partial: AssistantMessage,
698 },
699 #[serde(rename = "text_delta")]
701 TextDelta {
702 #[serde(rename = "contentIndex")]
704 content_index: u64,
705 delta: String,
707 partial: AssistantMessage,
709 },
710 #[serde(rename = "text_end")]
712 TextEnd {
713 #[serde(rename = "contentIndex")]
715 content_index: u64,
716 content: String,
718 partial: AssistantMessage,
720 },
721 #[serde(rename = "thinking_start")]
723 ThinkingStart {
724 #[serde(rename = "contentIndex")]
726 content_index: u64,
727 partial: AssistantMessage,
729 },
730 #[serde(rename = "thinking_delta")]
732 ThinkingDelta {
733 #[serde(rename = "contentIndex")]
735 content_index: u64,
736 delta: String,
738 partial: AssistantMessage,
740 },
741 #[serde(rename = "thinking_end")]
743 ThinkingEnd {
744 #[serde(rename = "contentIndex")]
746 content_index: u64,
747 content: String,
749 partial: AssistantMessage,
751 },
752 #[serde(rename = "toolcall_start")]
754 ToolCallStart {
755 #[serde(rename = "contentIndex")]
757 content_index: u64,
758 partial: AssistantMessage,
760 },
761 #[serde(rename = "toolcall_delta")]
763 ToolCallDelta {
764 #[serde(rename = "contentIndex")]
766 content_index: u64,
767 delta: String,
769 partial: AssistantMessage,
771 },
772 #[serde(rename = "toolcall_end")]
774 ToolCallEnd {
775 #[serde(rename = "contentIndex")]
777 content_index: u64,
778 #[serde(rename = "toolCall")]
780 tool_call: ToolCall,
781 partial: AssistantMessage,
783 },
784 #[serde(rename = "done")]
786 Done {
787 reason: DoneReason,
789 message: AssistantMessage,
791 },
792 #[serde(rename = "error")]
794 Error {
795 reason: ErrorReason,
797 error: AssistantMessage,
799 },
800}
801
802#[cfg(test)]
803mod tests {
804 use super::*;
805 use serde_json::json;
806
807 fn assistant() -> AssistantMessage {
808 AssistantMessage::new("custom-api", "custom-provider", "model", 1_700_000_000_000)
809 }
810
811 #[test]
812 fn sibling_content_tags_are_literal() -> Result<(), Box<dyn std::error::Error>> {
813 let text = TextContent::new("hello");
814 assert_eq!(
815 serde_json::to_value(text)?,
816 json!({"type": "text", "text": "hello"})
817 );
818 assert!(
819 serde_json::from_value::<TextContent>(json!({
820 "type": "image",
821 "text": "hello"
822 }))
823 .is_err()
824 );
825
826 let image = ImageContent::new("AA==", "image/png");
827 assert_eq!(
828 serde_json::to_value(image)?,
829 json!({"type": "image", "data": "AA==", "mimeType": "image/png"})
830 );
831 Ok(())
832 }
833
834 #[test]
835 fn sibling_message_roles_are_literal() -> Result<(), Box<dyn std::error::Error>> {
836 let message = UserMessage::new(UserMessageContent::Text("hi".into()), 7);
837 assert_eq!(
838 serde_json::to_value(message)?,
839 json!({"role": "user", "content": "hi", "timestamp": 7})
840 );
841 assert!(
842 serde_json::from_value::<UserMessage>(json!({
843 "role": "assistant",
844 "content": "hi",
845 "timestamp": 7
846 }))
847 .is_err()
848 );
849
850 let assistant = Message::Assistant(assistant());
851 let assistant_json = serde_json::to_value(&assistant)?;
852 assert_eq!(assistant_json["role"], "assistant");
853 assert_eq!(
854 serde_json::from_value::<Message>(assistant_json)?,
855 assistant
856 );
857
858 let tool_result = Message::ToolResult(ToolResultMessage::new(
859 "call-1",
860 "read",
861 Vec::new(),
862 false,
863 8,
864 ));
865 let tool_result_json = serde_json::to_value(&tool_result)?;
866 assert_eq!(tool_result_json["role"], "toolResult");
867 assert_eq!(
868 serde_json::from_value::<Message>(tool_result_json)?,
869 tool_result
870 );
871 Ok(())
872 }
873
874 #[test]
875 fn events_use_exact_tags_fields_and_tool_use() -> Result<(), Box<dyn std::error::Error>> {
876 let events = [
877 AssistantMessageEvent::Start {
878 partial: assistant(),
879 },
880 AssistantMessageEvent::TextStart {
881 content_index: 0,
882 partial: assistant(),
883 },
884 AssistantMessageEvent::TextDelta {
885 content_index: 0,
886 delta: "x".into(),
887 partial: assistant(),
888 },
889 AssistantMessageEvent::TextEnd {
890 content_index: 0,
891 content: "x".into(),
892 partial: assistant(),
893 },
894 AssistantMessageEvent::ThinkingStart {
895 content_index: 1,
896 partial: assistant(),
897 },
898 AssistantMessageEvent::ThinkingDelta {
899 content_index: 1,
900 delta: "x".into(),
901 partial: assistant(),
902 },
903 AssistantMessageEvent::ThinkingEnd {
904 content_index: 1,
905 content: "x".into(),
906 partial: assistant(),
907 },
908 AssistantMessageEvent::ToolCallStart {
909 content_index: 2,
910 partial: assistant(),
911 },
912 AssistantMessageEvent::ToolCallDelta {
913 content_index: 2,
914 delta: "{}".into(),
915 partial: assistant(),
916 },
917 AssistantMessageEvent::ToolCallEnd {
918 content_index: 2,
919 tool_call: ToolCall::new("call-1", "read", Map::new()),
920 partial: assistant(),
921 },
922 AssistantMessageEvent::Done {
923 reason: DoneReason::ToolUse,
924 message: assistant(),
925 },
926 AssistantMessageEvent::Error {
927 reason: ErrorReason::Error,
928 error: assistant(),
929 },
930 ];
931 let encoded = events
932 .into_iter()
933 .map(serde_json::to_value)
934 .collect::<Result<Vec<_>, _>>()?;
935 let tags = encoded
936 .iter()
937 .map(|event| &event["type"])
938 .collect::<Vec<_>>();
939 assert_eq!(
940 tags,
941 [
942 "start",
943 "text_start",
944 "text_delta",
945 "text_end",
946 "thinking_start",
947 "thinking_delta",
948 "thinking_end",
949 "toolcall_start",
950 "toolcall_delta",
951 "toolcall_end",
952 "done",
953 "error",
954 ]
955 );
956 assert_eq!(encoded[9]["contentIndex"], 2);
957 assert_eq!(encoded[9]["toolCall"]["type"], "toolCall");
958 assert_eq!(encoded[10]["reason"], "toolUse");
959 assert_eq!(encoded[10]["message"]["role"], "assistant");
960 assert!(encoded[10].get("error").is_none());
961 assert_eq!(encoded[11]["error"]["role"], "assistant");
962 assert!(encoded[11].get("message").is_none());
963 Ok(())
964 }
965
966 #[test]
967 fn optional_fields_are_omitted() -> Result<(), Box<dyn std::error::Error>> {
968 let value = serde_json::to_value(assistant())?;
969 for key in ["responseModel", "responseId", "diagnostics", "errorMessage"] {
970 assert!(value.get(key).is_none(), "unexpected field {key}");
971 }
972 assert!(value["usage"].get("cacheWrite1h").is_none());
973 assert!(value["usage"].get("reasoning").is_none());
974 Ok(())
975 }
976
977 #[test]
978 fn thinking_level_map_preserves_null_values() -> Result<(), Box<dyn std::error::Error>> {
979 let map: ThinkingLevelMap = serde_json::from_value(json!({
980 "off": null,
981 "high": "high"
982 }))?;
983 assert_eq!(
984 serde_json::to_value(map)?,
985 json!({"off": null, "high": "high"})
986 );
987 Ok(())
988 }
989
990 #[test]
991 fn model_preserves_unknown_fields() -> Result<(), Box<dyn std::error::Error>> {
992 let input = json!({
993 "id": "m",
994 "name": "Model",
995 "api": "future-api",
996 "provider": "future-provider",
997 "baseUrl": "https://example.test",
998 "reasoning": false,
999 "input": ["text"],
1000 "cost": {"input": 0.0, "output": 0.0, "cacheRead": 0.0, "cacheWrite": 0.0},
1001 "contextWindow": 1000,
1002 "maxTokens": 100,
1003 "futureField": {"nested": true}
1004 });
1005 let model: Model = serde_json::from_value(input.clone())?;
1006 assert_eq!(serde_json::to_value(model)?, input);
1007 Ok(())
1008 }
1009
1010 #[test]
1011 fn tool_arguments_must_be_objects() -> Result<(), Box<dyn std::error::Error>> {
1012 let input = json!({
1013 "type": "toolCall",
1014 "id": "1",
1015 "name": "read",
1016 "arguments": {"path": "a.txt"}
1017 });
1018 let valid: ToolCall = serde_json::from_value(input.clone())?;
1019 assert_eq!(serde_json::to_value(valid)?, input);
1020
1021 for invalid in [json!(null), json!([]), json!("x"), json!(1)] {
1022 assert!(
1023 serde_json::from_value::<ToolCall>(json!({
1024 "type": "toolCall",
1025 "id": "1",
1026 "name": "read",
1027 "arguments": invalid
1028 }))
1029 .is_err()
1030 );
1031 }
1032 Ok(())
1033 }
1034
1035 #[test]
1036 fn done_and_error_reasons_reject_the_other_domain() -> Result<(), Box<dyn std::error::Error>> {
1037 assert!(serde_json::from_value::<DoneReason>(json!("error")).is_err());
1038 assert!(serde_json::from_value::<DoneReason>(json!("aborted")).is_err());
1039 assert!(serde_json::from_value::<ErrorReason>(json!("stop")).is_err());
1040 assert!(serde_json::from_value::<ErrorReason>(json!("toolUse")).is_err());
1041
1042 let mut invalid_done = serde_json::to_value(AssistantMessageEvent::Done {
1043 reason: DoneReason::Stop,
1044 message: assistant(),
1045 })?;
1046 invalid_done["reason"] = json!("error");
1047 assert!(serde_json::from_value::<AssistantMessageEvent>(invalid_done).is_err());
1048 Ok(())
1049 }
1050}