1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use serde_json::Value;
3use std::fmt;
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct ResultMessage {
8 pub subtype: ResultSubtype,
9 pub is_error: bool,
10 pub duration_ms: u64,
11 pub duration_api_ms: u64,
12
13 #[serde(skip_serializing_if = "Option::is_none")]
15 pub ttft_ms: Option<u64>,
16
17 #[serde(skip_serializing_if = "Option::is_none")]
19 pub ttft_stream_ms: Option<u64>,
20
21 #[serde(skip_serializing_if = "Option::is_none")]
23 pub time_to_request_ms: Option<u64>,
24
25 #[serde(skip_serializing_if = "Option::is_none")]
27 pub time_to_request_from_spawn_ms: Option<u64>,
28
29 #[serde(skip_serializing_if = "Option::is_none")]
31 pub warm_spare_claimed: Option<bool>,
32
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub time_origin_ms: Option<u64>,
36
37 #[serde(skip_serializing_if = "Option::is_none")]
40 pub request_sent_wall_ms: Option<f64>,
41
42 #[serde(skip_serializing_if = "Option::is_none")]
44 pub user_message_uuid: Option<String>,
45
46 pub num_turns: i32,
47
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub result: Option<String>,
50
51 #[serde(alias = "sessionId")]
52 pub session_id: String,
53 pub total_cost_usd: f64,
54
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub usage: Option<UsageInfo>,
57
58 #[serde(default)]
60 pub permission_denials: Vec<PermissionDenial>,
61
62 #[serde(default)]
67 pub errors: Vec<String>,
68
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub uuid: Option<String>,
71
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub api_error_status: Option<u16>,
75
76 #[serde(skip_serializing_if = "Option::is_none")]
78 pub stop_reason: Option<String>,
79
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub terminal_reason: Option<String>,
83
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub fast_mode_state: Option<String>,
87
88 #[serde(skip_serializing_if = "Option::is_none")]
92 pub fast_mode_disabled_reason: Option<FastModeDisabledReason>,
93
94 #[serde(skip_serializing_if = "Option::is_none", rename = "modelUsage")]
96 pub model_usage: Option<std::collections::BTreeMap<String, ModelUsageEntry>>,
97
98 #[serde(skip_serializing_if = "Option::is_none")]
100 pub structured_output: Option<Value>,
101
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub deferred_tool_use: Option<DeferredToolUse>,
105
106 #[serde(skip_serializing_if = "Option::is_none")]
108 pub origin: Option<super::message_types::MessageOrigin>,
109}
110
111#[derive(Debug, Clone, Default, Serialize, Deserialize)]
117#[serde(rename_all = "camelCase")]
118pub struct ModelUsageEntry {
119 #[serde(default)]
120 pub input_tokens: u64,
121 #[serde(default)]
122 pub output_tokens: u64,
123 #[serde(default)]
124 pub cache_read_input_tokens: u64,
125 #[serde(default)]
126 pub cache_creation_input_tokens: u64,
127 #[serde(default, rename = "costUSD")]
128 pub cost_usd: f64,
129 #[serde(default)]
130 pub web_search_requests: u32,
131 #[serde(default)]
132 pub context_window: u64,
133 #[serde(default)]
134 pub max_output_tokens: u64,
135 #[serde(flatten)]
136 pub extra: serde_json::Map<String, serde_json::Value>,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
141pub struct DeferredToolUse {
142 pub id: String,
143 pub name: String,
144 pub input: Value,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
152pub struct PermissionDenial {
153 pub tool_name: String,
155
156 pub tool_input: Value,
158
159 pub tool_use_id: String,
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Hash)]
166pub enum FastModeDisabledReason {
167 Free,
169 Preference,
171 ExtraUsageDisabled,
173 NetworkError,
175 UnknownReason,
177 NotFirstParty,
179 DisabledByEnv,
181 ModelNotAllowed,
183 SdkOptInRequired,
185 Pending,
187 Unknown(String),
189}
190
191impl FastModeDisabledReason {
192 pub fn as_str(&self) -> &str {
193 match self {
194 Self::Free => "free",
195 Self::Preference => "preference",
196 Self::ExtraUsageDisabled => "extra_usage_disabled",
197 Self::NetworkError => "network_error",
198 Self::UnknownReason => "unknown",
199 Self::NotFirstParty => "not_first_party",
200 Self::DisabledByEnv => "disabled_by_env",
201 Self::ModelNotAllowed => "model_not_allowed",
202 Self::SdkOptInRequired => "sdk_opt_in_required",
203 Self::Pending => "pending",
204 Self::Unknown(s) => s.as_str(),
205 }
206 }
207}
208
209impl fmt::Display for FastModeDisabledReason {
210 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211 f.write_str(self.as_str())
212 }
213}
214
215impl From<&str> for FastModeDisabledReason {
216 fn from(s: &str) -> Self {
217 match s {
218 "free" => Self::Free,
219 "preference" => Self::Preference,
220 "extra_usage_disabled" => Self::ExtraUsageDisabled,
221 "network_error" => Self::NetworkError,
222 "unknown" => Self::UnknownReason,
223 "not_first_party" => Self::NotFirstParty,
224 "disabled_by_env" => Self::DisabledByEnv,
225 "model_not_allowed" => Self::ModelNotAllowed,
226 "sdk_opt_in_required" => Self::SdkOptInRequired,
227 "pending" => Self::Pending,
228 other => Self::Unknown(other.to_string()),
229 }
230 }
231}
232
233impl Serialize for FastModeDisabledReason {
234 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
235 serializer.serialize_str(self.as_str())
236 }
237}
238
239impl<'de> Deserialize<'de> for FastModeDisabledReason {
240 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
241 let s = String::deserialize(deserializer)?;
242 Ok(Self::from(s.as_str()))
243 }
244}
245
246#[derive(Debug, Clone, PartialEq, Eq, Hash)]
248pub enum ResultSubtype {
249 Success,
250 ErrorMaxTurns,
251 ErrorDuringExecution,
252 ErrorMaxBudgetUsd,
253 ErrorMaxStructuredOutputRetries,
254 Unknown(String),
255}
256
257impl ResultSubtype {
258 pub fn as_str(&self) -> &str {
259 match self {
260 Self::Success => "success",
261 Self::ErrorMaxTurns => "error_max_turns",
262 Self::ErrorDuringExecution => "error_during_execution",
263 Self::ErrorMaxBudgetUsd => "error_max_budget_usd",
264 Self::ErrorMaxStructuredOutputRetries => "error_max_structured_output_retries",
265 Self::Unknown(s) => s.as_str(),
266 }
267 }
268}
269
270impl fmt::Display for ResultSubtype {
271 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
272 f.write_str(self.as_str())
273 }
274}
275
276impl From<&str> for ResultSubtype {
277 fn from(s: &str) -> Self {
278 match s {
279 "success" => Self::Success,
280 "error_max_turns" => Self::ErrorMaxTurns,
281 "error_during_execution" => Self::ErrorDuringExecution,
282 "error_max_budget_usd" => Self::ErrorMaxBudgetUsd,
283 "error_max_structured_output_retries" => Self::ErrorMaxStructuredOutputRetries,
284 other => Self::Unknown(other.to_string()),
285 }
286 }
287}
288
289impl Serialize for ResultSubtype {
290 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
291 serializer.serialize_str(self.as_str())
292 }
293}
294
295impl<'de> Deserialize<'de> for ResultSubtype {
296 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
297 let s = String::deserialize(deserializer)?;
298 Ok(Self::from(s.as_str()))
299 }
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize)]
320pub struct UsageInfo {
321 #[serde(default)]
323 pub input_tokens: u32,
324 #[serde(default)]
326 pub cache_creation_input_tokens: u32,
327 #[serde(default)]
331 pub cache_read_input_tokens: u32,
332 #[serde(default)]
334 pub output_tokens: u32,
335 #[serde(default)]
336 pub server_tool_use: ServerToolUse,
337 #[serde(default)]
338 pub service_tier: String,
339
340 #[serde(skip_serializing_if = "Option::is_none")]
342 pub cache_creation: Option<super::message_types::CacheCreationDetails>,
343
344 #[serde(skip_serializing_if = "Option::is_none")]
346 pub inference_geo: Option<String>,
347
348 #[serde(default, skip_serializing_if = "Vec::is_empty")]
352 pub iterations: Vec<UsageIteration>,
353
354 #[serde(skip_serializing_if = "Option::is_none")]
356 pub speed: Option<String>,
357
358 #[serde(skip_serializing_if = "Option::is_none")]
361 pub output_tokens_details: Option<OutputTokensDetails>,
362}
363
364#[derive(Debug, Clone, Serialize, Deserialize, Default)]
367pub struct OutputTokensDetails {
368 #[serde(skip_serializing_if = "Option::is_none")]
370 pub thinking_tokens: Option<u64>,
371}
372
373#[derive(Debug, Clone, Serialize, Deserialize)]
382pub struct UsageIteration {
383 #[serde(default)]
384 pub input_tokens: u32,
385 #[serde(default)]
386 pub output_tokens: u32,
387 #[serde(default, skip_serializing_if = "Option::is_none")]
389 pub cache_read_input_tokens: Option<u32>,
390 #[serde(default, skip_serializing_if = "Option::is_none")]
392 pub cache_creation_input_tokens: Option<u32>,
393 #[serde(default, skip_serializing_if = "Option::is_none")]
395 pub cache_creation: Option<super::message_types::CacheCreationDetails>,
396 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
398 pub kind: Option<String>,
399}
400
401#[derive(Debug, Clone, Default, Serialize, Deserialize)]
403pub struct ServerToolUse {
404 #[serde(default)]
405 pub web_search_requests: u32,
406 #[serde(default)]
408 pub web_fetch_requests: u32,
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414 use crate::io::ClaudeOutput;
415
416 #[test]
417 fn test_deserialize_result_message() {
418 let json = r#"{
419 "type": "result",
420 "subtype": "success",
421 "is_error": false,
422 "duration_ms": 100,
423 "duration_api_ms": 200,
424 "num_turns": 1,
425 "result": "Done",
426 "session_id": "123",
427 "total_cost_usd": 0.01,
428 "permission_denials": []
429 }"#;
430
431 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
432 assert!(!output.is_error());
433 }
434
435 #[test]
436 fn test_result_subtype_new_and_unknown_values_do_not_fail() {
437 let json = r#"{
438 "type": "result",
439 "subtype": "error_max_budget_usd",
440 "is_error": true,
441 "duration_ms": 100,
442 "duration_api_ms": 200,
443 "num_turns": 1,
444 "session_id": "123",
445 "total_cost_usd": 0.01
446 }"#;
447
448 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
449 let ClaudeOutput::Result(result) = output else {
450 panic!("Expected Result");
451 };
452 assert_eq!(result.subtype, ResultSubtype::ErrorMaxBudgetUsd);
453
454 let json = r#"{
455 "type": "result",
456 "subtype": "future_result_subtype",
457 "is_error": true,
458 "duration_ms": 100,
459 "duration_api_ms": 200,
460 "num_turns": 1,
461 "session_id": "123",
462 "total_cost_usd": 0.01
463 }"#;
464
465 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
466 let ClaudeOutput::Result(result) = output else {
467 panic!("Expected Result");
468 };
469 assert_eq!(
470 result.subtype,
471 ResultSubtype::Unknown("future_result_subtype".to_string())
472 );
473 }
474
475 #[test]
476 fn test_deserialize_result_with_permission_denials() {
477 let json = r#"{
478 "type": "result",
479 "subtype": "success",
480 "is_error": false,
481 "duration_ms": 100,
482 "duration_api_ms": 200,
483 "num_turns": 2,
484 "result": "Done",
485 "session_id": "123",
486 "total_cost_usd": 0.01,
487 "permission_denials": [
488 {
489 "tool_name": "Bash",
490 "tool_input": {"command": "rm -rf /", "description": "Delete everything"},
491 "tool_use_id": "toolu_123"
492 }
493 ]
494 }"#;
495
496 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
497 if let ClaudeOutput::Result(result) = output {
498 assert_eq!(result.permission_denials.len(), 1);
499 assert_eq!(result.permission_denials[0].tool_name, "Bash");
500 assert_eq!(result.permission_denials[0].tool_use_id, "toolu_123");
501 assert_eq!(
502 result.permission_denials[0]
503 .tool_input
504 .get("command")
505 .unwrap(),
506 "rm -rf /"
507 );
508 } else {
509 panic!("Expected Result");
510 }
511 }
512
513 #[test]
514 fn test_permission_denial_roundtrip() {
515 let denial = PermissionDenial {
516 tool_name: "Write".to_string(),
517 tool_input: serde_json::json!({"file_path": "/etc/passwd", "content": "bad"}),
518 tool_use_id: "toolu_456".to_string(),
519 };
520
521 let json = serde_json::to_string(&denial).unwrap();
522 assert!(json.contains("\"tool_name\":\"Write\""));
523 assert!(json.contains("\"tool_use_id\":\"toolu_456\""));
524 assert!(json.contains("/etc/passwd"));
525
526 let parsed: PermissionDenial = serde_json::from_str(&json).unwrap();
527 assert_eq!(parsed, denial);
528 }
529
530 #[test]
531 fn test_deserialize_result_message_with_errors() {
532 let json = r#"{
533 "type": "result",
534 "subtype": "error_during_execution",
535 "duration_ms": 0,
536 "duration_api_ms": 0,
537 "is_error": true,
538 "num_turns": 0,
539 "session_id": "27934753-425a-4182-892c-6b1c15050c3f",
540 "total_cost_usd": 0,
541 "errors": ["No conversation found with session ID: d56965c9-c855-4042-a8f5-f12bbb14d6f6"],
542 "permission_denials": []
543 }"#;
544
545 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
546 assert!(output.is_error());
547
548 if let ClaudeOutput::Result(res) = output {
549 assert!(res.is_error);
550 assert_eq!(res.errors.len(), 1);
551 assert!(res.errors[0].contains("No conversation found"));
552 } else {
553 panic!("Expected Result message");
554 }
555 }
556
557 #[test]
558 fn test_deserialize_result_message_errors_defaults_empty() {
559 let json = r#"{
560 "type": "result",
561 "subtype": "success",
562 "is_error": false,
563 "duration_ms": 100,
564 "duration_api_ms": 200,
565 "num_turns": 1,
566 "session_id": "123",
567 "total_cost_usd": 0.01
568 }"#;
569
570 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
571 if let ClaudeOutput::Result(res) = output {
572 assert!(res.errors.is_empty());
573 } else {
574 panic!("Expected Result message");
575 }
576 }
577
578 #[test]
579 fn test_result_message_errors_roundtrip() {
580 let json = r#"{
581 "type": "result",
582 "subtype": "error_during_execution",
583 "is_error": true,
584 "duration_ms": 0,
585 "duration_api_ms": 0,
586 "num_turns": 0,
587 "session_id": "test-session",
588 "total_cost_usd": 0.0,
589 "errors": ["Error 1", "Error 2"]
590 }"#;
591
592 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
593 let reserialized = serde_json::to_string(&output).unwrap();
594
595 assert!(reserialized.contains("Error 1"));
596 assert!(reserialized.contains("Error 2"));
597 }
598
599 #[test]
600 fn test_result_with_new_fields() {
601 let json = r#"{
602 "type": "result",
603 "subtype": "success",
604 "is_error": false,
605 "duration_ms": 5000,
606 "duration_api_ms": 4500,
607 "num_turns": 1,
608 "result": "Done",
609 "session_id": "abc",
610 "total_cost_usd": 0.06,
611 "api_error_status": null,
612 "stop_reason": "end_turn",
613 "terminal_reason": "completed",
614 "fast_mode_state": "off",
615 "modelUsage": {
616 "claude-opus-4-7[1m]": {
617 "inputTokens": 3817,
618 "outputTokens": 14,
619 "costUSD": 0.06
620 }
621 },
622 "usage": {
623 "input_tokens": 3817,
624 "output_tokens": 14,
625 "cache_creation_input_tokens": 3540,
626 "cache_read_input_tokens": 0,
627 "server_tool_use": {
628 "web_search_requests": 0,
629 "web_fetch_requests": 2
630 },
631 "service_tier": "standard",
632 "inference_geo": "not_available",
633 "speed": "standard",
634 "iterations": [
635 {"input_tokens": 3817, "output_tokens": 14, "type": "turn"}
636 ]
637 }
638 }"#;
639
640 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
641 if let ClaudeOutput::Result(res) = output {
642 assert_eq!(res.stop_reason.as_deref(), Some("end_turn"));
643 assert_eq!(res.terminal_reason.as_deref(), Some("completed"));
644 assert_eq!(res.fast_mode_state.as_deref(), Some("off"));
645 let model_usage = res.model_usage.as_ref().unwrap();
646 let entry = model_usage
647 .get("claude-opus-4-7[1m]")
648 .expect("per-model entry present");
649 assert_eq!(entry.input_tokens, 3817);
650 assert_eq!(entry.output_tokens, 14);
651 assert_eq!(entry.cost_usd, 0.06);
652 assert!(res.api_error_status.is_none());
653
654 let usage = res.usage.unwrap();
655 assert_eq!(usage.server_tool_use.web_fetch_requests, 2);
656 assert_eq!(usage.inference_geo.as_deref(), Some("not_available"));
657 assert_eq!(usage.speed.as_deref(), Some("standard"));
658 assert_eq!(usage.iterations.len(), 1);
659 assert_eq!(usage.iterations[0].input_tokens, 3817);
660 assert_eq!(usage.iterations[0].output_tokens, 14);
661 assert_eq!(usage.iterations[0].kind.as_deref(), Some("turn"));
662 } else {
663 panic!("Expected Result");
664 }
665 }
666
667 #[test]
668 fn test_result_backwards_compatible_without_new_fields() {
669 let json = r#"{
671 "type": "result",
672 "subtype": "success",
673 "is_error": false,
674 "duration_ms": 100,
675 "duration_api_ms": 200,
676 "num_turns": 1,
677 "session_id": "abc",
678 "total_cost_usd": 0.01
679 }"#;
680
681 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
682 if let ClaudeOutput::Result(res) = output {
683 assert!(res.api_error_status.is_none());
684 assert!(res.stop_reason.is_none());
685 assert!(res.terminal_reason.is_none());
686 assert!(res.fast_mode_state.is_none());
687 assert!(res.model_usage.is_none());
688 } else {
689 panic!("Expected Result");
690 }
691 }
692
693 #[test]
694 fn test_result_fast_mode_disabled_reason() {
695 let json = r#"{
696 "type":"result","subtype":"success","is_error":false,
697 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
698 "session_id":"s1","total_cost_usd":0.01,
699 "fast_mode_state":"off",
700 "fast_mode_disabled_reason":"sdk_opt_in_required"
701 }"#;
702 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
703 let crate::ClaudeOutput::Result(res) = &output else {
704 panic!("expected Result");
705 };
706 assert_eq!(
707 res.fast_mode_disabled_reason,
708 Some(FastModeDisabledReason::SdkOptInRequired)
709 );
710 assert!(serde_json::to_string(&output)
711 .unwrap()
712 .contains("\"fast_mode_disabled_reason\":\"sdk_opt_in_required\""));
713
714 assert_eq!(
717 FastModeDisabledReason::from("unknown"),
718 FastModeDisabledReason::UnknownReason
719 );
720 let novel = FastModeDisabledReason::from("solar_flare");
721 assert_eq!(novel, FastModeDisabledReason::Unknown("solar_flare".into()));
722 assert_eq!(novel.as_str(), "solar_flare");
723 }
724
725 #[test]
726 fn test_result_timing_and_user_message_uuid_fields() {
727 let json = r#"{
728 "type":"result","subtype":"success","is_error":false,
729 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
730 "session_id":"s1","total_cost_usd":0.01,
731 "request_sent_wall_ms":1753212345678.25,
732 "user_message_uuid":"um-1"
733 }"#;
734 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
735 let crate::ClaudeOutput::Result(res) = &output else {
736 panic!("expected Result");
737 };
738 assert_eq!(res.request_sent_wall_ms, Some(1753212345678.25));
739 assert_eq!(res.user_message_uuid.as_deref(), Some("um-1"));
740
741 let reserialized = serde_json::to_string(&output).unwrap();
742 assert!(reserialized.contains("\"user_message_uuid\":\"um-1\""));
743 }
744}