perspt_sdk/model/
transport.rs1use std::future::Future;
16use std::pin::Pin;
17
18use super::capabilities::ProviderCapabilities;
19use super::conversation::Conversation;
20use super::family::ModelFamily;
21use super::id::ModelId;
22use super::tool::{ToolChoicePolicy, ToolSpec, TurnOutput};
23use crate::error::Result;
24use crate::prompt::PromptRoute;
25
26#[derive(Debug, Clone, Default, PartialEq)]
28pub struct GenerationOptions {
29 pub max_tokens: Option<u32>,
30 pub temperature: Option<f32>,
31 pub stop_sequences: Vec<String>,
32}
33
34pub type TransportFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T>> + Send + 'a>>;
36
37pub trait ModelTransport: Send + Sync {
39 fn chat_turn<'a>(
45 &'a self,
46 model: &'a ModelId,
47 conversation: &'a Conversation,
48 tools: &'a [ToolSpec],
49 choice: ToolChoicePolicy,
50 ) -> TransportFuture<'a, TurnOutput>;
51
52 fn chat_turn_with_options<'a>(
55 &'a self,
56 model: &'a ModelId,
57 conversation: &'a Conversation,
58 tools: &'a [ToolSpec],
59 choice: ToolChoicePolicy,
60 _options: GenerationOptions,
61 ) -> TransportFuture<'a, TurnOutput> {
62 self.chat_turn(model, conversation, tools, choice)
63 }
64
65 fn capabilities(&self, model: &ModelId) -> ProviderCapabilities;
67
68 fn family_of(&self, model: &ModelId) -> ModelFamily;
70
71 fn adapter_kind(&self) -> &'static str;
76
77 fn prompt_route(&self, model: &ModelId) -> PromptRoute {
81 PromptRoute {
82 adapter: self.adapter_kind().to_owned(),
83 family: self.family_of(model),
84 exact_model: Some(model.model.clone()),
85 }
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 struct Scripted(Vec<TurnOutput>);
96
97 impl ModelTransport for Scripted {
98 fn chat_turn<'a>(
99 &'a self,
100 _model: &'a ModelId,
101 conversation: &'a Conversation,
102 _tools: &'a [ToolSpec],
103 _choice: ToolChoicePolicy,
104 ) -> TransportFuture<'a, TurnOutput> {
105 let index = conversation.len().min(self.0.len().saturating_sub(1));
106 let output = self.0[index].clone();
107 Box::pin(async move { Ok(output) })
108 }
109
110 fn capabilities(&self, _model: &ModelId) -> ProviderCapabilities {
111 ProviderCapabilities::text_only(8192)
112 }
113
114 fn family_of(&self, model: &ModelId) -> ModelFamily {
115 ModelFamily::from_model_name(&model.model)
116 }
117
118 fn adapter_kind(&self) -> &'static str {
119 "scripted"
120 }
121 }
122
123 #[test]
124 fn the_port_is_object_safe() {
125 let transport: Box<dyn ModelTransport> =
126 Box::new(Scripted(vec![TurnOutput::Text("ok".into())]));
127 let model = ModelId::new("test", "scripted");
128 assert_eq!(
129 transport.family_of(&model),
130 ModelFamily::Other("scripted".into())
131 );
132 let route = transport.prompt_route(&model);
133 assert_eq!(route.adapter, "scripted");
134 assert_eq!(route.exact_model.as_deref(), Some("scripted"));
135 }
136
137 #[test]
138 fn a_scripted_turn_resolves_without_a_runtime_dependency() {
139 let transport = Scripted(vec![TurnOutput::Text("done".into())]);
140 let model = ModelId::new("test", "scripted");
141 let conversation = Conversation::with_system("s");
142 let future = transport.chat_turn(&model, &conversation, &[], ToolChoicePolicy::Auto);
143 let output = futures_executor(future).unwrap();
145 assert_eq!(output, TurnOutput::Text("done".into()));
146 }
147
148 fn futures_executor<T>(mut future: TransportFuture<'_, T>) -> Result<T> {
150 use std::task::{Context, Poll, Waker};
151 let waker = Waker::noop();
152 let mut context = Context::from_waker(waker);
153 loop {
154 match future.as_mut().poll(&mut context) {
155 Poll::Ready(value) => return value,
156 Poll::Pending => std::thread::yield_now(),
157 }
158 }
159 }
160}