[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
+}