use std::future::Future;
use futures::Stream;
use serde::{Deserialize, Serialize};
use crate::error::ProbeError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ProbeRole {
System,
User,
Assistant,
Tool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ProbeContent {
Text(String),
Parts(Vec<ProbeContentPart>),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ProbeContentPart {
Text { text: String },
ImageBase64 { media_type: String, data: String },
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProbeMessage {
pub role: ProbeRole,
pub content: ProbeContent,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ProbeToolCall>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProbeFinish {
Stop,
ToolCalls,
Length,
Other,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProbeStreamChunk {
TextDelta {
text: String,
},
ToolCallStart {
id: String,
name: String,
},
ToolCallArgDelta {
delta: String,
},
ToolCallEnd,
Finished {
finish: ProbeFinish,
},
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CatalogPriors {
pub advertised_context_tokens: Option<u32>,
pub supports_vision: Option<bool>,
pub supports_tools: Option<bool>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ProbeTool {
pub name: String,
pub description: String,
pub parameters: serde_json::Value,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ProbeRequest {
pub messages: Vec<ProbeMessage>,
pub tools: Vec<ProbeTool>,
pub model: String,
pub temperature: Option<f32>,
pub max_tokens: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ProbeUsage {
pub prompt_tokens: Option<u32>,
pub completion_tokens: Option<u32>,
pub reasoning_tokens: Option<u32>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ProbeResponse {
pub text: String,
pub tool_calls: Vec<ProbeToolCall>,
pub finish: ProbeFinish,
pub usage: Option<ProbeUsage>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ProbeToolCall {
pub id: String,
pub name: String,
pub arguments: serde_json::Map<String, serde_json::Value>,
}
pub trait ProbeClient: Send + Sync {
fn chat(
&self,
req: ProbeRequest,
) -> impl Future<Output = Result<ProbeResponse, ProbeError>> + Send;
fn stream_chat(
&self,
req: ProbeRequest,
) -> impl Stream<Item = Result<ProbeStreamChunk, ProbeError>> + Send;
fn model_id(&self) -> &str;
fn provider(&self) -> &str;
fn catalog(&self) -> CatalogPriors {
CatalogPriors::default()
}
}
#[derive(Debug)]
pub struct MockLlm {
model_id: String,
provider: String,
error: Option<ProbeError>,
catalog: CatalogPriors,
}
impl MockLlm {
pub fn new(model_id: impl Into<String>, provider: impl Into<String>) -> Self {
Self {
model_id: model_id.into(),
provider: provider.into(),
error: None,
catalog: CatalogPriors::default(),
}
}
pub fn with_error(mut self, error: ProbeError) -> Self {
self.error = Some(error);
self
}
pub fn with_catalog(mut self, catalog: CatalogPriors) -> Self {
self.catalog = catalog;
self
}
fn injected_error(&self) -> Option<ProbeError> {
self.error.as_ref().map(clone_probe_error)
}
}
fn clone_probe_error(err: &ProbeError) -> ProbeError {
match err {
ProbeError::Auth(s) => ProbeError::Auth(s.clone()),
ProbeError::NotFound(s) => ProbeError::NotFound(s.clone()),
ProbeError::Llm(s) => ProbeError::Llm(s.clone()),
ProbeError::Transient(s) => ProbeError::Transient(s.clone()),
ProbeError::RateLimit { retry_after } => ProbeError::RateLimit {
retry_after: *retry_after,
},
ProbeError::Io(e) => ProbeError::Io(std::io::Error::new(e.kind(), e.to_string())),
ProbeError::Json(e) => ProbeError::Internal(format!("JSON error: {e}")),
ProbeError::Internal(s) => ProbeError::Internal(s.clone()),
}
}
impl ProbeClient for MockLlm {
fn chat(
&self,
req: ProbeRequest,
) -> impl Future<Output = Result<ProbeResponse, ProbeError>> + Send {
let err = self.injected_error();
let resp = if req.tools.is_empty() {
ProbeResponse {
text: "ok".to_owned(),
tool_calls: Vec::new(),
finish: ProbeFinish::Stop,
usage: None,
}
} else {
ProbeResponse {
text: String::new(),
tool_calls: vec![ProbeToolCall {
id: "call_1".to_owned(),
name: req.tools[0].name.clone(),
arguments: serde_json::json!({"path": "/tmp/test.txt"})
.as_object()
.unwrap()
.clone(),
}],
finish: ProbeFinish::ToolCalls,
usage: None,
}
};
async move {
if let Some(err) = err {
return Err(err);
}
Ok(resp)
}
}
fn stream_chat(
&self,
req: ProbeRequest,
) -> impl Stream<Item = Result<ProbeStreamChunk, ProbeError>> + Send {
let items = if let Some(err) = self.injected_error() {
vec![Err(err)]
} else if req.tools.is_empty() {
vec![Ok(ProbeStreamChunk::TextDelta {
text: "ok".to_owned(),
})]
} else {
vec![
Ok(ProbeStreamChunk::ToolCallStart {
id: "call_1".to_owned(),
name: req.tools[0].name.clone(),
}),
Ok(ProbeStreamChunk::ToolCallArgDelta {
delta: "{}".to_owned(),
}),
Ok(ProbeStreamChunk::ToolCallEnd),
]
};
futures::stream::iter(items)
}
fn model_id(&self) -> &str {
&self.model_id
}
fn provider(&self) -> &str {
&self.provider
}
fn catalog(&self) -> CatalogPriors {
self.catalog.clone()
}
}