Skip to main content

claude_api/messages/
response.rs

1//! Response types returned by the Messages API.
2//!
3//! | Type | Source |
4//! |---|---|
5//! | [`Message`] | `POST /v1/messages` (non-streaming) or [`EventStream::aggregate`](crate::messages::stream::EventStream::aggregate) |
6//! | [`CountTokensResponse`] | `POST /v1/messages/count_tokens` |
7//! | [`ContainerInfo`] | Nested in `Message::container` when code-execution containers are active |
8//!
9//! `Message.content` is `Vec<ContentBlock>` -- iterate it with a match on
10//! [`crate::messages::ContentBlock::Known`] to extract text, tool calls, etc.
11
12use serde::{Deserialize, Serialize};
13
14use crate::forward_compat::dispatch_known_or_other;
15use crate::messages::content::ContentBlock;
16use crate::types::{ModelId, Role, StopReason, Usage};
17
18/// A complete (non-streaming) Messages-API response.
19///
20/// Usually produced by the SDK from a wire payload rather than built by
21/// hand. Tests that need a fixture should round-trip a JSON literal through
22/// [`serde_json::from_value`].
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24#[non_exhaustive]
25pub struct Message {
26    /// Unique message identifier (e.g. `msg_01ABC...`).
27    pub id: String,
28    /// Wire `type` discriminant. Always `"message"` for non-streaming responses;
29    /// retained on the struct for full round-trip fidelity.
30    #[serde(rename = "type", default = "default_message_kind")]
31    pub kind: String,
32    /// Author of the message. Always [`Role::Assistant`] for responses.
33    #[serde(default = "default_assistant_role")]
34    pub role: Role,
35    /// Ordered list of content blocks the model produced.
36    #[serde(default)]
37    pub content: Vec<ContentBlock>,
38    /// The model that produced this response.
39    pub model: ModelId,
40    /// Why the model stopped, when known.
41    #[serde(default, skip_serializing_if = "Option::is_none")]
42    pub stop_reason: Option<StopReason>,
43    /// The stop sequence that triggered termination, if applicable.
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    pub stop_sequence: Option<String>,
46    /// Structured information about *why* the model stopped (e.g. for
47    /// `refusal` stops, the policy category and an explanation). `None`
48    /// when no extra detail is reported.
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    pub stop_details: Option<StopDetails>,
51    /// Token usage and related counters.
52    #[serde(default)]
53    pub usage: Usage,
54    /// Context-management edits applied to the request (e.g. trimmed
55    /// thinking blocks or tool-use history). Present only when a
56    /// context-management strategy was active and triggered.
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub context_management: Option<ResponseContextManagement>,
59    /// Container metadata, present when the request used the code-execution
60    /// container tool.
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub container: Option<ContainerInfo>,
63}
64
65// ---------------------------------------------------------------------------
66// stop_details
67// ---------------------------------------------------------------------------
68
69/// Structured information about why the model stopped.
70///
71/// Forward-compatible: unknown `type` tags deserialize into
72/// [`StopDetails::Other`] with the raw JSON preserved byte-for-byte.
73/// Currently only one variant (`Refusal`) is known; new variants may appear
74/// in future API versions.
75#[derive(Debug, Clone, PartialEq)]
76pub enum StopDetails {
77    /// A stop-details payload whose `type` is recognized.
78    Known(KnownStopDetails),
79    /// A stop-details payload whose `type` is not recognized; raw JSON
80    /// preserved.
81    Other(serde_json::Value),
82}
83
84/// All stop-details variants known to this SDK version.
85#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
86#[serde(tag = "type", rename_all = "snake_case")]
87#[non_exhaustive]
88pub enum KnownStopDetails {
89    /// The model stopped because it refused the request on policy grounds.
90    Refusal(RefusalStopDetails),
91}
92
93/// Policy-refusal stop details.
94#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
95#[non_exhaustive]
96pub struct RefusalStopDetails {
97    /// The policy category that triggered the refusal (`"cyber"`, `"bio"`,
98    /// or `None` when the refusal doesn't map to a named category).
99    #[serde(default, skip_serializing_if = "Option::is_none")]
100    pub category: Option<String>,
101    /// Human-readable explanation of the refusal. Not guaranteed to be
102    /// stable; `None` when no explanation is available.
103    #[serde(default, skip_serializing_if = "Option::is_none")]
104    pub explanation: Option<String>,
105}
106
107const KNOWN_STOP_DETAILS_TAGS: &[&str] = &["refusal"];
108
109impl Serialize for StopDetails {
110    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
111        match self {
112            StopDetails::Known(k) => k.serialize(s),
113            StopDetails::Other(v) => v.serialize(s),
114        }
115    }
116}
117
118impl<'de> Deserialize<'de> for StopDetails {
119    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
120        let raw = serde_json::Value::deserialize(d)?;
121        dispatch_known_or_other(
122            raw,
123            KNOWN_STOP_DETAILS_TAGS,
124            StopDetails::Known,
125            StopDetails::Other,
126        )
127        .map_err(serde::de::Error::custom)
128    }
129}
130
131impl From<KnownStopDetails> for StopDetails {
132    fn from(k: KnownStopDetails) -> Self {
133        StopDetails::Known(k)
134    }
135}
136
137// ---------------------------------------------------------------------------
138// context_management
139// ---------------------------------------------------------------------------
140
141/// Context-management edits applied during the request.
142///
143/// Present when a context-management strategy (e.g. `compact_20260112`,
144/// `clear_thinking_20251015`) was active and triggered. Each edit in
145/// `applied_edits` is forward-compatible: unknown `type` tags fall through
146/// to [`ContextEdit::Other`].
147#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
148#[non_exhaustive]
149pub struct ResponseContextManagement {
150    /// The edits applied, in order.
151    #[serde(default)]
152    pub applied_edits: Vec<ContextEdit>,
153}
154
155/// One context-management edit.
156///
157/// Forward-compatible: unknown `type` tags deserialize into
158/// [`ContextEdit::Other`] with the raw JSON preserved byte-for-byte.
159#[derive(Debug, Clone, PartialEq)]
160pub enum ContextEdit {
161    /// An edit whose `type` is recognized.
162    Known(KnownContextEdit),
163    /// An edit whose `type` is not recognized; raw JSON preserved.
164    Other(serde_json::Value),
165}
166
167/// All context-edit variants known to this SDK version.
168#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
169#[serde(tag = "type", rename_all = "snake_case")]
170#[non_exhaustive]
171pub enum KnownContextEdit {
172    /// Cleared one or more thinking blocks from the history.
173    #[serde(rename = "clear_thinking_20251015")]
174    ClearThinking(ClearThinkingEdit),
175    /// Cleared one or more tool-use / tool-result pairs from the history.
176    #[serde(rename = "clear_tool_uses_20250919")]
177    ClearToolUses(ClearToolUsesEdit),
178}
179
180/// Details for a `clear_thinking_20251015` context-management edit.
181#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
182#[non_exhaustive]
183pub struct ClearThinkingEdit {
184    /// Number of input tokens cleared by this edit.
185    pub cleared_input_tokens: u64,
186    /// Number of thinking turns that were cleared.
187    pub cleared_thinking_turns: u64,
188}
189
190/// Details for a `clear_tool_uses_20250919` context-management edit.
191#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
192#[non_exhaustive]
193pub struct ClearToolUsesEdit {
194    /// Number of input tokens cleared by this edit.
195    pub cleared_input_tokens: u64,
196    /// Number of tool-use/tool-result pairs that were cleared.
197    pub cleared_tool_uses: u64,
198}
199
200const KNOWN_CONTEXT_EDIT_TAGS: &[&str] = &["clear_thinking_20251015", "clear_tool_uses_20250919"];
201
202impl Serialize for ContextEdit {
203    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
204        match self {
205            ContextEdit::Known(k) => k.serialize(s),
206            ContextEdit::Other(v) => v.serialize(s),
207        }
208    }
209}
210
211impl<'de> Deserialize<'de> for ContextEdit {
212    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
213        let raw = serde_json::Value::deserialize(d)?;
214        dispatch_known_or_other(
215            raw,
216            KNOWN_CONTEXT_EDIT_TAGS,
217            ContextEdit::Known,
218            ContextEdit::Other,
219        )
220        .map_err(serde::de::Error::custom)
221    }
222}
223
224impl From<KnownContextEdit> for ContextEdit {
225    fn from(k: KnownContextEdit) -> Self {
226        ContextEdit::Known(k)
227    }
228}
229
230// ---------------------------------------------------------------------------
231
232fn default_message_kind() -> String {
233    "message".to_owned()
234}
235
236fn default_assistant_role() -> Role {
237    Role::Assistant
238}
239
240/// Container metadata returned when a request used the code-execution
241/// container tool.
242#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
243#[non_exhaustive]
244pub struct ContainerInfo {
245    /// Container identifier.
246    pub id: String,
247    /// Container expiration timestamp (ISO-8601), if reported.
248    #[serde(default, skip_serializing_if = "Option::is_none")]
249    pub expires_at: Option<String>,
250}
251
252/// Response payload for `POST /v1/messages/count_tokens`.
253#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
254#[non_exhaustive]
255pub struct CountTokensResponse {
256    /// Number of input tokens the request would consume.
257    pub input_tokens: u32,
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263    use crate::messages::content::KnownBlock;
264    use pretty_assertions::assert_eq;
265    use serde_json::json;
266
267    #[test]
268    fn realistic_message_response_round_trips() {
269        let raw = json!({
270            "id": "msg_01ABCDEF",
271            "type": "message",
272            "role": "assistant",
273            "content": [
274                {"type": "text", "text": "Hello!"}
275            ],
276            "model": "claude-sonnet-4-6",
277            "stop_reason": "end_turn",
278            "stop_sequence": null,
279            "usage": {
280                "input_tokens": 10,
281                "output_tokens": 5
282            }
283        });
284
285        let msg: Message = serde_json::from_value(raw).expect("deserialize");
286        assert_eq!(msg.id, "msg_01ABCDEF");
287        assert_eq!(msg.kind, "message");
288        assert_eq!(msg.role, Role::Assistant);
289        assert_eq!(msg.model, ModelId::SONNET_4_6);
290        assert_eq!(msg.stop_reason, Some(StopReason::EndTurn));
291        assert_eq!(msg.usage.input_tokens, 10);
292        assert_eq!(msg.usage.output_tokens, 5);
293        assert_eq!(msg.content.len(), 1);
294        assert_eq!(msg.content[0].type_tag(), Some("text"));
295
296        let reserialized = serde_json::to_value(&msg).expect("serialize");
297        let parsed_again: Message = serde_json::from_value(reserialized).expect("re-deserialize");
298        assert_eq!(parsed_again, msg, "round-trip mismatch");
299    }
300
301    #[test]
302    fn message_with_unknown_content_block_round_trips() {
303        let raw = json!({
304            "id": "msg_X",
305            "type": "message",
306            "role": "assistant",
307            "content": [
308                {"type": "text", "text": "hi"},
309                {"type": "future_block", "payload": 42}
310            ],
311            "model": "claude-opus-4-7",
312            "usage": {"input_tokens": 1, "output_tokens": 1}
313        });
314
315        let msg: Message = serde_json::from_value(raw.clone()).expect("deserialize");
316        assert_eq!(msg.content.len(), 2);
317        assert_eq!(msg.content[0].type_tag(), Some("text"));
318        assert_eq!(msg.content[1].type_tag(), Some("future_block"));
319        assert!(msg.content[1].other().is_some());
320
321        // Reserializing must put the unknown block back byte-for-byte.
322        let reserialized = serde_json::to_value(&msg).expect("serialize");
323        let blocks = reserialized.get("content").unwrap().as_array().unwrap();
324        assert_eq!(blocks[1], json!({"type": "future_block", "payload": 42}));
325    }
326
327    #[test]
328    fn message_kind_defaults_when_missing() {
329        // A wire payload missing the `type` field still parses, with kind defaulting to "message".
330        let raw = json!({
331            "id": "msg_1",
332            "role": "assistant",
333            "content": [],
334            "model": "claude-sonnet-4-6",
335            "usage": {"input_tokens": 0, "output_tokens": 0}
336        });
337        let msg: Message = serde_json::from_value(raw).expect("deserialize");
338        assert_eq!(msg.kind, "message");
339    }
340
341    #[test]
342    fn message_with_tool_use_block_round_trips() {
343        let msg = Message {
344            id: "msg_tool".into(),
345            kind: "message".into(),
346            role: Role::Assistant,
347            content: vec![ContentBlock::Known(KnownBlock::ToolUse {
348                id: "toolu_1".into(),
349                name: "lookup".into(),
350                input: json!({"q": "rust"}),
351            })],
352            model: ModelId::HAIKU_4_5,
353            stop_reason: Some(StopReason::ToolUse),
354            stop_sequence: None,
355            stop_details: None,
356            usage: Usage {
357                input_tokens: 7,
358                output_tokens: 3,
359                ..Usage::default()
360            },
361            context_management: None,
362            container: None,
363        };
364
365        let v = serde_json::to_value(&msg).expect("serialize");
366        let parsed: Message = serde_json::from_value(v).expect("deserialize");
367        assert_eq!(parsed, msg);
368    }
369
370    #[test]
371    fn stop_details_refusal_round_trips() {
372        let raw = json!({
373            "type": "refusal",
374            "category": "cyber",
375            "explanation": "Request involves offensive cyber techniques."
376        });
377        let sd: StopDetails = serde_json::from_value(raw.clone()).unwrap();
378        match &sd {
379            StopDetails::Known(KnownStopDetails::Refusal(r)) => {
380                assert_eq!(r.category.as_deref(), Some("cyber"));
381                assert!(r.explanation.is_some());
382            }
383            other => panic!("expected Refusal, got {other:?}"),
384        }
385        assert_eq!(serde_json::to_value(&sd).unwrap(), raw);
386    }
387
388    #[test]
389    fn stop_details_null_category_round_trips() {
390        let raw = json!({"type": "refusal", "category": null, "explanation": null});
391        let sd: StopDetails = serde_json::from_value(raw).unwrap();
392        if let StopDetails::Known(KnownStopDetails::Refusal(r)) = &sd {
393            assert!(r.category.is_none());
394        } else {
395            panic!("expected Refusal");
396        }
397    }
398
399    #[test]
400    fn stop_details_unknown_type_falls_through_to_other() {
401        let raw = json!({"type": "future_stop_reason", "detail": 42});
402        let sd: StopDetails = serde_json::from_value(raw.clone()).unwrap();
403        assert!(matches!(sd, StopDetails::Other(_)));
404        assert_eq!(serde_json::to_value(&sd).unwrap(), raw);
405    }
406
407    #[test]
408    fn context_edit_clear_thinking_round_trips() {
409        let raw = json!({
410            "type": "clear_thinking_20251015",
411            "cleared_input_tokens": 1500,
412            "cleared_thinking_turns": 3
413        });
414        let edit: ContextEdit = serde_json::from_value(raw.clone()).unwrap();
415        match &edit {
416            ContextEdit::Known(KnownContextEdit::ClearThinking(e)) => {
417                assert_eq!(e.cleared_input_tokens, 1500);
418                assert_eq!(e.cleared_thinking_turns, 3);
419            }
420            other => panic!("expected ClearThinking, got {other:?}"),
421        }
422        assert_eq!(serde_json::to_value(&edit).unwrap(), raw);
423    }
424
425    #[test]
426    fn context_edit_clear_tool_uses_round_trips() {
427        let raw = json!({
428            "type": "clear_tool_uses_20250919",
429            "cleared_input_tokens": 800,
430            "cleared_tool_uses": 2
431        });
432        let edit: ContextEdit = serde_json::from_value(raw.clone()).unwrap();
433        if let ContextEdit::Known(KnownContextEdit::ClearToolUses(e)) = &edit {
434            assert_eq!(e.cleared_tool_uses, 2);
435        } else {
436            panic!("expected ClearToolUses");
437        }
438        assert_eq!(serde_json::to_value(&edit).unwrap(), raw);
439    }
440
441    #[test]
442    fn context_edit_unknown_type_falls_through_to_other() {
443        let raw = json!({"type": "compact_20260112", "summary": "..."});
444        let edit: ContextEdit = serde_json::from_value(raw.clone()).unwrap();
445        assert!(matches!(edit, ContextEdit::Other(_)));
446        assert_eq!(serde_json::to_value(&edit).unwrap(), raw);
447    }
448
449    #[test]
450    fn response_context_management_round_trips() {
451        let raw = json!({
452            "applied_edits": [
453                {"type": "clear_thinking_20251015", "cleared_input_tokens": 500, "cleared_thinking_turns": 1},
454                {"type": "clear_tool_uses_20250919", "cleared_input_tokens": 200, "cleared_tool_uses": 1}
455            ]
456        });
457        let cm: ResponseContextManagement = serde_json::from_value(raw.clone()).unwrap();
458        assert_eq!(cm.applied_edits.len(), 2);
459        assert!(matches!(
460            &cm.applied_edits[0],
461            ContextEdit::Known(KnownContextEdit::ClearThinking(_))
462        ));
463        assert_eq!(serde_json::to_value(&cm).unwrap(), raw);
464    }
465
466    #[test]
467    fn message_with_stop_details_and_context_management_round_trips() {
468        let raw = json!({
469            "id": "msg_refusal",
470            "type": "message",
471            "role": "assistant",
472            "content": [],
473            "model": "claude-sonnet-4-6",
474            "stop_reason": "refusal",
475            "usage": {"input_tokens": 5, "output_tokens": 0},
476            "stop_details": {"type": "refusal", "category": "bio", "explanation": "Biosecurity policy."},
477            "context_management": {
478                "applied_edits": [
479                    {"type": "clear_thinking_20251015", "cleared_input_tokens": 300, "cleared_thinking_turns": 2}
480                ]
481            }
482        });
483        let msg: Message = serde_json::from_value(raw).unwrap();
484        assert!(msg.stop_details.is_some());
485        assert!(msg.context_management.is_some());
486        let cm = msg.context_management.as_ref().unwrap();
487        assert_eq!(cm.applied_edits.len(), 1);
488    }
489
490    #[test]
491    fn count_tokens_response_round_trips() {
492        let r = CountTokensResponse { input_tokens: 42 };
493        let v = serde_json::to_value(&r).expect("serialize");
494        assert_eq!(v, json!({"input_tokens": 42}));
495        let parsed: CountTokensResponse = serde_json::from_value(v).expect("deserialize");
496        assert_eq!(parsed, r);
497    }
498
499    #[test]
500    fn container_info_round_trips() {
501        let c = ContainerInfo {
502            id: "cnt_01".into(),
503            expires_at: Some("2026-01-01T00:00:00Z".into()),
504        };
505        let v = serde_json::to_value(&c).expect("serialize");
506        assert_eq!(
507            v,
508            json!({"id": "cnt_01", "expires_at": "2026-01-01T00:00:00Z"})
509        );
510        let parsed: ContainerInfo = serde_json::from_value(v).expect("deserialize");
511        assert_eq!(parsed, c);
512    }
513
514    #[test]
515    fn message_with_container_round_trips() {
516        let raw = json!({
517            "id": "msg_with_container",
518            "type": "message",
519            "role": "assistant",
520            "content": [],
521            "model": "claude-opus-4-7",
522            "usage": {"input_tokens": 0, "output_tokens": 0},
523            "container": {"id": "cnt_42"}
524        });
525        let msg: Message = serde_json::from_value(raw).expect("deserialize");
526        assert_eq!(msg.container.as_ref().unwrap().id, "cnt_42");
527    }
528}