1use crate::value::VmDictExt;
22
23use serde_json::{json, Value as JsonValue};
24
25use crate::schema::json_to_vm_value;
26use crate::stdlib::host::{dispatch_host_call_bridge, dispatch_mock_host_call};
27use crate::value::{VmError, VmValue};
28
29pub const SAMPLING_METHOD: &str = "sampling/createMessage";
31
32#[derive(Debug, Clone)]
35struct SamplingRequest {
36 messages: Vec<JsonValue>,
39 system: Option<String>,
41 max_tokens: i64,
43 temperature: Option<f64>,
45 stop_sequences: Option<Vec<String>>,
47 model_preferences: Option<JsonValue>,
49 tools: Option<JsonValue>,
52 tool_choice: Option<JsonValue>,
54 thinking: Option<JsonValue>,
57 metadata: Option<JsonValue>,
60 include_context: Option<String>,
64}
65
66#[derive(Debug, Clone)]
68enum ApprovalDecision {
69 Accept(crate::value::DictMap),
72 Decline(String),
75}
76
77pub async fn dispatch_inbound_sampling(server_name: &str, request: &JsonValue) -> JsonValue {
92 let id = request.get("id").cloned().unwrap_or(JsonValue::Null);
93 let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
94
95 if let Some(session_id) = crate::llm::current_agent_session_id() {
101 crate::agent_events::emit_event(&crate::agent_events::AgentEvent::McpNotification {
102 session_id,
103 server: server_name.to_string(),
104 method: SAMPLING_METHOD.to_string(),
105 direction: "request".to_string(),
106 params: params.clone(),
107 });
108 }
109
110 let parsed = match parse_sampling_request(¶ms) {
111 Ok(p) => p,
112 Err(detail) => return crate::jsonrpc::error_response(id, -32602, &detail),
113 };
114
115 let approval = ask_host_approval(server_name, ¶ms).await;
116 let overrides = match approval {
117 ApprovalDecision::Accept(map) => map,
118 ApprovalDecision::Decline(reason) => {
119 return crate::jsonrpc::error_response_with_data(
120 id,
121 -32603,
122 &format!("Sampling declined: {reason}"),
123 json!({
124 "type": "mcp.samplingDeclined",
125 "method": SAMPLING_METHOD,
126 "reason": reason,
127 }),
128 );
129 }
130 };
131
132 match run_llm_call(&parsed, overrides).await {
133 Ok(outcome) => crate::jsonrpc::response(id, build_spec_response(outcome, &parsed)),
134 Err(detail) => crate::jsonrpc::error_response_with_data(
135 id,
136 -32000,
137 &format!("Sampling failed: {detail}"),
138 json!({
139 "type": "mcp.samplingFailed",
140 "method": SAMPLING_METHOD,
141 "reason": detail,
142 }),
143 ),
144 }
145}
146
147fn parse_sampling_request(params: &JsonValue) -> Result<SamplingRequest, String> {
151 let object = params
152 .as_object()
153 .ok_or_else(|| "sampling params must be a JSON object".to_string())?;
154
155 let messages = match object.get("messages") {
156 Some(JsonValue::Array(items)) => items.clone(),
157 Some(_) => return Err("sampling params 'messages' must be an array".into()),
158 None => return Err("sampling params 'messages' is required".into()),
159 };
160 if messages.is_empty() {
161 return Err("sampling params 'messages' must not be empty".into());
162 }
163 for (idx, message) in messages.iter().enumerate() {
164 let role = message
165 .get("role")
166 .and_then(|value| value.as_str())
167 .ok_or_else(|| format!("sampling messages[{idx}].role is required"))?;
168 if !matches!(role, "user" | "assistant" | "system") {
169 return Err(format!(
170 "sampling messages[{idx}].role must be 'user'/'assistant'/'system' (got {role:?})"
171 ));
172 }
173 if message.get("content").is_none() {
174 return Err(format!("sampling messages[{idx}].content is required"));
175 }
176 }
177
178 let system = object
179 .get("systemPrompt")
180 .and_then(|value| value.as_str())
181 .filter(|value| !value.is_empty())
182 .map(str::to_string);
183
184 let max_tokens = object
185 .get("maxTokens")
186 .and_then(|value| value.as_i64())
187 .ok_or_else(|| {
188 "sampling params 'maxTokens' is required and must be an integer".to_string()
189 })?;
190 if max_tokens <= 0 {
191 return Err(format!(
192 "sampling params 'maxTokens' must be positive (got {max_tokens})"
193 ));
194 }
195
196 let temperature =
197 match object.get("temperature") {
198 Some(JsonValue::Number(n)) => Some(n.as_f64().ok_or_else(|| {
199 "sampling params 'temperature' must be a finite number".to_string()
200 })?),
201 Some(JsonValue::Null) | None => None,
202 Some(_) => return Err("sampling params 'temperature' must be a number".into()),
203 };
204
205 let stop_sequences = match object.get("stopSequences") {
206 Some(JsonValue::Array(items)) => {
207 let mut out = Vec::with_capacity(items.len());
208 for (idx, item) in items.iter().enumerate() {
209 let s = item.as_str().ok_or_else(|| {
210 format!("sampling params 'stopSequences[{idx}]' must be a string")
211 })?;
212 out.push(s.to_string());
213 }
214 Some(out)
215 }
216 Some(JsonValue::Null) | None => None,
217 Some(_) => return Err("sampling params 'stopSequences' must be an array".into()),
218 };
219
220 let include_context = object
221 .get("includeContext")
222 .and_then(|value| value.as_str())
223 .map(str::to_string);
224
225 let tool_choice = object
229 .get("toolChoice")
230 .or_else(|| object.get("tool_choice"))
231 .cloned();
232
233 Ok(SamplingRequest {
234 messages,
235 system,
236 max_tokens,
237 temperature,
238 stop_sequences,
239 model_preferences: object.get("modelPreferences").cloned(),
240 tools: object.get("tools").cloned(),
241 tool_choice,
242 thinking: object.get("thinking").cloned(),
243 metadata: object.get("metadata").cloned(),
244 include_context,
245 })
246}
247
248async fn ask_host_approval(server_name: &str, params: &JsonValue) -> ApprovalDecision {
262 let mut bridge_params: crate::value::DictMap = crate::value::DictMap::new();
263 bridge_params.put_str("server", server_name);
264 bridge_params.insert(crate::value::intern_key("params"), json_to_vm_value(params));
265
266 let result = dispatch_mock_host_call("mcp", "sample", &bridge_params)
267 .or_else(|| dispatch_host_call_bridge("mcp", "sample", &bridge_params));
268
269 let raw = match result {
270 Some(Ok(value)) => value,
271 Some(Err(error)) => {
272 return ApprovalDecision::Decline(host_error_to_string(error));
273 }
274 None => {
275 return ApprovalDecision::Decline(
276 "no host bridge installed for ('mcp', 'sample')".into(),
277 );
278 }
279 };
280
281 coerce_bridge_response(raw)
282}
283
284fn coerce_bridge_response(value: VmValue) -> ApprovalDecision {
285 match value {
286 VmValue::Nil => ApprovalDecision::Decline("host bridge returned nil".into()),
287 VmValue::Bool(false) => ApprovalDecision::Decline("host bridge declined".into()),
288 VmValue::Bool(true) => ApprovalDecision::Accept(crate::value::DictMap::new()),
289 VmValue::Dict(dict) => {
290 let map = dict.as_ref().clone();
291 match map.get("action").and_then(|v| match v {
292 VmValue::String(s) => Some(s.to_string()),
293 _ => None,
294 }) {
295 Some(action) if action == "decline" || action == "cancel" => {
296 let reason = map
297 .get("message")
298 .or_else(|| map.get("reason"))
299 .map(VmValue::display)
300 .unwrap_or_else(|| "host bridge declined".to_string());
301 ApprovalDecision::Decline(reason)
302 }
303 Some(action) if action == "accept" => {
304 let overrides = map
305 .get("options")
306 .and_then(|v| match v {
307 VmValue::Dict(d) => Some(d.as_ref().clone()),
308 _ => None,
309 })
310 .unwrap_or_default();
311 ApprovalDecision::Accept(overrides)
312 }
313 Some(other) => ApprovalDecision::Decline(format!(
314 "host bridge returned unknown action {other:?}"
315 )),
316 None => {
317 ApprovalDecision::Accept(map)
320 }
321 }
322 }
323 other => ApprovalDecision::Decline(format!(
324 "host bridge returned unsupported value: {}",
325 other.display()
326 )),
327 }
328}
329
330fn host_error_to_string(error: VmError) -> String {
331 match error {
332 VmError::Thrown(VmValue::String(s)) => s.to_string(),
333 VmError::Thrown(other) => other.display(),
334 VmError::Runtime(s) | VmError::TypeError(s) => s,
335 other => format!("{other:?}"),
336 }
337}
338
339#[derive(Debug, Clone)]
343struct LlmOutcome {
344 text: String,
345 model: String,
346}
347
348async fn run_llm_call(
352 parsed: &SamplingRequest,
353 overrides: crate::value::DictMap,
354) -> Result<LlmOutcome, String> {
355 let (vm_args, options_dict) = build_llm_call_args(parsed, overrides);
356
357 let opts = crate::llm::extract_llm_options(&vm_args).map_err(host_error_to_string)?;
358 let result = crate::llm::execute_llm_call(None, opts, Some(options_dict), None, None)
359 .await
360 .map_err(host_error_to_string)?;
361
362 extract_assistant_outcome(&result)
363}
364
365fn extract_assistant_outcome(result: &VmValue) -> Result<LlmOutcome, String> {
366 match result {
367 VmValue::String(s) => Ok(LlmOutcome {
368 text: s.to_string(),
369 model: String::new(),
370 }),
371 VmValue::Dict(d) => {
372 let text = match d.get("text") {
373 Some(VmValue::String(s)) => s.to_string(),
374 Some(other) => other.display(),
375 None => return Err("llm_call result missing 'text' field".into()),
376 };
377 let model = d.get("model").map(VmValue::display).unwrap_or_default();
378 Ok(LlmOutcome { text, model })
379 }
380 other => Ok(LlmOutcome {
381 text: other.display(),
382 model: String::new(),
383 }),
384 }
385}
386
387fn build_llm_call_args(
392 parsed: &SamplingRequest,
393 overrides: crate::value::DictMap,
394) -> (Vec<VmValue>, crate::value::DictMap) {
395 let mut options: crate::value::DictMap = crate::value::DictMap::new();
396
397 let messages_vm: Vec<VmValue> = parsed.messages.iter().map(json_to_vm_value).collect();
400 options.insert(
401 crate::value::intern_key("messages"),
402 VmValue::List(std::sync::Arc::new(messages_vm)),
403 );
404
405 options.insert(
406 crate::value::intern_key("max_tokens"),
407 VmValue::Int(parsed.max_tokens),
408 );
409
410 if let Some(temperature) = parsed.temperature {
411 options.insert(
412 crate::value::intern_key("temperature"),
413 VmValue::Float(temperature),
414 );
415 }
416
417 if let Some(stop) = parsed.stop_sequences.as_ref() {
418 let stop_vm: Vec<VmValue> = stop
419 .iter()
420 .map(|s| VmValue::String(arcstr::ArcStr::from(s.as_str())))
421 .collect();
422 options.insert(
423 crate::value::intern_key("stop"),
424 VmValue::List(std::sync::Arc::new(stop_vm)),
425 );
426 }
427
428 if let Some(hint) = pick_model_hint(parsed.model_preferences.as_ref()) {
429 options.put_str("model", hint.as_str());
430 }
431
432 if let Some(tools) = parsed.tools.as_ref() {
433 options.insert(crate::value::intern_key("tools"), json_to_vm_value(tools));
434 }
435 if let Some(tool_choice) = parsed.tool_choice.as_ref() {
436 options.insert(
437 crate::value::intern_key("tool_choice"),
438 json_to_vm_value(tool_choice),
439 );
440 }
441 if let Some(thinking) = parsed.thinking.as_ref() {
442 options.insert(
443 crate::value::intern_key("thinking"),
444 json_to_vm_value(thinking),
445 );
446 }
447
448 if let Some(metadata) = parsed.metadata.as_ref() {
451 options.insert(
452 crate::value::intern_key("metadata"),
453 json_to_vm_value(metadata),
454 );
455 }
456 if let Some(include_context) = parsed.include_context.as_ref() {
457 options.put_str("include_context", include_context.as_str());
458 }
459
460 for (key, value) in overrides {
463 options.insert(key, value);
464 }
465
466 let system_value = parsed
467 .system
468 .as_ref()
469 .map(|s| VmValue::String(arcstr::ArcStr::from(s.as_str())))
470 .unwrap_or(VmValue::Nil);
471
472 let args = vec![
473 VmValue::String(arcstr::ArcStr::from("")),
474 system_value,
475 VmValue::dict(options.clone()),
476 ];
477
478 (args, options)
479}
480
481fn pick_model_hint(prefs: Option<&JsonValue>) -> Option<String> {
485 let prefs = prefs?;
486 let hints = prefs.get("hints")?.as_array()?;
487 for hint in hints {
488 if let Some(name) = hint.get("name").and_then(|value| value.as_str()) {
489 if !name.is_empty() {
490 return Some(name.to_string());
491 }
492 }
493 }
494 None
495}
496
497fn build_spec_response(outcome: LlmOutcome, parsed: &SamplingRequest) -> JsonValue {
505 let stop_reason = if parsed.stop_sequences.is_some() {
506 "stopSequence"
507 } else {
508 "endTurn"
509 };
510
511 let model = if outcome.model.is_empty() {
512 pick_model_hint(parsed.model_preferences.as_ref()).unwrap_or_default()
513 } else {
514 outcome.model
515 };
516
517 json!({
518 "role": "assistant",
519 "content": {
520 "type": "text",
521 "text": outcome.text,
522 },
523 "model": model,
524 "stopReason": stop_reason,
525 })
526}
527
528#[cfg(test)]
529mod tests {
530 use super::*;
531
532 use crate::value::VmDictExt;
533 use std::sync::Arc;
534
535 fn minimal_request() -> JsonValue {
536 json!({
537 "messages": [
538 {"role": "user", "content": {"type": "text", "text": "hi"}}
539 ],
540 "maxTokens": 64,
541 })
542 }
543
544 #[test]
545 fn parse_rejects_missing_messages() {
546 let err = parse_sampling_request(&json!({"maxTokens": 1})).unwrap_err();
547 assert!(err.contains("messages"));
548 }
549
550 #[test]
551 fn parse_rejects_empty_messages() {
552 let err = parse_sampling_request(&json!({"messages": [], "maxTokens": 1})).unwrap_err();
553 assert!(err.contains("must not be empty"));
554 }
555
556 #[test]
557 fn parse_rejects_missing_max_tokens() {
558 let err = parse_sampling_request(&json!({
559 "messages": [{"role": "user", "content": {"type": "text", "text": "hi"}}]
560 }))
561 .unwrap_err();
562 assert!(err.contains("maxTokens"));
563 }
564
565 #[test]
566 fn parse_rejects_zero_max_tokens() {
567 let err = parse_sampling_request(&json!({
568 "messages": [{"role": "user", "content": {"type": "text", "text": "hi"}}],
569 "maxTokens": 0,
570 }))
571 .unwrap_err();
572 assert!(err.contains("positive"));
573 }
574
575 #[test]
576 fn parse_rejects_unknown_role() {
577 let err = parse_sampling_request(&json!({
578 "messages": [{"role": "tool", "content": {}}],
579 "maxTokens": 1,
580 }))
581 .unwrap_err();
582 assert!(err.contains("'user'/'assistant'/'system'"));
583 }
584
585 #[test]
586 fn parse_extracts_optional_fields() {
587 let parsed = parse_sampling_request(&json!({
588 "messages": [{"role": "user", "content": {"type": "text", "text": "hi"}}],
589 "maxTokens": 32,
590 "systemPrompt": "be brief",
591 "temperature": 0.2,
592 "stopSequences": ["END"],
593 "modelPreferences": {"hints": [{"name": "claude-3-5-sonnet"}]},
594 "includeContext": "thisServer",
595 "metadata": {"trace": "abc"},
596 }))
597 .unwrap();
598 assert_eq!(parsed.max_tokens, 32);
599 assert_eq!(parsed.system.as_deref(), Some("be brief"));
600 assert_eq!(parsed.temperature, Some(0.2));
601 assert_eq!(
602 parsed.stop_sequences.as_deref(),
603 Some(&["END".to_string()][..])
604 );
605 assert_eq!(parsed.include_context.as_deref(), Some("thisServer"));
606 assert_eq!(
607 pick_model_hint(parsed.model_preferences.as_ref()),
608 Some("claude-3-5-sonnet".to_string())
609 );
610 }
611
612 #[test]
613 fn pick_model_hint_picks_first_non_empty() {
614 let prefs = json!({"hints": [{"name": ""}, {"name": "gpt-4"}]});
615 assert_eq!(pick_model_hint(Some(&prefs)), Some("gpt-4".to_string()));
616 }
617
618 #[test]
619 fn pick_model_hint_returns_none_for_empty_chain() {
620 assert!(pick_model_hint(None).is_none());
621 assert!(pick_model_hint(Some(&json!({"hints": []}))).is_none());
622 assert!(pick_model_hint(Some(&json!({}))).is_none());
623 }
624
625 #[test]
626 fn coerce_bridge_response_nil_declines() {
627 match coerce_bridge_response(VmValue::Nil) {
628 ApprovalDecision::Decline(_) => {}
629 other => panic!("expected decline, got {other:?}"),
630 }
631 }
632
633 #[test]
634 fn coerce_bridge_response_true_accepts_with_no_overrides() {
635 match coerce_bridge_response(VmValue::Bool(true)) {
636 ApprovalDecision::Accept(map) => assert!(map.is_empty()),
637 other => panic!("expected accept, got {other:?}"),
638 }
639 }
640
641 #[test]
642 fn coerce_bridge_response_accept_with_options() {
643 let mut dict = crate::value::DictMap::new();
644 dict.put_str("action", "accept");
645 let mut options = crate::value::DictMap::new();
646 options.put_str("provider", "mock");
647 dict.insert(crate::value::intern_key("options"), VmValue::dict(options));
648 match coerce_bridge_response(VmValue::dict(dict)) {
649 ApprovalDecision::Accept(map) => {
650 assert_eq!(
651 map.get("provider").map(|v| v.display()).as_deref(),
652 Some("mock")
653 );
654 }
655 other => panic!("expected accept, got {other:?}"),
656 }
657 }
658
659 #[test]
660 fn coerce_bridge_response_decline_with_message() {
661 let mut dict = crate::value::DictMap::new();
662 dict.put_str("action", "decline");
663 dict.put_str("message", "rate limit");
664 match coerce_bridge_response(VmValue::dict(dict)) {
665 ApprovalDecision::Decline(reason) => assert_eq!(reason, "rate limit"),
666 other => panic!("expected decline, got {other:?}"),
667 }
668 }
669
670 #[test]
671 fn coerce_bridge_response_bare_dict_is_overrides() {
672 let mut dict = crate::value::DictMap::new();
673 dict.put_str("provider", "mock");
674 match coerce_bridge_response(VmValue::dict(dict)) {
675 ApprovalDecision::Accept(map) => {
676 assert_eq!(
677 map.get("provider").map(|v| v.display()).as_deref(),
678 Some("mock")
679 );
680 }
681 other => panic!("expected accept, got {other:?}"),
682 }
683 }
684
685 fn outcome(text: &str, model: &str) -> LlmOutcome {
686 LlmOutcome {
687 text: text.to_string(),
688 model: model.to_string(),
689 }
690 }
691
692 #[test]
693 fn build_spec_response_flags_stop_sequence() {
694 let parsed = parse_sampling_request(&json!({
695 "messages": [{"role": "user", "content": {"type": "text", "text": "hi"}}],
696 "maxTokens": 4,
697 "stopSequences": ["END"],
698 }))
699 .unwrap();
700 let response = build_spec_response(outcome("done", "actual-model"), &parsed);
701 assert_eq!(response["stopReason"], json!("stopSequence"));
702 assert_eq!(response["role"], json!("assistant"));
703 assert_eq!(response["content"]["type"], json!("text"));
704 assert_eq!(response["content"]["text"], json!("done"));
705 assert_eq!(response["model"], json!("actual-model"));
706 }
707
708 #[test]
709 fn build_spec_response_default_stop_reason_is_end_turn() {
710 let parsed = parse_sampling_request(&minimal_request()).unwrap();
711 let response = build_spec_response(outcome("done", ""), &parsed);
712 assert_eq!(response["stopReason"], json!("endTurn"));
713 }
714
715 #[test]
716 fn build_spec_response_falls_back_to_hint_when_outcome_model_missing() {
717 let parsed = parse_sampling_request(&json!({
718 "messages": [{"role": "user", "content": {"type": "text", "text": "hi"}}],
719 "maxTokens": 4,
720 "modelPreferences": {"hints": [{"name": "claude-3-5-sonnet"}]},
721 }))
722 .unwrap();
723 let response = build_spec_response(outcome("done", ""), &parsed);
724 assert_eq!(response["model"], json!("claude-3-5-sonnet"));
725 }
726
727 #[tokio::test(flavor = "current_thread")]
728 async fn dispatch_with_no_bridge_declines() {
729 let request = json!({
730 "jsonrpc": "2.0",
731 "id": "s-1",
732 "method": SAMPLING_METHOD,
733 "params": minimal_request(),
734 });
735 let response = dispatch_inbound_sampling("mock", &request).await;
736 assert_eq!(response["id"], json!("s-1"));
737 assert_eq!(response["error"]["code"], json!(-32603));
738 assert_eq!(
739 response["error"]["data"]["type"],
740 json!("mcp.samplingDeclined")
741 );
742 }
743
744 #[tokio::test(flavor = "current_thread")]
745 async fn dispatch_with_invalid_params_returns_invalid_params() {
746 let request = json!({
747 "jsonrpc": "2.0",
748 "id": 1,
749 "method": SAMPLING_METHOD,
750 "params": {"messages": []},
751 });
752 let response = dispatch_inbound_sampling("mock", &request).await;
753 assert_eq!(response["id"], json!(1));
754 assert_eq!(response["error"]["code"], json!(-32602));
755 }
756
757 struct ApproveSamplingBridge {
763 overrides: crate::value::DictMap,
764 }
765
766 impl crate::stdlib::host::HostCallBridge for ApproveSamplingBridge {
767 fn dispatch(
768 &self,
769 capability: &str,
770 operation: &str,
771 _params: &crate::value::DictMap,
772 ) -> Result<Option<VmValue>, VmError> {
773 if capability == "mcp" && operation == "sample" {
774 let mut envelope: crate::value::DictMap = crate::value::DictMap::new();
775 envelope.put_str("action", "accept");
776 envelope.insert(
777 crate::value::intern_key("options"),
778 VmValue::dict(self.overrides.clone()),
779 );
780 Ok(Some(VmValue::dict(envelope)))
781 } else {
782 Ok(None)
783 }
784 }
785 }
786
787 #[tokio::test(flavor = "current_thread")]
788 async fn dispatch_with_mock_bridge_routes_to_llm_call() {
789 crate::llm::mock::reset_llm_mock_state();
791
792 crate::llm::mock::push_llm_mock(crate::llm::mock::LlmMock {
797 text: "sampled text".to_string(),
798 tool_calls: Vec::new(),
799 match_pattern: None,
800 consume_on_match: true,
801 input_tokens: None,
802 output_tokens: None,
803 cache_read_tokens: None,
804 cache_write_tokens: None,
805 thinking: None,
806 thinking_summary: None,
807 stop_reason: None,
808 model: "mock-model".to_string(),
809 provider: Some("mock".to_string()),
810 blocks: None,
811 logprobs: Vec::new(),
812 error: None,
813 stream_chunks: Vec::new(),
814 });
815
816 let mut overrides: crate::value::DictMap = crate::value::DictMap::new();
819 overrides.put_str("provider", "mock");
820 overrides.put_str("model", "mock-model");
821 crate::stdlib::host::set_host_call_bridge(Arc::new(ApproveSamplingBridge { overrides }));
822
823 let request = json!({
824 "jsonrpc": "2.0",
825 "id": 7,
826 "method": SAMPLING_METHOD,
827 "params": {
828 "messages": [
829 {"role": "user", "content": {"type": "text", "text": "ping"}}
830 ],
831 "maxTokens": 32,
832 "modelPreferences": {"hints": [{"name": "mock-model"}]},
833 },
834 });
835
836 let response = dispatch_inbound_sampling("test-server", &request).await;
837
838 crate::llm::mock::reset_llm_mock_state();
839 crate::stdlib::host::clear_host_call_bridge();
840
841 assert_eq!(response["id"], json!(7));
842 assert!(
843 response.get("result").is_some(),
844 "expected success result, got {response:?}"
845 );
846 assert_eq!(response["result"]["role"], json!("assistant"));
847 assert_eq!(response["result"]["content"]["type"], json!("text"));
848 assert_eq!(response["result"]["content"]["text"], json!("sampled text"));
849 assert_eq!(response["result"]["stopReason"], json!("endTurn"));
850 assert_eq!(response["result"]["model"], json!("mock-model"));
851 }
852}