[Rust Frontend] Extract renderer fixture test utilities (#46719)

Signed-off-by: Bugen Zhao <i@bugenzhao.com>

Bugen Zhao committed Jun 26, 2026 at 08:12 UTC 1d3f4cb3a4d0a500b479f990b8f2793d0a1a0b2f
9 files changed +288 -346
rust/Cargo.lock
+1
@@ -5722,6 +5722,7 @@ dependencies = [
5722 "serde_json",
5723 "serde_with",
5724 "serial_test",
5725 + "strum",
5726 "subenum",
5727 "tempfile",
5728 "thiserror 2.0.18",
rust/Cargo.toml
+1
@@ -79,6 +79,7 @@ serde_with = "3.18.0"
79 serial_test = { version = "3.2.0", features = ["file_locks"] }
80 sha2 = "0.10.9"
81 socket2 = "0.6.3"
82 +strum = { version = "0.27.2", features = ["derive"] }
83 subenum = "1.1.3"
84 subtle = "2.6"
85 task-local = "0.1.1"
rust/src/chat/Cargo.toml
+1
@@ -21,6 +21,7 @@ serde.workspace = true
21 serde-json-fmt.workspace = true
22 serde_json.workspace = true
23 serde_with.workspace = true
24 +strum.workspace = true
25 subenum.workspace = true
26 thiserror.workspace = true
27 thiserror-ext.workspace = true
rust/src/chat/src/multimodal.rs
+7 -8
@@ -245,16 +245,15 @@ pub(crate) async fn finalize_rendered_prompt(
245 return Ok((rendered.prompt, None));
246 }
247 let info = info.ok_or(Error::UnsupportedMultimodalRenderer)?;
248 - let Prompt::Text(prompt) = rendered.prompt else {
249 - bail_multimodal!("multimodal chat renderer must return a text prompt before expansion");
248 + let mut prompt_token_ids = match rendered.prompt {
249 + Prompt::Text(prompt) => info
250 + .context
251 + .tokenizer()
252 + .encode(&prompt, request.add_special_tokens)
253 + .map_err(|error| multimodal!("{error}"))?,
254 + Prompt::TokenIds(token_ids) => token_ids,
255 };
256 let media_parts = extract_media_parts(request)?;
252 -
253 - let mut prompt_token_ids = info
254 - .context
255 - .tokenizer()
256 - .encode(&prompt, request.add_special_tokens)
257 - .map_err(|error| multimodal!("{error}"))?;
257 let prepared = info.prepare_multimodal(media_parts, &mut prompt_token_ids, model_dtype).await?;
258
259 Ok((Prompt::TokenIds(prompt_token_ids), Some(prepared)))
rust/src/chat/src/renderer/deepseek_v32/tests.rs
+7 -145
@@ -1,82 +1,18 @@
1 -use std::fs;
1 use std::path::PathBuf;
2
3 use expect_test::{ExpectFile, expect, expect_file};
5 -use serde::Deserialize;
4 use serde_json::{Value, json};
5 use thiserror_ext::AsReport;
6
7 use super::DeepSeekV32ChatRenderer;
8 use crate::error::Error;
9 use crate::event::{AssistantContentBlock, AssistantToolCall};
10 +use crate::renderer::test_utils::{FixtureRequestOptions, fixture_chat_request};
11 use crate::request::{
12 ChatContentPart, ChatMessage, ChatRequest, ChatTool, ChatToolChoice, GenerationPromptMode,
13 };
14 use crate::{ChatRenderer, ChatRole};
15
17 -#[derive(Debug, Deserialize)]
18 -struct FixtureRequest {
19 - #[serde(default)]
20 - tools: Vec<FixtureTool>,
21 - messages: Vec<FixtureMessage>,
22 -}
23 -
24 -#[derive(Debug, Deserialize)]
25 -struct FixtureTool {
26 - function: FixtureToolFunction,
27 -}
28 -
29 -#[derive(Debug, Deserialize)]
30 -struct FixtureToolFunction {
31 - name: String,
32 - description: Option<String>,
33 - parameters: Value,
34 - #[serde(default)]
35 - strict: Option<bool>,
36 -}
37 -
38 -#[derive(Debug, Deserialize)]
39 -#[serde(tag = "role", rename_all = "snake_case")]
40 -enum FixtureMessage {
41 - System {
42 - content: String,
43 - },
44 - Developer {
45 - content: String,
46 - #[serde(default)]
47 - tools: Vec<FixtureTool>,
48 - },
49 - User {
50 - content: String,
51 - },
52 - Assistant {
53 - #[serde(default)]
54 - content: String,
55 - #[serde(default)]
56 - reasoning_content: String,
57 - #[serde(default)]
58 - tool_calls: Vec<FixtureToolCall>,
59 - },
60 - Tool {
61 - content: String,
62 - #[serde(default)]
63 - tool_call_id: Option<String>,
64 - },
65 -}
66 -
67 -#[derive(Debug, Deserialize)]
68 -struct FixtureToolCall {
69 - #[serde(default)]
70 - id: Option<String>,
71 - function: FixtureToolCallFunction,
72 -}
73 -
74 -#[derive(Debug, Deserialize)]
75 -struct FixtureToolCallFunction {
76 - name: String,
77 - arguments: String,
78 -}
79 -
16 fn render_request(request: &ChatRequest) -> String {
17 DeepSeekV32ChatRenderer::new()
18 .render(request)
@@ -115,88 +51,14 @@ fn thinking_request(messages: Vec<ChatMessage>) -> ChatRequest {
51 }
52
53 fn fixture_request(input_name: &str) -> ChatRequest {
118 - let fixture = fs::read_to_string(fixture_path(input_name)).unwrap();
119 - let fixture: FixtureRequest = serde_json::from_str(&fixture).unwrap();
120 - let mut request = ChatRequest {
121 - request_id: "deepseek-v32-fixture".to_string(),
122 - messages: fixture
123 - .messages
124 - .into_iter()
125 - .enumerate()
126 - .map(|(index, message)| match message {
127 - FixtureMessage::System { content } => ChatMessage::system(content),
128 - FixtureMessage::Developer { content, tools } => ChatMessage::developer(
129 - content,
130 - (!tools.is_empty()).then(|| to_chat_tools(&tools)),
131 - ),
132 - FixtureMessage::User { content } => ChatMessage::user(content),
133 - FixtureMessage::Assistant {
134 - content,
135 - reasoning_content,
136 - tool_calls,
137 - } => {
138 - let mut blocks = Vec::new();
139 - if !reasoning_content.is_empty() {
140 - blocks.push(AssistantContentBlock::Reasoning {
141 - text: reasoning_content,
142 - });
143 - }
144 - if !content.is_empty() {
145 - blocks.push(AssistantContentBlock::Text { text: content });
146 - }
147 - blocks.extend(tool_calls.into_iter().enumerate().map(
148 - |(tool_index, tool_call)| {
149 - AssistantContentBlock::ToolCall(AssistantToolCall {
150 - id: tool_call.id.unwrap_or_else(|| {
151 - format!("fixture-tool-call-{index}-{tool_index}")
152 - }),
153 - name: tool_call.function.name,
154 - arguments: tool_call.function.arguments,
155 - })
156 - },
157 - ));
158 - ChatMessage::assistant_blocks(blocks)
159 - }
160 - FixtureMessage::Tool {
161 - content,
162 - tool_call_id,
163 - } => ChatMessage::tool_response(
164 - content,
165 - tool_call_id.unwrap_or_else(|| format!("fixture-tool-response-{index}")),
166 - ),
167 - })
168 - .collect(),
169 - tools: to_chat_tools(&fixture.tools),
170 - tool_choice: if fixture.tools.is_empty() {
171 - ChatToolChoice::None
172 - } else {
173 - ChatToolChoice::Auto
174 - },
175 - ..ChatRequest::for_test()
176 - };
177 - if matches!(
178 - request.messages.last().map(ChatMessage::role),
179 - Some(ChatRole::Assistant)
180 - ) {
181 - request.chat_options.generation_prompt_mode = GenerationPromptMode::NoGenerationPrompt;
182 - }
183 - request
184 - .chat_options
185 - .template_kwargs
186 - .insert("thinking".to_string(), Value::Bool(true));
187 - request
54 + fixture_chat_request(&fixture_path(input_name), deepseek_fixture_options())
55 }
56
190 -fn to_chat_tools(tools: &[FixtureTool]) -> Vec<ChatTool> {
191 - tools
192 - .iter()
193 - .map(|tool| ChatTool {
194 - name: tool.function.name.clone(),
195 - description: tool.function.description.clone(),
196 - parameters: tool.function.parameters.clone(),
197 - strict: tool.function.strict,
198 - })
199 - .collect()
57 +fn deepseek_fixture_options() -> FixtureRequestOptions {
58 + FixtureRequestOptions {
59 + enable_thinking: true,
60 + no_generation_prompt_when_last_assistant: true,
61 + }
62 }
63
64 fn fixture_path(name: &str) -> PathBuf {
rust/src/chat/src/renderer/deepseek_v4/tests.rs
+9 -165
@@ -1,95 +1,13 @@
1 -use std::fs;
1 use std::path::PathBuf;
2
3 use expect_test::{ExpectFile, expect, expect_file};
5 -use serde::Deserialize;
4 use serde_json::Value;
5
6 use super::DeepSeekV4ChatRenderer;
7 +use crate::ChatRenderer;
8 use crate::event::{AssistantContentBlock, AssistantToolCall};
10 -use crate::request::{
11 - ChatMessage, ChatRequest, ChatTool, ChatToolChoice, GenerationPromptMode, ReasoningEffort,
12 -};
13 -use crate::{ChatRenderer, ChatRole};
14 -
15 -#[derive(Debug, Deserialize)]
16 -#[serde(untagged)]
17 -enum FixtureFile {
18 - WithTools(FixtureRequest),
19 - MessagesOnly(Vec<FixtureMessage>),
20 -}
21 -
22 -#[derive(Debug, Deserialize)]
23 -struct FixtureRequest {
24 - #[serde(default)]
25 - tools: Vec<FixtureTool>,
26 - messages: Vec<FixtureMessage>,
27 -}
28 -
29 -impl FixtureFile {
30 - fn into_parts(self) -> (Vec<FixtureTool>, Vec<FixtureMessage>) {
31 - match self {
32 - Self::WithTools(req) => (req.tools, req.messages),
33 - Self::MessagesOnly(messages) => (Vec::new(), messages),
34 - }
35 - }
36 -}
37 -
38 -#[derive(Debug, Deserialize)]
39 -struct FixtureTool {
40 - function: FixtureToolFunction,
41 -}
42 -
43 -#[derive(Debug, Deserialize)]
44 -struct FixtureToolFunction {
45 - name: String,
46 - description: Option<String>,
47 - parameters: Value,
48 - #[serde(default)]
49 - strict: Option<bool>,
50 -}
51 -
52 -#[derive(Debug, Deserialize)]
53 -#[serde(tag = "role", rename_all = "snake_case")]
54 -enum FixtureMessage {
55 - System {
56 - content: String,
57 - },
58 - Developer {
59 - content: String,
60 - #[serde(default)]
61 - tools: Vec<FixtureTool>,
62 - },
63 - User {
64 - content: String,
65 - },
66 - Assistant {
67 - #[serde(default)]
68 - content: String,
69 - #[serde(default)]
70 - reasoning_content: String,
71 - #[serde(default)]
72 - tool_calls: Vec<FixtureToolCall>,
73 - },
74 - Tool {
75 - content: String,
76 - #[serde(default)]
77 - tool_call_id: Option<String>,
78 - },
79 -}
80 -
81 -#[derive(Debug, Deserialize)]
82 -struct FixtureToolCall {
83 - #[serde(default)]
84 - id: Option<String>,
85 - function: FixtureToolCallFunction,
86 -}
87 -
88 -#[derive(Debug, Deserialize)]
89 -struct FixtureToolCallFunction {
90 - name: String,
91 - arguments: String,
92 -}
9 +use crate::renderer::test_utils::{FixtureRequestOptions, fixture_chat_request};
10 +use crate::request::{ChatMessage, ChatRequest, GenerationPromptMode, ReasoningEffort};
11
12 fn render_request(request: &ChatRequest) -> String {
13 DeepSeekV4ChatRenderer::new()
@@ -101,88 +19,14 @@ fn render_request(request: &ChatRequest) -> String {
19 }
20
21 fn fixture_request(input_name: &str) -> ChatRequest {
104 - let fixture = fs::read_to_string(fixture_path(input_name)).unwrap();
105 - let fixture: FixtureFile = serde_json::from_str(&fixture).unwrap();
106 - let (fixture_tools, fixture_messages) = fixture.into_parts();
107 - let mut request = ChatRequest {
108 - request_id: "deepseek-v4-fixture".to_string(),
109 - messages: fixture_messages
110 - .into_iter()
111 - .enumerate()
112 - .map(|(index, message)| match message {
113 - FixtureMessage::System { content } => ChatMessage::system(content),
114 - FixtureMessage::Developer { content, tools } => ChatMessage::developer(
115 - content,
116 - (!tools.is_empty()).then(|| to_chat_tools(&tools)),
117 - ),
118 - FixtureMessage::User { content } => ChatMessage::user(content),
119 - FixtureMessage::Assistant {
120 - content,
121 - reasoning_content,
122 - tool_calls,
123 - } => {
124 - let mut blocks = Vec::new();
125 - if !reasoning_content.is_empty() {
126 - blocks.push(AssistantContentBlock::Reasoning {
127 - text: reasoning_content,
128 - });
129 - }
130 - if !content.is_empty() {
131 - blocks.push(AssistantContentBlock::Text { text: content });
132 - }
133 - blocks.extend(tool_calls.into_iter().enumerate().map(
134 - |(tool_index, tool_call)| {
135 - AssistantContentBlock::ToolCall(AssistantToolCall {
136 - id: tool_call.id.unwrap_or_else(|| {
137 - format!("fixture-tool-call-{index}-{tool_index}")
138 - }),
139 - name: tool_call.function.name,
140 - arguments: tool_call.function.arguments,
141 - })
142 - },
143 - ));
144 - ChatMessage::assistant_blocks(blocks)
145 - }
146 - FixtureMessage::Tool {
147 - content,
148 - tool_call_id,
149 - } => ChatMessage::tool_response(
150 - content,
151 - tool_call_id.unwrap_or_else(|| format!("fixture-tool-response-{index}")),
152 - ),
153 - })
154 - .collect(),
155 - tools: to_chat_tools(&fixture_tools),
156 - tool_choice: if fixture_tools.is_empty() {
157 - ChatToolChoice::None
158 - } else {
159 - ChatToolChoice::Auto
160 - },
161 - ..ChatRequest::for_test()
162 - };
163 - if matches!(
164 - request.messages.last().map(ChatMessage::role),
165 - Some(ChatRole::Assistant)
166 - ) {
167 - request.chat_options.generation_prompt_mode = GenerationPromptMode::NoGenerationPrompt;
168 - }
169 - request
170 - .chat_options
171 - .template_kwargs
172 - .insert("thinking".to_string(), Value::Bool(true));
173 - request
22 + fixture_chat_request(&fixture_path(input_name), deepseek_fixture_options())
23 }
24
176 -fn to_chat_tools(tools: &[FixtureTool]) -> Vec<ChatTool> {
177 - tools
178 - .iter()
179 - .map(|tool| ChatTool {
180 - name: tool.function.name.clone(),
181 - description: tool.function.description.clone(),
182 - parameters: tool.function.parameters.clone(),
183 - strict: tool.function.strict,
184 - })
185 - .collect()
25 +fn deepseek_fixture_options() -> FixtureRequestOptions {
26 + FixtureRequestOptions {
27 + enable_thinking: true,
28 + no_generation_prompt_when_last_assistant: true,
29 + }
30 }
31
32 fn fixture_path(name: &str) -> PathBuf {
rust/src/chat/src/renderer/mod.rs
+2
@@ -11,6 +11,8 @@ pub mod deepseek_v32;
11 pub mod deepseek_v4;
12 pub mod hf;
13 mod selection;
14 +#[cfg(test)]
15 +mod test_utils;
16
17 pub use deepseek_v4::DeepSeekV4ChatRenderer;
18 pub use deepseek_v32::DeepSeekV32ChatRenderer;
rust/src/chat/src/renderer/selection.rs
+21 -28
@@ -1,10 +1,14 @@
1 use std::fmt;
2 use std::str::FromStr;
3
4 +use itertools::Itertools;
5 use serde_with::{DeserializeFromStr, SerializeDisplay};
6 +use strum::{EnumIter, IntoEnumIterator};
7
8 /// Specify which chat renderer implementation to use.
7 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, DeserializeFromStr, SerializeDisplay)]
9 +#[derive(
10 + Debug, Clone, Copy, PartialEq, Eq, Default, DeserializeFromStr, SerializeDisplay, EnumIter,
11 +)]
12 pub enum RendererSelection {
13 /// Use model-based auto-detection.
14 #[default]
@@ -51,7 +55,8 @@ impl FromStr for RendererSelection {
55 Ok(Self::DeepSeekV4)
56 } else {
57 Err(format!(
54 - "unknown renderer `{value}` (expected one of: auto, hf, deepseek_v32, deepseek_v4)"
58 + "unknown renderer `{value}` (expected one of: {})",
59 + Self::iter().join(", ")
60 ))
61 }
62 }
@@ -70,40 +75,28 @@ impl fmt::Display for RendererSelection {
75
76 #[cfg(test)]
77 mod tests {
73 - use super::RendererSelection;
78 + use std::str::FromStr as _;
79
75 - #[test]
76 - fn renderer_selection_parses_known_values() {
77 - assert_eq!(
78 - "auto".parse::<RendererSelection>().unwrap(),
79 - RendererSelection::Auto
80 - );
81 - assert_eq!(
82 - "hf".parse::<RendererSelection>().unwrap(),
83 - RendererSelection::Hf
84 - );
85 - assert_eq!(
86 - "deepseek_v32".parse::<RendererSelection>().unwrap(),
87 - RendererSelection::DeepSeekV32
88 - );
89 - assert_eq!(
90 - "deepseek_v4".parse::<RendererSelection>().unwrap(),
91 - RendererSelection::DeepSeekV4
92 - );
93 - }
80 + use strum::IntoEnumIterator;
81 +
82 + use super::RendererSelection;
83
84 #[test]
85 fn renderer_selection_display_round_trips() {
97 - for selection in [
98 - RendererSelection::Auto,
99 - RendererSelection::Hf,
100 - RendererSelection::DeepSeekV32,
101 - RendererSelection::DeepSeekV4,
102 - ] {
86 + for selection in RendererSelection::iter() {
87 assert_eq!(
88 selection.to_string().parse::<RendererSelection>().unwrap(),
89 selection
90 );
91 }
92 }
93 +
94 + #[test]
95 + fn renderer_selection_expected_error_message() {
96 + let err = RendererSelection::from_str("unknown").unwrap_err();
97 + expect_test::expect![
98 + "unknown renderer `unknown` (expected one of: auto, hf, deepseek_v32, deepseek_v4)"
99 + ]
100 + .assert_eq(&err);
101 + }
102 }
rust/src/chat/src/renderer/test_utils.rs new
+239
@@ -0,0 +1,239 @@
1 +use std::fs;
2 +use std::path::Path;
3 +
4 +use serde::Deserialize;
5 +use serde_json::Value;
6 +
7 +use crate::event::{AssistantContentBlock, AssistantToolCall};
8 +use crate::request::{
9 + ChatContent, ChatContentPart, ChatMessage, ChatRequest, ChatTool, ChatToolChoice,
10 + GenerationPromptMode,
11 +};
12 +
13 +/// Options for constructing a [`ChatRequest`] from a fixture file.
14 +#[derive(Debug, Clone, Copy)]
15 +pub(crate) struct FixtureRequestOptions {
16 + /// Whether to set the template kwarg `[enable_]thinking=true`.
17 + pub enable_thinking: bool,
18 + /// Whether fixtures ending in an assistant message should omit the
19 + /// trailing generation prompt.
20 + pub no_generation_prompt_when_last_assistant: bool,
21 +}
22 +
23 +/// Read a fixture file from the given path and convert it into a [`ChatRequest`]
24 +/// using the provided options.
25 +pub(crate) fn fixture_chat_request(path: &Path, options: FixtureRequestOptions) -> ChatRequest {
26 + let fixture = fs::read_to_string(path).unwrap();
27 + let fixture: FixtureFile = serde_json::from_str(&fixture).unwrap();
28 + fixture.into_request().into_chat_request(options)
29 +}
30 +
31 +/// Fixture file format for chat-renderer tests.
32 +#[derive(Debug, Deserialize)]
33 +#[serde(untagged)]
34 +pub(crate) enum FixtureFile {
35 + WithRequest(FixtureRequest),
36 + MessagesOnly(Vec<FixtureMessage>),
37 +}
38 +
39 +#[derive(Debug, Deserialize)]
40 +pub(crate) struct FixtureRequest {
41 + #[serde(default)]
42 + tools: Vec<FixtureTool>,
43 + messages: Vec<FixtureMessage>,
44 + add_generation_prompt: Option<bool>,
45 +}
46 +
47 +impl FixtureFile {
48 + fn into_request(self) -> FixtureRequest {
49 + match self {
50 + Self::WithRequest(request) => request,
51 + Self::MessagesOnly(messages) => FixtureRequest {
52 + tools: Vec::new(),
53 + messages,
54 + add_generation_prompt: None,
55 + },
56 + }
57 + }
58 +}
59 +
60 +#[derive(Debug, Deserialize)]
61 +#[serde(tag = "role", rename_all = "snake_case")]
62 +pub(crate) enum FixtureMessage {
63 + System {
64 + content: FixtureContent,
65 + },
66 + Developer {
67 + content: FixtureContent,
68 + #[serde(default)]
69 + tools: Vec<FixtureTool>,
70 + },
71 + User {
72 + content: FixtureContent,
73 + },
74 + Assistant {
75 + #[serde(default)]
76 + content: String,
77 + #[serde(default)]
78 + reasoning_content: String,
79 + #[serde(default)]
80 + tool_calls: Vec<FixtureToolCall>,
81 + },
82 + Tool {
83 + content: FixtureContent,
84 + #[serde(default)]
85 + tool_call_id: Option<String>,
86 + },
87 +}
88 +
89 +#[derive(Debug, Deserialize)]
90 +#[serde(untagged)]
91 +pub(crate) enum FixtureContent {
92 + Text(String),
93 + Parts(Vec<FixtureContentPart>),
94 +}
95 +
96 +#[derive(Debug, Deserialize)]
97 +#[serde(tag = "type", rename_all = "snake_case")]
98 +pub(crate) enum FixtureContentPart {
99 + Text { text: String },
100 + ImageUrl { image_url: String },
101 +}
102 +
103 +#[derive(Debug, Deserialize)]
104 +pub(crate) struct FixtureTool {
105 + function: FixtureToolFunction,
106 +}
107 +
108 +#[derive(Debug, Deserialize)]
109 +struct FixtureToolFunction {
110 + name: String,
111 + description: Option<String>,
112 + parameters: Value,
113 + #[serde(default)]
114 + strict: Option<bool>,
115 +}
116 +
117 +#[derive(Debug, Deserialize)]
118 +pub(crate) struct FixtureToolCall {
119 + #[serde(default)]
120 + id: Option<String>,
121 + function: FixtureToolCallFunction,
122 +}
123 +
124 +#[derive(Debug, Deserialize)]
125 +struct FixtureToolCallFunction {
126 + name: String,
127 + arguments: String,
128 +}
129 +
130 +impl FixtureRequest {
131 + fn into_chat_request(self, options: FixtureRequestOptions) -> ChatRequest {
132 + let mut request = ChatRequest {
133 + request_id: "renderer-fixture".to_string(),
134 + messages: self
135 + .messages
136 + .into_iter()
137 + .enumerate()
138 + .map(|(index, message)| fixture_message_to_chat_message(index, message))
139 + .collect(),
140 + tools: to_chat_tools(&self.tools),
141 + tool_choice: if self.tools.is_empty() {
142 + ChatToolChoice::None
143 + } else {
144 + ChatToolChoice::Auto
145 + },
146 + ..ChatRequest::for_test()
147 + };
148 +
149 + if options.no_generation_prompt_when_last_assistant
150 + && matches!(request.messages.last(), Some(ChatMessage::Assistant { .. }))
151 + {
152 + request.chat_options.generation_prompt_mode = GenerationPromptMode::NoGenerationPrompt;
153 + }
154 + if self.add_generation_prompt == Some(false) {
155 + request.chat_options.generation_prompt_mode = GenerationPromptMode::NoGenerationPrompt;
156 + }
157 + if options.enable_thinking {
158 + for key in ["thinking", "enable_thinking"] {
159 + request.chat_options.template_kwargs.insert(key.to_string(), Value::Bool(true));
160 + }
161 + }
162 +
163 + request
164 + }
165 +}
166 +
167 +fn fixture_message_to_chat_message(index: usize, message: FixtureMessage) -> ChatMessage {
168 + match message {
169 + FixtureMessage::System { content } => ChatMessage::system(to_chat_content(content)),
170 + FixtureMessage::Developer { content, tools } => ChatMessage::developer(
171 + to_chat_content(content),
172 + (!tools.is_empty()).then(|| to_chat_tools(&tools)),
173 + ),
174 + FixtureMessage::User { content } => ChatMessage::user(to_chat_content(content)),
175 + FixtureMessage::Assistant {
176 + content,
177 + reasoning_content,
178 + tool_calls,
179 + } => {
180 + let mut blocks = Vec::new();
181 + if !reasoning_content.is_empty() {
182 + blocks.push(AssistantContentBlock::Reasoning {
183 + text: reasoning_content,
184 + });
185 + }
186 + if !content.is_empty() {
187 + blocks.push(AssistantContentBlock::Text { text: content });
188 + }
189 + blocks.extend(
190 + tool_calls.into_iter().enumerate().map(|(tool_index, tool_call)| {
191 + AssistantContentBlock::ToolCall(AssistantToolCall {
192 + id: tool_call
193 + .id
194 + .unwrap_or_else(|| format!("fixture-tool-call-{index}-{tool_index}")),
195 + name: tool_call.function.name,
196 + arguments: tool_call.function.arguments,
197 + })
198 + }),
199 + );
200 + ChatMessage::assistant_blocks(blocks)
201 + }
202 + FixtureMessage::Tool {
203 + content,
204 + tool_call_id,
205 + } => ChatMessage::tool_response(
206 + to_chat_content(content),
207 + tool_call_id.unwrap_or_else(|| format!("fixture-tool-response-{index}")),
208 + ),
209 + }
210 +}
211 +
212 +fn to_chat_content(content: FixtureContent) -> ChatContent {
213 + match content {
214 + FixtureContent::Text(text) => ChatContent::Text(text),
215 + FixtureContent::Parts(parts) => ChatContent::Parts(
216 + parts
217 + .into_iter()
218 + .map(|part| match part {
219 + FixtureContentPart::Text { text } => ChatContentPart::text(text),
220 + FixtureContentPart::ImageUrl { image_url } => {
221 + ChatContentPart::image_url(image_url)
222 + }
223 + })
224 + .collect(),
225 + ),
226 + }
227 +}
228 +
229 +fn to_chat_tools(tools: &[FixtureTool]) -> Vec<ChatTool> {
230 + tools
231 + .iter()
232 + .map(|tool| ChatTool {
233 + name: tool.function.name.clone(),
234 + description: tool.function.description.clone(),
235 + parameters: tool.function.parameters.clone(),
236 + strict: tool.function.strict,
237 + })
238 + .collect()
239 +}