1use aisdk::core::capabilities::{
4 ReasoningSupport, StructuredOutputSupport, TextInputSupport, TextOutputSupport,
5 ToolCallSupport,
6};
7use aisdk::core::language_model::{
8 LanguageModel, LanguageModelOptions, LanguageModelResponse, LanguageModelStreamChunk,
9 LanguageModelStreamChunkType,
10};
11use aisdk::core::Message;
12use aisdk::Result as AisdkResult;
13use async_trait::async_trait;
14use futures_util::stream;
15use std::pin::Pin;
16use std::sync::{Arc, Mutex};
17
18#[derive(Debug, Clone)]
20pub struct FakeCall {
21 pub system: Option<String>,
23 pub prompts: Vec<String>,
25}
26
27#[derive(Debug, Default)]
28struct FakeInner {
29 stubs: Vec<String>,
30 default: String,
31 calls: Vec<FakeCall>,
32}
33
34#[derive(Clone, Debug, Default)]
36pub struct FakeAi {
37 inner: Arc<Mutex<FakeInner>>,
38}
39
40impl FakeAi {
41 pub fn new() -> Self {
42 Self::default()
43 }
44
45 pub fn push_text(self, text: impl Into<String>) -> Self {
47 self.inner.lock().unwrap().stubs.push(text.into());
48 self
49 }
50
51 pub fn stub_text(self, text: impl Into<String>) -> Self {
53 self.inner.lock().unwrap().default = text.into();
54 self
55 }
56
57 fn next_text(&self) -> String {
58 let mut g = self.inner.lock().unwrap();
59 if !g.stubs.is_empty() {
60 g.stubs.remove(0)
61 } else {
62 g.default.clone()
63 }
64 }
65
66 fn record(&self, options: &LanguageModelOptions) {
67 let prompts = options
68 .messages()
69 .into_iter()
70 .filter_map(|m| match m {
71 Message::User(u) => Some(u.content),
72 Message::System(s) => Some(s.content),
73 _ => None,
74 })
75 .collect();
76 self.inner.lock().unwrap().calls.push(FakeCall {
77 system: options.system.clone(),
78 prompts,
79 });
80 }
81
82 pub fn calls(&self) -> Vec<FakeCall> {
84 self.inner.lock().unwrap().calls.clone()
85 }
86
87 pub fn prompts(&self) -> Vec<String> {
89 self.calls()
90 .into_iter()
91 .flat_map(|c| c.prompts)
92 .collect()
93 }
94
95 pub fn call_count(&self) -> usize {
96 self.inner.lock().unwrap().calls.len()
97 }
98
99 pub fn assert_called(&self) {
100 assert!(
101 self.call_count() > 0,
102 "FakeAi: expected at least one generate/stream call"
103 );
104 }
105
106 pub fn assert_called_times(&self, n: usize) {
107 assert_eq!(
108 self.call_count(),
109 n,
110 "FakeAi: expected {n} calls, got {}",
111 self.call_count()
112 );
113 }
114}
115
116impl TextInputSupport for FakeAi {}
117impl TextOutputSupport for FakeAi {}
118impl ToolCallSupport for FakeAi {}
119impl StructuredOutputSupport for FakeAi {}
120impl ReasoningSupport for FakeAi {}
121
122#[async_trait]
123impl LanguageModel for FakeAi {
124 fn name(&self) -> String {
125 "fake".into()
126 }
127
128 async fn generate_text(
129 &mut self,
130 options: LanguageModelOptions,
131 ) -> AisdkResult<LanguageModelResponse> {
132 self.record(&options);
133 Ok(LanguageModelResponse::new(self.next_text()))
134 }
135
136 async fn stream_text(
137 &mut self,
138 options: LanguageModelOptions,
139 ) -> AisdkResult<
140 Pin<Box<dyn futures_util::Stream<Item = AisdkResult<Vec<LanguageModelStreamChunk>>> + Send>>,
141 > {
142 self.record(&options);
143 let text = self.next_text();
144 let chunks = vec![
145 LanguageModelStreamChunk::Delta(LanguageModelStreamChunkType::Start),
146 LanguageModelStreamChunk::Delta(LanguageModelStreamChunkType::Text(text)),
147 LanguageModelStreamChunk::Delta(LanguageModelStreamChunkType::End(Default::default())),
148 ];
149 Ok(Box::pin(stream::once(async move { Ok(chunks) })))
150 }
151}