use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum SamplingContent {
#[serde(rename = "text")]
Text { text: String },
#[serde(rename = "image")]
Image { data: String, mime_type: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SamplingMessage {
pub role: String,
pub content: SamplingContent,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelPreference {
#[serde(default)]
pub hints: Vec<ModelHint>,
#[serde(default)]
pub cost_priority: f32,
#[serde(default)]
pub speed_priority: f32,
#[serde(default)]
pub intelligence_priority: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelHint {
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[derive(Default)]
pub enum SamplingRoute {
#[default]
Cloud,
LocalPreferred,
LocalOnly,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateMessageParams {
pub messages: Vec<SamplingMessage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model_preference: Option<ModelPreference>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub system_prompt: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub stop_sequences: Vec<String>,
#[serde(default)]
pub route: SamplingRoute,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateMessageResult {
pub role: String,
pub content: SamplingContent,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<TokenUsage>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenUsage {
pub input_tokens: u32,
pub output_tokens: u32,
}
impl crate::HostProxy {
pub async fn create_message(
&self,
params: CreateMessageParams,
) -> crate::Result<CreateMessageResult> {
let value = self
.request("sampling/create_message", serde_json::to_value(¶ms)?)
.await?;
serde_json::from_value(value).map_err(|e| crate::Error::parse_error(e.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_message_params_roundtrip() {
let params = CreateMessageParams {
messages: vec![SamplingMessage {
role: "user".to_string(),
content: SamplingContent::Text {
text: "Classify this: ...".to_string(),
},
}],
model_preference: Some(ModelPreference {
hints: vec![ModelHint {
name: "claude-sonnet".to_string(),
}],
cost_priority: 0.8,
speed_priority: 0.5,
intelligence_priority: 0.3,
}),
max_tokens: Some(500),
system_prompt: Some("You are a classifier.".to_string()),
temperature: Some(0.0),
stop_sequences: vec![],
route: SamplingRoute::LocalPreferred,
};
let json = serde_json::to_string(¶ms).unwrap();
let parsed: CreateMessageParams = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.messages.len(), 1);
assert_eq!(parsed.max_tokens, Some(500));
assert!(matches!(parsed.route, SamplingRoute::LocalPreferred));
}
#[test]
fn test_create_message_params_minimal() {
let json = r#"{"messages":[{"role":"user","content":{"type":"text","text":"hi"}}]}"#;
let parsed: CreateMessageParams = serde_json::from_str(json).unwrap();
assert_eq!(parsed.messages.len(), 1);
assert!(parsed.model_preference.is_none());
assert!(parsed.max_tokens.is_none());
assert!(matches!(parsed.route, SamplingRoute::Cloud));
}
#[test]
fn test_create_message_result_roundtrip() {
let result = CreateMessageResult {
role: "assistant".to_string(),
content: SamplingContent::Text {
text: "Category: B".to_string(),
},
model: Some("claude-sonnet-4-20250514".to_string()),
stop_reason: Some("end_turn".to_string()),
usage: Some(TokenUsage {
input_tokens: 45,
output_tokens: 8,
}),
};
let json = serde_json::to_string(&result).unwrap();
let parsed: CreateMessageResult = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.role, "assistant");
assert_eq!(parsed.model.as_deref(), Some("claude-sonnet-4-20250514"));
assert_eq!(parsed.usage.as_ref().unwrap().input_tokens, 45);
}
#[test]
fn test_sampling_content_text() {
let content = SamplingContent::Text {
text: "hello".to_string(),
};
let json = serde_json::to_string(&content).unwrap();
let parsed: SamplingContent = serde_json::from_str(&json).unwrap();
match parsed {
SamplingContent::Text { text } => assert_eq!(text, "hello"),
_ => panic!("expected Text"),
}
}
#[test]
fn test_sampling_route_default() {
let route = SamplingRoute::default();
assert!(matches!(route, SamplingRoute::Cloud));
}
#[test]
fn test_sampling_route_serialization() {
assert_eq!(
serde_json::to_string(&SamplingRoute::Cloud).unwrap(),
r#""cloud""#
);
assert_eq!(
serde_json::to_string(&SamplingRoute::LocalPreferred).unwrap(),
r#""local_preferred""#
);
assert_eq!(
serde_json::to_string(&SamplingRoute::LocalOnly).unwrap(),
r#""local_only""#
);
}
#[test]
fn test_model_preference() {
let pref = ModelPreference {
hints: vec![
ModelHint {
name: "claude-sonnet".to_string(),
},
ModelHint {
name: "claude-haiku".to_string(),
},
],
cost_priority: 0.8,
speed_priority: 0.5,
intelligence_priority: 0.3,
};
let json = serde_json::to_string(&pref).unwrap();
let parsed: ModelPreference = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.hints.len(), 2);
assert_eq!(parsed.hints[0].name, "claude-sonnet");
}
}