Skip to main content

typesafe_sdk/
models.rs

1//! The models endpoint.
2//!
3//! Listing the models is also how a client checks that its API key works and
4//! opens its connection before the first real call: the request carries no
5//! body, so a key that is rejected is rejected cheaply.
6//!
7//! [`Models`] is the resource, reached through [`Client::models`]; a model
8//! card carries three strings, and members the API adds later are ignored by
9//! the decoder and stay readable in the raw body.
10
11use std::{borrow::Cow, fmt, time::Duration};
12
13use bytes::Bytes;
14use http::{HeaderMap, Method, StatusCode, Uri};
15use serde::{
16    Deserialize, Deserializer, Serialize, Serializer,
17    de::{self, IgnoredAny, MapAccess, Visitor},
18    ser::SerializeStruct,
19};
20
21use crate::{
22    client::Client,
23    codec,
24    de::{KeyIn, invalid_response},
25    error::Error,
26    request::{CallHeaders, Deadline},
27    response::ResponseMeta,
28    retry::{self, RetryPolicy},
29    transport::{self, Exchange, HttpService},
30};
31
32// ------------------------------------------------------------- resource
33
34/// The models endpoint of a client, from [`Client::models`].
35pub struct Models<'a, S> {
36    client: &'a Client<S>,
37}
38
39impl<'a, S> Models<'a, S> {
40    pub(crate) fn new(client: &'a Client<S>) -> Self {
41        Self { client }
42    }
43
44    /// A request for the models the account can use.
45    pub fn list(&self) -> ListModels<'a, S> {
46        ListModels {
47            client: self.client,
48            deadline: Deadline::Client,
49            headers: CallHeaders::default(),
50            retry: None,
51        }
52    }
53}
54
55impl<S> fmt::Debug for Models<'_, S> {
56    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
57        formatter.debug_struct("Models").finish_non_exhaustive()
58    }
59}
60
61/// A request for the model list, ready to be configured and sent.
62///
63/// Made by [`Models::list`]. Nothing is checked until
64/// [`send`](Self::send).
65#[must_use = "a request does nothing until it is sent"]
66pub struct ListModels<'a, S> {
67    client: &'a Client<S>,
68    deadline: Deadline,
69    headers: CallHeaders<'a>,
70    /// This call's own policy, in place of the client's.
71    retry: Option<RetryPolicy>,
72}
73
74impl<'a, S> ListModels<'a, S> {
75    /// The deadline of each attempt of this call, instead of the client's.
76    pub fn timeout(mut self, timeout: Duration) -> Self {
77        self.deadline = Deadline::After(timeout);
78        self
79    }
80
81    /// No deadline on any attempt of this call.
82    pub fn no_timeout(mut self) -> Self {
83        self.deadline = Deadline::Never;
84        self
85    }
86
87    /// A header for this call only; see
88    /// [`SystemOne::header`](crate::request::SystemOne::header).
89    pub fn header(mut self, name: impl Into<Cow<'a, str>>, value: impl Into<Cow<'a, str>>) -> Self {
90        self.headers.push(name.into(), value.into());
91        self
92    }
93
94    /// The retry policy of this call, in place of the client's; see
95    /// [`SystemOne::retry`](crate::request::SystemOne::retry).
96    pub fn retry(mut self, policy: RetryPolicy) -> Self {
97        self.retry = Some(policy);
98        self
99    }
100}
101
102impl<S> ListModels<'_, S>
103where
104    S: HttpService,
105{
106    /// Sends the request and decodes the list, retrying a failure as the
107    /// call's retry policy, or else the client's, says. When retrying stops,
108    /// the error is the one the last attempt failed with.
109    ///
110    /// # Errors
111    ///
112    /// - [`ErrorKind::InvalidRequest`](crate::ErrorKind::InvalidRequest),
113    ///   before anything is sent: a header that is not a valid header, or a
114    ///   deadline of zero.
115    /// - [`ErrorKind::Api`](crate::ErrorKind::Api) for a status outside 2xx.
116    /// - [`ErrorKind::Timeout`](crate::ErrorKind::Timeout) when the attempt
117    ///   ran past its deadline.
118    /// - [`ErrorKind::Connection`](crate::ErrorKind::Connection) when no
119    ///   response could be read.
120    /// - [`ErrorKind::ResponseTooLarge`](crate::ErrorKind::ResponseTooLarge)
121    ///   when a success response's body was larger than the client's limit.
122    /// - [`ErrorKind::ResponseValidation`](crate::ErrorKind::ResponseValidation)
123    ///   when the body is not a model list.
124    pub async fn send(self) -> Result<ListModelsResponse, Error> {
125        let shared = self.client.shared();
126        let deadline = self.deadline.resolve(shared.config.timeout())?;
127        let headers = self.headers.parse(false)?;
128
129        let uri = shared.config.endpoints().models();
130        let exchange = Exchange {
131            method: &Method::GET,
132            uri,
133            base_headers: &shared.get_headers,
134            call_headers: &headers,
135            deadline,
136            max_response_bytes: shared.config.max_response_bytes(),
137        };
138        let policy = self.retry.as_ref().unwrap_or(&shared.retry);
139        retry::run(policy, &Method::GET, uri, |retry| async move {
140            let (status, headers, body) =
141                transport::attempt(&shared.service, exchange, retry, None).await?;
142            // Decoding is part of the attempt, so a retry predicate sees a
143            // response that did not decode, as the Python SDK's does.
144            decode_list_models(body, status, headers, Some((&Method::GET, uri)))
145        })
146        .await
147    }
148}
149
150impl<S> fmt::Debug for ListModels<'_, S> {
151    /// The deadline and the header names; never a header value. A retry
152    /// policy is shown when the call has one of its own.
153    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
154        let mut shown = formatter.debug_struct("ListModels");
155        shown.field("deadline", &self.deadline).field("headers", &self.headers);
156        if let Some(retry) = &self.retry {
157            shown.field("retry", retry);
158        }
159        shown.finish_non_exhaustive()
160    }
161}
162
163// ------------------------------------------------------------ payload
164
165/// One model the account can use.
166#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
167pub struct ModelMetadata {
168    name: String,
169    description: String,
170    release_date: String,
171}
172
173impl ModelMetadata {
174    /// The name or alias a request's `model` accepts, such as `jev-latest`.
175    #[must_use]
176    pub fn name(&self) -> &str {
177        &self.name
178    }
179
180    /// What the model is for.
181    #[must_use]
182    pub fn description(&self) -> &str {
183        &self.description
184    }
185
186    /// The release date, as the API writes it: `YYYY-MM-DD`.
187    #[must_use]
188    pub fn release_date(&self) -> &str {
189        &self.release_date
190    }
191}
192
193/// The models available to the account, with the HTTP response they came in.
194///
195/// Serializing it writes `models` only; the HTTP metadata is runtime state,
196/// not part of the API payload.
197#[derive(Debug, Clone, PartialEq)]
198pub struct ListModelsResponse {
199    models: Vec<ModelMetadata>,
200    meta: ResponseMeta,
201}
202
203impl ListModelsResponse {
204    /// The models, in the order the API listed them.
205    #[must_use]
206    pub fn models(&self) -> &[ModelMetadata] {
207        &self.models
208    }
209
210    /// The status, headers and raw body of the HTTP response.
211    #[must_use]
212    pub fn meta(&self) -> &ResponseMeta {
213        &self.meta
214    }
215
216    /// Gives up everything but the models.
217    #[must_use]
218    pub fn into_models(self) -> Vec<ModelMetadata> {
219        self.models
220    }
221}
222
223impl Serialize for ListModelsResponse {
224    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
225    where
226        S: Serializer,
227    {
228        let mut out = serializer.serialize_struct("ListModelsResponse", 1)?;
229        out.serialize_field("models", &self.models)?;
230        out.end()
231    }
232}
233
234impl<'de> Deserialize<'de> for ModelMetadata {
235    /// Reads a model card from an object; members this version does not know
236    /// are ignored.
237    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
238    where
239        D: Deserializer<'de>,
240    {
241        deserializer.deserialize_map(CardVisitor)
242    }
243}
244
245/// Reads a model card as an object only. serde's derived reader would also
246/// take a JSON array positionally, which the API's schema does not allow.
247struct CardVisitor;
248
249impl<'de> Visitor<'de> for CardVisitor {
250    type Value = ModelMetadata;
251
252    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
253        formatter.write_str("a model card")
254    }
255
256    fn visit_map<M>(self, mut map: M) -> Result<ModelMetadata, M::Error>
257    where
258        M: MapAccess<'de>,
259    {
260        let (mut name, mut description, mut release_date) = (None, None, None);
261        while let Some(index) =
262            map.next_key_seed(KeyIn(&["name", "description", "release_date"]))?
263        {
264            match index {
265                Some(0) => name = Some(map.next_value()?),
266                Some(1) => description = Some(map.next_value()?),
267                Some(2) => release_date = Some(map.next_value()?),
268                _ => {
269                    map.next_value::<IgnoredAny>()?;
270                }
271            }
272        }
273        Ok(ModelMetadata {
274            name: name.ok_or_else(|| de::Error::missing_field("name"))?,
275            description: description.ok_or_else(|| de::Error::missing_field("description"))?,
276            release_date: release_date.ok_or_else(|| de::Error::missing_field("release_date"))?,
277        })
278    }
279}
280
281/// The body of a models response.
282struct ModelList {
283    models: Vec<ModelMetadata>,
284}
285
286impl<'de> Deserialize<'de> for ModelList {
287    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
288    where
289        D: Deserializer<'de>,
290    {
291        deserializer.deserialize_map(ModelListVisitor)
292    }
293}
294
295struct ModelListVisitor;
296
297impl<'de> Visitor<'de> for ModelListVisitor {
298    type Value = ModelList;
299
300    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
301        formatter.write_str("a list of models")
302    }
303
304    fn visit_map<M>(self, mut map: M) -> Result<ModelList, M::Error>
305    where
306        M: MapAccess<'de>,
307    {
308        let mut models = None;
309        while let Some(index) = map.next_key_seed(KeyIn(&["models"]))? {
310            match index {
311                Some(_) => models = Some(map.next_value()?),
312                None => {
313                    map.next_value::<IgnoredAny>()?;
314                }
315            }
316        }
317        Ok(ModelList { models: models.ok_or_else(|| de::Error::missing_field("models"))? })
318    }
319}
320
321/// Decodes the body of a successful models response.
322///
323/// # Errors
324///
325/// Returns [`ErrorKind::ResponseValidation`](crate::ErrorKind::ResponseValidation)
326/// when the body does not have the documented shape; its field path names the
327/// card and the member, such as `models[1].name`.
328pub(crate) fn decode_list_models(
329    body: Bytes,
330    status: StatusCode,
331    headers: HeaderMap,
332    endpoint: Option<(&Method, &Uri)>,
333) -> Result<ListModelsResponse, Error> {
334    let meta = ResponseMeta::new(status, headers, body);
335    let decoded = codec::decode::<ModelList>(meta.raw_body());
336    match decoded {
337        Ok(ModelList { models }) => Ok(ListModelsResponse { models, meta }),
338        Err(source) => Err(invalid_response(meta, endpoint, source)),
339    }
340}
341
342#[cfg(test)]
343#[path = "models_tests.rs"]
344mod tests;