Skip to main content

turnframe_provider/
provider.rs

1//! The trait every adapter implements (spec §20.1).
2//!
3//! One [`ModelProvider`] instance is one **provider-model profile**: one
4//! endpoint, one credential, one model, one honest set of declared
5//! capabilities. Two models behind the same vendor are two instances, because
6//! conformance is per provider-model combination and a capability declaration
7//! that covers "the vendor" is a declaration about nothing.
8//!
9//! An adapter's whole job is translation: normalized request in, vendor request
10//! out, vendor response in, normalized response out, vendor error in,
11//! [`ProviderError`] out. It holds no workflow policy, never inspects a
12//! workflow view, has no opinion about which acts are safe, and never receives
13//! a command handler or an external credential (spec §21.3).
14//!
15//! # The one rule an adapter can break on its own
16//!
17//! [`capabilities`](ModelProvider::capabilities) must be true. Everything
18//! downstream — routing, the refusal to downgrade a critical stage, the choice
19//! to trust a parsed plan — rests on it. An optimistic declaration is the most
20//! dangerous misconfiguration in the system, and the conformance suite exists
21//! largely to catch it: if the profile says
22//! [`NativeJsonSchema`](crate::capabilities::StructuredOutputCapability::NativeJsonSchema),
23//! the request that goes on the wire must actually carry the schema.
24
25use async_trait::async_trait;
26
27use crate::capabilities::{
28    CapabilityMismatch, CapabilityRequirements, ModelProfile, ProviderCapabilities,
29};
30use crate::error::ProviderError;
31use crate::ids::{ModelKey, ModelRef, ProviderKey};
32use crate::request::ModelRequest;
33use crate::response::ModelResponse;
34use crate::stream::ModelStream;
35
36/// One configured provider-model pair the runtime can call.
37///
38/// ```
39/// use async_trait::async_trait;
40/// use turnframe_provider::prelude::*;
41/// use turnframe_provider::stream::ModelStream;
42///
43/// struct EchoProvider;
44///
45/// #[async_trait]
46/// impl ModelProvider for EchoProvider {
47///     fn provider_key(&self) -> ProviderKey {
48///         ProviderKey::from("echo")
49///     }
50///     fn model_key(&self) -> ModelKey {
51///         ModelKey::from("echo-1")
52///     }
53///     fn capabilities(&self) -> ProviderCapabilities {
54///         ProviderCapabilities::minimal()
55///     }
56///     async fn generate(&self, request: ModelRequest) -> Result<ModelResponse, ProviderError> {
57///         let text = request.messages.last().map(Message::text).unwrap_or_default();
58///         Ok(ModelResponse::new(request.request_id, self.provider_key(), self.model_key())
59///             .with_text(text))
60///     }
61/// }
62///
63/// futures::executor::block_on(async {
64///     let provider = EchoProvider;
65///     let request = ModelRequest::new(ModelPurpose::Acknowledge).with_message(Message::user("ciao"));
66///     let response = provider.generate(request).await.unwrap();
67///     assert_eq!(response.text(), "ciao");
68///
69///     // Streaming is not implemented, and the default says so instead of
70///     // chunking a complete answer to look like a stream.
71///     let refused = provider.stream(ModelRequest::new(ModelPurpose::Acknowledge)).await;
72///     assert!(refused.is_err());
73/// });
74/// ```
75#[async_trait]
76pub trait ModelProvider: Send + Sync {
77    /// The configured provider key, e.g. `"openai"`. Labels every metric,
78    /// attempt record and replay entry this provider produces.
79    fn provider_key(&self) -> ProviderKey;
80
81    /// The configured model key, e.g. `"gpt-4o-2024-08-06"`.
82    fn model_key(&self) -> ModelKey;
83
84    /// What this provider-model pair can actually do (spec §20.3).
85    ///
86    /// Configured or probed, never inferred from the brand.
87    fn capabilities(&self) -> ProviderCapabilities;
88
89    /// The full routing profile: keys, capabilities, and the cost, region and
90    /// tags routing filters on.
91    ///
92    /// The default builds a profile with no cost, no region and no tags, which
93    /// is enough for a router that only filters on capabilities. An adapter
94    /// that knows its price or its region overrides this.
95    fn profile(&self) -> ModelProfile {
96        ModelProfile::new(self.provider_key(), self.model_key(), self.capabilities())
97    }
98
99    /// The provider-model pair, as routing and health tracking key it.
100    fn reference(&self) -> ModelRef {
101        ModelRef {
102            provider: self.provider_key(),
103            model: self.model_key(),
104        }
105    }
106
107    /// Checks the declared capabilities against `requirements`.
108    ///
109    /// The single place a caller asks "may this profile serve this stage?".
110    ///
111    /// # Errors
112    ///
113    /// Returns the [`CapabilityMismatch`] naming every unmet requirement.
114    fn supports(&self, requirements: &CapabilityRequirements) -> Result<(), CapabilityMismatch> {
115        requirements.satisfied_by(&self.capabilities())
116    }
117
118    /// Runs one model call and returns the whole answer.
119    ///
120    /// The implementation must honour [`ModelRequest::timeout`], must send
121    /// [`ModelRequest::request_id`] as an idempotency hint where the vendor
122    /// supports one, and must be cancel-safe: dropping the returned future
123    /// aborts the call and leaves nothing running.
124    ///
125    /// # Errors
126    ///
127    /// Returns the normalized [`ProviderError`] for the vendor failure.
128    async fn generate(&self, request: ModelRequest) -> Result<ModelResponse, ProviderError>;
129
130    /// Runs one model call and returns its answer incrementally.
131    ///
132    /// The default refuses with
133    /// [`Unsupported`](crate::error::ProviderErrorKind::Unsupported), which is
134    /// the right answer for an adapter whose profile declares
135    /// [`streaming: false`](crate::capabilities::ProviderCapabilities::streaming).
136    /// Emulating a stream by chunking a complete response would make the
137    /// declaration a lie and defeat the point of streaming.
138    ///
139    /// An implementation must reassemble, through
140    /// [`reconstruct`](crate::stream::reconstruct), to the same response
141    /// [`generate`](Self::generate) returns for the same exchange.
142    ///
143    /// # Errors
144    ///
145    /// Returns the normalized [`ProviderError`] for the vendor failure, before
146    /// the stream starts or as an item within it.
147    async fn stream(&self, request: ModelRequest) -> Result<ModelStream, ProviderError> {
148        let _ = request;
149        Err(ProviderError::unsupported("streaming").with_model(&self.reference()))
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use crate::capabilities::StructuredOutputCapability;
157    use crate::ids::RequestId;
158    use crate::purpose::ModelPurpose;
159    use crate::request::Message;
160
161    struct Fixed {
162        capabilities: ProviderCapabilities,
163    }
164
165    #[async_trait]
166    impl ModelProvider for Fixed {
167        fn provider_key(&self) -> ProviderKey {
168            ProviderKey::from("fixed")
169        }
170
171        fn model_key(&self) -> ModelKey {
172            ModelKey::from("fixed-1")
173        }
174
175        fn capabilities(&self) -> ProviderCapabilities {
176            self.capabilities.clone()
177        }
178
179        async fn generate(&self, request: ModelRequest) -> Result<ModelResponse, ProviderError> {
180            Ok(
181                ModelResponse::new(request.request_id, self.provider_key(), self.model_key())
182                    .with_text("ok"),
183            )
184        }
185    }
186
187    fn provider() -> Fixed {
188        Fixed {
189            capabilities: ProviderCapabilities::minimal()
190                .with_structured_output(StructuredOutputCapability::NativeJsonSchema),
191        }
192    }
193
194    #[tokio::test]
195    async fn the_default_profile_mirrors_the_declared_capabilities() {
196        let provider = provider();
197        let profile = provider.profile();
198        assert_eq!(profile.provider, provider.provider_key());
199        assert_eq!(profile.model, provider.model_key());
200        assert_eq!(profile.capabilities, provider.capabilities());
201        assert_eq!(provider.reference().to_string(), "fixed/fixed-1");
202        assert!(profile.max_cost_per_million().is_none());
203    }
204
205    #[tokio::test]
206    async fn supports_answers_with_the_mismatch() {
207        let provider = provider();
208        let ok = ModelPurpose::Extract.requirements();
209        assert!(provider.supports(&ok).is_ok());
210
211        let needs_streaming = CapabilityRequirements::none().with_streaming();
212        let mismatch = provider.supports(&needs_streaming).unwrap_err();
213        assert_eq!(mismatch.missing.len(), 1);
214        assert!(!mismatch.structured_output_unmet());
215    }
216
217    #[tokio::test]
218    async fn streaming_defaults_to_an_honest_refusal() {
219        let provider = provider();
220        let request =
221            ModelRequest::new(ModelPurpose::Acknowledge).with_message(Message::user("hi"));
222        let error = provider.stream(request).await.unwrap_err();
223        assert!(matches!(
224            error.kind(),
225            crate::error::ProviderErrorKind::Unsupported { .. }
226        ));
227        assert_eq!(error.retry_class(), crate::error::RetryClass::Fallback);
228        assert_eq!(error.provider().map(ProviderKey::as_str), Some("fixed"));
229    }
230
231    #[tokio::test]
232    async fn generate_echoes_the_request_id() {
233        let provider = provider();
234        let request =
235            ModelRequest::new(ModelPurpose::Acknowledge).with_request_id(RequestId::nil());
236        let response = provider.generate(request).await.unwrap();
237        assert_eq!(response.request_id, RequestId::nil());
238    }
239
240    #[tokio::test]
241    async fn the_trait_is_object_safe() {
242        let provider: Box<dyn ModelProvider> = Box::new(provider());
243        assert_eq!(provider.provider_key().as_str(), "fixed");
244        let response = provider
245            .generate(ModelRequest::new(ModelPurpose::Acknowledge))
246            .await
247            .unwrap();
248        assert_eq!(response.text(), "ok");
249    }
250}