Skip to main content

rig_core/client/
completion.rs

1use crate::completion::CompletionModel;
2
3/// A provider client with completion capabilities.
4///
5/// Clients remain `Clone` for conversions between client types; the models
6/// they construct no longer need to be.
7pub trait CompletionClient {
8    /// The type of CompletionModel used by the client.
9    type CompletionModel: CompletionModel;
10
11    /// Create a completion model with the given model.
12    ///
13    /// Construction lives here rather than on [`CompletionModel`] so a model
14    /// type can be implemented — and used — without any client type at all.
15    /// Implement this by calling the model's own inherent constructor.
16    ///
17    /// # Example with OpenAI
18    /// ```no_run
19    /// use rig_core::prelude::*;
20    /// use rig_core::providers::openai::{Client, self};
21    ///
22    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
23    /// // Initialize the OpenAI client
24    /// let openai = Client::new("your-open-ai-api-key")?;
25    ///
26    /// let gpt = openai.completion_model(openai::GPT_5_2);
27    /// # Ok(())
28    /// # }
29    /// ```
30    fn completion_model(&self, model: impl Into<String>) -> Self::CompletionModel;
31}
32
33/// Construction hook for the blanket [`CompletionClient`] implementation over
34/// [`crate::client::Client`].
35///
36/// That blanket implementation is generic over whichever model type a provider
37/// extension declares, so it needs some way to build that model. Coherence
38/// rules out one blanket implementation per provider family — they would all
39/// overlap on `Client<Ext, H>` — and the alternative of a public bound such as
40/// `From<(Client<Ext, H>, String)>` would push a synthetic conversion into
41/// every provider model's public API.
42///
43/// This trait is public because it is the extension point for out-of-tree
44/// provider extensions built on the generic [`crate::client::Client`]: such a
45/// crate cannot implement [`CompletionClient`] for rig's foreign
46/// `Client<Ext, H>` type directly (orphan rule), so it implements this trait
47/// on its own model type instead, and the blanket implementation supplies
48/// `completion_model` for it. Providers with their own client type simply
49/// implement [`CompletionClient`] directly and never need this trait.
50pub trait ConstructCompletionModel<C>: Sized {
51    /// Build this model from its provider client and a model identifier.
52    fn construct(client: &C, model: String) -> Self;
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58    use crate::completion::{CompletionError, CompletionRequest, CompletionResponse};
59    use crate::streaming::StreamingCompletionResponse;
60
61    /// A model implemented entirely outside rig's provider machinery: no
62    /// response associated types, no client associated type, and no
63    /// construction hook.
64    #[derive(Clone)]
65    struct ExternalModel {
66        name: String,
67    }
68
69    impl CompletionModel for ExternalModel {
70        async fn completion(
71            &self,
72            _request: CompletionRequest,
73        ) -> Result<CompletionResponse, CompletionError> {
74            Err(CompletionError::ResponseError(format!(
75                "{} is a compile-coverage model",
76                self.name
77            )))
78        }
79
80        async fn stream(
81            &self,
82            _request: CompletionRequest,
83        ) -> Result<StreamingCompletionResponse, CompletionError> {
84            Err(CompletionError::ResponseError(format!(
85                "{} is a compile-coverage model",
86                self.name
87            )))
88        }
89    }
90
91    struct ExternalClient;
92
93    impl CompletionClient for ExternalClient {
94        type CompletionModel = ExternalModel;
95
96        fn completion_model(&self, model: impl Into<String>) -> Self::CompletionModel {
97            ExternalModel { name: model.into() }
98        }
99    }
100
101    #[test]
102    fn external_model_needs_no_client_or_response_associated_types() {
103        let model = ExternalClient.completion_model("external-model");
104        assert_eq!(model.name, "external-model");
105    }
106
107    #[test]
108    fn external_model_is_usable_without_a_client() {
109        // A bare model with no client at all still satisfies `CompletionModel`.
110        fn assert_completion_model<M: CompletionModel>(_: &M) {}
111
112        assert_completion_model(&ExternalModel {
113            name: "standalone".to_owned(),
114        });
115    }
116
117    /// Compile coverage for an out-of-tree provider extension built on the
118    /// generic [`crate::client::Client`]: implementing the public
119    /// [`ConstructCompletionModel`] hook is all it takes for the blanket
120    /// [`CompletionClient`] implementation to apply. Everything here uses only
121    /// public API, mirroring what a downstream crate can write.
122    mod external_generic_extension {
123        use super::*;
124        use crate::client::{
125            BearerAuth, Capabilities, Capable, Client, ClientBuilder, DebugExt, Nothing, Provider,
126            ProviderBuilder,
127        };
128        use crate::http_client::{self, HttpClientExt};
129
130        #[derive(Debug, Default, Clone, Copy)]
131        struct ExternalExt;
132        #[derive(Debug, Default, Clone, Copy)]
133        struct ExternalExtBuilder;
134
135        impl Provider for ExternalExt {
136            type Builder = ExternalExtBuilder;
137            const VERIFY_PATH: &'static str = "/";
138        }
139
140        impl ProviderBuilder for ExternalExtBuilder {
141            type Extension<H>
142                = ExternalExt
143            where
144                H: HttpClientExt;
145            type ApiKey = BearerAuth;
146
147            const BASE_URL: &'static str = "https://external.invalid";
148
149            fn build<H>(
150                _builder: &ClientBuilder<Self, Self::ApiKey, H>,
151            ) -> http_client::Result<Self::Extension<H>>
152            where
153                H: HttpClientExt,
154            {
155                Ok(ExternalExt)
156            }
157        }
158
159        impl<H> Capabilities<H> for ExternalExt {
160            type Completion = Capable<ExternalGenericModel<H>>;
161            type Embeddings = Nothing;
162            type Transcription = Nothing;
163            type ModelListing = Nothing;
164            #[cfg(feature = "image")]
165            type ImageGeneration = Nothing;
166            #[cfg(feature = "audio")]
167            type AudioGeneration = Nothing;
168            type Rerank = Nothing;
169        }
170
171        impl DebugExt for ExternalExt {}
172
173        #[derive(Clone)]
174        struct ExternalGenericModel<H> {
175            _client: Client<ExternalExt, H>,
176            model: String,
177        }
178
179        impl<H> CompletionModel for ExternalGenericModel<H>
180        where
181            H: Clone + Send + Sync + std::fmt::Debug + 'static,
182        {
183            async fn completion(
184                &self,
185                _request: CompletionRequest,
186            ) -> Result<CompletionResponse, CompletionError> {
187                Err(CompletionError::ResponseError(format!(
188                    "{} is a compile-coverage model",
189                    self.model
190                )))
191            }
192
193            async fn stream(
194                &self,
195                _request: CompletionRequest,
196            ) -> Result<StreamingCompletionResponse, CompletionError> {
197                Err(CompletionError::ResponseError(format!(
198                    "{} is a compile-coverage model",
199                    self.model
200                )))
201            }
202        }
203
204        impl<H> ConstructCompletionModel<Client<ExternalExt, H>> for ExternalGenericModel<H>
205        where
206            H: Clone + Send + Sync + std::fmt::Debug + 'static,
207        {
208            fn construct(client: &Client<ExternalExt, H>, model: String) -> Self {
209                Self {
210                    _client: client.clone(),
211                    model,
212                }
213            }
214        }
215
216        #[test]
217        fn external_extension_reaches_the_blanket_completion_client_impl() {
218            fn assert_completion_client<C: CompletionClient>() {}
219
220            assert_completion_client::<Client<ExternalExt, reqwest::Client>>();
221        }
222    }
223}