Skip to main content

rig_core/driver/
dyn_model.rs

1//! A model erased to its operation: what a consumer stores when it holds
2//! any model of one operation without naming its wire and transport. A
3//! [`DynModel`] runs the same driver as the [`Model`] it was made from.
4
5use std::fmt;
6use std::sync::Arc;
7
8use super::{Model, Sending, Transport};
9use crate::error::ProviderError;
10use crate::observe::AdapterContext;
11use crate::streaming::Streamed;
12use crate::wasm_compat::{WasmCompatSend, WasmCompatSync};
13use crate::wire::{Capabilities, Descriptor, Mode, Operation, Wire};
14
15/// Object-safe mirror of the calls a [`Model`] answers, with the wire and
16/// transport fixed. Private: the only way to reach it is through
17/// [`DynModel`], which re-exposes the public surface.
18pub(crate) trait ErasedModel<Op: Operation>: WasmCompatSend + WasmCompatSync {
19    fn describe(&self) -> Descriptor<'_>;
20
21    fn open(
22        &self,
23        request: Op::Request,
24        mode: Mode,
25        sending: Sending,
26    ) -> Result<Streamed<Op>, ProviderError>;
27
28    fn dry_run(&self, request: Op::Request) -> Result<(), ProviderError>;
29}
30
31impl<W, T> ErasedModel<W::Op> for Model<W, T>
32where
33    W: Wire,
34    T: Transport<W>,
35{
36    fn describe(&self) -> Descriptor<'_> {
37        self.wire.describe()
38    }
39
40    fn open(
41        &self,
42        request: <W::Op as Operation>::Request,
43        mode: Mode,
44        sending: Sending,
45    ) -> Result<Streamed<W::Op>, ProviderError> {
46        Model::open(self, request, mode, sending)
47    }
48
49    fn dry_run(&self, request: <W::Op as Operation>::Request) -> Result<(), ProviderError> {
50        Model::dry_run(self, request)
51    }
52}
53
54/// A model of one operation with its wire and transport erased. Clones share
55/// the model. Built with [`Model::erase`] or `From<Model<W, T>>`, so a
56/// consumer takes `impl Into<DynModel<Op>>` and accepts either.
57///
58/// Every call runs the driver the concrete model runs: spans, request ids,
59/// the operation's fold and error enrichment are the same.
60///
61/// A model used by one consumer is passed as is; a model shared by several
62/// is erased once and the handle is cloned. A model a handler table serves
63/// is a `ModelHandle` in the agent runtime instead: its calls are
64/// dispatched, recorded and observed there.
65///
66/// ```no_run
67/// use rig_core::embeddings::EmbeddingsBuilder;
68/// use rig_core::vector_store::in_memory_store::InMemoryVectorStore;
69/// use rig_core::{Model, providers::openai::{self, OpenAIConfig}};
70///
71/// # async fn example(http: rig_core::http_client::DynHttpClient) -> Result<(), Box<dyn std::error::Error>> {
72/// let model = OpenAIConfig::from_env()?.connect(http).embedding(openai::TEXT_EMBEDDING_3_SMALL, None).erase();
73/// let embeddings = EmbeddingsBuilder::new(model.clone())
74///     .documents(["a document".to_owned()])?
75///     .build()
76///     .await?;
77/// let index = InMemoryVectorStore::from_documents(embeddings).index(model);
78/// # let _ = index;
79/// # Ok(())
80/// # }
81/// ```
82pub struct DynModel<Op: Operation> {
83    inner: Arc<dyn ErasedModel<Op>>,
84}
85
86impl<W, T> Model<W, T>
87where
88    W: Wire,
89    T: Transport<W>,
90{
91    /// Erase this model to its operation.
92    pub fn erase(self) -> DynModel<W::Op> {
93        DynModel {
94            inner: Arc::new(self),
95        }
96    }
97}
98
99impl<W, T> From<Model<W, T>> for DynModel<W::Op>
100where
101    W: Wire,
102    T: Transport<W>,
103{
104    fn from(model: Model<W, T>) -> Self {
105        model.erase()
106    }
107}
108
109impl<Op: Operation> Clone for DynModel<Op> {
110    fn clone(&self) -> Self {
111        Self {
112            inner: Arc::clone(&self.inner),
113        }
114    }
115}
116
117impl<Op: Operation> fmt::Debug for DynModel<Op> {
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119        f.debug_struct("DynModel")
120            .field("name", &self.name())
121            .field("id", &self.id())
122            .finish()
123    }
124}
125
126impl<Op: Operation> DynModel<Op> {
127    /// The wire's provider descriptor name (`"anthropic"`).
128    pub fn name(&self) -> &str {
129        self.inner.describe().name
130    }
131
132    /// The model id the wire addresses, when the operation addresses one.
133    pub fn id(&self) -> Option<&str> {
134        self.inner.describe().model
135    }
136
137    /// The catalog facts of the model the wire addresses: the spec it was
138    /// connected to, else the catalog's entry for its id. `None` for an
139    /// operation that addresses no model, or a model the catalog does not
140    /// list.
141    pub fn spec(&self) -> Option<&crate::catalog::ModelSpec> {
142        self.inner.describe().spec()
143    }
144
145    /// What a runtime accounts for about this model.
146    pub fn capabilities(&self) -> Capabilities {
147        self.inner.describe().capabilities
148    }
149
150    /// Send `request` and fold the whole reply into the operation's
151    /// response; [`Model::call`] with the model erased. The call owns what
152    /// it sends, so it can be spawned.
153    pub fn call(
154        &self,
155        request: impl Into<Op::Request>,
156    ) -> impl Future<Output = Result<Op::Response, ProviderError>> + WasmCompatSend + 'static {
157        self.finished(request.into(), Sending::default())
158    }
159
160    /// [`Self::call`], with the attempt observed under `observation`.
161    pub fn call_observed(
162        &self,
163        request: impl Into<Op::Request>,
164        observation: AdapterContext,
165    ) -> impl Future<Output = Result<Op::Response, ProviderError>> + WasmCompatSend + 'static {
166        self.finished(request.into(), Sending::observed(observation))
167    }
168
169    /// The call opens when first polled, as [`Model::call`] does.
170    pub(crate) fn finished(
171        &self,
172        request: Op::Request,
173        sending: Sending,
174    ) -> impl Future<Output = Result<Op::Response, ProviderError>> + WasmCompatSend + 'static {
175        let inner = self.inner.clone();
176        async move { inner.open(request, Mode::Unary, sending)?.finish().await }
177    }
178
179    /// Open a streamed reply; [`Model::stream`] with the model erased.
180    pub fn stream(&self, request: impl Into<Op::Request>) -> Result<Streamed<Op>, ProviderError> {
181        self.opened(request.into(), Sending::default())
182    }
183
184    /// [`Self::stream`], with the attempt observed under `observation`.
185    pub fn stream_observed(
186        &self,
187        request: impl Into<Op::Request>,
188        observation: AdapterContext,
189    ) -> Result<Streamed<Op>, ProviderError> {
190        self.opened(request.into(), Sending::observed(observation))
191    }
192
193    /// Open a streamed reply, sent as `sending` says.
194    pub(crate) fn opened(
195        &self,
196        request: Op::Request,
197        sending: Sending,
198    ) -> Result<Streamed<Op>, ProviderError> {
199        self.inner.open(request, Mode::Streaming, sending)
200    }
201}
202
203impl DynModel<crate::operation::Completion> {
204    /// Every option of `request` this model would refuse, with nothing
205    /// sent; [`Model::check`] with the model erased.
206    ///
207    /// ```no_run
208    /// use rig_core::catalog::Catalog;
209    /// use rig_core::completion::{CheckError, CompletionRequest, Effort};
210    /// use rig_core::providers::registry::ConnectOptions;
211    ///
212    /// let model = Catalog::builtin().connect_with("openai/gpt-6-sol", ConnectOptions::new())?;
213    /// let request = CompletionRequest::new("hi")
214    ///     .temperature(0.2)
215    ///     .reasoning(Effort::High);
216    /// if let Err(CheckError::Unsupported(refused)) = model.check(&request) {
217    ///     for refusal in refused {
218    ///         println!("{refusal}");
219    ///     }
220    /// }
221    /// # Ok::<(), Box<dyn std::error::Error>>(())
222    /// ```
223    ///
224    /// # Errors
225    ///
226    /// As [`Model::check`].
227    pub fn check(
228        &self,
229        request: &crate::completion::CompletionRequest,
230    ) -> Result<(), crate::completion::CheckError> {
231        super::check_with(request, |request| self.inner.dry_run(request))
232    }
233}
234
235#[cfg(test)]
236mod tests;