1use std::{
4 collections::VecDeque,
5 sync::{Arc, Mutex, MutexGuard},
6};
7
8use crate::{
9 completion::{
10 AssistantContent, CompletionError, CompletionModel, CompletionRequest, CompletionResponse,
11 Usage,
12 },
13 message::{ToolCall, ToolFunction},
14 streaming::StreamingCompletionResponse,
15};
16
17use super::streaming::{MOCK_PROVIDER, MockStreamEvent};
18
19#[derive(Clone, Debug)]
21pub enum MockError {
22 Provider(String),
24 Request(String),
26 ProviderResponse(crate::provider_response::ProviderResponseError),
28}
29
30impl MockError {
31 pub fn provider(message: impl Into<String>) -> Self {
33 Self::Provider(message.into())
34 }
35
36 pub fn request(message: impl Into<String>) -> Self {
38 Self::Request(message.into())
39 }
40
41 pub(crate) fn into_completion_error(self) -> CompletionError {
42 match self {
43 Self::Provider(message) => CompletionError::ProviderError(message),
44 Self::Request(message) => CompletionError::RequestError(message.into()),
45 Self::ProviderResponse(response) => CompletionError::ProviderResponse(response),
46 }
47 }
48}
49
50#[derive(Clone, Debug)]
52pub struct MockTurn {
53 response: Result<MockTurnResponse, MockError>,
54}
55
56#[derive(Clone, Debug)]
57struct MockTurnResponse {
58 choice: Vec<AssistantContent>,
59 usage: Usage,
60 message_id: Option<String>,
61 response_id: Option<String>,
62 provider_request_id: Option<String>,
63 finish_reason: Option<crate::completion::FinishReason>,
64 raw: serde_json::Value,
65}
66
67impl MockTurn {
68 pub fn text(text: impl Into<String>) -> Self {
70 Self::from_content(AssistantContent::text(text.into()))
71 }
72
73 pub fn tool_call(
75 id: impl Into<String>,
76 name: impl Into<String>,
77 arguments: serde_json::Value,
78 ) -> Self {
79 Self::from_content(AssistantContent::ToolCall(ToolCall::from_wire(
80 id,
81 ToolFunction::new(name.into(), arguments),
82 )))
83 }
84
85 pub fn error(message: impl Into<String>) -> Self {
87 Self {
88 response: Err(MockError::provider(message)),
89 }
90 }
91
92 pub fn provider_response_error(
96 status: http::StatusCode,
97 body: impl Into<String>,
98 request_id: impl Into<String>,
99 ) -> Self {
100 Self {
101 response: Err(MockError::ProviderResponse(
102 crate::provider_response::ProviderResponseError::new(status, body)
103 .with_provider_request_id(Some(request_id.into())),
104 )),
105 }
106 }
107
108 pub fn request_error(message: impl Into<String>) -> Self {
110 Self {
111 response: Err(MockError::request(message)),
112 }
113 }
114
115 pub fn from_content(content: AssistantContent) -> Self {
117 Self {
118 response: Ok(MockTurnResponse {
119 choice: vec![content],
120 usage: Usage::new(),
121 message_id: None,
122 response_id: None,
123 provider_request_id: None,
124 finish_reason: None,
125 raw: serde_json::Value::Null,
126 }),
127 }
128 }
129
130 pub fn from_contents(content: impl IntoIterator<Item = AssistantContent>) -> Self {
135 Self {
136 response: Ok(MockTurnResponse {
137 choice: content.into_iter().collect(),
138 usage: Usage::new(),
139 message_id: None,
140 response_id: None,
141 provider_request_id: None,
142 finish_reason: None,
143 raw: serde_json::Value::Null,
144 }),
145 }
146 }
147
148 pub fn with_call_id(mut self, call_id: impl Into<String>) -> Self {
150 let call_id = call_id.into();
151 if let Ok(response) = &mut self.response {
152 for content in response.choice.iter_mut() {
153 if let AssistantContent::ToolCall(tool_call) = content {
154 tool_call.provider = crate::message::ProviderCallId::new(call_id);
155 break;
156 }
157 }
158 }
159 self
160 }
161
162 pub fn with_usage(mut self, usage: Usage) -> Self {
164 if let Ok(response) = &mut self.response {
165 response.usage = usage;
166 }
167 self
168 }
169
170 pub fn with_message_id(mut self, message_id: impl Into<String>) -> Self {
172 if let Ok(response) = &mut self.response {
173 response.message_id = Some(message_id.into());
174 }
175 self
176 }
177
178 pub fn with_response_id(mut self, response_id: impl Into<String>) -> Self {
180 if let Ok(response) = &mut self.response {
181 response.response_id = Some(response_id.into());
182 }
183 self
184 }
185
186 pub fn with_provider_request_id(mut self, request_id: impl Into<String>) -> Self {
188 if let Ok(response) = &mut self.response {
189 response.provider_request_id = Some(request_id.into());
190 }
191 self
192 }
193
194 pub fn with_finish_reason(mut self, finish_reason: crate::completion::FinishReason) -> Self {
201 if let Ok(response) = &mut self.response {
202 response.finish_reason = Some(finish_reason);
203 }
204 self
205 }
206
207 pub fn with_raw(mut self, raw: serde_json::Value) -> Self {
214 if let Ok(response) = &mut self.response {
215 response.raw = raw;
216 }
217 self
218 }
219
220 fn into_completion_response(self) -> Result<CompletionResponse, CompletionError> {
221 let response = self.response.map_err(MockError::into_completion_error)?;
222 Ok(
223 CompletionResponse::new(response.choice, response.usage, MOCK_PROVIDER)
224 .with_optional_message_id(response.message_id)
225 .with_optional_response_id(response.response_id)
226 .with_optional_provider_request_id(response.provider_request_id)
227 .with_optional_finish_reason(response.finish_reason)
228 .with_raw(response.raw),
229 )
230 }
231}
232
233#[derive(Default)]
234struct MockCompletionModelState {
235 turns: Mutex<VecDeque<MockTurn>>,
236 stream_turns: Mutex<VecDeque<Vec<MockStreamEvent>>>,
237 requests: Mutex<Vec<CompletionRequest>>,
238}
239
240#[derive(Clone, Default)]
246pub struct MockCompletionModel {
247 state: Arc<MockCompletionModelState>,
248}
249
250impl MockCompletionModel {
251 pub fn new(turns: impl IntoIterator<Item = MockTurn>) -> Self {
253 Self::from_turns(turns)
254 }
255
256 pub fn text(text: impl Into<String>) -> Self {
258 Self::from_turns([MockTurn::text(text)])
259 }
260
261 pub fn from_turns(turns: impl IntoIterator<Item = MockTurn>) -> Self {
263 Self {
264 state: Arc::new(MockCompletionModelState {
265 turns: Mutex::new(turns.into_iter().collect()),
266 stream_turns: Mutex::new(VecDeque::new()),
267 requests: Mutex::new(Vec::new()),
268 }),
269 }
270 }
271
272 pub fn from_stream_turns(
274 stream_turns: impl IntoIterator<Item = impl IntoIterator<Item = MockStreamEvent>>,
275 ) -> Self {
276 Self {
277 state: Arc::new(MockCompletionModelState {
278 turns: Mutex::new(VecDeque::new()),
279 stream_turns: Mutex::new(
280 stream_turns
281 .into_iter()
282 .map(|turn| turn.into_iter().collect())
283 .collect(),
284 ),
285 requests: Mutex::new(Vec::new()),
286 }),
287 }
288 }
289
290 pub fn requests(&self) -> Vec<CompletionRequest> {
292 self.requests_guard().clone()
293 }
294
295 pub fn request_count(&self) -> usize {
297 self.requests_guard().len()
298 }
299
300 fn record_request(&self, request: CompletionRequest) {
301 self.requests_guard().push(request);
302 }
303
304 fn next_turn(&self) -> Option<MockTurn> {
305 self.turns_guard().pop_front()
306 }
307
308 fn next_stream_turn(&self) -> Option<Vec<MockStreamEvent>> {
309 self.stream_turns_guard().pop_front()
310 }
311
312 fn turns_guard(&self) -> MutexGuard<'_, VecDeque<MockTurn>> {
313 match self.state.turns.lock() {
314 Ok(guard) => guard,
315 Err(poisoned) => poisoned.into_inner(),
316 }
317 }
318
319 fn stream_turns_guard(&self) -> MutexGuard<'_, VecDeque<Vec<MockStreamEvent>>> {
320 match self.state.stream_turns.lock() {
321 Ok(guard) => guard,
322 Err(poisoned) => poisoned.into_inner(),
323 }
324 }
325
326 fn requests_guard(&self) -> MutexGuard<'_, Vec<CompletionRequest>> {
327 match self.state.requests.lock() {
328 Ok(guard) => guard,
329 Err(poisoned) => poisoned.into_inner(),
330 }
331 }
332}
333
334impl CompletionModel for MockCompletionModel {
335 async fn completion(
336 &self,
337 request: CompletionRequest,
338 ) -> Result<CompletionResponse, CompletionError> {
339 self.record_request(request);
340 let Some(turn) = self.next_turn() else {
341 return Err(CompletionError::ProviderError(
342 "mock completion model has no scripted completion turn".to_string(),
343 ));
344 };
345
346 turn.into_completion_response()
347 }
348
349 async fn stream(
350 &self,
351 request: CompletionRequest,
352 ) -> Result<StreamingCompletionResponse, CompletionError> {
353 self.record_request(request);
354 let Some(events) = self.next_stream_turn() else {
355 return Err(CompletionError::ProviderError(
356 "mock completion model has no scripted streaming turn".to_string(),
357 ));
358 };
359
360 let stream = async_stream::stream! {
361 for event in events {
362 yield event.into_raw_choice();
363 }
364 };
365 let stream = crate::streaming::normalize_stream(Box::pin(stream), Ok);
371 Ok(StreamingCompletionResponse::stream(MOCK_PROVIDER, stream))
372 }
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378 use crate::{
379 message::Message,
380 streaming::{StreamFinal, StreamedAssistantContent, ToolCallDeltaContent},
381 };
382 use futures::StreamExt;
383
384 fn request(prompt: &str) -> CompletionRequest {
385 CompletionRequest {
386 model: None,
387 preamble: None,
388 chat_history: vec![Message::user(prompt)],
389 documents: Vec::new(),
390 tools: Vec::new(),
391 temperature: None,
392 max_tokens: None,
393 tool_choice: None,
394 additional_params: None,
395 output_schema: None,
396 record_telemetry_content: false,
397 }
398 }
399
400 #[tokio::test]
401 async fn completion_consumes_scripted_turns_and_records_requests() {
402 let model = MockCompletionModel::new([
403 MockTurn::text("first").with_message_id("msg_1"),
404 MockTurn::tool_call("tool_1", "calculator", serde_json::json!({"x": 1}))
405 .with_call_id("call_1"),
406 ]);
407
408 let first = model
409 .completion(request("hello"))
410 .await
411 .expect("first scripted turn should succeed");
412 assert_eq!(first.message_id.as_deref(), Some("msg_1"));
413 assert!(matches!(
414 first.choice.first(),
415 Some(AssistantContent::Text(text)) if text.text == "first"
416 ));
417
418 let second = model
419 .completion(request("use a tool"))
420 .await
421 .expect("second scripted turn should succeed");
422 assert!(matches!(
423 second.choice.first(),
424 Some(AssistantContent::ToolCall(tool_call))
425 if tool_call.id == "tool_1"
426 && tool_call
427 .provider
428 .as_ref()
429 .is_some_and(|provider| provider.call_id == "call_1")
430 ));
431
432 assert_eq!(model.request_count(), 2);
433 assert_eq!(model.requests().len(), 2);
434 }
435
436 #[tokio::test]
441 async fn completion_attaches_scripted_raw_and_reports_null_when_unscripted() {
442 let payload = serde_json::json!({"provider_only": "kept", "id": "resp_1"});
443 let model = MockCompletionModel::new([
444 MockTurn::text("first").with_raw(payload.clone()),
445 MockTurn::text("second"),
446 ]);
447
448 let scripted = model
449 .completion(request("hello"))
450 .await
451 .expect("first scripted turn should succeed");
452 assert_eq!(scripted.raw, payload);
453
454 let unscripted = model
455 .completion(request("hello"))
456 .await
457 .expect("second scripted turn should succeed");
458 assert!(unscripted.raw.is_null());
459
460 assert_eq!(model.requests().len(), 2);
461 }
462
463 #[tokio::test]
468 async fn stream_terminal_raw_is_the_scripted_terminal_serialized() {
469 let model = MockCompletionModel::from_stream_turns([vec![
470 MockStreamEvent::text("hello"),
471 MockStreamEvent::final_response(Usage {
472 input_tokens: 1,
473 output_tokens: 2,
474 total_tokens: 3,
475 ..Usage::new()
476 }),
477 ]]);
478
479 let mut stream = model
480 .stream(request("hello"))
481 .await
482 .expect("stream should open");
483 while stream.next().await.is_some() {}
484 let terminal = stream.response.expect("terminal record");
485 let raw = &terminal.raw;
486 let typed: StreamFinal = serde_json::from_value(raw.clone()).expect("terminal type");
487 assert_eq!(typed.usage.total_tokens, 3);
488 assert!(
489 typed.raw.is_null(),
490 "the scripted terminal itself carried no raw (Value::Null)"
491 );
492 assert_eq!(
493 serde_json::to_value(&typed).expect("re-serialize"),
494 *raw,
495 "the capture must be exactly what the scripted terminal serializes to"
496 );
497 assert_eq!(terminal.usage.total_tokens, 3);
498 }
499
500 #[tokio::test]
501 async fn missing_completion_turn_returns_provider_error() {
502 let model = MockCompletionModel::default();
503
504 let err = model
505 .completion(request("hello"))
506 .await
507 .expect_err("missing turn should error");
508
509 assert!(matches!(
510 err,
511 CompletionError::ProviderError(message)
512 if message.contains("no scripted completion turn")
513 ));
514 }
515
516 #[tokio::test]
517 async fn stream_yields_scripted_events_and_records_requests() {
518 let model = MockCompletionModel::from_stream_turns([[
519 MockStreamEvent::message_id("msg_stream"),
520 MockStreamEvent::text("hel"),
521 MockStreamEvent::text("lo"),
522 MockStreamEvent::tool_call_name_delta("tool_1", "calculator"),
523 MockStreamEvent::tool_call_arguments_delta("tool_1", "{\"x\":1}"),
524 MockStreamEvent::tool_call("tool_1", "calculator", serde_json::json!({"x": 1}))
525 .with_call_id("call_1"),
526 MockStreamEvent::final_response_with_total_tokens(7),
527 ]]);
528
529 let mut stream = model
530 .stream(request("stream"))
531 .await
532 .expect("stream should be created");
533
534 let mut text = String::new();
535 let mut saw_name_delta = false;
536 let mut saw_arguments_delta = false;
537 let mut saw_tool_call = false;
538 let mut saw_final = false;
539
540 while let Some(item) = stream.next().await {
541 match item.expect("stream event should succeed") {
542 StreamedAssistantContent::Text(chunk) => text.push_str(&chunk.text),
543 StreamedAssistantContent::ToolCallDelta { content, .. } => match content {
544 ToolCallDeltaContent::Name(name) => {
545 saw_name_delta = name == "calculator";
546 }
547 ToolCallDeltaContent::Delta(arguments) => {
548 saw_arguments_delta = arguments == "{\"x\":1}";
549 }
550 },
551 StreamedAssistantContent::ToolCall { tool_call, .. } => {
552 saw_tool_call = tool_call
553 .provider
554 .as_ref()
555 .is_some_and(|provider| provider.call_id == "call_1");
556 }
557 StreamedAssistantContent::Final(response) => {
558 saw_final = matches!(
559 response.usage,
560 Usage {
561 total_tokens: 7,
562 ..
563 }
564 );
565 }
566 _ => {}
567 }
568 }
569
570 assert_eq!(text, "hello");
571 assert!(saw_name_delta);
572 assert!(saw_arguments_delta);
573 assert!(saw_tool_call);
574 assert!(saw_final);
575 assert_eq!(stream.message_id.as_deref(), Some("msg_stream"));
576 assert_eq!(model.request_count(), 1);
577 }
578
579 #[tokio::test]
580 async fn stream_error_event_is_returned() {
581 let model = MockCompletionModel::from_stream_turns([[MockStreamEvent::error("boom")]]);
582 let mut stream = model
583 .stream(request("stream"))
584 .await
585 .expect("stream should be created");
586
587 let err = stream
588 .next()
589 .await
590 .expect("stream should yield one event")
591 .expect_err("scripted event should error");
592
593 assert!(matches!(
594 err,
595 CompletionError::ProviderError(message) if message == "boom"
596 ));
597 }
598}