1use std::{
4 collections::VecDeque,
5 sync::{Arc, Mutex, MutexGuard},
6};
7
8use crate::driver::{Exchange, Model, Opened, Opening, Transport};
9use crate::error::{EncodeError, ProviderError};
10use crate::operation::Completion;
11use crate::wire::{Capabilities, Descriptor, Mode, Wire};
12use crate::{
13 completion::{AssistantContent, CompletionRequest, CompletionResponse, Usage},
14 message::{ToolCall, ToolFunction},
15};
16
17use super::streaming::{MOCK_PROVIDER, MockDecoder, MockDocument, MockFrame, MockStreamEvent};
18
19#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
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) -> ProviderError {
42 match self {
43 Self::Provider(message) => ProviderError::Provider(message),
44 Self::Request(message) => ProviderError::request(message),
45 Self::ProviderResponse(response) => ProviderError::ProviderResponse(response),
46 }
47 }
48}
49
50#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
55pub struct MockTurn {
56 response: Result<MockTurnResponse, MockError>,
57}
58
59#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
60struct MockTurnResponse {
61 choice: Vec<AssistantContent>,
62 usage: Usage,
63 response_id: Option<String>,
64 provider_request_id: Option<String>,
65 finish_reason: Option<crate::completion::FinishReason>,
66 #[serde(
71 default,
72 skip_serializing_if = "Option::is_none",
73 deserialize_with = "deserialize_scripted_raw"
74 )]
75 raw: Option<serde_json::Value>,
76}
77
78fn deserialize_scripted_raw<'de, D: serde::Deserializer<'de>>(
79 deserializer: D,
80) -> Result<Option<serde_json::Value>, D::Error> {
81 serde::Deserialize::deserialize(deserializer).map(Some)
83}
84
85impl MockTurn {
86 pub fn text(text: impl Into<String>) -> Self {
88 Self::from_content(AssistantContent::text(text.into()))
89 }
90
91 pub fn tool_call(
94 id: impl Into<String>,
95 name: impl Into<String>,
96 arguments: serde_json::Value,
97 ) -> Self {
98 match crate::message::ToolName::new(name) {
99 Ok(name) => Self::from_content(AssistantContent::ToolCall(ToolCall::from_wire(
100 id,
101 ToolFunction::new(name, arguments),
102 ))),
103 Err(error) => Self::error(error.to_string()),
104 }
105 }
106
107 pub fn error(message: impl Into<String>) -> Self {
109 Self {
110 response: Err(MockError::provider(message)),
111 }
112 }
113
114 pub fn provider_response_error(
118 status: http::StatusCode,
119 body: impl Into<String>,
120 request_id: impl Into<String>,
121 ) -> Self {
122 Self {
123 response: Err(MockError::ProviderResponse(
124 crate::provider_response::ProviderResponseError::new(status, body)
125 .with_provider_request_id(Some(request_id.into())),
126 )),
127 }
128 }
129
130 pub fn request_error(message: impl Into<String>) -> Self {
132 Self {
133 response: Err(MockError::request(message)),
134 }
135 }
136
137 pub fn from_content(content: AssistantContent) -> Self {
139 Self {
140 response: Ok(MockTurnResponse {
141 choice: vec![content],
142 usage: Usage::default(),
143 response_id: None,
144 provider_request_id: None,
145 finish_reason: None,
146 raw: None,
147 }),
148 }
149 }
150
151 pub fn from_contents(content: impl IntoIterator<Item = AssistantContent>) -> Self {
156 Self {
157 response: Ok(MockTurnResponse {
158 choice: content.into_iter().collect(),
159 usage: Usage::default(),
160 response_id: None,
161 provider_request_id: None,
162 finish_reason: None,
163 raw: None,
164 }),
165 }
166 }
167
168 pub fn with_call_id(mut self, call_id: impl Into<String>) -> Self {
170 let call_id = call_id.into();
171 if let Ok(response) = &mut self.response {
172 for content in response.choice.iter_mut() {
173 if let AssistantContent::ToolCall(tool_call) = content {
174 tool_call.id = crate::message::CallId::from_wire(call_id);
175 break;
176 }
177 }
178 }
179 self
180 }
181
182 pub fn with_usage(mut self, usage: Usage) -> Self {
184 if let Ok(response) = &mut self.response {
185 response.usage = usage;
186 }
187 self
188 }
189
190 pub fn with_response_id(mut self, response_id: impl Into<String>) -> Self {
192 if let Ok(response) = &mut self.response {
193 response.response_id = Some(response_id.into());
194 }
195 self
196 }
197
198 pub fn with_provider_request_id(mut self, request_id: impl Into<String>) -> Self {
200 if let Ok(response) = &mut self.response {
201 response.provider_request_id = Some(request_id.into());
202 }
203 self
204 }
205
206 pub fn with_finish_reason(mut self, finish_reason: crate::completion::FinishReason) -> Self {
213 if let Ok(response) = &mut self.response {
214 response.finish_reason = Some(finish_reason);
215 }
216 self
217 }
218
219 pub fn raw(&self) -> Result<serde_json::Value, ProviderError> {
225 let response = self
226 .response
227 .as_ref()
228 .map_err(|error| error.clone().into_completion_error())?;
229 match &response.raw {
230 Some(raw) => Ok(raw.clone()),
231 None => Ok(serde_json::to_value(response)?),
232 }
233 }
234
235 fn into_completion_response(self) -> Result<CompletionResponse, ProviderError> {
236 let raw = self.raw()?;
237 let response = self.response.map_err(MockError::into_completion_error)?;
238 let mut origin = crate::message::Origin::new(MOCK_API, MOCK_PROVIDER, "");
239 origin.response_id = response.response_id;
240 let mut completion = CompletionResponse::new(response.choice, response.usage, origin, raw)
241 .with_optional_finish_reason(response.finish_reason);
242 completion.provider_request_id = response.provider_request_id;
243 Ok(completion)
244 }
245}
246
247type MockInvocation = (CompletionRequest, Option<crate::observe::AdapterContext>);
248
249#[derive(Default)]
250struct MockScriptState {
251 turns: Mutex<VecDeque<MockTurn>>,
252 stream_turns: Mutex<VecDeque<Vec<MockStreamEvent>>>,
253 requests: Mutex<Vec<MockInvocation>>,
254}
255
256#[derive(Clone, Debug, PartialEq, Eq)]
261pub struct MockScript {
262 name: String,
263 id: Option<String>,
264 capabilities: Capabilities,
265 facts: crate::catalog::ModelFacts,
267}
268
269impl MockScript {
270 pub fn new(name: impl Into<String>) -> Self {
273 Self {
274 name: name.into(),
275 id: None,
276 capabilities: Capabilities::default(),
277 facts: crate::catalog::ModelFacts::default(),
278 }
279 }
280
281 pub fn with_id(mut self, id: impl Into<String>) -> Self {
283 self.id = Some(id.into());
284 self
285 }
286
287 pub fn with_capabilities(mut self, capabilities: Capabilities) -> Self {
289 self.capabilities = capabilities;
290 self
291 }
292
293 pub fn priced_as(self, spec: impl Into<std::sync::Arc<crate::catalog::ModelSpec>>) -> Self {
298 let spec = spec.into();
299 Self {
300 name: spec.provider.vendor().to_owned(),
301 id: Some(spec.id.clone()),
302 facts: crate::catalog::ModelFacts::new(spec),
303 ..self
304 }
305 }
306}
307
308pub const MOCK_API: crate::message::Api = crate::message::Api::from_static("mock.script");
310
311pub const MOCK_MODEL: &str = "mock-model";
313
314impl crate::completion::ReplayTarget for MockScript {
315 fn api(&self) -> crate::message::Api {
316 MOCK_API
317 }
318
319 fn map_options(
322 &self,
323 _request: &crate::completion::CompletionRequest,
324 fields: crate::completion::options::OptionFields<'_>,
325 ) -> crate::completion::options::OptionMap {
326 each_set(fields, || {
327 crate::completion::options::Mapping::Omit("a scripted reply ignores options")
328 })
329 }
330
331 fn states_finish_reason(&self) -> bool {
333 false
334 }
335
336 fn provider(&self) -> &str {
337 &self.name
338 }
339
340 fn model(&self) -> &str {
342 self.id.as_deref().unwrap_or(MOCK_MODEL)
343 }
344
345 fn accepts(&self, _model: &str) -> crate::completion::Accepts {
346 crate::completion::Accepts::ALL
347 }
348}
349
350impl Default for MockScript {
351 fn default() -> Self {
352 Self::new(MOCK_PROVIDER)
353 }
354}
355
356impl Wire for MockScript {
357 type Op = Completion;
358 type Payload = CompletionRequest;
359 type Frame = MockFrame;
360 type Event = MockFrame;
361 type Decoder<'id> = MockDecoder<'id>;
362 type Reassembler = MockDocument;
363
364 fn describe(&self) -> Descriptor<'_> {
365 Descriptor::new(&self.name)
366 .model(self.id.as_deref())
367 .capabilities(self.capabilities)
368 .facts(&self.facts)
369 .replay(self)
370 }
371
372 fn encode(
375 &self,
376 request: CompletionRequest,
377 _mode: Mode,
378 ) -> Result<CompletionRequest, EncodeError> {
379 Ok(request)
380 }
381
382 fn decoder<'id>(&self) -> MockDecoder<'id> {
383 MockDecoder::default()
384 }
385}
386
387#[derive(Clone, Default)]
394pub struct MockRuntime {
395 state: Arc<MockScriptState>,
396}
397
398pub type MockCompletionModel = Model<MockScript, MockRuntime>;
401
402impl MockCompletionModel {
403 pub fn text(text: impl Into<String>) -> Self {
405 Self::from_turns([MockTurn::text(text)])
406 }
407
408 pub fn from_turns(turns: impl IntoIterator<Item = MockTurn>) -> Self {
410 Self::scripted(turns.into_iter().collect(), VecDeque::new())
411 }
412
413 pub fn from_stream_turns(
415 stream_turns: impl IntoIterator<Item = impl IntoIterator<Item = MockStreamEvent>>,
416 ) -> Self {
417 Self::scripted(
418 VecDeque::new(),
419 stream_turns
420 .into_iter()
421 .map(|turn| turn.into_iter().collect())
422 .collect(),
423 )
424 }
425
426 fn scripted(turns: VecDeque<MockTurn>, stream_turns: VecDeque<Vec<MockStreamEvent>>) -> Self {
427 Model::new(
428 MockScript::default(),
429 MockRuntime {
430 state: Arc::new(MockScriptState {
431 turns: Mutex::new(turns),
432 stream_turns: Mutex::new(stream_turns),
433 requests: Mutex::new(Vec::new()),
434 }),
435 },
436 )
437 }
438
439 pub fn priced_as(mut self, spec: impl Into<std::sync::Arc<crate::catalog::ModelSpec>>) -> Self {
443 self.wire = self.wire.priced_as(spec);
444 self
445 }
446
447 pub fn requests(&self) -> Vec<CompletionRequest> {
449 self.transport
450 .requests_guard()
451 .iter()
452 .map(|(request, _)| request.clone())
453 .collect()
454 }
455
456 pub fn contexts(&self) -> Vec<Option<crate::observe::AdapterContext>> {
458 self.transport
459 .requests_guard()
460 .iter()
461 .map(|(_, context)| context.clone())
462 .collect()
463 }
464
465 pub fn request_count(&self) -> usize {
467 self.transport.requests_guard().len()
468 }
469
470 pub fn script(&self) -> Vec<MockTurn> {
473 lock(&self.transport.state.turns).iter().cloned().collect()
474 }
475}
476
477impl MockRuntime {
478 fn requests_guard(&self) -> MutexGuard<'_, Vec<MockInvocation>> {
479 lock(&self.state.requests)
480 }
481}
482
483fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
484 match mutex.lock() {
485 Ok(guard) => guard,
486 Err(poisoned) => poisoned.into_inner(),
487 }
488}
489
490impl Transport<MockScript> for MockRuntime {
491 fn send(&self, request: CompletionRequest, exchange: Exchange) -> Opening<MockFrame> {
492 let mode = exchange.mode;
493 self.requests_guard().push((request, exchange.observation));
494 match mode {
495 Mode::Unary => {
498 let Some(turn) = lock(&self.state.turns).pop_front() else {
499 return Opening::failed(ProviderError::Provider(
500 "mock completion model has no scripted completion turn".to_string(),
501 ));
502 };
503 match turn.into_completion_response() {
504 Ok(response) => {
505 let document = response.raw.clone();
506 let request_id = response.provider_request_id.clone();
507 Opening::ready(
508 Opened::new(futures::stream::iter([Ok(MockFrame::Response(
509 Box::new(response),
510 ))]))
511 .with_document(document)
512 .with_request_id(request_id),
513 )
514 }
515 Err(error) => Opening::ready(Opened::failed(error)),
516 }
517 }
518 Mode::Streaming => {
519 let Some(turn) = lock(&self.state.stream_turns).pop_front() else {
520 return Opening::failed(ProviderError::Provider(
521 "mock completion model has no scripted streaming turn".to_string(),
522 ));
523 };
524 let request_id = turn.iter().find_map(|event| match event {
525 MockStreamEvent::RequestId(id) => Some(id.clone()),
526 _ => None,
527 });
528 Opening::ready(
529 Opened::new(futures::stream::iter(
530 turn.into_iter()
531 .filter(|event| !matches!(event, MockStreamEvent::RequestId(_)))
532 .map(|event| Ok(MockFrame::Event(event))),
533 ))
534 .with_request_id(request_id),
535 )
536 }
537 }
538 }
539}
540
541#[cfg(test)]
542mod tests;
543
544pub fn refuse_options(
546 fields: crate::completion::options::OptionFields<'_>,
547) -> crate::completion::options::OptionMap {
548 each_set(fields, || {
549 crate::completion::options::Mapping::unsupported("the test target takes no options")
550 })
551}
552
553fn each_set(
555 fields: crate::completion::options::OptionFields<'_>,
556 answer: impl Fn() -> crate::completion::options::Mapping,
557) -> crate::completion::options::OptionMap {
558 use crate::completion::options::{Mapping, OptionMap};
559 let [
560 reasoning,
561 cache,
562 service_tier,
563 verbosity,
564 parallel_tool_calls,
565 top_p,
566 seed,
567 stop,
568 _,
569 ] = fields
570 .set()
571 .map(|set| if set { answer() } else { Mapping::Nothing });
572 OptionMap {
573 reasoning,
574 cache,
575 service_tier,
576 verbosity,
577 parallel_tool_calls,
578 top_p,
579 seed,
580 stop,
581 cache_key: Mapping::unrouted(fields.cache_key),
582 }
583}