use std::borrow::Cow;
use std::collections::{HashMap, VecDeque};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use crate::{
CapabilitySupport, ChatCapability, ChatProvider, ChatRequest, ChatResponse, ChatStream,
ChatStreamExt, ContentBlock, Error, ExtraMap, FinishReason, Message, OtherContent,
ProviderData, ProviderIdentity, ResponseFormat, Result, SingleResponseStream, StreamEvent,
ToolCall, ToolChoice, Usage, Warning,
};
type Capabilities = HashMap<ChatCapability, CapabilitySupport>;
fn capability(map: &Capabilities, capability: ChatCapability) -> CapabilitySupport {
map.get(&capability).copied().unwrap_or_default()
}
#[derive(Debug, Clone)]
pub struct MockProvider {
state: Arc<Mutex<MockChatState>>,
chat_capabilities: Capabilities,
provider_name: Cow<'static, str>,
}
#[derive(Debug, Default)]
struct MockChatState {
responses: VecDeque<MockResponse>,
requests: Vec<ChatRequest>,
}
impl MockProvider {
#[must_use]
pub fn build(configure: impl FnOnce(MockProviderBuilder) -> MockProviderBuilder) -> Self {
configure(MockProviderBuilder::new()).build()
}
#[must_use]
pub fn empty() -> Self {
Self::new(std::iter::empty::<MockResponse>())
}
#[must_use]
pub fn new<I, R>(responses: I) -> Self
where
I: IntoIterator<Item = R>,
R: Into<MockResponse>,
{
Self {
state: Arc::new(Mutex::new(MockChatState {
responses: responses.into_iter().map(Into::into).collect(),
requests: Vec::new(),
})),
chat_capabilities: Capabilities::new(),
provider_name: Cow::Borrowed("mock"),
}
}
#[must_use]
pub fn with_response(response: ChatResponse) -> Self {
Self::new([response])
}
#[must_use]
pub fn with_error(error: Error) -> Self {
Self::new([MockResponse::Error(error)])
}
#[must_use]
pub fn with_text(text: impl Into<String>) -> Self {
Self::new([MockResponse::text(text)])
}
#[must_use]
pub fn with_tool_call(
id: impl Into<String>,
name: impl Into<String>,
arguments: serde_json::Value,
) -> Self {
Self::new([MockResponse::tool_call(id, name, arguments)])
}
#[must_use]
pub fn tool_round_trip(
id: impl Into<String>,
name: impl Into<String>,
arguments: serde_json::Value,
final_text: impl Into<String>,
) -> Self {
Self::new([
MockResponse::tool_call(id, name, arguments),
MockResponse::text(final_text),
])
}
#[must_use]
pub fn with_chat_capability(
mut self,
capability: ChatCapability,
support: CapabilitySupport,
) -> Self {
self.chat_capabilities.insert(capability, support);
self
}
#[must_use]
pub fn with_supported_chat_capabilities<I>(mut self, capabilities: I) -> Self
where
I: IntoIterator<Item = ChatCapability>,
{
for capability in capabilities {
self.chat_capabilities
.insert(capability, CapabilitySupport::Supported);
}
self
}
#[must_use]
pub fn with_provider_name(mut self, provider_name: impl Into<Cow<'static, str>>) -> Self {
self.provider_name = provider_name.into();
self
}
pub fn push_response(&self, response: impl Into<MockResponse>) {
self.state
.lock()
.unwrap()
.responses
.push_back(response.into());
}
#[must_use]
pub fn requests(&self) -> Vec<ChatRequest> {
self.state.lock().unwrap().requests.clone()
}
#[must_use]
pub fn last_request(&self) -> Option<ChatRequest> {
self.state.lock().unwrap().requests.last().cloned()
}
#[must_use]
pub fn call_count(&self) -> usize {
self.state.lock().unwrap().requests.len()
}
#[cfg(test)]
pub(crate) fn pending_responses(&self) -> usize {
self.state.lock().unwrap().responses.len()
}
}
impl ProviderIdentity for MockProvider {
fn provider_name(&self) -> &str {
&self.provider_name
}
fn reads_namespace(&self, namespace: &str) -> bool {
namespace == self.provider_name
}
fn downcast_any(&self, id: std::any::TypeId) -> Option<&(dyn std::any::Any + Send + Sync)> {
crate::expose(self, id)
}
}
impl ChatProvider for MockProvider {
type Stream = SingleResponseStream;
async fn chat(&self, request: &ChatRequest) -> Result<ChatResponse> {
let mut prepared = crate::adapter::chat::prepare(self, request)?;
let response = {
let mut state = self.state.lock().unwrap();
state.requests.push(request.clone());
state.responses.pop_front()
};
let Some(response) = response else {
return Err(
Error::configuration("MockProvider: no more responses configured").authored(),
);
};
prepared.mark_sent();
Ok(prepared.finish(response.into_result(request).await?))
}
async fn chat_stream(&self, request: &ChatRequest) -> Result<Self::Stream> {
Ok(self.chat(request).await?.into())
}
fn chat_capability(&self, _model: &str, capability_: ChatCapability) -> CapabilitySupport {
capability(&self.chat_capabilities, capability_)
}
}
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
#[non_exhaustive]
pub enum MockResponse {
Success(ChatResponse),
Error(Error),
Delayed(Duration, Box<MockResponse>),
Structured(serde_json::Value),
}
impl MockResponse {
#[must_use]
pub fn text(text: impl Into<String>) -> Self {
ChatResponseBuilder::new().text(text).build().into()
}
#[must_use]
pub fn reasoning_text(reasoning: impl Into<String>, text: impl Into<String>) -> Self {
ChatResponseBuilder::new()
.reasoning(reasoning)
.text(text)
.build()
.into()
}
#[must_use]
pub fn tool_call(
id: impl Into<String>,
name: impl Into<String>,
arguments: serde_json::Value,
) -> Self {
ChatResponseBuilder::new()
.tool_call(id, name, arguments)
.build()
.into()
}
#[must_use]
pub fn tool_calls(calls: impl IntoIterator<Item = ToolCall>) -> Self {
ChatResponseBuilder::new().tool_calls(calls).build().into()
}
#[cfg_attr(feature = "extract", doc = "[extraction]: crate::extract")]
#[cfg_attr(not(feature = "extract"), doc = "[extraction]: crate#cargo-features")]
#[must_use]
pub fn structured(value: serde_json::Value) -> Self {
Self::Structured(value)
}
#[must_use]
pub fn delayed(duration: Duration, response: impl Into<MockResponse>) -> Self {
Self::Delayed(duration, Box::new(response.into()))
}
async fn into_result(self, request: &ChatRequest) -> Result<ChatResponse> {
let mut next = self;
loop {
match next {
Self::Success(response) => return Ok(response),
Self::Error(error) => return Err(error),
Self::Delayed(duration, response) => {
futures_timer::Delay::new(duration).await;
next = *response;
}
Self::Structured(value) => {
let builder = ChatResponseBuilder::new();
let builder = match forced_tool(request) {
Some(name) => builder.tool_call("call_structured", name, value),
None => builder.text(value.to_string()),
};
return Ok(builder.build());
}
}
}
}
}
impl From<ChatResponse> for MockResponse {
fn from(response: ChatResponse) -> Self {
Self::Success(response)
}
}
impl From<Error> for MockResponse {
fn from(error: Error) -> Self {
Self::Error(error)
}
}
impl From<Result<ChatResponse>> for MockResponse {
fn from(result: Result<ChatResponse>) -> Self {
match result {
Ok(response) => Self::Success(response),
Err(error) => Self::Error(error),
}
}
}
#[derive(Debug)]
pub struct MockProviderBuilder {
responses: Vec<MockResponse>,
chat_capabilities: Capabilities,
provider_name: Cow<'static, str>,
}
impl Default for MockProviderBuilder {
fn default() -> Self {
Self::new()
}
}
impl MockProviderBuilder {
#[must_use]
pub fn new() -> Self {
Self {
responses: Vec::new(),
chat_capabilities: Capabilities::new(),
provider_name: Cow::Borrowed("mock"),
}
}
#[must_use]
pub fn response(mut self, response: impl Into<MockResponse>) -> Self {
self.responses.push(response.into());
self
}
#[must_use]
pub fn text(self, text: impl Into<String>) -> Self {
self.response(MockResponse::text(text))
}
#[must_use]
pub fn error(self, error: Error) -> Self {
self.response(error)
}
#[must_use]
pub fn tool_call(
self,
id: impl Into<String>,
name: impl Into<String>,
arguments: serde_json::Value,
) -> Self {
self.response(MockResponse::tool_call(id, name, arguments))
}
#[must_use]
pub fn tool_calls(self, calls: impl IntoIterator<Item = ToolCall>) -> Self {
self.response(MockResponse::tool_calls(calls))
}
#[must_use]
pub fn structured(self, value: serde_json::Value) -> Self {
self.response(MockResponse::structured(value))
}
#[must_use]
pub fn chat_capability(
mut self,
capability: ChatCapability,
support: CapabilitySupport,
) -> Self {
self.chat_capabilities.insert(capability, support);
self
}
#[must_use]
pub fn provider_name(mut self, provider_name: impl Into<Cow<'static, str>>) -> Self {
self.provider_name = provider_name.into();
self
}
#[must_use]
pub fn build(self) -> MockProvider {
let mut provider = MockProvider::new(self.responses).with_provider_name(self.provider_name);
provider.chat_capabilities = self.chat_capabilities;
provider
}
}
#[derive(Debug, Clone)]
pub struct MockStreamingProvider {
state: Arc<Mutex<MockStreamingState>>,
chat_capabilities: Capabilities,
provider_name: Cow<'static, str>,
}
#[derive(Debug, Default)]
struct MockStreamingState {
streams: VecDeque<ScriptedStream>,
requests: Vec<ChatRequest>,
}
fn streaming_capabilities() -> Capabilities {
HashMap::from([
(ChatCapability::Streaming, CapabilitySupport::Supported),
(
ChatCapability::NativeStreaming,
CapabilitySupport::Supported,
),
])
}
impl MockStreamingProvider {
#[must_use]
pub fn build(
configure: impl FnOnce(MockStreamingProviderBuilder) -> MockStreamingProviderBuilder,
) -> Self {
configure(MockStreamingProviderBuilder::new()).build()
}
#[must_use]
pub fn new<I, S, E>(streams: I) -> Self
where
I: IntoIterator<Item = S>,
S: IntoIterator<Item = E>,
E: Into<MockStreamEvent>,
{
Self::scripted(streams.into_iter().map(ScriptedStream::events))
}
fn scripted(entries: impl IntoIterator<Item = ScriptedStream>) -> Self {
Self {
state: Arc::new(Mutex::new(MockStreamingState {
streams: entries.into_iter().collect(),
requests: Vec::new(),
})),
chat_capabilities: streaming_capabilities(),
provider_name: Cow::Borrowed("mock_stream"),
}
}
#[must_use]
pub fn with_stream<S, E>(stream: S) -> Self
where
S: IntoIterator<Item = E>,
E: Into<MockStreamEvent>,
{
Self::new([stream])
}
#[must_use]
pub fn with_text(text: impl Into<String>) -> Self {
Self::with_stream(MockStreamEvent::text_response(text))
}
#[must_use]
pub fn from_responses<I: IntoIterator<Item = ChatResponse>>(responses: I) -> Self {
Self::new(responses.into_iter().map(MockStreamEvent::from_response))
}
#[must_use]
pub fn with_chat_capability(
mut self,
capability: ChatCapability,
support: CapabilitySupport,
) -> Self {
self.chat_capabilities.insert(capability, support);
self
}
#[must_use]
pub fn with_provider_name(mut self, provider_name: impl Into<Cow<'static, str>>) -> Self {
self.provider_name = provider_name.into();
self
}
#[must_use]
pub fn requests(&self) -> Vec<ChatRequest> {
self.state.lock().unwrap().requests.clone()
}
#[must_use]
pub fn call_count(&self) -> usize {
self.state.lock().unwrap().requests.len()
}
}
impl ProviderIdentity for MockStreamingProvider {
fn provider_name(&self) -> &str {
&self.provider_name
}
fn reads_namespace(&self, namespace: &str) -> bool {
namespace == self.provider_name
}
fn downcast_any(&self, id: std::any::TypeId) -> Option<&(dyn std::any::Any + Send + Sync)> {
crate::expose(self, id)
}
}
impl ChatProvider for MockStreamingProvider {
type Stream = ChatStream;
async fn chat(&self, request: &ChatRequest) -> Result<ChatResponse> {
self.chat_stream(request).await?.collect_response().await
}
async fn chat_stream(&self, request: &ChatRequest) -> Result<Self::Stream> {
let mut prepared = crate::adapter::chat::prepare(self, request)?;
let entry = {
let mut state = self.state.lock().unwrap();
state.requests.push(request.clone());
state.streams.pop_front()
};
let pending = match entry {
Some(ScriptedStream::Events(events)) => events,
Some(ScriptedStream::SetupError(error)) => return Err(error),
None => {
return Err(Error::configuration(
"MockStreamingProvider: no more streams configured",
)
.authored());
}
};
prepared.mark_sent();
let stream = futures_util::stream::unfold(pending, |mut pending| async move {
loop {
match pending.pop_front()? {
MockStreamEvent::Delay(duration) => futures_timer::Delay::new(duration).await,
MockStreamEvent::Event(event) => return Some((Ok(event), pending)),
MockStreamEvent::Error(error) => return Some((Err(error), pending)),
}
}
});
Ok(prepared.finish_stream(stream))
}
fn chat_capability(&self, _model: &str, capability_: ChatCapability) -> CapabilitySupport {
capability(&self.chat_capabilities, capability_)
}
}
#[derive(Debug)]
enum ScriptedStream {
Events(VecDeque<MockStreamEvent>),
SetupError(Error),
}
impl ScriptedStream {
fn events<S, E>(stream: S) -> Self
where
S: IntoIterator<Item = E>,
E: Into<MockStreamEvent>,
{
Self::Events(stream.into_iter().map(Into::into).collect())
}
}
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
#[non_exhaustive]
pub enum MockStreamEvent {
Event(StreamEvent),
Error(Error),
Delay(Duration),
}
impl MockStreamEvent {
#[must_use]
pub fn delayed(duration: Duration) -> Self {
Self::Delay(duration)
}
#[must_use]
pub fn from_response(response: ChatResponse) -> Vec<Self> {
response.into_stream_events().map(Self::Event).collect()
}
#[must_use]
pub fn text_response(text: impl Into<String>) -> Vec<Self> {
Self::from_response(ChatResponseBuilder::new().text(text).build())
}
#[must_use]
pub fn tool_call_response(
id: impl Into<String>,
name: impl Into<String>,
arguments: serde_json::Value,
) -> Vec<Self> {
Self::from_response(
ChatResponseBuilder::new()
.tool_call(id, name, arguments)
.build(),
)
}
}
impl From<StreamEvent> for MockStreamEvent {
fn from(event: StreamEvent) -> Self {
Self::Event(event)
}
}
impl From<Error> for MockStreamEvent {
fn from(error: Error) -> Self {
Self::Error(error)
}
}
impl From<Result<StreamEvent>> for MockStreamEvent {
fn from(result: Result<StreamEvent>) -> Self {
result.map_or_else(Self::Error, Self::Event)
}
}
#[derive(Debug)]
pub struct MockStreamingProviderBuilder {
streams: Vec<ScriptedStream>,
chat_capabilities: Capabilities,
provider_name: Cow<'static, str>,
}
impl Default for MockStreamingProviderBuilder {
fn default() -> Self {
Self::new()
}
}
impl MockStreamingProviderBuilder {
#[must_use]
pub fn new() -> Self {
Self {
streams: Vec::new(),
chat_capabilities: streaming_capabilities(),
provider_name: Cow::Borrowed("mock_stream"),
}
}
#[must_use]
pub fn stream<S, E>(mut self, stream: S) -> Self
where
S: IntoIterator<Item = E>,
E: Into<MockStreamEvent>,
{
self.streams.push(ScriptedStream::events(stream));
self
}
#[must_use]
pub fn setup_error(mut self, error: Error) -> Self {
self.streams.push(ScriptedStream::SetupError(error));
self
}
#[must_use]
pub fn response(self, response: ChatResponse) -> Self {
self.stream(MockStreamEvent::from_response(response))
}
#[must_use]
pub fn text(self, text: impl Into<String>) -> Self {
self.stream(MockStreamEvent::text_response(text))
}
#[must_use]
pub fn tool_call(
self,
id: impl Into<String>,
name: impl Into<String>,
arguments: serde_json::Value,
) -> Self {
self.stream(MockStreamEvent::tool_call_response(id, name, arguments))
}
#[must_use]
pub fn tool_calls(self, calls: impl IntoIterator<Item = ToolCall>) -> Self {
self.response(ChatResponseBuilder::new().tool_calls(calls).build())
}
#[must_use]
pub fn chat_capability(
mut self,
capability: ChatCapability,
support: CapabilitySupport,
) -> Self {
self.chat_capabilities.insert(capability, support);
self
}
#[must_use]
pub fn provider_name(mut self, provider_name: impl Into<Cow<'static, str>>) -> Self {
self.provider_name = provider_name.into();
self
}
#[must_use]
pub fn build(self) -> MockStreamingProvider {
let mut provider =
MockStreamingProvider::scripted(self.streams).with_provider_name(self.provider_name);
provider.chat_capabilities = self.chat_capabilities;
provider
}
}
type ResponseFn = dyn Fn(&ChatRequest) -> Result<ChatResponse> + Send + Sync;
#[derive(Clone)]
pub struct FunctionProvider {
respond: Arc<ResponseFn>,
chat_capabilities: Capabilities,
provider_name: Cow<'static, str>,
}
impl std::fmt::Debug for FunctionProvider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FunctionProvider")
.field("provider_name", &self.provider_name)
.finish_non_exhaustive()
}
}
impl FunctionProvider {
#[must_use]
pub fn new<F>(respond: F) -> Self
where
F: Fn(&ChatRequest) -> Result<ChatResponse> + Send + Sync + 'static,
{
Self {
respond: Arc::new(respond),
chat_capabilities: Capabilities::new(),
provider_name: Cow::Borrowed("function"),
}
}
#[must_use]
pub fn with_chat_capability(
mut self,
capability: ChatCapability,
support: CapabilitySupport,
) -> Self {
self.chat_capabilities.insert(capability, support);
self
}
#[must_use]
pub fn with_provider_name(mut self, provider_name: impl Into<Cow<'static, str>>) -> Self {
self.provider_name = provider_name.into();
self
}
}
impl ProviderIdentity for FunctionProvider {
fn provider_name(&self) -> &str {
&self.provider_name
}
fn reads_namespace(&self, namespace: &str) -> bool {
namespace == self.provider_name
}
fn downcast_any(&self, id: std::any::TypeId) -> Option<&(dyn std::any::Any + Send + Sync)> {
crate::expose(self, id)
}
}
impl ChatProvider for FunctionProvider {
type Stream = SingleResponseStream;
async fn chat(&self, request: &ChatRequest) -> Result<ChatResponse> {
let mut prepared = crate::adapter::chat::prepare(self, request)?;
prepared.mark_sent();
Ok(prepared.finish((self.respond)(request)?))
}
async fn chat_stream(&self, request: &ChatRequest) -> Result<Self::Stream> {
Ok(self.chat(request).await?.into())
}
fn chat_capability(&self, _model: &str, capability_: ChatCapability) -> CapabilitySupport {
capability(&self.chat_capabilities, capability_)
}
}
#[derive(Debug, Clone, Default)]
pub struct TestProvider {
calls: Arc<Mutex<usize>>,
}
impl TestProvider {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn call_count(&self) -> usize {
*self.calls.lock().unwrap()
}
fn respond(&self, request: &ChatRequest) -> ChatResponse {
*self.calls.lock().unwrap() += 1;
let mut response = self.answer(request);
let input_tokens = word_count(
request
.system
.iter()
.map(|prompt| prompt.content.as_str())
.chain(request.messages.iter().flat_map(Message::text_segments)),
);
let output_tokens = word_count(response.content.iter().filter_map(|block| match block {
ContentBlock::Text(text) => Some(text.text.as_str()),
ContentBlock::Refusal(refusal) => Some(refusal.text.as_str()),
ContentBlock::ToolCall(call) => Some(call.arguments.as_str()),
_ => None,
}));
let assistant_turns = request
.messages
.iter()
.filter(|message| matches!(message, Message::Assistant(_)))
.count();
response.model = Some(request.model.clone());
response.provider = Some(Cow::Borrowed(TEST_PROVIDER_NAME));
response.id = Some(format!("test_response_{}", assistant_turns + 1));
response.usage = Some(
Usage::new()
.input_tokens(input_tokens)
.output_tokens(output_tokens),
);
response
}
fn answer(&self, request: &ChatRequest) -> ChatResponse {
let answered_tool = matches!(request.messages.last(), Some(Message::Tool(_)));
let forced = match &request.tool_choice {
Some(ToolChoice::Specific { name }) => Some(name.as_str()),
_ => None,
};
let disabled = matches!(request.tool_choice, Some(ToolChoice::Disabled));
let tools = &request.tools;
if !disabled
&& !answered_tool
&& let Some(first) = tools.first()
{
let tool = forced
.and_then(|name| tools.iter().find(|t| t.name == name))
.unwrap_or(first);
let arguments = example_value(&tool.parameters, &tool.parameters, 0);
return ChatResponseBuilder::new()
.tool_call(next_tool_call_id(request), tool.name.clone(), arguments)
.build();
}
match &request.response_format {
Some(ResponseFormat::JsonSchema { schema, .. }) => ChatResponseBuilder::new()
.text(example_value(schema, schema, 0).to_string())
.build(),
_ => text_answer(request),
}
}
}
const TEST_PROVIDER_NAME: &str = "test";
fn forced_tool(request: &ChatRequest) -> Option<&str> {
match request.tool_choice.as_ref()? {
ToolChoice::Specific { name } => Some(name),
ToolChoice::Required => match request.tools.as_slice() {
[tool] => Some(&tool.name),
_ => None,
},
_ => None,
}
}
fn text_answer(request: &ChatRequest) -> ChatResponse {
ChatResponseBuilder::new()
.text(format!(
"test response ({} messages)",
request.messages.len()
))
.build()
}
fn next_tool_call_id(request: &ChatRequest) -> String {
let used: std::collections::HashSet<&str> = request
.messages
.iter()
.filter_map(Message::as_assistant)
.flat_map(|message| &message.content)
.filter_map(|block| match block {
ContentBlock::ToolCall(call) => Some(call.id.as_str()),
_ => None,
})
.collect();
(used.len() + 1..)
.map(|n| format!("test_call_{n}"))
.find(|id| !used.contains(id.as_str()))
.expect("an unused id exists")
}
fn word_count<'a>(texts: impl Iterator<Item = &'a str>) -> u64 {
let words: usize = texts.map(|text| text.split_whitespace().count()).sum();
u64::try_from(words).unwrap_or(u64::MAX).max(1)
}
impl ProviderIdentity for TestProvider {
fn provider_name(&self) -> &str {
TEST_PROVIDER_NAME
}
fn reads_namespace(&self, namespace: &str) -> bool {
namespace == TEST_PROVIDER_NAME
}
fn downcast_any(&self, id: std::any::TypeId) -> Option<&(dyn std::any::Any + Send + Sync)> {
crate::expose(self, id)
}
}
impl ChatProvider for TestProvider {
type Stream = SingleResponseStream;
async fn chat(&self, request: &ChatRequest) -> Result<ChatResponse> {
Ok(self.respond(request))
}
async fn chat_stream(&self, request: &ChatRequest) -> Result<Self::Stream> {
Ok(self.respond(request).into())
}
fn chat_capability(&self, _model: &str, capability: ChatCapability) -> CapabilitySupport {
match capability {
ChatCapability::ToolCalls
| ChatCapability::Streaming
| ChatCapability::StructuredOutput => CapabilitySupport::Supported,
_ => CapabilitySupport::Unknown,
}
}
}
fn example_value(
schema: &serde_json::Value,
root: &serde_json::Value,
depth: u8,
) -> serde_json::Value {
use serde_json::{Value, json};
if depth > 8 {
return Value::Null;
}
let Some(object) = schema.as_object() else {
return json!({});
};
if let Some(reference) = object.get("$ref").and_then(Value::as_str)
&& let Some(path) = reference.strip_prefix("#/")
{
let target = path
.split('/')
.try_fold(root, |node, key| node.get(key))
.cloned()
.unwrap_or(Value::Null);
return example_value(&target, root, depth + 1);
}
if let Some(value) = object.get("const") {
return value.clone();
}
if let Some(first) = object
.get("enum")
.and_then(Value::as_array)
.and_then(|v| v.first())
{
return first.clone();
}
for key in ["anyOf", "oneOf", "allOf"] {
if let Some(first) = object
.get(key)
.and_then(Value::as_array)
.and_then(|v| v.first())
{
return example_value(first, root, depth + 1);
}
}
let ty = match object.get("type") {
Some(Value::String(t)) => t.as_str(),
Some(Value::Array(types)) => types
.iter()
.filter_map(Value::as_str)
.find(|t| *t != "null")
.unwrap_or("null"),
_ if object.contains_key("properties") => "object",
_ => "object",
};
match ty {
"string" => json!("example"),
"integer" => json!(object.get("minimum").and_then(Value::as_i64).unwrap_or(1)),
"number" => json!(object.get("minimum").and_then(Value::as_f64).unwrap_or(1.0)),
"boolean" => json!(true),
"null" => Value::Null,
"array" => {
let item = object
.get("items")
.map_or(Value::Null, |items| example_value(items, root, depth + 1));
json!([item])
}
_ => {
let mut out = serde_json::Map::new();
if let Some(properties) = object.get("properties").and_then(Value::as_object) {
let required: Vec<&str> = object
.get("required")
.and_then(Value::as_array)
.map(|r| r.iter().filter_map(Value::as_str).collect())
.unwrap_or_default();
for (name, property) in properties {
if required.is_empty() || required.contains(&name.as_str()) {
out.insert(name.clone(), example_value(property, root, depth + 1));
}
}
}
Value::Object(out)
}
}
}
#[derive(Debug, Clone)]
pub struct ChatResponseBuilder {
response: ChatResponse,
finish_reason_set: bool,
}
impl Default for ChatResponseBuilder {
fn default() -> Self {
Self::new()
}
}
impl ChatResponseBuilder {
#[must_use]
pub fn new() -> Self {
Self {
response: ChatResponse::default(),
finish_reason_set: false,
}
}
#[must_use]
pub fn text(mut self, text: impl Into<String>) -> Self {
self.response.content.push(ContentBlock::text(text));
self
}
#[must_use]
pub fn tool_call(
mut self,
id: impl Into<String>,
name: impl Into<String>,
arguments: serde_json::Value,
) -> Self {
self.response
.content
.push(ContentBlock::ToolCall(ToolCall::new(
id,
name,
arguments.to_string(),
)));
self
}
#[must_use]
pub fn tool_calls(mut self, calls: impl IntoIterator<Item = ToolCall>) -> Self {
self.response
.content
.extend(calls.into_iter().map(ContentBlock::ToolCall));
self
}
#[must_use]
pub fn reasoning(mut self, text: impl Into<String>) -> Self {
self.response.content.push(ContentBlock::reasoning(text));
self
}
#[must_use]
pub fn refusal(mut self, text: impl Into<String>) -> Self {
self.response.content.push(ContentBlock::refusal(text));
self
}
#[must_use]
pub fn other(mut self, type_name: impl Into<String>, data: ExtraMap) -> Self {
self.response
.content
.push(ContentBlock::Other(OtherContent::new(type_name, data)));
self
}
#[must_use]
pub fn block(mut self, block: impl Into<ContentBlock>) -> Self {
self.response.content.push(block.into());
self
}
#[must_use]
pub fn finish_reason(mut self, reason: FinishReason) -> Self {
self.response.finish_reason = Some(reason);
self.finish_reason_set = true;
self
}
#[must_use]
pub fn usage(mut self, input_tokens: u64, output_tokens: u64) -> Self {
self.response.usage = Some(
Usage::new()
.input_tokens(input_tokens)
.output_tokens(output_tokens),
);
self
}
#[must_use]
pub fn usage_value(mut self, usage: Usage) -> Self {
self.response.usage = Some(usage);
self
}
#[must_use]
pub fn model(mut self, model: impl Into<String>) -> Self {
self.response.model = Some(model.into());
self
}
#[must_use]
pub fn id(mut self, id: impl Into<String>) -> Self {
self.response.id = Some(id.into());
self
}
#[must_use]
pub fn warning(mut self, warning: Warning) -> Self {
self.response.warnings.push(warning);
self
}
#[must_use]
pub fn provider_data(mut self, data: ProviderData) -> Self {
self.response.provider_data = data;
self
}
#[must_use]
pub fn build(mut self) -> ChatResponse {
if !self.finish_reason_set {
self.response.finish_reason = Some(if self.response.has_tool_calls() {
FinishReason::ToolCalls
} else {
FinishReason::Stop
});
}
self.response
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ToolDefinition;
use serde_json::json;
#[tokio::test]
async fn tool_calls_script_one_turn_with_several_calls() {
let calls = [
ToolCall::new("call_1", "weather", r#"{"city":"Paris"}"#),
ToolCall::new("call_2", "weather", r#"{"city":"Rome"}"#),
];
let request = ChatRequest::new("m").user("weather?");
let provider = MockProvider::build(|b| b.tool_calls(calls.clone()));
let response = provider.chat(&request).await.unwrap();
let ids: Vec<_> = response.tool_calls().map(|call| call.id.as_str()).collect();
assert_eq!(ids, ["call_1", "call_2"]);
assert_eq!(response.finish_reason, Some(FinishReason::ToolCalls));
let streaming = MockStreamingProvider::build(|b| b.tool_calls(calls));
let stream = streaming.chat_stream(&request).await.unwrap();
let collected = stream.collect_response().await.unwrap();
assert_eq!(collected.tool_calls().count(), 2);
}
#[tokio::test]
async fn structured_answers_in_the_form_the_request_asks_for() {
let value = json!({"name": "Ada"});
let tool = |name: &str| ToolDefinition::new(name, json!({"type": "object"}));
let respond = |request: ChatRequest| {
let value = value.clone();
async move {
MockProvider::new([MockResponse::structured(value)])
.chat(&request)
.await
.unwrap()
}
};
let base = ChatRequest::new("m").user("q");
let text = respond(base.clone()).await;
assert_eq!(text.text().as_deref(), Some(r#"{"name":"Ada"}"#));
let auto = respond(base.clone().tools([tool("a")])).await;
assert!(auto.text().is_some());
for request in [
base.clone()
.tools([tool("a"), tool("b")])
.tool_choice(ToolChoice::Specific { name: "b".into() }),
base.clone()
.tools([tool("b")])
.tool_choice(ToolChoice::Required),
] {
let response = respond(request).await;
let call = response.first_tool_call().unwrap();
assert_eq!(call.name, "b");
assert_eq!(call.parse_arguments::<serde_json::Value>().unwrap(), value);
}
let ambiguous = respond(
base.tools([tool("a"), tool("b")])
.tool_choice(ToolChoice::Required),
)
.await;
assert!(ambiguous.text().is_some());
}
#[tokio::test]
async fn mock_records_requests_in_order() {
let provider = MockProvider::build(|b| b.text("first").text("second"));
let request = ChatRequest::new("m").user("hi");
assert_eq!(
provider.chat(&request).await.unwrap().text().as_deref(),
Some("first")
);
assert_eq!(
provider.chat(&request).await.unwrap().text().as_deref(),
Some("second")
);
assert_eq!(provider.call_count(), 2);
assert_eq!(provider.last_request().unwrap().model, "m");
assert_eq!(provider.pending_responses(), 0);
}
#[tokio::test]
async fn mocks_preflight_like_an_adapter() {
use crate::{ReasoningEffort, ResponseFormat, WarningKind};
let hint = ChatRequest::new("m")
.user("hi")
.reasoning(ReasoningEffort::High);
let semantic =
ChatRequest::new("m")
.user("hi")
.response_format(ResponseFormat::json_schema(
serde_json::json!({"type": "object"}),
));
let unsupported = [
ChatCapability::ReasoningConfig,
ChatCapability::StructuredOutput,
];
let mock = unsupported
.into_iter()
.fold(MockProvider::build(|b| b.text("a").text("b")), |p, c| {
p.with_chat_capability(c, CapabilitySupport::Unsupported)
});
let response = mock.chat(&hint).await.unwrap();
assert_eq!(response.warnings[0].kind, WarningKind::Unsupported);
assert_eq!(response.warnings[0].provider.as_deref(), Some("mock"));
assert!(mock.last_request().unwrap().reasoning.is_some());
assert!(mock.chat(&semantic).await.is_err());
assert_eq!((mock.call_count(), mock.pending_responses()), (1, 1));
let streaming = unsupported.into_iter().fold(
MockStreamingProvider::from_responses([ChatResponseBuilder::new().text("t").build()]),
|p, c| p.with_chat_capability(c, CapabilitySupport::Unsupported),
);
assert!(streaming.chat_stream(&semantic).await.is_err());
let response = streaming.chat(&hint).await.unwrap();
assert_eq!(response.text().as_deref(), Some("t"));
assert_eq!(response.warnings.len(), 1);
assert_eq!(response.warnings[0].kind, WarningKind::Unsupported);
let function = unsupported.into_iter().fold(
FunctionProvider::new(|_| Ok(ChatResponseBuilder::new().text("f").build())),
|p, c| p.with_chat_capability(c, CapabilitySupport::Unsupported),
);
let response = function.chat(&hint).await.unwrap();
assert_eq!(response.warnings.len(), 1);
assert!(function.chat(&semantic).await.is_err());
}
#[tokio::test]
async fn delayed_responses_resolve() {
let provider = MockProvider::new([MockResponse::delayed(
Duration::from_millis(1),
MockResponse::text("late"),
)]);
let response = provider.chat(&ChatRequest::new("m")).await.unwrap();
assert_eq!(response.text().as_deref(), Some("late"));
}
#[tokio::test]
async fn streaming_mock_streams_and_collects() {
let provider = MockStreamingProvider::from_responses([ChatResponseBuilder::new()
.reasoning("r")
.text("t")
.build()]);
let response = provider.chat(&ChatRequest::new("m")).await.unwrap();
assert_eq!(response.text().as_deref(), Some("t"));
assert_eq!(response.reasoning_text().as_deref(), Some("r"));
}
#[tokio::test]
async fn exhausted_mock_returns_configuration_error() {
let provider = MockProvider::build(|b| b.text("only"));
let request = ChatRequest::new("m");
provider.chat(&request).await.unwrap();
let error = provider.chat(&request).await.unwrap_err();
assert!(matches!(error, Error::Configuration { .. }));
assert_eq!(
error.message(),
"MockProvider: no more responses configured"
);
assert!(provider.chat_stream(&request).await.is_err());
assert_eq!(provider.call_count(), 3);
}
#[tokio::test]
async fn mock_errors_fail_the_stream_call() {
let provider = MockProvider::with_error(Error::rate_limited("slow down"));
let error = provider
.chat_stream(&ChatRequest::new("m"))
.await
.err()
.unwrap();
assert!(matches!(error, Error::RateLimited { .. }));
}
#[tokio::test]
async fn exhausted_streaming_mock_returns_configuration_error() {
let provider = MockStreamingProvider::with_text("only");
let request = ChatRequest::new("m");
provider.chat(&request).await.unwrap();
let error = provider.chat_stream(&request).await.err().unwrap();
assert!(matches!(error, Error::Configuration { .. }));
assert_eq!(
error.message(),
"MockStreamingProvider: no more streams configured"
);
assert!(provider.chat(&request).await.is_err());
assert_eq!(provider.call_count(), 3);
}
#[tokio::test]
async fn setup_errors_fail_the_call_and_the_next_entry_streams() {
let provider = MockStreamingProvider::build(|b| {
b.text("first")
.setup_error(Error::rate_limited("slow down"))
.text("second")
});
let request = ChatRequest::new("m").user("hi");
let first = provider.chat(&request).await.unwrap();
assert_eq!(first.text().as_deref(), Some("first"));
let error = provider.chat_stream(&request).await.err().unwrap();
assert!(matches!(error, Error::RateLimited { .. }));
let second = provider
.chat_stream(&request)
.await
.unwrap()
.collect_response()
.await
.unwrap();
assert_eq!(second.text().as_deref(), Some("second"));
assert_eq!(provider.call_count(), 3);
assert_eq!(provider.requests()[1], request);
}
#[tokio::test]
async fn stream_errors_arrive_after_the_call_succeeds() {
use futures_util::StreamExt;
let provider = MockStreamingProvider::with_stream([
MockStreamEvent::from(StreamEvent::response_start(None, None)),
MockStreamEvent::from(Error::stream(
crate::StreamErrorKind::Transport,
"connection dropped",
)),
]);
let mut stream = provider.chat_stream(&ChatRequest::new("m")).await.unwrap();
assert!(stream.next().await.unwrap().is_ok());
assert!(stream.next().await.unwrap().is_err());
}
#[test]
fn builder_defaults_finish_reason() {
assert_eq!(
ChatResponseBuilder::new().text("x").build().finish_reason,
Some(FinishReason::Stop)
);
assert_eq!(
ChatResponseBuilder::new()
.tool_call("c", "f", json!({}))
.build()
.finish_reason,
Some(FinishReason::ToolCalls)
);
}
#[tokio::test]
async fn test_provider_calls_tools_with_schema_valid_arguments() {
let schema = json!({
"type": "object",
"properties": {
"city": {"type": "string"},
"days": {"type": "integer", "minimum": 2},
"unit": {"enum": ["c", "f"]},
"tags": {"type": "array", "items": {"$ref": "#/$defs/tag"}}
},
"required": ["city", "days", "unit", "tags"],
"$defs": {"tag": {"type": "string"}}
});
let provider = TestProvider::new();
let mut request = ChatRequest::new("m")
.user("weather?")
.tools([ToolDefinition::new("forecast", schema)]);
let first = provider.chat(&request).await.unwrap();
let call = first.first_tool_call().unwrap().clone();
let args: serde_json::Value = call.parse_arguments().unwrap();
assert_eq!(
args,
json!({"city": "example", "days": 2, "unit": "c", "tags": ["example"]})
);
request.push_message(first.to_assistant_message());
request.push_tool_result(&call, "sunny");
let second = provider.chat(&request).await.unwrap();
assert!(second.text().is_some());
assert_eq!(provider.call_count(), 2);
}
#[tokio::test]
async fn test_provider_returns_a_complete_envelope() {
let request = ChatRequest::new("demo-model")
.system("Be brief.")
.user("What is the weather\ntoday?");
for response in [
TestProvider::new().chat(&request).await.unwrap(),
TestProvider::new()
.chat_stream(&request)
.await
.unwrap()
.collect_response()
.await
.unwrap(),
] {
let envelope = response.envelope();
assert_eq!(envelope.model.as_deref(), Some("demo-model"));
assert_eq!(envelope.provider.as_deref(), Some("test"));
assert_eq!(envelope.id.as_deref(), Some("test_response_1"));
assert_eq!(envelope.finish_reason, Some(FinishReason::Stop));
assert_eq!(envelope.request_id, None);
let usage = envelope.usage.unwrap();
assert_eq!(usage.input_tokens, Some(7));
assert_eq!(usage.output_tokens, Some(4));
}
let again = TestProvider::new().chat(&request).await.unwrap();
assert_eq!(again.usage.unwrap().input_tokens, Some(7));
}
#[tokio::test]
async fn test_provider_answers_a_json_schema_format_with_a_schema_valid_value() {
let schema = json!({
"type": "object",
"properties": {
"category": {"enum": ["billing", "bug"]},
"urgent": {"type": "boolean"},
"score": {"type": "integer", "minimum": 3}
},
"required": ["category", "urgent", "score"]
});
let provider = TestProvider::new();
assert_eq!(
provider.chat_capability("m", ChatCapability::StructuredOutput),
CapabilitySupport::Supported
);
let request = ChatRequest::new("m")
.user("triage this")
.response_format(ResponseFormat::json_schema(schema.clone()));
let response = provider.chat(&request).await.unwrap();
let value: serde_json::Value = serde_json::from_str(&response.text().unwrap()).unwrap();
assert_eq!(
value,
json!({"category": "billing", "urgent": true, "score": 3})
);
let with_tools = request
.clone()
.tools([ToolDefinition::new("lookup", json!({"type": "object"}))]);
let response = provider.chat(&with_tools).await.unwrap();
assert_eq!(response.first_tool_call().unwrap().name, "lookup");
let disabled = with_tools.tool_choice(ToolChoice::Disabled);
let response = provider.chat(&disabled).await.unwrap();
assert!(response.first_tool_call().is_none());
assert!(response.text().unwrap().starts_with('{'));
let text = ChatRequest::new("m")
.user("hi")
.response_format(ResponseFormat::Json);
let response = provider.chat(&text).await.unwrap();
assert_eq!(
response.text().as_deref(),
Some("test response (1 messages)")
);
}
#[tokio::test]
async fn test_provider_tool_call_usage_counts_the_arguments() {
let request = ChatRequest::new("m").user("go").tools([ToolDefinition::new(
"f",
json!({"type": "object", "properties": {"q": {"type": "string"}}, "required": ["q"]}),
)]);
let response = TestProvider::new().chat(&request).await.unwrap();
assert_eq!(response.finish_reason, Some(FinishReason::ToolCalls));
let usage = response.usage.unwrap();
assert_eq!(usage.input_tokens, Some(1));
assert_eq!(usage.output_tokens, Some(1));
let empty = TestProvider::new()
.chat(&ChatRequest::new("m"))
.await
.unwrap();
assert_eq!(empty.usage.unwrap().input_tokens, Some(1));
}
#[tokio::test]
async fn test_provider_ids_are_unique_across_a_conversation() {
let tool = ToolDefinition::new("lookup", json!({"type": "object"}));
let mut request = ChatRequest::new("m").user("look it up twice").tools([tool]);
let mut ids = Vec::new();
let mut response_ids = Vec::new();
for _ in 0..3 {
let response = TestProvider::new().chat(&request).await.unwrap();
response_ids.push(response.id.clone().unwrap());
let call = response.first_tool_call().unwrap().clone();
ids.push(call.id.clone());
request.push_message(response.to_assistant_message());
request.push_tool_result(&call, "found");
request.push_message(Message::user("again"));
}
assert_eq!(ids, ["test_call_1", "test_call_2", "test_call_3"]);
assert_eq!(
response_ids,
["test_response_1", "test_response_2", "test_response_3"]
);
crate::history::validate(&request.messages, crate::history::ValidationMode::Sendable)
.unwrap();
}
#[tokio::test]
async fn test_provider_skips_ids_the_conversation_already_uses() {
let tool = ToolDefinition::new("lookup", json!({"type": "object"}));
let earlier = ToolCall::new("test_call_2", "lookup", "{}");
let mut request = ChatRequest::new("m").user("look").tools([tool]);
request.push_message(Message::assistant_blocks(vec![ContentBlock::ToolCall(
earlier.clone(),
)]));
request.push_tool_result(&earlier, "found");
request.push_message(Message::user("again"));
let response = TestProvider::new().chat(&request).await.unwrap();
assert_eq!(response.first_tool_call().unwrap().id, "test_call_3");
}
}