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