1use crate::message::{Message, ToolCall};
13use crate::provider::{
14 ChatRequest, ChatResponse, FinishReason, Provider, ProviderCapabilities, ProviderError,
15 ProviderRequestContext, StreamEvent, TimeoutStage, Usage,
16};
17use futures::stream::BoxStream;
18use std::collections::VecDeque;
19use std::sync::{Arc, Mutex};
20
21const EXHAUSTED_MSG: &str = "fake provider: script exhausted";
23
24#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum FakeReply {
27 Text(String),
29 TextWithReasoning {
32 content: String,
34 reasoning: String,
36 },
37 ToolCalls {
40 content: String,
42 calls: Vec<ToolCall>,
44 },
45 WithUsage {
50 reply: Box<FakeReply>,
52 usage: Usage,
54 },
55 Error(ProviderError),
57}
58
59impl FakeReply {
60 pub fn text_with_usage(content: impl Into<String>, usage: Usage) -> Self {
62 Self::WithUsage {
63 reply: Box::new(Self::Text(content.into())),
64 usage,
65 }
66 }
67}
68
69#[derive(Debug, Default)]
105pub struct FakeProvider {
106 replies: Mutex<VecDeque<FakeReply>>,
108 requests: Mutex<Vec<ChatRequest>>,
111}
112
113impl Clone for FakeProvider {
114 fn clone(&self) -> Self {
118 let replies = self
121 .replies
122 .lock()
123 .expect("FakeProvider internal lock poisoned")
124 .clone();
125 let requests = self
126 .requests
127 .lock()
128 .expect("FakeProvider internal lock poisoned")
129 .clone();
130 Self {
131 replies: Mutex::new(replies),
132 requests: Mutex::new(requests),
133 }
134 }
135}
136
137impl FakeProvider {
138 pub fn new(replies: impl IntoIterator<Item = FakeReply>) -> Self {
141 Self {
142 replies: Mutex::new(replies.into_iter().collect()),
143 requests: Mutex::new(Vec::new()),
144 }
145 }
146
147 pub fn push(&self, reply: FakeReply) {
150 self.replies
151 .lock()
152 .expect("FakeProvider internal lock poisoned")
153 .push_back(reply);
154 }
155
156 pub fn requests(&self) -> Vec<ChatRequest> {
180 self.requests
181 .lock()
182 .expect("FakeProvider internal lock poisoned")
183 .clone()
184 }
185}
186
187#[async_trait::async_trait]
188impl Provider for FakeProvider {
189 fn capabilities(&self) -> ProviderCapabilities {
190 ProviderCapabilities {
191 streaming: true,
192 reasoning: true,
193 tool_calls: true,
194 parallel_tool_calls: true,
195 structured_output: true,
196 usage: true,
197 context_cancellation: true,
198 context_deadline: true,
199 }
200 }
201
202 async fn chat(&self, request: ChatRequest) -> Result<ChatResponse, ProviderError> {
203 self.requests
204 .lock()
205 .expect("FakeProvider internal lock poisoned")
206 .push(request);
207 match self
208 .replies
209 .lock()
210 .expect("FakeProvider internal lock poisoned")
211 .pop_front()
212 {
213 None => Err(ProviderError::Protocol {
214 message: EXHAUSTED_MSG.to_string(),
215 }),
216 Some(reply) => chat_response(reply),
217 }
218 }
219
220 async fn chat_with_context(
221 &self,
222 request: ChatRequest,
223 context: &ProviderRequestContext,
224 ) -> Result<ChatResponse, ProviderError> {
225 check_context(context)?;
226 self.chat(request).await
227 }
228
229 async fn stream_chat(
230 &self,
231 request: ChatRequest,
232 ) -> Result<BoxStream<'static, Result<StreamEvent, ProviderError>>, ProviderError> {
233 self.requests
234 .lock()
235 .expect("FakeProvider internal lock poisoned")
236 .push(request);
237 let (reply, usage_override) = match self
241 .replies
242 .lock()
243 .expect("FakeProvider internal lock poisoned")
244 .pop_front()
245 {
246 None => {
247 return Err(ProviderError::Protocol {
248 message: EXHAUSTED_MSG.to_string(),
249 });
250 }
251 Some(reply) => unwrap_usage(reply),
252 };
253 let mut events = stream_events(reply)?;
254 if let Some(usage) = usage_override {
255 if let Some(Ok(StreamEvent::Done {
258 usage: done_usage, ..
259 })) = events.last_mut()
260 {
261 *done_usage = Some(usage);
262 }
263 }
264 Ok(Box::pin(futures::stream::iter(events)))
265 }
266
267 async fn stream_chat_with_context(
268 &self,
269 request: ChatRequest,
270 context: &ProviderRequestContext,
271 ) -> Result<BoxStream<'static, Result<StreamEvent, ProviderError>>, ProviderError> {
272 check_context(context)?;
273 self.stream_chat(request).await
274 }
275}
276
277fn check_context(context: &ProviderRequestContext) -> Result<(), ProviderError> {
278 if context.is_cancelled() {
279 Err(ProviderError::Cancelled)
280 } else if context.is_expired() {
281 Err(ProviderError::Timeout(TimeoutStage::Request))
282 } else {
283 Ok(())
284 }
285}
286
287fn chat_response(reply: FakeReply) -> Result<ChatResponse, ProviderError> {
293 match reply {
294 FakeReply::Error(e) => Err(e),
297 FakeReply::WithUsage { reply, usage } => {
298 let mut response = chat_response(*reply)?;
299 response.usage = Some(usage);
300 Ok(response)
301 }
302 FakeReply::Text(content) => Ok(ChatResponse {
303 message: Message::assistant(content),
304 finish_reason: FinishReason::Stop,
305 usage: None,
306 }),
307 FakeReply::TextWithReasoning { content, reasoning } => Ok(ChatResponse {
308 message: Message::assistant_with_reasoning(content, reasoning),
309 finish_reason: FinishReason::Stop,
310 usage: None,
311 }),
312 FakeReply::ToolCalls { content, calls } => Ok(ChatResponse {
313 message: Message::Assistant {
314 content,
315 reasoning: None,
316 tool_calls: calls,
317 },
318 finish_reason: FinishReason::Stop,
319 usage: None,
320 }),
321 }
322}
323
324fn unwrap_usage(reply: FakeReply) -> (FakeReply, Option<Usage>) {
328 match reply {
329 FakeReply::WithUsage { reply, usage } => {
330 let (inner, inner_usage) = unwrap_usage(*reply);
331 (inner, Some(usage).or(inner_usage))
332 }
333 other => (other, None),
334 }
335}
336
337fn stream_events(
345 reply: FakeReply,
346) -> Result<Vec<Result<StreamEvent, ProviderError>>, ProviderError> {
347 match reply {
348 FakeReply::Error(e) => Err(e),
351 FakeReply::Text(content) => Ok(vec![
352 Ok(StreamEvent::Delta(content)),
353 Ok(StreamEvent::Done {
354 reason: FinishReason::Stop,
355 usage: None,
356 }),
357 ]),
358 FakeReply::TextWithReasoning { content, reasoning } => Ok(vec![
361 Ok(StreamEvent::Delta(content)),
362 Ok(StreamEvent::Reasoning(reasoning)),
363 Ok(StreamEvent::Done {
364 reason: FinishReason::Stop,
365 usage: None,
366 }),
367 ]),
368 FakeReply::ToolCalls { content, calls } => {
369 let mut events = Vec::new();
370 if !content.is_empty() {
371 events.push(Ok(StreamEvent::Delta(content)));
372 }
373 events.extend(calls.into_iter().map(|c| {
374 Ok(StreamEvent::ToolCall {
375 id: c.id,
376 name: c.name,
377 arguments: c.arguments,
378 })
379 }));
380 events.push(Ok(StreamEvent::Done {
381 reason: FinishReason::Stop,
382 usage: None,
383 }));
384 Ok(events)
385 }
386 FakeReply::WithUsage { .. } => Err(ProviderError::Protocol {
390 message: "internal error: WithUsage must be unwrapped before stream_events".into(),
391 }),
392 }
393}
394
395#[async_trait::async_trait]
399impl Provider for Arc<FakeProvider> {
400 fn capabilities(&self) -> ProviderCapabilities {
401 self.as_ref().capabilities()
402 }
403
404 async fn chat(&self, request: ChatRequest) -> Result<ChatResponse, ProviderError> {
405 self.as_ref().chat(request).await
406 }
407
408 async fn chat_with_context(
409 &self,
410 request: ChatRequest,
411 context: &ProviderRequestContext,
412 ) -> Result<ChatResponse, ProviderError> {
413 self.as_ref().chat_with_context(request, context).await
414 }
415
416 async fn stream_chat(
417 &self,
418 request: ChatRequest,
419 ) -> Result<BoxStream<'static, Result<StreamEvent, ProviderError>>, ProviderError> {
420 self.as_ref().stream_chat(request).await
421 }
422
423 async fn stream_chat_with_context(
424 &self,
425 request: ChatRequest,
426 context: &ProviderRequestContext,
427 ) -> Result<BoxStream<'static, Result<StreamEvent, ProviderError>>, ProviderError> {
428 self.as_ref()
429 .stream_chat_with_context(request, context)
430 .await
431 }
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437 use crate::message::Message;
438 use futures::StreamExt;
439
440 #[tokio::test]
441 async fn chat_consumes_script_in_order() {
442 let fake = FakeProvider::new([FakeReply::Text("a".into()), FakeReply::Text("b".into())]);
443
444 let r1 = fake.chat(ChatRequest::default()).await.unwrap();
445 assert_eq!(r1.message, Message::assistant("a"));
446 assert_eq!(r1.finish_reason, FinishReason::Stop);
447
448 let r2 = fake.chat(ChatRequest::default()).await.unwrap();
449 assert_eq!(r2.message, Message::assistant("b"));
450 }
451
452 #[tokio::test]
453 async fn chat_returns_error_when_script_exhausted() {
454 let fake = FakeProvider::new([FakeReply::Text("only".into())]);
455 fake.chat(ChatRequest::default()).await.unwrap();
456
457 let err = fake.chat(ChatRequest::default()).await.unwrap_err();
458 assert!(matches!(err, ProviderError::Protocol { message: m } if m.contains("exhausted")));
459
460 assert_eq!(fake.requests().len(), 2);
463 }
464
465 #[tokio::test]
466 async fn chat_with_text_and_reasoning() {
467 let fake = FakeProvider::new([FakeReply::TextWithReasoning {
468 content: "answer".into(),
469 reasoning: "think".into(),
470 }]);
471
472 let r = fake.chat(ChatRequest::default()).await.unwrap();
473 assert_eq!(
474 r.message,
475 Message::assistant_with_reasoning("answer", "think")
476 );
477 }
478
479 #[tokio::test]
480 async fn chat_with_tool_calls() {
481 let calls = vec![ToolCall {
482 id: "c1".into(),
483 name: "add".into(),
484 arguments: r#"{"a":1,"b":2}"#.into(),
485 }];
486 let fake = FakeProvider::new([FakeReply::ToolCalls {
487 content: String::new(),
488 calls: calls.clone(),
489 }]);
490
491 let r = fake.chat(ChatRequest::default()).await.unwrap();
492 match r.message {
493 Message::Assistant {
494 content,
495 reasoning,
496 tool_calls,
497 } => {
498 assert_eq!(content, "");
499 assert_eq!(reasoning, None);
500 assert_eq!(tool_calls, calls);
501 }
502 other => panic!("expected assistant, got {other:?}"),
503 }
504 }
505
506 #[tokio::test]
507 async fn chat_with_usage_override() {
508 let usage = Usage::new(7, 3);
509 let fake = FakeProvider::new([FakeReply::text_with_usage("hi", usage)]);
510
511 let r = fake.chat(ChatRequest::default()).await.unwrap();
512 assert_eq!(r.message, Message::assistant("hi"));
513 assert_eq!(r.usage, Some(usage));
514 }
515
516 #[tokio::test]
517 async fn stream_with_usage_override() {
518 let usage = Usage::new(7, 3);
519 let fake = FakeProvider::new([FakeReply::text_with_usage("hi", usage)]);
520
521 let mut stream = fake.stream_chat(ChatRequest::default()).await.unwrap();
522 let events: Vec<StreamEvent> = stream.by_ref().map(|e| e.unwrap()).collect().await;
523 assert_eq!(
526 events.last(),
527 Some(&StreamEvent::Done {
528 reason: FinishReason::Stop,
529 usage: Some(usage),
530 })
531 );
532 }
533
534 #[tokio::test]
535 async fn stream_nested_usage_override_no_panic() {
536 let usage = Usage::new(7, 3);
539 let fake = FakeProvider::new([FakeReply::WithUsage {
540 reply: Box::new(FakeReply::WithUsage {
541 reply: Box::new(FakeReply::Text("hi".into())),
542 usage: Usage::new(1, 1),
543 }),
544 usage,
545 }]);
546
547 let mut stream = fake.stream_chat(ChatRequest::default()).await.unwrap();
548 let events: Vec<StreamEvent> = stream.by_ref().map(|e| e.unwrap()).collect().await;
549 assert_eq!(events[0], StreamEvent::Delta("hi".into()));
550 assert_eq!(
551 events.last(),
552 Some(&StreamEvent::Done {
553 reason: FinishReason::Stop,
554 usage: Some(usage),
555 })
556 );
557 }
558
559 #[tokio::test]
560 async fn error_reply_passthrough_and_script_continues() {
561 let fake = FakeProvider::new([
562 FakeReply::Error(ProviderError::RateLimited { retry_after: None }),
563 FakeReply::Text("ok".into()),
564 ]);
565
566 let err = fake.chat(ChatRequest::default()).await.unwrap_err();
568 assert!(matches!(err, ProviderError::RateLimited { .. }));
569
570 let r = fake.chat(ChatRequest::default()).await.unwrap();
573 assert_eq!(r.message, Message::assistant("ok"));
574 }
575
576 #[tokio::test]
577 async fn stream_chat_text_reply() {
578 let fake = FakeProvider::new([FakeReply::TextWithReasoning {
579 content: "hi".into(),
580 reasoning: "think".into(),
581 }]);
582
583 let mut stream = fake.stream_chat(ChatRequest::default()).await.unwrap();
584 assert_eq!(
587 stream.next().await.unwrap().unwrap(),
588 StreamEvent::Delta("hi".into())
589 );
590 assert_eq!(
591 stream.next().await.unwrap().unwrap(),
592 StreamEvent::Reasoning("think".into())
593 );
594 assert_eq!(
595 stream.next().await.unwrap().unwrap(),
596 StreamEvent::Done {
597 reason: FinishReason::Stop,
598 usage: None,
599 }
600 );
601 assert!(stream.next().await.is_none());
602 }
603
604 #[tokio::test]
605 async fn stream_chat_tool_call_reply() {
606 let fake = FakeProvider::new([FakeReply::ToolCalls {
607 content: "thinking aloud".into(),
608 calls: vec![ToolCall {
609 id: "c1".into(),
610 name: "add".into(),
611 arguments: r#"{"a":1}"#.into(),
612 }],
613 }]);
614
615 let mut stream = fake.stream_chat(ChatRequest::default()).await.unwrap();
616 assert_eq!(
619 stream.next().await.unwrap().unwrap(),
620 StreamEvent::Delta("thinking aloud".into())
621 );
622 assert_eq!(
623 stream.next().await.unwrap().unwrap(),
624 StreamEvent::ToolCall {
625 id: "c1".into(),
626 name: "add".into(),
627 arguments: r#"{"a":1}"#.into(),
628 }
629 );
630 assert_eq!(
631 stream.next().await.unwrap().unwrap(),
632 StreamEvent::Done {
633 reason: FinishReason::Stop,
634 usage: None,
635 }
636 );
637 assert!(stream.next().await.is_none());
638 }
639
640 #[tokio::test]
641 async fn stream_chat_error_reply_returns_err() {
642 let fake = FakeProvider::new([FakeReply::Error(ProviderError::Api {
643 status: 400,
644 code: None,
645 message: "boom".into(),
646 })]);
647 match fake.stream_chat(ChatRequest::default()).await {
650 Err(ProviderError::Api {
651 status: 400,
652 code: None,
653 message: m,
654 }) => assert_eq!(m, "boom"),
655 Ok(_) => panic!("expected error"),
656 Err(other) => panic!("expected Api error, got {other:?}"),
657 }
658 }
659
660 #[tokio::test]
661 async fn stream_chat_exhausted_returns_err() {
662 let fake = FakeProvider::new([FakeReply::Text("only".into())]);
663 drop(fake.stream_chat(ChatRequest::default()).await.unwrap());
666
667 match fake.stream_chat(ChatRequest::default()).await {
668 Err(ProviderError::Protocol { message: m }) => assert!(m.contains("exhausted")),
669 Ok(_) => panic!("expected error"),
670 Err(other) => panic!("expected Protocol error, got {other:?}"),
671 }
672 }
673
674 #[tokio::test]
675 async fn requests_records_messages_passed() {
676 let fake = FakeProvider::new([FakeReply::Text("hi".into())]);
677 let messages = vec![Message::system("sys"), Message::user("hello")];
678
679 fake.chat(ChatRequest {
680 messages: messages.clone(),
681 ..Default::default()
682 })
683 .await
684 .unwrap();
685
686 let requests = fake.requests();
687 assert_eq!(requests.len(), 1);
688 assert_eq!(requests[0].messages, messages);
689 }
690
691 #[tokio::test]
692 async fn push_appends_to_script() {
693 let fake = FakeProvider::new([FakeReply::Text("first".into())]);
694 fake.push(FakeReply::Text("second".into()));
695
696 let r1 = fake.chat(ChatRequest::default()).await.unwrap();
697 assert_eq!(r1.message, Message::assistant("first"));
698 assert_eq!(r1.usage, None);
701 let r2 = fake.chat(ChatRequest::default()).await.unwrap();
702 assert_eq!(r2.message, Message::assistant("second"));
703 assert_eq!(r2.usage, None);
704 }
705
706 #[tokio::test]
707 async fn works_as_boxed_dyn_provider() {
708 let fake: Box<dyn Provider> = Box::new(FakeProvider::new([FakeReply::Text("hi".into())]));
711 let r = fake.chat(ChatRequest::default()).await.unwrap();
712 assert_eq!(r.message, Message::assistant("hi"));
713 }
714}