use crate::{
completion::{CompletionError, Usage},
message::ReasoningContent,
streaming::{RawStreamingChoice, RawStreamingToolCall, StreamFinal, ToolCallDeltaContent},
};
pub const MOCK_PROVIDER: &str = "mock";
pub fn mock_final(usage: Usage) -> StreamFinal {
StreamFinal::new(MOCK_PROVIDER, usage)
}
fn fixture_additional_params(
value: serde_json::Value,
) -> Result<Option<crate::message::AdditionalParams>, CompletionError> {
crate::message::AdditionalParams::try_from_value(value).map_err(|other| {
CompletionError::ProviderError(format!(
"mock stream fixture `additional_params` must be a JSON object, got: {other}"
))
})
}
pub fn mock_final_with_total_tokens(total_tokens: u64) -> StreamFinal {
let mut usage = Usage::new();
usage.total_tokens = total_tokens;
mock_final(usage)
}
#[derive(Clone, Debug)]
pub enum MockStreamEvent {
Text(String),
TextStart {
id: String,
additional_params: Option<serde_json::Value>,
},
TextAdditionalParams(serde_json::Value),
ToolCall {
id: String,
name: String,
arguments: serde_json::Value,
call_id: Option<String>,
},
ToolCallDelta {
id: String,
content: ToolCallDeltaContent,
},
Reasoning {
id: String,
content: ReasoningContent,
},
ReasoningDelta { id: String, reasoning: String },
MessageId(String),
Unknown(serde_json::Value),
FinalResponse(StreamFinal),
Error(MockError),
}
use super::completion::MockError;
fn fixture_part_id(id: String) -> crate::streaming::StreamPartId {
use crate::streaming::MintKind;
for (namespace, kind) in [
("reasoning-", MintKind::Reasoning),
("block-", MintKind::Block),
("output-", MintKind::Output),
("tool-", MintKind::Tool),
("text-", MintKind::Text),
] {
if let Some(rest) = id.strip_prefix(namespace)
&& let Ok(index) = rest.parse::<u64>()
{
return kind.for_wire_index(index);
}
}
crate::streaming::StreamPartId::wire(id)
}
impl MockStreamEvent {
pub fn text(text: impl Into<String>) -> Self {
Self::Text(text.into())
}
pub fn text_start(id: impl Into<String>, additional_params: Option<serde_json::Value>) -> Self {
Self::TextStart {
id: id.into(),
additional_params,
}
}
pub fn text_additional_params(additional_params: serde_json::Value) -> Self {
Self::TextAdditionalParams(additional_params)
}
pub fn tool_call(
id: impl Into<String>,
name: impl Into<String>,
arguments: serde_json::Value,
) -> Self {
Self::ToolCall {
id: id.into(),
name: name.into(),
arguments,
call_id: None,
}
}
pub fn with_call_id(mut self, call_id: impl Into<String>) -> Self {
if let Self::ToolCall { call_id: id, .. } = &mut self {
*id = Some(call_id.into());
}
self
}
pub fn tool_call_name_delta(id: impl Into<String>, name: impl Into<String>) -> Self {
Self::ToolCallDelta {
id: id.into(),
content: ToolCallDeltaContent::Name(name.into()),
}
}
pub fn tool_call_arguments_delta(id: impl Into<String>, arguments: impl Into<String>) -> Self {
Self::ToolCallDelta {
id: id.into(),
content: ToolCallDeltaContent::Delta(arguments.into()),
}
}
pub fn reasoning(reasoning: impl Into<String>) -> Self {
Self::Reasoning {
id: "reasoning-0".to_string(),
content: ReasoningContent::Text {
text: reasoning.into(),
signature: None,
},
}
}
pub fn with_reasoning_id(mut self, reasoning_id: impl Into<String>) -> Self {
if let Self::Reasoning { id, .. } = &mut self {
*id = reasoning_id.into();
}
self
}
pub fn reasoning_delta(reasoning: impl Into<String>) -> Self {
Self::reasoning_delta_with_id("reasoning-0", reasoning)
}
pub fn reasoning_delta_with_id(id: impl Into<String>, reasoning: impl Into<String>) -> Self {
Self::ReasoningDelta {
id: id.into(),
reasoning: reasoning.into(),
}
}
pub fn message_id(id: impl Into<String>) -> Self {
Self::MessageId(id.into())
}
pub fn unknown(value: serde_json::Value) -> Self {
Self::Unknown(value)
}
pub fn final_response(usage: Usage) -> Self {
Self::FinalResponse(mock_final(usage))
}
pub fn final_response_with_default_usage() -> Self {
Self::FinalResponse(mock_final(Usage::new()))
}
pub fn final_response_with_total_tokens(total_tokens: u64) -> Self {
Self::FinalResponse(mock_final_with_total_tokens(total_tokens))
}
pub fn error(message: impl Into<String>) -> Self {
Self::Error(MockError::provider(message))
}
pub(crate) fn into_raw_choice(self) -> Result<RawStreamingChoice, CompletionError> {
match self {
Self::Text(text) => Ok(RawStreamingChoice::Message(text)),
Self::TextStart {
id,
additional_params,
} => Ok(RawStreamingChoice::TextStart {
id: fixture_part_id(id),
additional_params: additional_params
.map(fixture_additional_params)
.transpose()?
.flatten(),
}),
Self::TextAdditionalParams(additional_params) => {
match fixture_additional_params(additional_params)? {
None => Err(CompletionError::ProviderError(
"mock stream fixture `TextAdditionalParams` carries no data — \
drop the event instead"
.to_string(),
)),
Some(params) => Ok(RawStreamingChoice::TextAdditionalParams(params)),
}
}
Self::ToolCall {
id,
name,
arguments,
call_id,
} => {
let mut tool_call = RawStreamingToolCall::new(fixture_part_id(id), name, arguments);
if let Some(call_id) = call_id {
tool_call = tool_call.with_call_id(call_id);
}
Ok(RawStreamingChoice::ToolCall(tool_call))
}
Self::ToolCallDelta { id, content } => Ok(RawStreamingChoice::ToolCallDelta {
id: fixture_part_id(id),
content,
}),
Self::Reasoning { id, content } => {
let key = fixture_part_id(id.clone());
let provider_id = match &key {
key_is_wire if key_is_wire.wire_str().is_some() => {
crate::streaming::WireId::new(id)
}
_ => None,
};
Ok(RawStreamingChoice::Reasoning {
id: key,
provider_id,
content,
})
}
Self::ReasoningDelta { id, reasoning } => {
let key = fixture_part_id(id.clone());
let provider_id = match &key {
key_is_wire if key_is_wire.wire_str().is_some() => {
crate::streaming::WireId::new(id)
}
_ => None,
};
Ok(RawStreamingChoice::ReasoningDelta {
id: key,
provider_id,
reasoning,
})
}
Self::MessageId(id) => Ok(RawStreamingChoice::MessageId(id)),
Self::Unknown(value) => Ok(RawStreamingChoice::Unknown(value.into())),
Self::FinalResponse(response) => Ok(RawStreamingChoice::FinalResponse(response)),
Self::Error(error) => Err(error.into_completion_error()),
}
}
}