use async_trait::async_trait;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InferResult {
pub text: String,
pub tokens_in: u32,
pub tokens_out: u32,
}
#[derive(Debug, Default)]
pub struct InferRequest<'a> {
pub prompt: &'a str,
pub max_tokens: u32,
pub images: &'a [Vec<u8>],
}
impl<'a> InferRequest<'a> {
pub fn new(prompt: &'a str, max_tokens: u32) -> Self {
Self {
prompt,
max_tokens,
images: &[],
}
}
}
#[async_trait]
pub trait InferBackend: Send + Sync {
async fn infer(
&self,
req: InferRequest<'_>,
on_token: &mut (dyn for<'t> FnMut(&'t str) -> bool + Send),
) -> anyhow::Result<InferResult>;
fn model_name(&self) -> String;
fn supports_images(&self) -> bool {
false
}
async fn embed(&self, _texts: &[String]) -> anyhow::Result<Vec<Vec<f32>>> {
anyhow::bail!(
"this backend ({}) cannot produce embeddings. Declare an \
`embedding_model` your provider serves — with Ollama, a model built for it \
such as `nomic-embed-text`, not a chat model.",
self.model_name()
)
}
fn supports_embeddings(&self) -> bool {
false
}
}
pub struct StubBackend {
pub reply: String,
}
impl Default for StubBackend {
fn default() -> Self {
Self {
reply: "a stub summary".into(),
}
}
}
#[async_trait]
impl InferBackend for StubBackend {
async fn infer(
&self,
req: InferRequest<'_>,
on_token: &mut (dyn for<'t> FnMut(&'t str) -> bool + Send),
) -> anyhow::Result<InferResult> {
let mut out = String::new();
let mut tokens_out = 0u32;
let reply = if req.images.is_empty() {
self.reply.clone()
} else {
format!("[{} image(s)] {}", req.images.len(), self.reply)
};
for word in reply.split_whitespace().take(req.max_tokens as usize) {
let piece = if out.is_empty() {
word.to_string()
} else {
format!(" {word}")
};
tokens_out += 1;
let keep_going = on_token(&piece);
out.push_str(&piece);
if !keep_going {
break;
}
tokio::task::yield_now().await;
}
Ok(InferResult {
text: out,
tokens_in: req.prompt.split_whitespace().count() as u32,
tokens_out,
})
}
fn model_name(&self) -> String {
"stub".into()
}
fn supports_images(&self) -> bool {
true
}
}
pub struct StubFactory;
impl crate::backend::BackendFactory for StubFactory {
fn provider(&self) -> &'static str {
"stub"
}
fn describe(&self) -> &'static str {
"deterministic canned responses; no model required"
}
fn build(&self, target: &str) -> anyhow::Result<std::sync::Arc<dyn InferBackend>> {
Ok(std::sync::Arc::new(if target.is_empty() {
StubBackend::default()
} else {
StubBackend {
reply: target.to_string(),
}
}))
}
}