use std::future::Future;
use std::pin::Pin;
use super::capabilities::ProviderCapabilities;
use super::conversation::Conversation;
use super::family::ModelFamily;
use super::id::ModelId;
use super::tool::{ToolChoicePolicy, ToolSpec, TurnOutput};
use crate::error::Result;
use crate::prompt::PromptRoute;
#[derive(Debug, Clone, Default, PartialEq)]
pub struct GenerationOptions {
pub max_tokens: Option<u32>,
pub temperature: Option<f32>,
pub stop_sequences: Vec<String>,
}
pub type TransportFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T>> + Send + 'a>>;
pub trait ModelTransport: Send + Sync {
fn chat_turn<'a>(
&'a self,
model: &'a ModelId,
conversation: &'a Conversation,
tools: &'a [ToolSpec],
choice: ToolChoicePolicy,
) -> TransportFuture<'a, TurnOutput>;
fn chat_turn_with_options<'a>(
&'a self,
model: &'a ModelId,
conversation: &'a Conversation,
tools: &'a [ToolSpec],
choice: ToolChoicePolicy,
_options: GenerationOptions,
) -> TransportFuture<'a, TurnOutput> {
self.chat_turn(model, conversation, tools, choice)
}
fn capabilities(&self, model: &ModelId) -> ProviderCapabilities;
fn family_of(&self, model: &ModelId) -> ModelFamily;
fn adapter_kind(&self) -> &'static str;
fn prompt_route(&self, model: &ModelId) -> PromptRoute {
PromptRoute {
adapter: self.adapter_kind().to_owned(),
family: self.family_of(model),
exact_model: Some(model.model.clone()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Scripted(Vec<TurnOutput>);
impl ModelTransport for Scripted {
fn chat_turn<'a>(
&'a self,
_model: &'a ModelId,
conversation: &'a Conversation,
_tools: &'a [ToolSpec],
_choice: ToolChoicePolicy,
) -> TransportFuture<'a, TurnOutput> {
let index = conversation.len().min(self.0.len().saturating_sub(1));
let output = self.0[index].clone();
Box::pin(async move { Ok(output) })
}
fn capabilities(&self, _model: &ModelId) -> ProviderCapabilities {
ProviderCapabilities::text_only(8192)
}
fn family_of(&self, model: &ModelId) -> ModelFamily {
ModelFamily::from_model_name(&model.model)
}
fn adapter_kind(&self) -> &'static str {
"scripted"
}
}
#[test]
fn the_port_is_object_safe() {
let transport: Box<dyn ModelTransport> =
Box::new(Scripted(vec![TurnOutput::Text("ok".into())]));
let model = ModelId::new("test", "scripted");
assert_eq!(
transport.family_of(&model),
ModelFamily::Other("scripted".into())
);
let route = transport.prompt_route(&model);
assert_eq!(route.adapter, "scripted");
assert_eq!(route.exact_model.as_deref(), Some("scripted"));
}
#[test]
fn a_scripted_turn_resolves_without_a_runtime_dependency() {
let transport = Scripted(vec![TurnOutput::Text("done".into())]);
let model = ModelId::new("test", "scripted");
let conversation = Conversation::with_system("s");
let future = transport.chat_turn(&model, &conversation, &[], ToolChoicePolicy::Auto);
let output = futures_executor(future).unwrap();
assert_eq!(output, TurnOutput::Text("done".into()));
}
fn futures_executor<T>(mut future: TransportFuture<'_, T>) -> Result<T> {
use std::task::{Context, Poll, Waker};
let waker = Waker::noop();
let mut context = Context::from_waker(waker);
loop {
match future.as_mut().poll(&mut context) {
Poll::Ready(value) => return value,
Poll::Pending => std::thread::yield_now(),
}
}
}
}