1use crate::{
4 completion::{CompletionError, Usage},
5 message::ReasoningContent,
6 streaming::{RawStreamingChoice, RawStreamingToolCall, StreamFinal, ToolCallDeltaContent},
7};
8
9pub const MOCK_PROVIDER: &str = "mock";
11
12pub fn mock_final(usage: Usage) -> StreamFinal {
14 StreamFinal::new(MOCK_PROVIDER, usage)
15}
16
17fn fixture_additional_params(
21 value: serde_json::Value,
22) -> Result<Option<crate::message::AdditionalParams>, CompletionError> {
23 crate::message::AdditionalParams::try_from_value(value).map_err(|other| {
24 CompletionError::ProviderError(format!(
25 "mock stream fixture `additional_params` must be a JSON object, got: {other}"
26 ))
27 })
28}
29
30pub fn mock_final_with_total_tokens(total_tokens: u64) -> StreamFinal {
32 let mut usage = Usage::new();
33 usage.total_tokens = total_tokens;
34 mock_final(usage)
35}
36
37#[derive(Clone, Debug)]
39pub enum MockStreamEvent {
40 Text(String),
42 TextStart {
44 id: String,
45 additional_params: Option<serde_json::Value>,
46 },
47 TextAdditionalParams(serde_json::Value),
49 ToolCall {
51 id: String,
52 name: String,
53 arguments: serde_json::Value,
54 call_id: Option<String>,
55 },
56 ToolCallDelta {
58 id: String,
59 content: ToolCallDeltaContent,
60 },
61 Reasoning {
63 id: String,
64 content: ReasoningContent,
65 },
66 ReasoningDelta { id: String, reasoning: String },
68 MessageId(String),
70 Unknown(serde_json::Value),
72 FinalResponse(StreamFinal),
74 Error(MockError),
76}
77
78use super::completion::MockError;
79
80fn fixture_part_id(id: String) -> crate::streaming::StreamPartId {
89 use crate::streaming::MintKind;
90 for (namespace, kind) in [
91 ("reasoning-", MintKind::Reasoning),
92 ("block-", MintKind::Block),
93 ("output-", MintKind::Output),
94 ("tool-", MintKind::Tool),
95 ("text-", MintKind::Text),
96 ] {
97 if let Some(rest) = id.strip_prefix(namespace)
98 && let Ok(index) = rest.parse::<u64>()
99 {
100 return kind.for_wire_index(index);
101 }
102 }
103 crate::streaming::StreamPartId::wire(id)
104}
105
106impl MockStreamEvent {
107 pub fn text(text: impl Into<String>) -> Self {
109 Self::Text(text.into())
110 }
111
112 pub fn text_start(id: impl Into<String>, additional_params: Option<serde_json::Value>) -> Self {
114 Self::TextStart {
115 id: id.into(),
116 additional_params,
117 }
118 }
119
120 pub fn text_additional_params(additional_params: serde_json::Value) -> Self {
122 Self::TextAdditionalParams(additional_params)
123 }
124
125 pub fn tool_call(
127 id: impl Into<String>,
128 name: impl Into<String>,
129 arguments: serde_json::Value,
130 ) -> Self {
131 Self::ToolCall {
132 id: id.into(),
133 name: name.into(),
134 arguments,
135 call_id: None,
136 }
137 }
138
139 pub fn with_call_id(mut self, call_id: impl Into<String>) -> Self {
141 if let Self::ToolCall { call_id: id, .. } = &mut self {
142 *id = Some(call_id.into());
143 }
144 self
145 }
146
147 pub fn tool_call_name_delta(id: impl Into<String>, name: impl Into<String>) -> Self {
149 Self::ToolCallDelta {
150 id: id.into(),
151 content: ToolCallDeltaContent::Name(name.into()),
152 }
153 }
154
155 pub fn tool_call_arguments_delta(id: impl Into<String>, arguments: impl Into<String>) -> Self {
157 Self::ToolCallDelta {
158 id: id.into(),
159 content: ToolCallDeltaContent::Delta(arguments.into()),
160 }
161 }
162
163 pub fn reasoning(reasoning: impl Into<String>) -> Self {
167 Self::Reasoning {
168 id: "reasoning-0".to_string(),
169 content: ReasoningContent::Text {
170 text: reasoning.into(),
171 signature: None,
172 },
173 }
174 }
175
176 pub fn with_reasoning_id(mut self, reasoning_id: impl Into<String>) -> Self {
178 if let Self::Reasoning { id, .. } = &mut self {
179 *id = reasoning_id.into();
180 }
181 self
182 }
183
184 pub fn reasoning_delta(reasoning: impl Into<String>) -> Self {
188 Self::reasoning_delta_with_id("reasoning-0", reasoning)
189 }
190
191 pub fn reasoning_delta_with_id(id: impl Into<String>, reasoning: impl Into<String>) -> Self {
193 Self::ReasoningDelta {
194 id: id.into(),
195 reasoning: reasoning.into(),
196 }
197 }
198
199 pub fn message_id(id: impl Into<String>) -> Self {
201 Self::MessageId(id.into())
202 }
203
204 pub fn unknown(value: serde_json::Value) -> Self {
206 Self::Unknown(value)
207 }
208
209 pub fn final_response(usage: Usage) -> Self {
211 Self::FinalResponse(mock_final(usage))
212 }
213
214 pub fn final_response_with_default_usage() -> Self {
216 Self::FinalResponse(mock_final(Usage::new()))
217 }
218
219 pub fn final_response_with_total_tokens(total_tokens: u64) -> Self {
221 Self::FinalResponse(mock_final_with_total_tokens(total_tokens))
222 }
223
224 pub fn error(message: impl Into<String>) -> Self {
226 Self::Error(MockError::provider(message))
227 }
228
229 pub(crate) fn into_raw_choice(self) -> Result<RawStreamingChoice, CompletionError> {
230 match self {
231 Self::Text(text) => Ok(RawStreamingChoice::Message(text)),
232 Self::TextStart {
233 id,
234 additional_params,
235 } => Ok(RawStreamingChoice::TextStart {
236 id: fixture_part_id(id),
237 additional_params: additional_params
238 .map(fixture_additional_params)
239 .transpose()?
240 .flatten(),
241 }),
242 Self::TextAdditionalParams(additional_params) => {
243 match fixture_additional_params(additional_params)? {
244 None => Err(CompletionError::ProviderError(
247 "mock stream fixture `TextAdditionalParams` carries no data — \
248 drop the event instead"
249 .to_string(),
250 )),
251 Some(params) => Ok(RawStreamingChoice::TextAdditionalParams(params)),
252 }
253 }
254 Self::ToolCall {
255 id,
256 name,
257 arguments,
258 call_id,
259 } => {
260 let mut tool_call = RawStreamingToolCall::new(fixture_part_id(id), name, arguments);
261 if let Some(call_id) = call_id {
262 tool_call = tool_call.with_call_id(call_id);
263 }
264 Ok(RawStreamingChoice::ToolCall(tool_call))
265 }
266 Self::ToolCallDelta { id, content } => Ok(RawStreamingChoice::ToolCallDelta {
267 id: fixture_part_id(id),
268 content,
269 }),
270 Self::Reasoning { id, content } => {
271 let key = fixture_part_id(id.clone());
274 let provider_id = match &key {
275 key_is_wire if key_is_wire.wire_str().is_some() => {
276 crate::streaming::WireId::new(id)
277 }
278 _ => None,
279 };
280 Ok(RawStreamingChoice::Reasoning {
281 id: key,
282 provider_id,
283 content,
284 })
285 }
286 Self::ReasoningDelta { id, reasoning } => {
287 let key = fixture_part_id(id.clone());
288 let provider_id = match &key {
289 key_is_wire if key_is_wire.wire_str().is_some() => {
290 crate::streaming::WireId::new(id)
291 }
292 _ => None,
293 };
294 Ok(RawStreamingChoice::ReasoningDelta {
295 id: key,
296 provider_id,
297 reasoning,
298 })
299 }
300 Self::MessageId(id) => Ok(RawStreamingChoice::MessageId(id)),
301 Self::Unknown(value) => Ok(RawStreamingChoice::Unknown(value.into())),
302 Self::FinalResponse(response) => Ok(RawStreamingChoice::FinalResponse(response)),
303 Self::Error(error) => Err(error.into_completion_error()),
304 }
305 }
306}