Skip to main content

turul_mcp_protocol_2026_07_28/
sampling.rs

1//! MCP Sampling Protocol Types
2//!
3//! # Deprecation status (2026-07-28)
4//!
5//! Per SEP-2577, the entire Sampling client capability (`sampling/createMessage`
6//! RPC, `SamplingMessage` shape, `SamplingCapabilities`) is **deprecated** in
7//! this revision. New implementations SHOULD NOT adopt it. Earliest removal:
8//! first revision released on or after **2027-07-28**.
9//!
10//! Replacement: integrate directly with LLM provider APIs.
11//!
12//! Soft-deprecated since 2025-11-25 and now reclassified per SEP-2596:
13//! `CreateMessageRequestParams.include_context` values `"thisServer"` and
14//! `"allServers"`. Omit the field or use `"none"`.
15//!
16//! [`ModelPreferences`], [`ToolChoice`], and [`ModelHint`] carry their own
17//! schema `@deprecated` markers and are `#[deprecated]` here accordingly;
18//! they remain referenced by the `sampling/createMessage` shape and the
19//! SEP-2322 MRTR `InputRequest::CreateMessage` variant through the migration
20//! window. [`ToolChoiceMode`] has no schema symbol of its own (it binds the
21//! inline `mode` union of the deprecated [`ToolChoice`]) and is left
22//! unmarked. The only genuinely non-deprecated export is [`Role`], which is
23//! used outside sampling (e.g. by `Annotations.audience` in `meta`).
24
25use crate::content::ContentBlock;
26use serde::{Deserialize, Serialize};
27use serde_json::Value;
28
29// The schema declares a single `Role` type ("user" | "assistant" — no
30// "system"); this module re-exports the one binding rather than duplicating it.
31pub use crate::prompts::Role;
32
33#[deprecated(
34    since = "0.4.0",
35    note = "Deprecated per SEP-2577 (2026-07-28). \
36            Replacement: integrate directly with LLM provider APIs. \
37            Earliest removal: first release on/after 2027-07-28."
38)]
39/// Model hint — an open-ended struct.
40///
41/// The `name` field can be any model identifier string. Clients use hints to
42/// express model preferences without restricting to a hardcoded set.
43#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
44#[serde(rename_all = "camelCase")]
45pub struct ModelHint {
46    /// Optional model name hint (e.g., "claude-3-5-sonnet-20241022", "gpt-4o")
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub name: Option<String>,
49}
50
51#[allow(deprecated)]
52impl ModelHint {
53    pub fn new(name: impl Into<String>) -> Self {
54        Self {
55            name: Some(name.into()),
56        }
57    }
58}
59
60#[deprecated(
61    since = "0.4.0",
62    note = "Deprecated per SEP-2577 (2026-07-28). \
63            Replacement: integrate directly with LLM provider APIs. \
64            Earliest removal: first release on/after 2027-07-28."
65)]
66/// Model preferences (per MCP spec)
67#[derive(Debug, Clone, Serialize, Deserialize)]
68#[serde(rename_all = "camelCase")]
69pub struct ModelPreferences {
70    /// Optional hints about which models to use
71    #[serde(skip_serializing_if = "Option::is_none")]
72    #[allow(deprecated)]
73    pub hints: Option<Vec<ModelHint>>,
74    /// Optional cost priority (0.0-1.0)
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub cost_priority: Option<f64>,
77    /// Optional speed priority (0.0-1.0)
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub speed_priority: Option<f64>,
80    /// Optional intelligence priority (0.0-1.0)
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub intelligence_priority: Option<f64>,
83}
84
85/// Tool choice mode for sampling requests.
86#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
87#[serde(rename_all = "lowercase")]
88pub enum ToolChoiceMode {
89    /// Model decides whether to use tools
90    Auto,
91    /// Model must not use any tools
92    None,
93    /// Model must use at least one tool. Wire value: `"required"`; legacy
94    /// `"any"` is accepted on deserialize for backward compatibility.
95    #[serde(alias = "any")]
96    Required,
97}
98
99#[deprecated(
100    since = "0.4.0",
101    note = "Deprecated per SEP-2577 (2026-07-28). \
102            Replacement: integrate directly with LLM provider APIs. \
103            Earliest removal: first release on/after 2027-07-28."
104)]
105/// Tool choice configuration for sampling requests.
106///
107/// Wire shape: `{ mode?: "auto" | "required" | "none" }` — `mode` is optional
108/// and absent means `"auto"`.
109#[derive(Debug, Clone, Serialize, Deserialize, Default)]
110#[serde(rename_all = "camelCase")]
111pub struct ToolChoice {
112    /// The mode for tool selection. Absent = `"auto"`.
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub mode: Option<ToolChoiceMode>,
115}
116
117#[allow(deprecated)]
118impl ToolChoice {
119    pub fn auto() -> Self {
120        Self {
121            mode: Some(ToolChoiceMode::Auto),
122        }
123    }
124
125    pub fn none() -> Self {
126        Self {
127            mode: Some(ToolChoiceMode::None),
128        }
129    }
130
131    /// Create tool choice requiring at least one tool. Wire value: `"required"`.
132    pub fn required() -> Self {
133        Self {
134            mode: Some(ToolChoiceMode::Required),
135        }
136    }
137
138    /// Alias for [`Self::required`] — accepts the older `"any"` name on the
139    /// caller side; emits `"required"` on the wire.
140    pub fn any() -> Self {
141        Self::required()
142    }
143
144    /// The effective mode: absent means `"auto"`.
145    pub fn effective_mode(&self) -> ToolChoiceMode {
146        self.mode.clone().unwrap_or(ToolChoiceMode::Auto)
147    }
148}
149
150/// Content block variant allowed inside a [`SamplingMessage`].
151///
152/// Strict 5-element union — excludes the `ResourceLink` and `EmbeddedResource`
153/// variants that the general [`ContentBlock`] allows. Discriminated on the
154/// `type` field exactly like [`ContentBlock`] for wire-format symmetry across
155/// the 5 shared shapes.
156///
157/// **Deprecated** per SEP-2577 — see module-level docs.
158#[deprecated(
159    since = "0.4.0",
160    note = "Deprecated per SEP-2577 (2026-07-28). \
161            Replacement: integrate directly with LLM provider APIs. \
162            Earliest removal: first release on/after 2027-07-28."
163)]
164#[derive(Debug, Clone, Serialize, Deserialize)]
165#[serde(tag = "type")]
166pub enum SamplingMessageContentBlock {
167    #[serde(rename = "text")]
168    Text {
169        text: String,
170        #[serde(skip_serializing_if = "Option::is_none")]
171        annotations: Option<crate::meta::Annotations>,
172        #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
173        meta: Option<std::collections::HashMap<String, Value>>,
174    },
175    #[serde(rename = "image")]
176    Image {
177        data: String,
178        #[serde(rename = "mimeType")]
179        mime_type: String,
180        #[serde(skip_serializing_if = "Option::is_none")]
181        annotations: Option<crate::meta::Annotations>,
182        #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
183        meta: Option<std::collections::HashMap<String, Value>>,
184    },
185    #[serde(rename = "audio")]
186    Audio {
187        data: String,
188        #[serde(rename = "mimeType")]
189        mime_type: String,
190        #[serde(skip_serializing_if = "Option::is_none")]
191        annotations: Option<crate::meta::Annotations>,
192        #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
193        meta: Option<std::collections::HashMap<String, Value>>,
194    },
195    #[serde(rename = "tool_use")]
196    ToolUse {
197        id: String,
198        name: String,
199        input: std::collections::HashMap<String, Value>,
200        #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
201        meta: Option<std::collections::HashMap<String, Value>>,
202    },
203    #[serde(rename = "tool_result")]
204    ToolResult {
205        #[serde(rename = "toolUseId")]
206        tool_use_id: String,
207        content: Vec<ContentBlock>,
208        #[serde(rename = "structuredContent", skip_serializing_if = "Option::is_none")]
209        structured_content: Option<Value>,
210        #[serde(rename = "isError", skip_serializing_if = "Option::is_none")]
211        is_error: Option<bool>,
212        #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
213        meta: Option<std::collections::HashMap<String, Value>>,
214    },
215}
216
217#[allow(deprecated)]
218impl SamplingMessageContentBlock {
219    /// Construct a text block.
220    pub fn text(text: impl Into<String>) -> Self {
221        Self::Text {
222            text: text.into(),
223            annotations: None,
224            meta: None,
225        }
226    }
227}
228
229/// Content payload of a [`SamplingMessage`] — single block OR array of blocks.
230///
231/// `content: SamplingMessageContentBlock | SamplingMessageContentBlock[]`.
232/// Untagged — the wire decides which shape is sent.
233///
234/// **Deprecated** per SEP-2577 — see module-level docs.
235#[deprecated(
236    since = "0.4.0",
237    note = "Deprecated per SEP-2577 (2026-07-28). \
238            Replacement: integrate directly with LLM provider APIs. \
239            Earliest removal: first release on/after 2027-07-28."
240)]
241#[allow(deprecated)]
242#[derive(Debug, Clone, Serialize, Deserialize)]
243#[serde(untagged)]
244pub enum SamplingMessageContent {
245    /// Single content block on the wire.
246    Single(SamplingMessageContentBlock),
247    /// Array of content blocks on the wire.
248    Multiple(Vec<SamplingMessageContentBlock>),
249}
250
251/// Sampling message.
252///
253/// **Deprecated** per SEP-2577 — see module-level docs.
254#[deprecated(
255    since = "0.4.0",
256    note = "Deprecated per SEP-2577 (2026-07-28). \
257            Replacement: integrate directly with LLM provider APIs. \
258            Earliest removal: first release on/after 2027-07-28."
259)]
260#[allow(deprecated)]
261#[derive(Debug, Clone, Serialize, Deserialize)]
262#[serde(rename_all = "camelCase")]
263pub struct SamplingMessage {
264    pub role: Role,
265    pub content: SamplingMessageContent,
266    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
267    pub meta: Option<std::collections::HashMap<String, Value>>,
268}
269
270/// Parameters for sampling/createMessage request (per MCP spec).
271///
272/// **Deprecated** per SEP-2577 — see module-level docs.
273///
274/// `include_context` value note: `"thisServer"` and `"allServers"` are
275/// soft-deprecated per SEP-2596 and conditional on
276/// `ClientCapabilities.sampling.context`. Omit the field or use `"none"`.
277#[deprecated(
278    since = "0.4.0",
279    note = "Deprecated per SEP-2577 (2026-07-28). \
280            Replacement: integrate directly with LLM provider APIs. \
281            Earliest removal: first release on/after 2027-07-28."
282)]
283#[allow(deprecated)]
284#[derive(Debug, Clone, Serialize, Deserialize)]
285#[serde(rename_all = "camelCase")]
286pub struct CreateMessageRequestParams {
287    /// Messages for context
288    pub messages: Vec<SamplingMessage>,
289    /// Optional model preferences
290    #[serde(skip_serializing_if = "Option::is_none")]
291    pub model_preferences: Option<ModelPreferences>,
292    /// Optional system prompt
293    #[serde(skip_serializing_if = "Option::is_none")]
294    pub system_prompt: Option<String>,
295    /// Optional include context
296    #[serde(skip_serializing_if = "Option::is_none")]
297    pub include_context: Option<String>,
298    /// Optional temperature
299    #[serde(skip_serializing_if = "Option::is_none")]
300    pub temperature: Option<f64>,
301    /// Maximum tokens (required field)
302    pub max_tokens: u32,
303    /// Optional stop sequences
304    #[serde(skip_serializing_if = "Option::is_none")]
305    pub stop_sequences: Option<Vec<String>>,
306    /// Optional metadata to pass through to the LLM provider.
307    ///
308    /// Schema: `metadata?: JSONObject` — same `HashMap<String, Value>`
309    /// convention this crate uses for other `JSONObject` fields (e.g.
310    /// `ClientCapabilities.experimental`).
311    #[serde(skip_serializing_if = "Option::is_none")]
312    pub metadata: Option<std::collections::HashMap<String, Value>>,
313    /// Optional tools the LLM can use during sampling.
314    #[serde(skip_serializing_if = "Option::is_none")]
315    pub tools: Option<Vec<crate::tools::Tool>>,
316    /// Optional tool choice configuration.
317    #[serde(skip_serializing_if = "Option::is_none")]
318    pub tool_choice: Option<ToolChoice>,
319    // Per schema, `CreateMessageRequestParams` does NOT extend `RequestParams`
320    // — no `_meta` field. The earlier Rust-side `meta: Option<HashMap>` was a
321    // non-spec carryover, removed for Protocol Crate Purity.
322}
323
324/// Complete sampling/createMessage request (matches TypeScript CreateMessageRequest interface).
325///
326/// **Deprecated** per SEP-2577 — see module-level docs.
327#[deprecated(
328    since = "0.4.0",
329    note = "Deprecated per SEP-2577 (2026-07-28). \
330            Replacement: integrate directly with LLM provider APIs. \
331            Earliest removal: first release on/after 2027-07-28."
332)]
333#[allow(deprecated)]
334#[derive(Debug, Clone, Serialize, Deserialize)]
335#[serde(rename_all = "camelCase")]
336pub struct CreateMessageRequest {
337    /// Method name (always "sampling/createMessage")
338    pub method: String,
339    /// Request parameters
340    pub params: CreateMessageRequestParams,
341}
342
343/// Result for `sampling/createMessage` — `extends SamplingMessage`
344/// (role, content, _meta) plus `model` and optional `stopReason`.
345///
346/// **Deprecated** per SEP-2577 — see module-level docs.
347#[deprecated(
348    since = "0.4.0",
349    note = "Deprecated per SEP-2577 (2026-07-28). \
350            Replacement: integrate directly with LLM provider APIs. \
351            Earliest removal: first release on/after 2027-07-28."
352)]
353#[allow(deprecated)]
354#[derive(Debug, Clone, Serialize, Deserialize)]
355#[serde(rename_all = "camelCase")]
356pub struct CreateMessageResult {
357    pub role: Role,
358    pub content: SamplingMessageContent,
359    pub model: String,
360    #[serde(skip_serializing_if = "Option::is_none")]
361    pub stop_reason: Option<String>,
362    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
363    pub meta: Option<crate::meta::ResultMetaObject>,
364}
365
366#[allow(deprecated)]
367impl CreateMessageRequestParams {
368    pub fn new(messages: Vec<SamplingMessage>, max_tokens: u32) -> Self {
369        Self {
370            messages,
371            model_preferences: None,
372            system_prompt: None,
373            include_context: None,
374            temperature: None,
375            max_tokens,
376            stop_sequences: None,
377            metadata: None,
378            tools: None,
379            tool_choice: None,
380        }
381    }
382
383    pub fn with_tools(mut self, tools: Vec<crate::tools::Tool>) -> Self {
384        self.tools = Some(tools);
385        self
386    }
387
388    pub fn with_tool_choice(mut self, tool_choice: ToolChoice) -> Self {
389        self.tool_choice = Some(tool_choice);
390        self
391    }
392
393    pub fn with_model_preferences(mut self, preferences: ModelPreferences) -> Self {
394        self.model_preferences = Some(preferences);
395        self
396    }
397
398    pub fn with_system_prompt(mut self, prompt: impl Into<String>) -> Self {
399        self.system_prompt = Some(prompt.into());
400        self
401    }
402
403    pub fn with_temperature(mut self, temperature: f64) -> Self {
404        self.temperature = Some(temperature);
405        self
406    }
407
408    pub fn with_stop_sequences(mut self, sequences: Vec<String>) -> Self {
409        self.stop_sequences = Some(sequences);
410        self
411    }
412
413    /// Sampling message-shape MUSTs:
414    ///
415    /// - A user-role message whose content contains a `ToolResult` block MUST
416    ///   contain ONLY `ToolResult` blocks (no mixing with text/image/etc.).
417    /// - Every assistant-role message containing a `ToolUse` block MUST be
418    ///   immediately followed by a user-role message consisting entirely of
419    ///   `ToolResult` blocks whose `tool_use_id`s match the preceding
420    ///   `ToolUse` ids.
421    pub fn validate_message_shape(&self) -> Result<(), String> {
422        fn blocks(content: &SamplingMessageContent) -> Vec<&SamplingMessageContentBlock> {
423            match content {
424                SamplingMessageContent::Single(b) => vec![b],
425                SamplingMessageContent::Multiple(bs) => bs.iter().collect(),
426            }
427        }
428
429        fn is_tool_result(block: &SamplingMessageContentBlock) -> bool {
430            matches!(block, SamplingMessageContentBlock::ToolResult { .. })
431        }
432
433        for (i, message) in self.messages.iter().enumerate() {
434            let content_blocks = blocks(&message.content);
435
436            if message.role == Role::User {
437                let any_tool_result = content_blocks.iter().any(|b| is_tool_result(b));
438                let all_tool_result = content_blocks.iter().all(|b| is_tool_result(b));
439                if any_tool_result && !all_tool_result {
440                    return Err(format!(
441                        "message {i}: a user message containing a ToolResult block must contain ONLY ToolResult blocks"
442                    ));
443                }
444            }
445
446            if message.role == Role::Assistant {
447                let tool_use_ids: Vec<&str> = content_blocks
448                    .iter()
449                    .filter_map(|b| match b {
450                        SamplingMessageContentBlock::ToolUse { id, .. } => Some(id.as_str()),
451                        _ => None,
452                    })
453                    .collect();
454                if tool_use_ids.is_empty() {
455                    continue;
456                }
457
458                let next = self.messages.get(i + 1).filter(|m| m.role == Role::User);
459                let Some(next) = next else {
460                    return Err(format!(
461                        "message {i}: assistant ToolUse must be immediately followed by a user message of ToolResult blocks"
462                    ));
463                };
464                let next_blocks = blocks(&next.content);
465                if next_blocks.is_empty() || !next_blocks.iter().all(|b| is_tool_result(b)) {
466                    return Err(format!(
467                        "message {i}: assistant ToolUse must be immediately followed by a user message consisting entirely of ToolResult blocks"
468                    ));
469                }
470                let next_ids: std::collections::HashSet<&str> = next_blocks
471                    .iter()
472                    .filter_map(|b| match b {
473                        SamplingMessageContentBlock::ToolResult { tool_use_id, .. } => {
474                            Some(tool_use_id.as_str())
475                        }
476                        _ => None,
477                    })
478                    .collect();
479                for id in tool_use_ids {
480                    if !next_ids.contains(id) {
481                        return Err(format!(
482                            "message {i}: ToolUse id '{id}' has no matching ToolResult in the following message"
483                        ));
484                    }
485                }
486            }
487        }
488
489        Ok(())
490    }
491}
492
493#[allow(deprecated)]
494impl CreateMessageRequest {
495    pub fn new(messages: Vec<SamplingMessage>, max_tokens: u32) -> Self {
496        Self {
497            method: "sampling/createMessage".to_string(),
498            params: CreateMessageRequestParams::new(messages, max_tokens),
499        }
500    }
501
502    /// Attach a fully-constructed params struct.
503    pub fn with_params(mut self, params: CreateMessageRequestParams) -> Self {
504        self.params = params;
505        self
506    }
507
508    pub fn with_model_preferences(mut self, preferences: ModelPreferences) -> Self {
509        self.params = self.params.with_model_preferences(preferences);
510        self
511    }
512
513    pub fn with_system_prompt(mut self, prompt: impl Into<String>) -> Self {
514        self.params = self.params.with_system_prompt(prompt);
515        self
516    }
517
518    pub fn with_temperature(mut self, temperature: f64) -> Self {
519        self.params = self.params.with_temperature(temperature);
520        self
521    }
522
523    pub fn with_stop_sequences(mut self, sequences: Vec<String>) -> Self {
524        self.params = self.params.with_stop_sequences(sequences);
525        self
526    }
527
528    pub fn with_tools(mut self, tools: Vec<crate::tools::Tool>) -> Self {
529        self.params = self.params.with_tools(tools);
530        self
531    }
532
533    pub fn with_tool_choice(mut self, tool_choice: ToolChoice) -> Self {
534        self.params = self.params.with_tool_choice(tool_choice);
535        self
536    }
537}
538
539#[allow(deprecated)]
540impl CreateMessageResult {
541    pub fn new(role: Role, content: SamplingMessageContent, model: impl Into<String>) -> Self {
542        Self {
543            role,
544            content,
545            model: model.into(),
546            stop_reason: None,
547            meta: None,
548        }
549    }
550
551    /// Convenience: single-block result (most common LLM response shape).
552    pub fn single(
553        role: Role,
554        block: SamplingMessageContentBlock,
555        model: impl Into<String>,
556    ) -> Self {
557        Self::new(role, SamplingMessageContent::Single(block), model)
558    }
559
560    pub fn with_stop_reason(mut self, reason: impl Into<String>) -> Self {
561        self.stop_reason = Some(reason.into());
562        self
563    }
564
565    pub fn with_meta(mut self, meta: impl Into<crate::meta::ResultMetaObject>) -> Self {
566        self.meta = Some(meta.into());
567        self
568    }
569}
570
571// Trait implementations for sampling.
572//
573// **Deprecated** per SEP-2577 — trait impls retained during the 12-month
574// migration window so existing call sites that depend on the trait surface
575// (e.g. `InputRequest::CreateMessage` in the MRTR flow) continue to compile.
576// Concrete `#[deprecated]` attributes live on the struct definitions above;
577// the `#[allow(deprecated)]` blocks below suppress the cascading warning
578// inside the protocol crate itself.
579
580use crate::traits::*;
581use std::collections::HashMap;
582
583// Trait implementations for CreateMessageRequestParams
584#[allow(deprecated)]
585impl Params for CreateMessageRequestParams {}
586
587#[allow(deprecated)]
588impl HasCreateMessageRequestParams for CreateMessageRequestParams {
589    fn messages(&self) -> &Vec<SamplingMessage> {
590        &self.messages
591    }
592
593    fn model_preferences(&self) -> Option<&ModelPreferences> {
594        self.model_preferences.as_ref()
595    }
596
597    fn system_prompt(&self) -> Option<&String> {
598        self.system_prompt.as_ref()
599    }
600
601    fn include_context(&self) -> Option<&String> {
602        self.include_context.as_ref()
603    }
604
605    fn temperature(&self) -> Option<&f64> {
606        self.temperature.as_ref()
607    }
608
609    fn max_tokens(&self) -> u32 {
610        self.max_tokens
611    }
612
613    fn stop_sequences(&self) -> Option<&Vec<String>> {
614        self.stop_sequences.as_ref()
615    }
616
617    fn metadata(&self) -> Option<&std::collections::HashMap<String, Value>> {
618        self.metadata.as_ref()
619    }
620}
621
622// `HasMetaParam` intentionally NOT implemented — per schema
623// `CreateMessageRequestParams` does NOT extend `RequestParams`, so it has no
624// `_meta` field on the wire.
625
626// Trait implementations for CreateMessageRequest
627#[allow(deprecated)]
628impl HasMethod for CreateMessageRequest {
629    fn method(&self) -> &str {
630        &self.method
631    }
632}
633
634#[allow(deprecated)]
635impl HasParams for CreateMessageRequest {
636    fn params(&self) -> Option<&dyn Params> {
637        Some(&self.params)
638    }
639}
640
641// Trait implementations for CreateMessageResult
642#[allow(deprecated)]
643impl HasData for CreateMessageResult {
644    fn data(&self) -> HashMap<String, Value> {
645        let mut data = HashMap::new();
646        data.insert(
647            "role".to_string(),
648            serde_json::to_value(&self.role).unwrap_or(Value::String("user".to_string())),
649        );
650        data.insert(
651            "content".to_string(),
652            serde_json::to_value(&self.content).unwrap_or(Value::Null),
653        );
654        data.insert("model".to_string(), Value::String(self.model.clone()));
655        if let Some(ref stop_reason) = self.stop_reason {
656            data.insert("stopReason".to_string(), Value::String(stop_reason.clone()));
657        }
658        data
659    }
660}
661
662#[allow(deprecated)]
663impl HasMeta for CreateMessageResult {
664    fn meta(&self) -> Option<&crate::meta::ResultMetaObject> {
665        self.meta.as_ref()
666    }
667}
668
669// `CreateMessageResult` does NOT implement `RpcResult` — per the schema it
670// `extends SamplingMessage` (not `Result`), so it has no `resultType`
671// discriminator and the `RpcResult: HasMeta + HasResultType` bound doesn't fit.
672// See `crate::traits::RpcResult` for the contract.
673
674#[allow(deprecated)]
675impl crate::traits::CreateMessageResult for CreateMessageResult {
676    fn role(&self) -> &Role {
677        &self.role
678    }
679
680    fn content(&self) -> &SamplingMessageContent {
681        &self.content
682    }
683
684    fn model(&self) -> &String {
685        &self.model
686    }
687
688    fn stop_reason(&self) -> Option<&String> {
689        self.stop_reason.as_ref()
690    }
691}
692
693// ===========================================
694// === Fine-Grained Sampling Traits ===
695// ===========================================
696
697// ================== CONVENIENCE CONSTRUCTORS ==================
698
699#[allow(deprecated)]
700impl ModelPreferences {
701    pub fn new() -> Self {
702        Self {
703            hints: None,
704            cost_priority: None,
705            speed_priority: None,
706            intelligence_priority: None,
707        }
708    }
709
710    pub fn with_hints(mut self, hints: Vec<ModelHint>) -> Self {
711        self.hints = Some(hints);
712        self
713    }
714
715    pub fn with_cost_priority(mut self, priority: f64) -> Self {
716        self.cost_priority = Some(priority);
717        self
718    }
719
720    pub fn with_speed_priority(mut self, priority: f64) -> Self {
721        self.speed_priority = Some(priority);
722        self
723    }
724
725    pub fn with_intelligence_priority(mut self, priority: f64) -> Self {
726        self.intelligence_priority = Some(priority);
727        self
728    }
729}
730
731#[allow(deprecated)]
732impl Default for ModelPreferences {
733    fn default() -> Self {
734        Self::new()
735    }
736}
737
738#[allow(deprecated)]
739impl SamplingMessage {
740    pub fn new(role: Role, content: SamplingMessageContent) -> Self {
741        Self {
742            role,
743            content,
744            meta: None,
745        }
746    }
747
748    /// Single-block convenience.
749    pub fn single(role: Role, block: SamplingMessageContentBlock) -> Self {
750        Self::new(role, SamplingMessageContent::Single(block))
751    }
752
753    pub fn user_text(text: impl Into<String>) -> Self {
754        Self::single(Role::User, SamplingMessageContentBlock::text(text))
755    }
756
757    pub fn assistant_text(text: impl Into<String>) -> Self {
758        Self::single(Role::Assistant, SamplingMessageContentBlock::text(text))
759    }
760
761    pub fn with_meta(mut self, meta: std::collections::HashMap<String, Value>) -> Self {
762        self.meta = Some(meta);
763        self
764    }
765}
766
767#[cfg(test)]
768#[allow(deprecated)]
769mod tests {
770    use super::*;
771
772    #[test]
773    fn test_tool_choice_mode_serializes_as_required() {
774        let tc = ToolChoice::required();
775        let json = serde_json::to_value(&tc).unwrap();
776        assert_eq!(json["mode"], "required");
777    }
778
779    #[test]
780    fn test_tool_choice_mode_deserializes_legacy_any() {
781        let json = serde_json::json!({"mode": "any"});
782        let tc: ToolChoice = serde_json::from_value(json).unwrap();
783        assert_eq!(tc.mode, Some(ToolChoiceMode::Required));
784    }
785
786    #[test]
787    fn test_tool_choice_mode_deserializes_required() {
788        let json = serde_json::json!({"mode": "required"});
789        let tc: ToolChoice = serde_json::from_value(json).unwrap();
790        assert_eq!(tc.mode, Some(ToolChoiceMode::Required));
791    }
792
793    #[test]
794    fn test_tool_choice_any_alias_returns_required() {
795        let tc = ToolChoice::any();
796        assert_eq!(tc.mode, Some(ToolChoiceMode::Required));
797    }
798
799    #[test]
800    fn test_tool_choice_absent_mode_is_auto() {
801        // Schema: mode is optional; absent means "auto".
802        let tc: ToolChoice = serde_json::from_value(serde_json::json!({})).unwrap();
803        assert_eq!(tc.mode, None);
804        assert_eq!(tc.effective_mode(), ToolChoiceMode::Auto);
805    }
806
807    #[test]
808    fn create_message_request_params_metadata_is_an_object() {
809        // Schema: `metadata?: JSONObject` — this crate's convention for
810        // JSONObject is `HashMap<String, Value>`, matching
811        // `ClientCapabilities.experimental` and similar fields.
812        let mut metadata = std::collections::HashMap::new();
813        metadata.insert("traceId".to_string(), serde_json::json!("abc-123"));
814        let params = CreateMessageRequestParams::new(vec![SamplingMessage::user_text("hi")], 100)
815            .with_temperature(0.5);
816        let mut params = params;
817        params.metadata = Some(metadata);
818
819        let v = serde_json::to_value(&params).unwrap();
820        assert!(
821            v["metadata"].is_object(),
822            "metadata must serialize as a JSON object"
823        );
824        assert_eq!(v["metadata"]["traceId"], "abc-123");
825
826        let back: CreateMessageRequestParams = serde_json::from_value(v).unwrap();
827        assert_eq!(
828            back.metadata.unwrap().get("traceId"),
829            Some(&serde_json::json!("abc-123"))
830        );
831    }
832
833    #[test]
834    fn create_message_request_params_metadata_rejects_non_object_wire_value() {
835        // A scalar `metadata` value must fail to deserialize now that the
836        // field is a typed JSONObject, not an unrestricted `Value`.
837        let wire = serde_json::json!({
838            "messages": [],
839            "maxTokens": 10,
840            "metadata": "not-an-object"
841        });
842        let result: Result<CreateMessageRequestParams, _> = serde_json::from_value(wire);
843        assert!(
844            result.is_err(),
845            "scalar metadata must be rejected per the JSONObject contract"
846        );
847    }
848
849    fn tool_use(id: &str) -> SamplingMessageContentBlock {
850        SamplingMessageContentBlock::ToolUse {
851            id: id.to_string(),
852            name: "get_weather".to_string(),
853            input: std::collections::HashMap::new(),
854            meta: None,
855        }
856    }
857
858    fn tool_result(id: &str) -> SamplingMessageContentBlock {
859        SamplingMessageContentBlock::ToolResult {
860            tool_use_id: id.to_string(),
861            content: vec![],
862            structured_content: None,
863            is_error: None,
864            meta: None,
865        }
866    }
867
868    #[test]
869    fn valid_tool_use_tool_result_pairing_passes() {
870        let params = CreateMessageRequestParams::new(
871            vec![
872                SamplingMessage::user_text("what's the weather?"),
873                SamplingMessage::new(
874                    Role::Assistant,
875                    SamplingMessageContent::Single(tool_use("call-1")),
876                ),
877                SamplingMessage::new(
878                    Role::User,
879                    SamplingMessageContent::Single(tool_result("call-1")),
880                ),
881            ],
882            100,
883        );
884        assert_eq!(params.validate_message_shape(), Ok(()));
885    }
886
887    #[test]
888    fn user_message_mixing_text_and_tool_result_fails() {
889        let params = CreateMessageRequestParams::new(
890            vec![
891                SamplingMessage::new(
892                    Role::Assistant,
893                    SamplingMessageContent::Single(tool_use("call-1")),
894                ),
895                SamplingMessage::new(
896                    Role::User,
897                    SamplingMessageContent::Multiple(vec![
898                        SamplingMessageContentBlock::text("also here's some text"),
899                        tool_result("call-1"),
900                    ]),
901                ),
902            ],
903            100,
904        );
905        assert!(
906            params.validate_message_shape().is_err(),
907            "a user message mixing text with a ToolResult block must be rejected"
908        );
909    }
910
911    #[test]
912    fn assistant_tool_use_not_followed_by_matching_tool_result_fails() {
913        // No following message at all.
914        let params = CreateMessageRequestParams::new(
915            vec![SamplingMessage::new(
916                Role::Assistant,
917                SamplingMessageContent::Single(tool_use("call-1")),
918            )],
919            100,
920        );
921        assert!(params.validate_message_shape().is_err());
922
923        // Following message is user but the ToolResult id doesn't match.
924        let params = CreateMessageRequestParams::new(
925            vec![
926                SamplingMessage::new(
927                    Role::Assistant,
928                    SamplingMessageContent::Single(tool_use("call-1")),
929                ),
930                SamplingMessage::new(
931                    Role::User,
932                    SamplingMessageContent::Single(tool_result("call-DIFFERENT")),
933                ),
934            ],
935            100,
936        );
937        assert!(
938            params.validate_message_shape().is_err(),
939            "a mismatched tool_use_id must be rejected"
940        );
941
942        // Following message is assistant, not user.
943        let params = CreateMessageRequestParams::new(
944            vec![
945                SamplingMessage::new(
946                    Role::Assistant,
947                    SamplingMessageContent::Single(tool_use("call-1")),
948                ),
949                SamplingMessage::assistant_text("no tool result here"),
950            ],
951            100,
952        );
953        assert!(params.validate_message_shape().is_err());
954    }
955}